Compare commits

..

8 Commits

20 changed files with 202 additions and 172 deletions

View File

@@ -48,3 +48,13 @@ export const reloadAllRecordCheckApi = async (dayOffset: number, options?: { sta
if (!data) throw new Error(`${data}`); if (!data) throw new Error(`${data}`);
return data; return data;
}; };
export const batchExportRecordCheckApi = async (params: { checkDuration: number; gapSeconds: number; stationCode: Station['code'][] }, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const { checkDuration, gapSeconds, stationCode } = params;
const client = userClient;
const endpoint = `/api/ndm/ndmRecordCheck/batchExportByTemplate`;
const resp = await client.post<Blob>(endpoint, { checkDuration, gapSeconds, stationCode }, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -1,29 +1,58 @@
<script setup lang="ts"> <script setup lang="ts">
import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type NdmRecordCheck, type RecordItem, type Station } from '@/apis'; import { getChannelListApi, getRecordCheckApi, reloadAllRecordCheckApi, reloadRecordCheckApi, type NdmNvrResultVO, type RecordItem, type Station } from '@/apis';
import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers'; import { exportRecordDiagCsv, transformRecordChecks } from '@/helpers';
import { useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils'; import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { isCancel } from 'axios'; import { isCancel } from 'axios';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next'; import { DownloadIcon, RotateCwIcon } from 'lucide-vue-next';
import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui'; import { NButton, NCard, NFlex, NIcon, NPagination, NPopconfirm, NPopover, NRadioButton, NRadioGroup, NTooltip, useThemeVars } from 'naive-ui';
import { computed, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'; import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue';
const props = defineProps<{ const props = defineProps<{
ndmDevice: NdmNvrResultVO; ndmDevice: NdmNvrResultVO;
station: Station; station: Station;
}>(); }>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const themeVars = useThemeVars(); const themeVars = useThemeVars();
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props); const { ndmDevice, station } = toRefs(props);
const recordChecks = ref<NdmRecordCheck[]>([]);
const lossInput = ref<number>(0); const lossInput = ref<number>(0);
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;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [NVR_RECORD_CHECK_KEY] });
}
});
const recordDiags = computed(() => { const recordDiags = computed(() => {
return transformRecordChecks(recordChecks.value).filter((recordDiag) => { return transformRecordChecks(recordChecks.value ?? []).filter((recordDiag) => {
if (lossInput.value === 0) { if (lossInput.value === 0) {
return true; return true;
} else if (lossInput.value === 1) { } else if (lossInput.value === 1) {
@@ -35,26 +64,6 @@ const recordDiags = computed(() => {
}); });
}); });
const abortController = ref<AbortController>(new AbortController());
const { mutate: getRecordCheckByParentId, isPending: loading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const checks = await getRecordCheckApi(ndmDevice.value, 90, [], { stationCode: station.value.code, signal: abortController.value.signal });
return checks;
},
onSuccess: (checks) => {
recordChecks.value = checks;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({ const { mutate: reloadAllRecordCheck, isPending: reloading } = useMutation({
mutationFn: async () => { mutationFn: async () => {
abortController.value.abort(); abortController.value.abort();
@@ -114,7 +123,7 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
} }
}, },
onSuccess: () => { onSuccess: () => {
getRecordCheckByParentId(); refetchRecordChecks();
}, },
onError: (error) => { onError: (error) => {
if (isCancel(error)) return; if (isCancel(error)) return;
@@ -124,20 +133,6 @@ const { mutate: reloadRecordCheckByGbId } = useMutation({
}, },
}); });
onMounted(() => {
getRecordCheckByParentId();
});
watch(
() => ndmDevice.value.id,
(devieDbId) => {
if (devieDbId) {
recordChecks.value = [];
getRecordCheckByParentId();
}
},
);
onBeforeUnmount(() => { onBeforeUnmount(() => {
abortController.value.abort(); abortController.value.abort();
}); });
@@ -162,7 +157,7 @@ onBeforeUnmount(() => {
<NFlex> <NFlex>
<NTooltip trigger="hover"> <NTooltip trigger="hover">
<template #trigger> <template #trigger>
<NButton size="small" quaternary circle :loading="loading" @click="() => getRecordCheckByParentId()"> <NButton size="small" quaternary circle :loading="loading" @click="() => refetchRecordChecks()">
<template #icon> <template #icon>
<NIcon :component="RotateCwIcon" /> <NIcon :component="RotateCwIcon" />
</template> </template>

View File

@@ -13,7 +13,9 @@ import {
type Station, type Station,
} from '@/apis'; } from '@/apis';
import { SecurityBoxCircuitLinkModal } from '@/components'; import { SecurityBoxCircuitLinkModal } from '@/components';
import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants'; import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useDeviceStore, useSettingStore } from '@/stores'; import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils'; import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
@@ -40,6 +42,8 @@ const { lineDevices } = storeToRefs(deviceStore);
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore); const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station, circuits } = toRefs(props); const { ndmDevice, station, circuits } = toRefs(props);
const showCard = computed(() => !!circuits.value && circuits.value.length > 0); const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
@@ -223,6 +227,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onContextmenu = (payload: PointerEvent, circuitIndex: number) => { const onContextmenu = (payload: PointerEvent, circuitIndex: number) => {
payload.stopPropagation(); payload.stopPropagation();
payload.preventDefault(); payload.preventDefault();
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const { clientX, clientY } = payload; const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, circuitIndex }; contextmenu.value = { x: clientX, y: clientY, circuitIndex };
showContextmenu.value = true; showContextmenu.value = true;

View File

@@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis'; import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis';
import { SwitchPortLinkModal } from '@/components'; import { SwitchPortLinkModal } from '@/components';
import { usePermission } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants'; import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { getPortStatusValue, transformPortSpeed } from '@/helpers'; import { getPortStatusValue, transformPortSpeed } from '@/helpers';
import { useDeviceStore, useSettingStore } from '@/stores'; import { useDeviceStore, useSettingStore } from '@/stores';
import { parseErrorFeedback } from '@/utils'; import { parseErrorFeedback } from '@/utils';
@@ -27,6 +29,8 @@ const { lineDevices } = storeToRefs(deviceStore);
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const { useLocalDB } = storeToRefs(settingStore); const { useLocalDB } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station, ports } = toRefs(props); const { ndmDevice, station, ports } = toRefs(props);
const showCard = computed(() => !!ports.value); const showCard = computed(() => !!ports.value);
@@ -172,6 +176,7 @@ const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => { const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => {
payload.stopPropagation(); payload.stopPropagation();
payload.preventDefault(); payload.preventDefault();
if (!hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const { clientX, clientY } = payload; const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, port }; contextmenu.value = { x: clientX, y: clientY, port };
showContextmenu.value = true; showContextmenu.value = true;

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -56,7 +56,7 @@ watch(activeRequests, (active) => {
<span>服务状态</span> <span>服务状态</span>
</template> </template>
<template #default> <template #default>
<template v-if="activeRequests"> <template v-if="!activeRequests">
<span>-</span> <span>-</span>
</template> </template>
<template v-else> <template v-else>

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -70,7 +70,7 @@ const streamPushStat = computed(() => {
<span>推流统计</span> <span>推流统计</span>
</template> </template>
<template #default> <template #default>
<template v-if="activeRequests"> <template v-if="!activeRequests">
<span>-</span> <span>-</span>
</template> </template>
<template v-else> <template v-else>

View File

@@ -35,8 +35,8 @@ const activeTabName = ref('当前诊断');
const onTabChange = (name: string) => { const onTabChange = (name: string) => {
activeTabName.value = name; activeTabName.value = name;
}; };
watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => { watch([ndmDevice, showDeviceRawData], ([newDevice, showRaw], [oldDevice]) => {
if (newDevice.id !== oldDevice.id || !enabled) { if (newDevice.id !== oldDevice.id || (!showRaw && activeTabName.value === '原始数据')) {
activeTabName.value = '当前诊断'; activeTabName.value = '当前诊断';
} }
}); });

View File

@@ -4,7 +4,7 @@ import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/compos
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums'; import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
import { isNvrCluster } from '@/helpers'; import { isNvrCluster } from '@/helpers';
import { useDeviceStore, usePermissionStore } from '@/stores'; import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchImmediate } from '@vueuse/core'; import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr'; import destr from 'destr';
import { isFunction } from 'es-toolkit'; import { isFunction } from 'es-toolkit';
import { import {
@@ -27,7 +27,7 @@ import {
type TreeProps, type TreeProps,
} from 'naive-ui'; } from 'naive-ui';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue'; import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{ const props = defineProps<{
/** /**
@@ -67,15 +67,15 @@ const {
selectedStationCode, selectedStationCode,
selectedDeviceType, selectedDeviceType,
selectedDevice, selectedDevice,
syncFromRoute,
syncToRoute,
selectDevice, selectDevice,
// 设备管理 // 设备管理
exportDevice, exportDevice,
exportDeviceTemplate, exportDeviceTemplate,
importDevice, importDevice,
deleteDevice, deleteDevice,
} = useDeviceTree({ } = useDeviceTree();
syncRoute: computed(() => !!syncRoute.value),
});
// 将 `selectDevice` 函数暴露给父组件 // 将 `selectDevice` 函数暴露给父组件
emit('exposeSelectDeviceFn', selectDevice); emit('exposeSelectDeviceFn', selectDevice);
@@ -484,11 +484,38 @@ const onLocateDeviceTree = async () => {
animated.value = true; animated.value = true;
}; };
// 渲染全线设备树时,当选择的设备发生变化,则定位设备树
// 当选择的设备发生变化时,定位设备树,并同步选中状态到路由参数
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动 // 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
watch(selectedDevice, async () => { watch(selectedDevice, async (newDevice, oldDevice) => {
if (!!station.value) return; if (!!station.value) return;
await onLocateDeviceTree(); if (newDevice?.id === oldDevice?.id) return;
// console.log('selectedDevice changed');
onLocateDeviceTree();
syncToRoute();
});
// 当全线设备发生变化时,从路由参数同步选中状态
// 但lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
if (syncRoute.value) {
// console.log('lineDevices changed');
syncFromRoute(newLineDevices);
}
},
{
debounce: 500,
deep: true,
},
);
onMounted(() => {
if (syncRoute.value) {
syncFromRoute(lineDevices.value);
}
}); });
</script> </script>

View File

@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { getRecordCheckApi, type NdmNvrResultVO, type Station } from '@/apis'; import { batchExportRecordCheckApi, pageDefParameterApi, type Station } from '@/apis';
import { exportRecordDiagCsv, isNvrCluster, transformRecordChecks } from '@/helpers'; import { downloadByData, parseErrorFeedback } from '@/utils';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios'; import { isCancel } from 'axios';
import { NButton, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui'; import dayjs from 'dayjs';
import { storeToRefs } from 'pinia'; import { NButton, NFlex, NGrid, NGridItem, NModal, NScrollbar, NSpin } from 'naive-ui';
import { computed, ref, toRefs } from 'vue'; import { ref, toRefs } from 'vue';
const props = defineProps<{ const props = defineProps<{
stations: Station[]; stations: Station[];
@@ -19,50 +17,66 @@ const emit = defineEmits<{
const show = defineModel<boolean>('show'); const show = defineModel<boolean>('show');
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const { stations } = toRefs(props); const { stations } = toRefs(props);
const nvrClusterRecord = computed(() => {
const clusterMap: Record<Station['code'], { stationName: Station['name']; clusters: NdmNvrResultVO[] }> = {};
stations.value.forEach((station) => {
clusterMap[station.code] = {
stationName: station.name,
clusters: [],
};
const stationDevices = lineDevices.value[station.code];
const nvrs = stationDevices?.['ndmNvr'] ?? [];
nvrs.forEach((nvr) => {
if (isNvrCluster(nvr)) {
clusterMap[station.code]?.clusters?.push(nvr);
}
});
});
return clusterMap;
});
const abortController = ref<AbortController>(new AbortController()); const abortController = ref<AbortController>(new AbortController());
const { mutate: exportRecordDiags, isPending: exporting } = useMutation({ const { mutate: batchExportRecordCheck, isPending: batchExporting } = useMutation({
mutationFn: async (params: { clusters: NdmNvrResultVO[]; stationCode: Station['code'] }) => { mutationFn: async (params: { stations: Station[] }) => {
const { clusters, stationCode } = params; const timer = setTimeout(() => {
if (clusters.length === 0) { if (!batchExporting.value) return;
const stationName = nvrClusterRecord.value[stationCode]?.stationName ?? ''; window.$message.info('导出耗时较长,请耐心等待...', { duration: 0 });
window.$message.info(`${stationName} 没有录像诊断数据`); }, 3000);
return;
try {
abortController.value.abort();
abortController.value = new AbortController();
const { records = [] } = await pageDefParameterApi(
{
model: {
key: 'NVR_GAP_SECONDS',
},
extra: {},
current: 1,
size: 1,
sort: 'id',
order: 'descending',
},
{
signal: abortController.value.signal,
},
);
const gapSeconds = parseInt(records.at(0)?.value ?? '5');
abortController.value.abort();
abortController.value = new AbortController();
const data = await batchExportRecordCheckApi(
{
checkDuration: 90,
gapSeconds,
stationCode: params.stations.map((station) => station.code),
},
{
signal: abortController.value.signal,
},
);
return data;
} finally {
window.$message.destroyAll();
clearTimeout(timer);
} }
const cluster = clusters.at(0);
if (!cluster) return;
abortController.value.abort();
abortController.value = new AbortController();
const checks = await getRecordCheckApi(cluster, 90, [], { stationCode: stationCode, signal: abortController.value.signal });
return checks;
}, },
onSuccess: (checks, { stationCode }) => { onSuccess: (data, { stations }) => {
if (!checks || checks.length === 0) return; const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const recordDiags = transformRecordChecks(checks); let stationName = '';
exportRecordDiagCsv(recordDiags, nvrClusterRecord.value[stationCode]?.stationName ?? ''); if (stations.length === 1) {
const name = stations.at(0)?.name;
if (!!name) {
stationName = `${name}_`;
}
}
downloadByData(data, `${stationName}录像缺失记录_${time}.xlsx`);
}, },
onError: (error) => { onError: (error) => {
if (isCancel(error)) return; if (isCancel(error)) return;
@@ -73,6 +87,7 @@ const { mutate: exportRecordDiags, isPending: exporting } = useMutation({
}); });
const onAfterLeave = () => { const onAfterLeave = () => {
abortController.value.abort();
emit('afterLeave'); emit('afterLeave');
}; };
</script> </script>
@@ -81,17 +96,22 @@ const onAfterLeave = () => {
<NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px"> <NModal v-model:show="show" preset="card" title="导出录像诊断" @after-leave="onAfterLeave" style="width: 800px">
<template #default> <template #default>
<NScrollbar style="height: 300px"> <NScrollbar style="height: 300px">
<NSpin size="small" :show="exporting"> <NSpin size="small" :show="batchExporting">
<NGrid :cols="6"> <NGrid :cols="6">
<template v-for="({ stationName, clusters }, code) in nvrClusterRecord" :key="code"> <template v-for="station in stations" :key="station.code">
<NGridItem> <NGridItem>
<NButton text type="info" style="height: 30px" @click="() => exportRecordDiags({ clusters, stationCode: code })">{{ stationName }}</NButton> <NButton text type="info" style="height: 30px" @click="() => batchExportRecordCheck({ stations: [station] })">{{ station.name }}</NButton>
</NGridItem> </NGridItem>
</template> </template>
</NGrid> </NGrid>
</NSpin> </NSpin>
</NScrollbar> </NScrollbar>
</template> </template>
<template #action>
<NFlex justify="flex-end" align="center">
<NButton secondary :loading="batchExporting" @click="() => batchExportRecordCheck({ stations })">导出全部</NButton>
</NFlex>
</template>
</NModal> </NModal>
</template> </template>

View File

@@ -1,39 +1,33 @@
import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis'; import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums'; import { tryGetDeviceType, type DeviceType } from '@/enums';
import { useDeviceStore } from '@/stores'; import { ref } from 'vue';
import { watchDebounced } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { onMounted, ref, toValue, watch, type MaybeRefOrGetter } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => { export const useDeviceSelection = () => {
const { syncRoute } = options ?? {};
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const selectedStationCode = ref<Station['code']>(); const selectedStationCode = ref<Station['code']>();
const selectedDeviceType = ref<DeviceType>(); const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>(); const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => { // 从路由参数同步选中的车站、设备类型以及设备
const { stationCode, deviceType, deviceDbId } = route.query; const syncFromRoute = (lineDevices: LineDevices) => {
if (stationCode) { // console.log('sync from route');
selectedStationCode.value = stationCode as Station['code']; const { stationCode: routeStationCode, deviceType: routeDeviceType, deviceDbId: routeDeviceDbId } = route.query;
if (routeStationCode) {
selectedStationCode.value = routeStationCode as Station['code'];
} }
if (deviceType) { if (routeDeviceType) {
selectedDeviceType.value = deviceType as DeviceType; selectedDeviceType.value = routeDeviceType as DeviceType;
} }
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) { if (routeDeviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string; const selectedDeviceDbId = routeDeviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value]; const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) { if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value]; const classifiedDevices = stationDevices[selectedDeviceType.value];
if (devices) { if (classifiedDevices) {
const device = devices.find((device) => device.id === selectedDeviceDbId); const device = classifiedDevices.find((device) => device.id === selectedDeviceDbId);
if (device) { if (device) {
selectedDevice.value = device; selectedDevice.value = device;
} }
@@ -51,7 +45,9 @@ export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<bool
} }
}; };
// 将选中的车站、设备类型以及设备ID同步到路由参数
const syncToRoute = () => { const syncToRoute = () => {
// console.log('sync to route');
const query = { ...route.query }; const query = { ...route.query };
// 当选中的设备发生变化时删除fromPage参数 // 当选中的设备发生变化时删除fromPage参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) { if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
@@ -69,39 +65,13 @@ export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<bool
router.replace({ query }); router.replace({ query });
}; };
watch(selectedDevice, () => {
if (toValue(syncRoute)) {
syncToRoute();
}
});
// lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
if (toValue(syncRoute)) {
initFromRoute(newLineDevices);
}
},
{
debounce: 500,
deep: true,
},
);
onMounted(() => {
if (toValue(syncRoute)) {
initFromRoute(lineDevices.value);
}
});
return { return {
selectedStationCode, selectedStationCode,
selectedDeviceType, selectedDeviceType,
selectedDevice, selectedDevice,
initFromRoute, syncFromRoute,
syncToRoute,
selectDevice, selectDevice,
}; };
}; };

View File

@@ -1,11 +1,8 @@
import type { MaybeRefOrGetter } from 'vue';
import { useDeviceManagement } from './use-device-management'; import { useDeviceManagement } from './use-device-management';
import { useDeviceSelection } from './use-device-selection'; import { useDeviceSelection } from './use-device-selection';
export const useDeviceTree = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => { export const useDeviceTree = () => {
const { syncRoute } = options ?? {}; const deviceSelection = useDeviceSelection();
const deviceSelection = useDeviceSelection({ syncRoute });
const deviceManagement = useDeviceManagement(); const deviceManagement = useDeviceManagement();
return { return {

View File

@@ -81,7 +81,7 @@ const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
window.$notification.info({ window.$notification.info({
title: '摄像机同步结果', title: '摄像机同步结果',
content: notices.join(''), content: notices.join(''),
duration: 3000, duration: 10000,
}); });
if (successRequests.length > 0) { if (successRequests.length > 0) {
// 摄像机同步后,需要重新查询一次设备 // 摄像机同步后,需要重新查询一次设备
@@ -123,7 +123,7 @@ const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
window.$notification.info({ window.$notification.info({
title: '录像机通道同步结果', title: '录像机通道同步结果',
content: notices.join(''), content: notices.join(''),
duration: 3000, duration: 10000,
}); });
cancelAction(); cancelAction();
}, },

View File

@@ -47,6 +47,7 @@ export const useSettingStore = defineStore(
activeRequests.value = true; activeRequests.value = true;
subscribeMessages.value = false; subscribeMessages.value = false;
mockUser.value = false; mockUser.value = false;
useLocalDB.value = false;
} }
}); });