refactor(nvr-record-diag-card): 重构录像诊断卡片组件,优化代码结构和功能
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmNvrResultVO, NdmRecordCheck, RecordInfo, RecordItem } from '@/apis/models';
|
||||
import { getRecordCheckByParentId as getRecordCheckByParentIdApi, reloadAllRecordCheck as reloadAllRecordCheckApi } from '@/apis/requests';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { formatDuration } from '@/utils/format-duration';
|
||||
import { VideocamOutline, TimeOutline, WarningOutline, CheckmarkCircleOutline, RefreshOutline } from '@vicons/ionicons5';
|
||||
import dayjs from 'dayjs';
|
||||
import { destr } from 'destr';
|
||||
import { groupBy } from 'es-toolkit';
|
||||
import { NCard, NFlex, NText, NTag, NTimeline, NTimelineItem, NIcon, NEmpty, NStatistic, NGrid, NGridItem, NCollapse, NCollapseItem, NButton, NPopconfirm, NScrollbar } from 'naive-ui';
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import { downloadByData } from '@/utils/download';
|
||||
import { DownloadOutlined } from '@vicons/antd';
|
||||
import { useStationStore } from '@/stores/station';
|
||||
|
||||
type NvrRecordDiag = {
|
||||
gbCode: string;
|
||||
channelName: string;
|
||||
recordDuration: RecordItem;
|
||||
lostRecordList: RecordItem[];
|
||||
};
|
||||
|
||||
// 过滤出丢失的录像时间段
|
||||
const transformRecordCheck = (rawRecordChecks: NdmRecordCheck[]): NvrRecordDiag[] => {
|
||||
// 1. 解析diagInfo
|
||||
const recordChecks = rawRecordChecks.map((recordCheck) => {
|
||||
return {
|
||||
...recordCheck,
|
||||
diagInfo: destr<RecordInfo>(recordCheck.diagInfo),
|
||||
};
|
||||
});
|
||||
// 2.按国标码分组
|
||||
const recordChecksByGbCode = groupBy(recordChecks, (recordCheck) => recordCheck.gbCode);
|
||||
// 3. 提取分组后的国标码和录像诊断记录
|
||||
const channelGbCodes = Object.keys(recordChecksByGbCode);
|
||||
const groupedRecordChecks = Object.values(recordChecksByGbCode);
|
||||
// 4. 初始化每个通道的诊断数据结构
|
||||
let recordDiagList = channelGbCodes.map((gbCode, index) => ({
|
||||
gbCode,
|
||||
channelName: groupedRecordChecks[index].at(-1)?.name ?? '',
|
||||
records: [] as RecordItem[],
|
||||
lostRecords: [] as RecordItem[],
|
||||
}));
|
||||
// 5. 写入同一gbCode的录像片段
|
||||
groupedRecordChecks.forEach((recordCheckGroup, index) => {
|
||||
recordCheckGroup.forEach((recordCheck) => {
|
||||
recordDiagList[index].records.push(...recordCheck.diagInfo.recordList);
|
||||
});
|
||||
});
|
||||
// 6. 过滤掉没有录像记录的通道
|
||||
recordDiagList = recordDiagList.filter((chunk) => chunk.records.length > 0);
|
||||
// 7. 计算每个通道丢失的录像时间片段
|
||||
recordDiagList.forEach((chunk) => {
|
||||
chunk.records.forEach((record, index, recordList) => {
|
||||
if (recordList[index + 1]) {
|
||||
// 如果下一段录像的开始时间不等于当前录像的结束时间,则判定为丢失
|
||||
if (recordList[index + 1].startTime !== record.endTime) {
|
||||
chunk.lostRecords.push({
|
||||
startTime: record.endTime,
|
||||
endTime: recordList[index + 1].startTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return recordDiagList.map((recordDiag) => ({
|
||||
gbCode: recordDiag.gbCode,
|
||||
channelName: recordDiag.channelName,
|
||||
recordDuration: {
|
||||
startTime: recordDiag.records.at(0)?.startTime ?? '',
|
||||
endTime: recordDiag.records.at(-1)?.endTime ?? '',
|
||||
},
|
||||
lostRecordList: recordDiag.lostRecords,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return dayjs(time).format('MM-DD HH:mm:ss');
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
stationCode: string;
|
||||
ndmNvr: NdmNvrResultVO;
|
||||
}>();
|
||||
|
||||
const { stationCode, ndmNvr } = toRefs(props);
|
||||
|
||||
const recordCheckList = ref<NdmRecordCheck[]>([]);
|
||||
|
||||
const transformedRecordCheck = computed(() => transformRecordCheck(recordCheckList.value));
|
||||
|
||||
const recordDiagStatistics = computed(() => {
|
||||
const channelCount = transformedRecordCheck.value.length;
|
||||
const channelWithLossCount = transformedRecordCheck.value.filter((diag) => diag.lostRecordList.length > 0).length;
|
||||
const lossCount = transformedRecordCheck.value.reduce((count, diag) => count + diag.lostRecordList.length, 0);
|
||||
|
||||
const totalLossDuration = transformedRecordCheck.value.reduce((totalDuration, diag) => {
|
||||
const channelLossDuration = diag.lostRecordList.reduce((duration, loss) => {
|
||||
return duration + dayjs(loss.endTime).diff(dayjs(loss.startTime));
|
||||
}, 0);
|
||||
return totalDuration + channelLossDuration;
|
||||
}, 0);
|
||||
|
||||
const formatTotalLossDuration = (duration: number) => {
|
||||
const dayjsDuration = dayjs.duration(duration);
|
||||
const asHours = dayjsDuration.asHours();
|
||||
const h = Math.floor(asHours);
|
||||
const m = dayjsDuration.minutes();
|
||||
const s = dayjsDuration.seconds();
|
||||
if (asHours >= 1) {
|
||||
return `${h}小时${m}分钟${s}秒`;
|
||||
}
|
||||
return `${m}分钟${s}秒`;
|
||||
};
|
||||
|
||||
return {
|
||||
totalChannels: channelCount,
|
||||
channelsWithLoss: channelWithLossCount,
|
||||
totalLossCount: lossCount,
|
||||
totalLossDuration: formatTotalLossDuration(totalLossDuration),
|
||||
};
|
||||
});
|
||||
|
||||
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const recordCheckList = await getRecordCheckByParentIdApi(stationCode.value, ndmNvr.value, 90);
|
||||
return recordCheckList;
|
||||
},
|
||||
onSuccess: (checkList) => {
|
||||
recordCheckList.value = checkList;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
window.$message.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
await reloadAllRecordCheckApi(stationCode.value, 90);
|
||||
},
|
||||
onSuccess: () => {
|
||||
window.$message.success('正在逐步刷新中,请稍后点击刷新按钮查看');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
window.$message.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getRecordCheckByParentId();
|
||||
});
|
||||
|
||||
const onClickExportRecordCheck = () => {
|
||||
const header = '通道名称,开始时间,结束时间,持续时长\n';
|
||||
const rows = transformedRecordCheck.value
|
||||
.map((channel) => {
|
||||
if (channel.lostRecordList.length === 0) {
|
||||
return `${channel.channelName},,,`;
|
||||
}
|
||||
return channel.lostRecordList
|
||||
.map((loss) => {
|
||||
const duration = formatDuration(loss.startTime, loss.endTime);
|
||||
return `${channel.channelName},${formatTime(loss.startTime)},${formatTime(loss.endTime)},${duration}`;
|
||||
})
|
||||
.join('\n');
|
||||
})
|
||||
.join('\n');
|
||||
const csvContent = header + rows;
|
||||
const stationStore = useStationStore();
|
||||
const stationName = stationStore.stationList.find((station) => station.code === stationCode.value)?.name;
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(csvContent, `${stationName}_录像缺失记录_${time}.csv`, 'text/csv;charset=utf-8', '\ufeff');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard size="small" hoverable>
|
||||
<template #header>
|
||||
<NFlex :align="'center'" :size="24">
|
||||
<div>录像诊断</div>
|
||||
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
|
||||
<template #trigger>
|
||||
<NButton secondary size="small" :loading="reloading">
|
||||
<span>点击更新所有通道录像诊断</span>
|
||||
</NButton>
|
||||
</template>
|
||||
<template #default>
|
||||
<span>确认更新所有通道录像诊断吗?</span>
|
||||
</template>
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<RefreshOutline />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="small" quaternary circle @click="onClickExportRecordCheck">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<DownloadOutlined />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
<NFlex vertical :size="16">
|
||||
<!-- 统计信息 -->
|
||||
<NGrid :cols="2" :x-gap="12">
|
||||
<NGridItem>
|
||||
<NStatistic label="总通道数">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.totalChannels }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="有缺失通道">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.channelsWithLoss }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<!-- <NGridItem>
|
||||
<NStatistic label="缺失次数">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.totalLossCount }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="缺失时长">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.totalLossDuration }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem> -->
|
||||
</NGrid>
|
||||
|
||||
<!-- 通道录像缺失详情 -->
|
||||
<div v-if="transformedRecordCheck.length > 0">
|
||||
<NCollapse accordion>
|
||||
<NCollapseItem v-for="channel in transformedRecordCheck" :key="channel.gbCode" :name="channel.gbCode">
|
||||
<template #header>
|
||||
<NFlex align="center" :size="8">
|
||||
<NIcon size="16" :color="channel.lostRecordList.length > 0 ? '#f5222d' : '#52c41a'">
|
||||
<VideocamOutline />
|
||||
</NIcon>
|
||||
<NText strong>{{ channel.channelName }}</NText>
|
||||
<NTag :type="channel.lostRecordList.length > 0 ? 'error' : 'success'" size="small">{{ channel.lostRecordList.length > 0 ? `${channel.lostRecordList.length}次缺失` : '正常' }}</NTag>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<NFlex align="center" :size="4">
|
||||
<NIcon size="14" color="#666">
|
||||
<TimeOutline />
|
||||
</NIcon>
|
||||
<NText depth="3" style="font-size: 12px">{{ formatTime(channel.recordDuration.startTime) }} ~ {{ formatTime(channel.recordDuration.endTime) }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<NScrollbar style="height: 500px">
|
||||
<div style="padding-left: 24px">
|
||||
<!-- 录像缺失时间轴 -->
|
||||
<div v-if="channel.lostRecordList.length > 0">
|
||||
<NText depth="2" style="margin-bottom: 12px; display: block">录像缺失时间段:</NText>
|
||||
<NTimeline>
|
||||
<NTimelineItem v-for="({ startTime, endTime }, index) in channel.lostRecordList" :key="index" type="error" :time="formatTime(startTime)">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<WarningOutline />
|
||||
</NIcon>
|
||||
</template>
|
||||
<template #default>
|
||||
<NFlex vertical :size="4">
|
||||
<NText>缺失时段:{{ formatTime(startTime) }} ~ {{ formatTime(endTime) }}</NText>
|
||||
<NText depth="3" style="font-size: 12px">持续时长:{{ formatDuration(startTime, endTime) }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NTimelineItem>
|
||||
</NTimeline>
|
||||
</div>
|
||||
|
||||
<!-- 无缺失状态 -->
|
||||
<div v-else>
|
||||
<NFlex align="center" :size="8" style="padding: 16px 0">
|
||||
<NIcon size="16" color="#52c41a">
|
||||
<CheckmarkCircleOutline />
|
||||
</NIcon>
|
||||
<NText type="success">该通道录像完整,无缺失时间段</NText>
|
||||
</NFlex>
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else>
|
||||
<NEmpty description="暂无录像诊断数据" style="padding: 40px 0" />
|
||||
</div>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,133 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import type { Station } from '@/apis/domains';
|
||||
import type { NdmNvrResultVO, NdmRecordCheck, RecordInfo, RecordItem } from '@/apis/models';
|
||||
import { getRecordCheckByParentId as getRecordCheckByParentIdApi, reloadAllRecordCheck as reloadAllRecordCheckApi } from '@/apis/requests';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { formatDuration } from '@/utils/format-duration';
|
||||
import { VideocamOutline, TimeOutline, WarningOutline, CheckmarkCircleOutline, RefreshOutline } from '@vicons/ionicons5';
|
||||
import dayjs from 'dayjs';
|
||||
import { destr } from 'destr';
|
||||
import { groupBy } from 'es-toolkit';
|
||||
import { NCard, NFlex, NText, NTag, NTimeline, NTimelineItem, NIcon, NEmpty, NStatistic, NGrid, NGridItem, NCollapse, NCollapseItem, NButton, NPopconfirm, NScrollbar } from 'naive-ui';
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
import { downloadByData } from '@/utils/download';
|
||||
import { DownloadOutlined } from '@vicons/antd';
|
||||
import {
|
||||
getChannelList,
|
||||
getRecordCheckByParentId as getRecordCheckByParentIdApi,
|
||||
reloadAllRecordCheck as reloadAllRecordCheckApi,
|
||||
reloadRecordCheckByGbId as reloadRecordCheckByGbIdApi,
|
||||
} from '@/apis/requests';
|
||||
import { useStationStore } from '@/stores/station';
|
||||
import { downloadByData } from '@/utils/download';
|
||||
import { formatDuration } from '@/utils/format-duration';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { DownloadOutlined } from '@vicons/antd';
|
||||
import { RefreshOutline } from '@vicons/ionicons5';
|
||||
import dayjs from 'dayjs';
|
||||
import destr from 'destr';
|
||||
import { groupBy } from 'es-toolkit';
|
||||
import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
|
||||
type NvrRecordDiag = {
|
||||
gbCode: string;
|
||||
channelName: string;
|
||||
recordDuration: RecordItem;
|
||||
lostRecordList: RecordItem[];
|
||||
lostChunks: RecordItem[];
|
||||
};
|
||||
|
||||
// 过滤出丢失的录像时间段
|
||||
const transformRecordCheck = (rawRecordChecks: NdmRecordCheck[]): NvrRecordDiag[] => {
|
||||
// 1. 解析diagInfo
|
||||
const recordChecks = rawRecordChecks.map((recordCheck) => {
|
||||
return {
|
||||
...recordCheck,
|
||||
diagInfo: destr<RecordInfo>(recordCheck.diagInfo),
|
||||
};
|
||||
});
|
||||
// 2.按国标码分组
|
||||
const recordChecksByGbCode = groupBy(recordChecks, (recordCheck) => recordCheck.gbCode);
|
||||
// 3. 提取分组后的国标码和录像诊断记录
|
||||
const channelGbCodes = Object.keys(recordChecksByGbCode);
|
||||
const groupedRecordChecks = Object.values(recordChecksByGbCode);
|
||||
// 4. 初始化每个通道的诊断数据结构
|
||||
let recordDiagList = channelGbCodes.map((gbCode, index) => ({
|
||||
gbCode,
|
||||
channelName: groupedRecordChecks[index].at(-1)?.name ?? '',
|
||||
records: [] as RecordItem[],
|
||||
lostRecords: [] as RecordItem[],
|
||||
// 解析出丢失的录像时间段
|
||||
const transformRecordChecks = (rawRecordChecks: NdmRecordCheck[]): NvrRecordDiag[] => {
|
||||
// 解析diagInfo
|
||||
const parsedRecordChecks = rawRecordChecks.map((recordCheck) => ({
|
||||
...recordCheck,
|
||||
diagInfo: destr<RecordInfo>(recordCheck.diagInfo),
|
||||
}));
|
||||
// 5. 写入同一gbCode的录像片段
|
||||
groupedRecordChecks.forEach((recordCheckGroup, index) => {
|
||||
recordCheckGroup.forEach((recordCheck) => {
|
||||
recordDiagList[index].records.push(...recordCheck.diagInfo.recordList);
|
||||
// 按国标码分组
|
||||
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);
|
||||
});
|
||||
});
|
||||
// 6. 过滤掉没有录像记录的通道
|
||||
recordDiagList = recordDiagList.filter((chunk) => chunk.records.length > 0);
|
||||
// 7. 计算每个通道丢失的录像时间片段
|
||||
recordDiagList.forEach((chunk) => {
|
||||
chunk.records.forEach((record, index, recordList) => {
|
||||
if (recordList[index + 1]) {
|
||||
// 过滤掉没有录像记录的通道
|
||||
const filteredRecordDiags = recordDiags.filter((recordDiag) => recordDiag.records.length > 0);
|
||||
// 计算每个通道丢失的录像时间片段
|
||||
filteredRecordDiags.forEach((recordDiag) => {
|
||||
recordDiag.records.forEach((record, index, records) => {
|
||||
if (!!records.at(index + 1)) {
|
||||
// 如果下一段录像的开始时间不等于当前录像的结束时间,则判定为丢失
|
||||
if (recordList[index + 1].startTime !== record.endTime) {
|
||||
chunk.lostRecords.push({
|
||||
startTime: record.endTime,
|
||||
endTime: recordList[index + 1].startTime,
|
||||
const nextStartTime = records[index + 1].startTime;
|
||||
const currEndTime = record.endTime;
|
||||
if (nextStartTime !== currEndTime) {
|
||||
recordDiag.lostChunks.push({
|
||||
startTime: currEndTime,
|
||||
endTime: nextStartTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return recordDiagList.map((recordDiag) => ({
|
||||
gbCode: recordDiag.gbCode,
|
||||
channelName: recordDiag.channelName,
|
||||
recordDuration: {
|
||||
startTime: recordDiag.records.at(0)?.startTime ?? '',
|
||||
endTime: recordDiag.records.at(-1)?.endTime ?? '',
|
||||
},
|
||||
lostRecordList: recordDiag.lostRecords,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return dayjs(time).format('MM-DD HH:mm:ss');
|
||||
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,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
stationCode: string;
|
||||
stationCode: Station['code'];
|
||||
ndmNvr: NdmNvrResultVO;
|
||||
}>();
|
||||
|
||||
const { stationCode, ndmNvr } = toRefs(props);
|
||||
|
||||
const recordCheckList = ref<NdmRecordCheck[]>([]);
|
||||
const recordChecks = ref<NdmRecordCheck[]>([]);
|
||||
|
||||
const transformedRecordCheck = computed(() => transformRecordCheck(recordCheckList.value));
|
||||
const lossInput = ref<number>(0);
|
||||
|
||||
const recordDiagStatistics = computed(() => {
|
||||
const channelCount = transformedRecordCheck.value.length;
|
||||
const channelWithLossCount = transformedRecordCheck.value.filter((diag) => diag.lostRecordList.length > 0).length;
|
||||
const lossCount = transformedRecordCheck.value.reduce((count, diag) => count + diag.lostRecordList.length, 0);
|
||||
|
||||
const totalLossDuration = transformedRecordCheck.value.reduce((totalDuration, diag) => {
|
||||
const channelLossDuration = diag.lostRecordList.reduce((duration, loss) => {
|
||||
return duration + dayjs(loss.endTime).diff(dayjs(loss.startTime));
|
||||
}, 0);
|
||||
return totalDuration + channelLossDuration;
|
||||
}, 0);
|
||||
|
||||
const formatTotalLossDuration = (duration: number) => {
|
||||
const dayjsDuration = dayjs.duration(duration);
|
||||
const asHours = dayjsDuration.asHours();
|
||||
const h = Math.floor(asHours);
|
||||
const m = dayjsDuration.minutes();
|
||||
const s = dayjsDuration.seconds();
|
||||
if (asHours >= 1) {
|
||||
return `${h}小时${m}分钟${s}秒`;
|
||||
const recordDiags = computed(() => {
|
||||
return transformRecordChecks(recordChecks.value).filter((recordDiag) => {
|
||||
if (lossInput.value === 0) {
|
||||
return true;
|
||||
} else if (lossInput.value === 1) {
|
||||
return recordDiag.lostChunks.length > 0;
|
||||
} else if (lossInput.value === 2) {
|
||||
return recordDiag.lostChunks.length === 0;
|
||||
}
|
||||
return `${m}分钟${s}秒`;
|
||||
};
|
||||
|
||||
return {
|
||||
totalChannels: channelCount,
|
||||
channelsWithLoss: channelWithLossCount,
|
||||
totalLossCount: lossCount,
|
||||
totalLossDuration: formatTotalLossDuration(totalLossDuration),
|
||||
};
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const recordCheckList = await getRecordCheckByParentIdApi(stationCode.value, ndmNvr.value, 90);
|
||||
return recordCheckList;
|
||||
const checks = await getRecordCheckByParentIdApi(stationCode.value, ndmNvr.value, 90);
|
||||
return checks;
|
||||
},
|
||||
onSuccess: (checkList) => {
|
||||
recordCheckList.value = checkList;
|
||||
onSuccess: (checks) => {
|
||||
recordChecks.value = checks;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
@@ -148,21 +134,19 @@ const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getRecordCheckByParentId();
|
||||
});
|
||||
|
||||
const onClickExportRecordCheck = () => {
|
||||
const onExportRecordCheck = () => {
|
||||
const header = '通道名称,开始时间,结束时间,持续时长\n';
|
||||
const rows = transformedRecordCheck.value
|
||||
const rows = recordDiags.value
|
||||
.map((channel) => {
|
||||
if (channel.lostRecordList.length === 0) {
|
||||
if (channel.lostChunks.length === 0) {
|
||||
return `${channel.channelName},,,`;
|
||||
}
|
||||
return channel.lostRecordList
|
||||
return channel.lostChunks
|
||||
.map((loss) => {
|
||||
const duration = formatDuration(loss.startTime, loss.endTime);
|
||||
return `${channel.channelName},${formatTime(loss.startTime)},${formatTime(loss.endTime)},${duration}`;
|
||||
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');
|
||||
})
|
||||
@@ -173,18 +157,66 @@ const onClickExportRecordCheck = () => {
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(csvContent, `${stationName}_录像缺失记录_${time}.csv`, 'text/csv;charset=utf-8', '\ufeff');
|
||||
};
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
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) => {
|
||||
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 { mutate: reloadRecordCheckByGbId } = useMutation({
|
||||
mutationFn: async (params: { gbCode: string }) => {
|
||||
const channelList = await getChannelList(stationCode.value, ndmNvr.value);
|
||||
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 reloadRecordCheckByGbIdApi(stationCode.value, channel, 90);
|
||||
window.$message.destroyAll();
|
||||
if (isSuccess) {
|
||||
window.$message.success('刷新成功');
|
||||
} else {
|
||||
window.$message.error('刷新失败');
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
getRecordCheckByParentId();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
window.$message.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getRecordCheckByParentId();
|
||||
});
|
||||
|
||||
const themeVars = useThemeVars();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard size="small" hoverable>
|
||||
<template #header>
|
||||
<NFlex :align="'center'" :size="24">
|
||||
<NFlex align="center" :size="24">
|
||||
<div>录像诊断</div>
|
||||
<NPopconfirm @positive-click="() => reloadAllRecordCheck()">
|
||||
<template #trigger>
|
||||
<NButton secondary size="small" :loading="reloading">
|
||||
<span>点击更新所有通道录像诊断</span>
|
||||
</NButton>
|
||||
<NButton secondary size="small" :loading="reloading">更新所有通道录像诊断</NButton>
|
||||
</template>
|
||||
<template #default>
|
||||
<span>确认更新所有通道录像诊断吗?</span>
|
||||
@@ -193,119 +225,81 @@ const onClickExportRecordCheck = () => {
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<RefreshOutline />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="small" quaternary circle @click="onClickExportRecordCheck">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<DownloadOutlined />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
<NFlex>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<RefreshOutline />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
<template #default>
|
||||
<span>刷新数据</span>
|
||||
</template>
|
||||
</NTooltip>
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NButton size="small" quaternary circle @click="onExportRecordCheck">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<DownloadOutlined />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
<template #default>
|
||||
<span>导出录像诊断</span>
|
||||
</template>
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NFlex vertical :size="16">
|
||||
<!-- 统计信息 -->
|
||||
<NGrid :cols="2" :x-gap="12">
|
||||
<NGridItem>
|
||||
<NStatistic label="总通道数">
|
||||
<template #default>
|
||||
<NFlex justify="flex-end" style="margin-bottom: 6px">
|
||||
<NRadioGroup size="small" v-model:value="lossInput">
|
||||
<NRadioButton label="全部" :value="0" />
|
||||
<NRadioButton label="有缺失" :value="1" />
|
||||
<NRadioButton label="无缺失" :value="2" />
|
||||
</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 style="font-size: large">{{ recordDiagStatistics.totalChannels }}</span>
|
||||
<span>是否确认刷新?</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="有缺失通道">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.channelsWithLoss }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<!-- <NGridItem>
|
||||
<NStatistic label="缺失次数">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.totalLossCount }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NStatistic label="缺失时长">
|
||||
<template #default>
|
||||
<span style="font-size: large">{{ recordDiagStatistics.totalLossDuration }}</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGridItem> -->
|
||||
</NGrid>
|
||||
|
||||
<!-- 通道录像缺失详情 -->
|
||||
<div v-if="transformedRecordCheck.length > 0">
|
||||
<NCollapse accordion>
|
||||
<NCollapseItem v-for="channel in transformedRecordCheck" :key="channel.gbCode" :name="channel.gbCode">
|
||||
<template #header>
|
||||
<NFlex align="center" :size="8">
|
||||
<NIcon size="16" :color="channel.lostRecordList.length > 0 ? '#f5222d' : '#52c41a'">
|
||||
<VideocamOutline />
|
||||
</NIcon>
|
||||
<NText strong>{{ channel.channelName }}</NText>
|
||||
<NTag :type="channel.lostRecordList.length > 0 ? 'error' : 'success'" size="small">{{ channel.lostRecordList.length > 0 ? `${channel.lostRecordList.length}次缺失` : '正常' }}</NTag>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<template #header-extra>
|
||||
<NFlex align="center" :size="4">
|
||||
<NIcon size="14" color="#666">
|
||||
<TimeOutline />
|
||||
</NIcon>
|
||||
<NText depth="3" style="font-size: 12px">{{ formatTime(channel.recordDuration.startTime) }} ~ {{ formatTime(channel.recordDuration.endTime) }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<NScrollbar style="height: 500px">
|
||||
<div style="padding-left: 24px">
|
||||
<!-- 录像缺失时间轴 -->
|
||||
<div v-if="channel.lostRecordList.length > 0">
|
||||
<NText depth="2" style="margin-bottom: 12px; display: block">录像缺失时间段:</NText>
|
||||
<NTimeline>
|
||||
<NTimelineItem v-for="({ startTime, endTime }, index) in channel.lostRecordList" :key="index" type="error" :time="formatTime(startTime)">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<WarningOutline />
|
||||
</NIcon>
|
||||
</template>
|
||||
<template #default>
|
||||
<NFlex vertical :size="4">
|
||||
<NText>缺失时段:{{ formatTime(startTime) }} ~ {{ formatTime(endTime) }}</NText>
|
||||
<NText depth="3" style="font-size: 12px">持续时长:{{ formatDuration(startTime, endTime) }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NTimelineItem>
|
||||
</NTimeline>
|
||||
</div>
|
||||
|
||||
<!-- 无缺失状态 -->
|
||||
<div v-else>
|
||||
<NFlex align="center" :size="8" style="padding: 16px 0">
|
||||
<NIcon size="16" color="#52c41a">
|
||||
<CheckmarkCircleOutline />
|
||||
</NIcon>
|
||||
<NText type="success">该通道录像完整,无缺失时间段</NText>
|
||||
</NFlex>
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else>
|
||||
<NEmpty description="暂无录像诊断数据" style="padding: 40px 0" />
|
||||
</div>
|
||||
</NFlex>
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
<div style="position: relative; height: 24px; margin: 2px 0" :style="{ backgroundColor: lostChunks.length > 0 ? themeVars.infoColor : themeVars.successColor }">
|
||||
<template v-for="{ startTime, endTime } in lostChunks" :key="`${startTime}-${endTime}`">
|
||||
<NPopover trigger="hover">
|
||||
<template #trigger>
|
||||
<div style="position: absolute; height: 100%; cursor: pointer; background-color: #eee" :style="getLostChunkDOMStyle({ startTime, endTime }, recordDuration)" />
|
||||
</template>
|
||||
<template #default>
|
||||
<div>开始时间:{{ dayjs(startTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||
<div>结束时间:{{ dayjs(endTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||
</template>
|
||||
</NPopover>
|
||||
</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)" />
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user