refactor: 移除旧版的录像诊断卡片与辅助函数

This commit is contained in:
yangsy
2026-02-26 11:15:55 +08:00
parent 89ff378eb7
commit 403c8d703e
6 changed files with 2 additions and 382 deletions

View File

@@ -2,22 +2,10 @@ import DeviceCommonCard from './device-common-card.vue';
import DeviceHardwareCard from './device-hardware-card.vue';
import DeviceHeaderCard from './device-header-card.vue';
import NvrDiskCard from './nvr-disk-card.vue';
import NvrRecordCard from './nvr-record-card.vue';
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
import SecurityBoxCircuitLinkModal from './security-box-circuit-link-modal.vue';
import SecurityBoxEnvCard from './security-box-env-card.vue';
import SwitchPortCard from './switch-port-card.vue';
import SwitchPortLinkModal from './switch-port-link-modal.vue';
export {
DeviceCommonCard,
DeviceHardwareCard,
DeviceHeaderCard,
NvrDiskCard,
NvrRecordCard,
SecurityBoxCircuitCard,
SecurityBoxCircuitLinkModal,
SecurityBoxEnvCard,
SwitchPortCard,
SwitchPortLinkModal,
};
export { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, SecurityBoxCircuitCard, SecurityBoxCircuitLinkModal, SecurityBoxEnvCard, SwitchPortCard, SwitchPortLinkModal };

View File

@@ -1,267 +0,0 @@
<script setup lang="ts">
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type NdmRecordCheck, type RecordItem, type Station } from '@/apis';
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
import { useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import axios, { isCancel } from 'axios';
import dayjs from 'dayjs';
import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next';
import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, ref, toRefs, watch, type CSSProperties } from 'vue';
const props = defineProps<{
ndmDevice: NdmNvrResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const themeVars = useThemeVars();
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const filterType = ref<'all' | 'some' | 'none'>('all');
const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr_record_check_query';
const {
data: recordChecks,
isFetching: loading,
refetch: refetchRecordChecks,
} = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
// const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal });
// return checks;
return (await axios.get<NdmRecordCheck[]>('./public/record-check.json', { signal })).data;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [NVR_RECORD_CHECK_KEY] });
}
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
await reloadAllRecordCheckApi(90, { stationCode: station.value.code, signal: abortController.value.signal });
},
onSuccess: () => {
window.$message.success('正在逐步刷新中,请稍后点击刷新按钮查看');
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reloadRecordCheckByGbId } = useMutation({
mutationFn: async (params: { gbCode: string }) => {
abortController.value.abort();
abortController.value = new AbortController();
const channelList = await getChannelListApi(ndmDevice.value, { stationCode: station.value.code, signal: abortController.value.signal });
const channel = channelList.find((channel) => channel.code === params.gbCode);
if (!channel) throw new Error('通道不存在');
window.$message.loading('刷新耗时较长, 请不要多次刷新, 并耐心等待...', {
duration: 1000 * 60 * 60 * 24 * 300,
});
const isSuccess = await reloadRecordCheckApi(channel, 90, { stationCode: station.value.code, signal: abortController.value.signal });
window.$message.destroyAll();
if (isSuccess) {
window.$message.success('刷新成功');
} else {
window.$message.error('刷新失败');
}
},
onSuccess: () => {
refetchRecordChecks();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onExportRecordCheck = () => {
exportRecordDiagCsv(recordDiags.value, station.value.name);
};
onBeforeUnmount(() => {
abortController.value.abort();
});
const recordDiags = computed(() => {
return transformRecordChecks(recordChecks.value ?? []).filter((recordDiag) => {
if (filterType.value === 'all') {
return true;
} else if (filterType.value === 'some') {
return recordDiag.lostChunks.length > 0;
} else if (filterType.value === 'none') {
return recordDiag.lostChunks.length === 0;
}
return false;
});
});
const page = ref(1);
const pageSize = ref(10);
watch([() => ndmDevice.value.id, () => ndmDevice.value.lastDiagTime, filterType], () => {
page.value = 1;
});
const pagedRecordDiags = computed(() => {
const startIndex = (page.value - 1) * pageSize.value;
const endIndex = page.value * pageSize.value;
return recordDiags.value.slice(startIndex, endIndex);
});
const getLostChunkDOMStyle = (lostChunk: RecordItem, duration: RecordItem): CSSProperties => {
const chunk = dayjs(lostChunk.endTime).diff(dayjs(lostChunk.startTime));
const offset = dayjs(lostChunk.startTime).diff(dayjs(duration.startTime));
const total = dayjs(duration.endTime).diff(dayjs(duration.startTime));
return {
left: `${(offset / total) * 100}%`,
width: `${(chunk / total) * 100}%`,
};
};
const lostChunkPopoverContext = ref<{ show: boolean; x: number; y: number; lostDuration?: RecordItem }>({ show: false, x: 0, y: 0 });
const onMouseEnterLostChunk = (event: MouseEvent, lostChunk: RecordItem) => {
const { target: lostChunkDiv } = event;
const { width, left, top } = (lostChunkDiv as HTMLDivElement).getBoundingClientRect();
const { startTime, endTime } = lostChunk;
lostChunkPopoverContext.value = {
show: true,
x: left + width / 2,
y: top,
lostDuration: { startTime, endTime },
};
};
const onMouseLeaveLostChunk = () => {
lostChunkPopoverContext.value.show = false;
lostChunkPopoverContext.value.lostDuration = undefined;
};
</script>
<template>
<NCard hoverable size="small">
<template #header>
<NFlex align="center" :size="24">
<div>录像诊断</div>
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
<template #trigger>
<NButton secondary size="small" :loading="reloading">更新所有通道录像诊断</NButton>
</template>
<template #default>
<span>确认更新所有通道录像诊断吗?</span>
</template>
</NPopconfirm>
</NFlex>
</template>
<template #header-extra>
<NFlex>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
<template #icon>
<NIcon :component="RotateCwIcon" />
</template>
</NButton>
</template>
<template #default>
<span>刷新数据</span>
</template>
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton size="small" quaternary circle @click="onExportRecordCheck">
<template #icon>
<NIcon :component="DownloadIcon" />
</template>
</NButton>
</template>
<template #default>
<span>导出录像诊断</span>
</template>
</NTooltip>
</NFlex>
</template>
<template #default>
<NFlex justify="flex-end" style="margin-bottom: 6px">
<NRadioGroup size="small" v-model:value="filterType">
<NRadioButton label="全部" :value="'all'" />
<NRadioButton label="有缺失" :value="'some'" />
<NRadioButton label="无缺失" :value="'none'" />
</NRadioGroup>
</NFlex>
<template v-for="{ gbCode, channelName, recordDuration, lostChunks } in pagedRecordDiags" :key="gbCode">
<div style="display: flex; justify-content: space-between">
<div>
<span>{{ channelName }}</span>
<span>{{ '\u3000' }}</span>
<span>{{ recordDuration.startTime }} - {{ recordDuration.endTime }}</span>
</div>
<NPopconfirm trigger="click" @positive-click="() => reloadRecordCheckByGbId({ gbCode })">
<template #trigger>
<NButton ghost size="tiny" type="info">刷新</NButton>
</template>
<template #default>
<span>是否确认刷新?</span>
</template>
</NPopconfirm>
</div>
<div style="position: relative; height: 24px; margin: 2px 0" :style="{ backgroundColor: lostChunks.length > 0 ? themeVars.infoColor : themeVars.successColor }">
<template v-if="!!recordDuration.startTime && !!recordDuration.endTime">
<template v-for="lostChunk in lostChunks" :key="`${lostChunk.startTime}-${lostChunk.endTime}`">
<div
style="position: absolute; height: 100%; cursor: pointer; background-color: #eee"
:style="getLostChunkDOMStyle(lostChunk, recordDuration)"
@mouseenter="(event) => onMouseEnterLostChunk(event, lostChunk)"
@mouseleave="onMouseLeaveLostChunk"
/>
</template>
</template>
<template v-else>
<div style="position: absolute; width: 100%; height: 100%; cursor: pointer; background-color: #eee" />
</template>
</div>
</template>
</template>
<template #action>
<NFlex justify="flex-end">
<NPagination size="small" :page="page" :page-size="pageSize" :page-count="Math.ceil(recordDiags.length / pageSize)" @update:page="(p) => (page = p)">
<template #prefix>
<span>{{ `共 ${recordDiags.length} 个通道` }}</span>
</template>
</NPagination>
</NFlex>
</template>
</NCard>
<NPopover v-if="lostChunkPopoverContext.lostDuration" trigger="manual" :show="lostChunkPopoverContext.show" :x="lostChunkPopoverContext.x" :y="lostChunkPopoverContext.y">
<div>录像缺失</div>
<div>开始时间:{{ dayjs(lostChunkPopoverContext.lostDuration.startTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div>结束时间:{{ dayjs(lostChunkPopoverContext.lostDuration.endTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</NPopover>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { NdmNvrDiagInfo, NdmNvrResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard } from '@/components';
import { isNvrCluster } from '@/helpers';
import { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard } from '@/components';
import destr from 'destr';
import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -47,7 +46,6 @@ const diskArray = computed(() => lastDiagInfo.value?.info?.groupInfoList);
<DeviceCommonCard :common-info="commonInfo" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
<NvrDiskCard :disk-health="diskHealth" :disk-array="diskArray" />
<NvrRecordCard v-if="isNvrCluster(ndmDevice)" :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>

View File

@@ -1,26 +0,0 @@
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');
};

View File

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

View File

@@ -1,71 +0,0 @@
import type { NdmRecordCheck, RecordInfo, RecordItem } from '@/apis';
import dayjs from 'dayjs';
import destr from 'destr';
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),
}));
// 使用 Map 按国标码分组,保留插入顺序
const recordChecksMap = new Map<string, typeof parsedRecordChecks>();
parsedRecordChecks.forEach((recordCheck) => {
if (!recordChecksMap.has(recordCheck.gbCode)) {
recordChecksMap.set(recordCheck.gbCode, []);
}
recordChecksMap.get(recordCheck.gbCode)!.push(recordCheck);
});
// 初始化每个通道的录像诊断数据结构
const recordDiags = Array.from(recordChecksMap.entries())
.map(([gbCode, recordChecks]) => {
return {
gbCode,
channelName: recordChecks.at(-1)?.name ?? '',
records: recordChecks.flatMap((check) => check.diagInfo.recordList),
lostChunks: [] as RecordItem[],
};
})
.sort((diag1, diag2) => {
return diag1.gbCode.localeCompare(diag2.gbCode);
});
// 过滤掉没有录像记录的通道
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,
};
});
};