refactor: 重构项目结构

- 优化 `车站-设备-告警`  轮询机制
- 改进设备卡片的布局
- 支持修改设备
- 告警轮询中获取完整告警数据
- 车站告警详情支持导出完整的 `今日告警列表`
- 支持将状态持久化到 `IndexedDB`
- 新增轮询控制 (调试模式)
- 新增离线开发模式 (调试模式)
- 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
yangsy
2025-12-11 13:42:22 +08:00
commit 37781216b2
278 changed files with 17988 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import type { NdmDeviceAlarmLogResultVO, NdmDeviceResultVO, NdmNvrResultVO } from '@/apis';
import { ALARM_TYPES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType } from '@/enums';
import dayjs from 'dayjs';
import { NButton, NPopover, NScrollbar, NTag, type TagProps } from 'naive-ui';
import { h } from 'vue';
export const renderAlarmDateCell = (rowData: NdmDeviceAlarmLogResultVO) => {
return dayjs(Number(rowData.alarmDate ?? 0)).format('YYYY-MM-DD HH:mm:ss');
};
export const renderDeviceTypeCell = (rowData: NdmDeviceAlarmLogResultVO) => {
const deviceTypeVal = tryGetDeviceType(rowData.deviceType);
if (!deviceTypeVal) return '-';
return DEVICE_TYPE_NAMES[deviceTypeVal];
};
export const renderAlarmTypeCell = (rowData: NdmDeviceAlarmLogResultVO) => {
const { alarmType } = rowData;
if (!alarmType) {
return '';
}
return h(NTag, { type: 'default' }, { default: () => ALARM_TYPES[alarmType] });
};
export const renderFaultLevelCell = (rowData: NdmDeviceAlarmLogResultVO) => {
const { faultLevel } = rowData;
if (!faultLevel) {
return '';
}
let type: TagProps['type'] = 'default';
if (faultLevel === '1') {
type = 'error';
} else if (faultLevel === '2') {
type = 'warning';
} else if (faultLevel === '3') {
type = 'info';
}
return h(NTag, { type }, { default: () => FAULT_LEVELS[faultLevel] });
};
export const renderFaultDescriptionCell = (rowData: NdmDeviceAlarmLogResultVO, ndmDevice: NdmDeviceResultVO) => {
const isNvrCluster = (ndmDevice: NdmDeviceResultVO) => {
const deviceType = tryGetDeviceType(ndmDevice.deviceType);
if (!deviceType) return false;
const isNvr = deviceType === DEVICE_TYPE_LITERALS.ndmNvr;
if (!isNvr) return false;
const maybeNvrCluster = ndmDevice as NdmNvrResultVO;
return !!maybeNvrCluster.clusterList?.trim() && maybeNvrCluster.clusterList !== maybeNvrCluster.ipAddress;
};
if (!isNvrCluster(ndmDevice)) {
return rowData.faultDescription;
}
return h(
NPopover,
{
trigger: 'click',
},
{
trigger: () => {
return h(
NButton,
{
text: true,
type: 'info',
},
{
default: () => '查看',
},
);
},
default: () => {
return h(
NScrollbar,
{
style: {
width: '800px',
'max-height': '400px',
},
},
{
default: () => {
return h('pre', {}, { default: () => rowData.faultDescription?.split('; ').join('\n') ?? '' });
},
},
);
},
},
);
};

View File

@@ -0,0 +1,26 @@
import type { Station } from '@/apis';
import type { NvrRecordDiag } from './record-check';
import { downloadByData, formatDuration } from '@/utils';
import dayjs from 'dayjs';
export const exportRecordDiagCsv = (recordDiags: NvrRecordDiag[], stationName: Station['name']) => {
const csvHeader = '通道名称,开始时间,结束时间,持续时长\n';
const csvRows = recordDiags
.map((channel) => {
if (channel.lostChunks.length === 0) {
return `${channel.channelName},,,`;
}
return channel.lostChunks
.map((loss) => {
const duration = formatDuration(loss.startTime, loss.endTime);
const startTime = dayjs(loss.startTime).format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(loss.endTime).format('YYYY-MM-DD HH:mm:ss');
return `${channel.channelName},${startTime},${endTime},${duration}`;
})
.join('\n');
})
.join('\n');
const csvContent = csvHeader.concat(csvRows);
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
downloadByData(csvContent, `${stationName}_录像缺失记录_${time}.csv`, 'text/csv;charset=utf-8', '\ufeff');
};

5
src/helpers/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from './device-alarm';
export * from './export-record-diag-csv';
export * from './nvr-cluster';
export * from './record-check';
export * from './switch-port';

View File

@@ -0,0 +1,8 @@
import type { NdmNvrResultVO } from '@/apis';
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
const { ipAddress, clusterList } = maybeNvrCluster;
if (!clusterList?.trim()) return false;
if (clusterList === ipAddress) return false;
return true;
};

View File

@@ -0,0 +1,69 @@
import type { NdmRecordCheck, RecordInfo, RecordItem } from '@/apis';
import dayjs from 'dayjs';
import destr from 'destr';
import { groupBy } from 'es-toolkit';
export type NvrRecordDiag = {
gbCode: string;
channelName: string;
recordDuration: RecordItem;
lostChunks: RecordItem[];
};
// 解析出丢失的录像时间段
export const transformRecordChecks = (rawRecordChecks: NdmRecordCheck[]): NvrRecordDiag[] => {
// 解析diagInfo
const parsedRecordChecks = rawRecordChecks.map((recordCheck) => ({
...recordCheck,
diagInfo: destr<RecordInfo>(recordCheck.diagInfo),
}));
// 按国标码分组
const recordChecksByGbCode = groupBy(parsedRecordChecks, (recordCheck) => recordCheck.gbCode);
// 提取分组后的国标码和录像诊断记录
const channelGbCodes = Object.keys(recordChecksByGbCode);
const recordChecksList = Object.values(recordChecksByGbCode);
// 初始化每个通道的录像诊断数据结构
const recordDiags = channelGbCodes.map((gbCode, index) => ({
gbCode,
channelName: recordChecksList.at(index)?.at(-1)?.name ?? '',
records: [] as RecordItem[],
lostChunks: [] as RecordItem[],
}));
// 写入同一gbCode的录像片段
recordChecksList.forEach((recordChecks, index) => {
recordChecks.forEach((recordCheck) => {
recordDiags.at(index)?.records.push(...recordCheck.diagInfo.recordList);
});
});
// 过滤掉没有录像记录的通道
const filteredRecordDiags = recordDiags.filter((recordDiag) => recordDiag.records.length > 0);
// 计算每个通道丢失的录像时间片段
filteredRecordDiags.forEach((recordDiag) => {
recordDiag.records.forEach((record, index, records) => {
const nextRecordItem = records.at(index + 1);
if (!!nextRecordItem) {
// 如果下一段录像的开始时间不等于当前录像的结束时间,则判定为丢失
const nextStartTime = nextRecordItem.startTime;
const currEndTime = record.endTime;
if (nextStartTime !== currEndTime) {
recordDiag.lostChunks.push({
startTime: currEndTime,
endTime: nextStartTime,
});
}
}
});
});
return recordDiags.map((recordDiag) => {
const firstRecord = recordDiag.records.at(0);
const startTime = firstRecord ? dayjs(firstRecord.startTime).format('YYYY-MM-DD HH:mm:ss') : '';
const lastRecord = recordDiag.records.at(-1);
const endTime = lastRecord ? dayjs(lastRecord.endTime).format('YYYY-MM-DD HH:mm:ss') : '';
return {
gbCode: recordDiag.gbCode,
channelName: recordDiag.channelName,
recordDuration: { startTime, endTime },
lostChunks: recordDiag.lostChunks,
};
});
};

View File

@@ -0,0 +1,26 @@
import type { NdmSwitchPortInfo } from '@/apis';
export const getPortStatusValue = (portInfo: NdmSwitchPortInfo): string => {
const { upDown } = portInfo;
return upDown === 1 ? '已连接' : upDown === 2 ? '未连接' : '-';
};
export const transformPortSpeed = (portInfo: NdmSwitchPortInfo, type: 'in' | 'out' | 'total'): string => {
const units = ['b/s', 'Kb/s', 'Mb/s', 'Gb/s', 'Tb/s'];
const { inFlow, outFlow, flow } = portInfo;
let result: number = 0;
if (type === 'in') {
result = inFlow;
} else if (type === 'out') {
result = outFlow;
} else if (type === 'total') {
result = flow;
}
let unit = 0;
result *= 8;
while (result >= 1024 && unit < units.length - 1) {
result /= 1024;
unit++;
}
return `${result.toFixed(3)} ${units[unit]}`;
};