Files
ndm-web-client/src/components/device-page/device-card/current-diag-card/nvr-record-diag-card.old.vue

313 lines
12 KiB
Vue

<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>