280 lines
9.9 KiB
Vue
280 lines
9.9 KiB
Vue
<script lang="ts"></script>
|
|
|
|
<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 } from 'naive-ui';
|
|
import { computed, onMounted, ref, toRefs } from 'vue';
|
|
|
|
type NvrRecordDiag = {
|
|
gbCode: string;
|
|
channelName: string;
|
|
recordDuration: RecordItem;
|
|
lostRecordList: RecordItem[];
|
|
};
|
|
|
|
// 过滤出丢失的录像时间段
|
|
const filterLostRecordList = (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 lostRecordList = computed(() => filterLostRecordList(recordCheckList.value));
|
|
|
|
const recordDiagStatistics = computed(() => {
|
|
const channelCount = lostRecordList.value.length;
|
|
const channelWithLossCount = lostRecordList.value.filter((diag) => diag.lostRecordList.length > 0).length;
|
|
const lossCount = lostRecordList.value.reduce((count, diag) => count + diag.lostRecordList.length, 0);
|
|
|
|
const totalLossDuration = lostRecordList.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();
|
|
});
|
|
</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>
|
|
</template>
|
|
<NFlex vertical :size="16">
|
|
<!-- 统计信息 -->
|
|
<NGrid :cols="4" :x-gap="12">
|
|
<NGridItem>
|
|
<NStatistic label="总通道数" :value="recordDiagStatistics.totalChannels" />
|
|
</NGridItem>
|
|
<NGridItem>
|
|
<NStatistic label="有缺失通道" :value="recordDiagStatistics.channelsWithLoss" />
|
|
</NGridItem>
|
|
<NGridItem>
|
|
<NStatistic label="缺失次数" :value="recordDiagStatistics.totalLossCount" />
|
|
</NGridItem>
|
|
<NGridItem>
|
|
<NStatistic label="缺失时长" :value="recordDiagStatistics.totalLossDuration" />
|
|
</NGridItem>
|
|
</NGrid>
|
|
|
|
<!-- 通道录像缺失详情 -->
|
|
<div v-if="lostRecordList.length > 0">
|
|
<NCollapse>
|
|
<NCollapseItem v-for="channel in lostRecordList" :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>
|
|
|
|
<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="(loss, index) in channel.lostRecordList" :key="index" type="error" :time="formatTime(loss.startTime)">
|
|
<template #icon>
|
|
<NIcon>
|
|
<WarningOutline />
|
|
</NIcon>
|
|
</template>
|
|
<template #default>
|
|
<NFlex vertical :size="4">
|
|
<NText> 缺失时段:{{ formatTime(loss.startTime) }} ~ {{ formatTime(loss.endTime) }} </NText>
|
|
<NText depth="3" style="font-size: 12px"> 持续时长:{{ formatDuration(loss.startTime, loss.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>
|
|
</NCollapseItem>
|
|
</NCollapse>
|
|
</div>
|
|
|
|
<!-- 空状态 -->
|
|
<div v-else>
|
|
<NEmpty description="暂无录像诊断数据" style="padding: 40px 0" />
|
|
</div>
|
|
</NFlex>
|
|
</NCard>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
// 自定义样式
|
|
.n-collapse-item {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.n-timeline {
|
|
padding-left: 8px;
|
|
}
|
|
|
|
.n-statistic {
|
|
text-align: center;
|
|
}
|
|
</style>
|