5 Commits

Author SHA1 Message Date
yangsy de241334a9 chore: 版本信息和更新日志 2026-04-10 15:43:12 +08:00
yangsy c75338cb70 feat: 添加视频服务器双机热备状态 2026-04-10 15:11:58 +08:00
yangsy 86e3e1726d fix: 修复设备查询缓存键冲突问题
- 将查询键中的设备ID改为由车站编码和设备ID组成的唯一键,防止不同车站下相同设备ID导致的数据混淆。
- 同时更新相关监控逻辑,当车站或设备IP变化时重置页面状态。
2026-04-10 10:15:32 +08:00
yangsy 943aa27de1 fix: 将“电路”统一更正为“空开” 2026-04-02 10:38:48 +08:00
yangsy b4442fe6c4 fix: 修复设备树节点key值生成逻辑,最大限度避免key值碰撞导致设备树渲染异常 2026-03-30 20:30:26 +08:00
16 changed files with 177 additions and 48 deletions
+8
View File
@@ -1,4 +1,12 @@
[ [
{
"version": "0.40.0",
"date": "2026-04-10",
"changes": {
"fixes": [{ "content": "修复设备树搜索时节点错乱的问题" }, { "content": "将安防箱面板的“电路”统一更正为“空开”" }, { "content": "修复设备查询缓存键冲突问题" }],
"feats": [{ "content": "添加视频服务器双机热备状态" }]
}
},
{ {
"version": "0.39.0", "version": "0.39.0",
"date": "2026-03-02", "date": "2026-03-02",
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"version": "0.39.0", "version": "0.40.0",
"buildTime": "2026-03-11 14:35:45" "buildTime": "2026-04-10 15:42:03"
} }
@@ -0,0 +1,5 @@
export interface HighAvailable {
pyip: string;
vip: string;
changeDate: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './diag'; export * from './diag';
export * from './high-available';
export * from './link-description'; export * from './link-description';
export * from './station'; export * from './station';
@@ -1,5 +1,26 @@
import { ndmClient, userClient, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis'; import { ndmClient, userClient, type HighAvailable, type MediaServerStatus, type SendRtpInfo, type Station } from '@/apis';
import { unwrapResponse } from '@/utils'; import { unwrapNullableResponse, unwrapResponse } from '@/utils';
import destr from 'destr';
// {
// "code": 0,
// "data": "{pyip:\"10.18.128.14\",vip:\"10.18.128.6\",changeDate:\"2026-03-23 15:55:00\"}",
// "msg": "ok",
// "path": null,
// "extra": null,
// "timestamp": "1774421387908",
// "errorMsg": "",
// "isSuccess": true
// }
export const getHighAvailableApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmServiceAvailable/highAvailable/get`;
const resp = await client.get<string | null>(endpoint, { signal });
const data = unwrapNullableResponse(resp);
return destr<HighAvailable | null>(data);
};
export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => { export const getAllPushApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {}; const { stationCode, signal } = options ?? {};
@@ -92,6 +92,8 @@ const abortController = ref<AbortController>(new AbortController());
const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query'; const NVR_RECORD_CHECK_KEY = 'nvr-record-check-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const DAY_OFFSET = 90; const DAY_OFFSET = 90;
const { const {
@@ -99,7 +101,7 @@ const {
isFetching: loading, isFetching: loading,
refetch: refetchRecordChecks, refetch: refetchRecordChecks,
} = useQuery({ } = useQuery({
queryKey: computed(() => [NVR_RECORD_CHECK_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [NVR_RECORD_CHECK_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value), enabled: computed(() => activeRequests.value),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -408,13 +410,13 @@ const ndmRecordChecksPaged = computed(() => {
return ndmRecordChecksFiltered.value.slice(startIndex, endIndex); return ndmRecordChecksFiltered.value.slice(startIndex, endIndex);
}); });
// 当设备ID、最后诊断时间或筛选类型变化时,重置分页为第一页 // 当车站号、设备IP、最后诊断时间或筛选类型变化时,重置分页为第一页
watch([() => ndmDevice.value.id, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => { watch([() => station.value.code, () => ndmDevice.value.ipAddress, () => ndmDevice.value.lastDiagTime, filterType, searchInputDebounced], () => {
page.value = 1; page.value = 1;
}); });
// 当设备ID变化时,重置搜索内容,并将筛选类型重置为「全部」 // 当车站号、设备IP变化时,重置搜索内容,并将筛选类型重置为「全部」
watch([() => ndmDevice.value.id], () => { watch([() => station.value.code, () => ndmDevice.value.ipAddress], () => {
searchInput.value = ''; searchInput.value = '';
filterType.value = 'all'; filterType.value = 'all';
}); });
@@ -202,7 +202,7 @@ const contextmenuOptions = computed<DropdownOption[]>(() => [
if (!lowerDevice) return; if (!lowerDevice) return;
window.$dialog.warning({ window.$dialog.warning({
title: '确认解除关联吗?', title: '确认解除关联吗?',
content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`, content: `将解除【空开${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
style: { width: '600px' }, style: { width: '600px' },
contentStyle: { height: '60px' }, contentStyle: { height: '60px' },
negativeText: '取消', negativeText: '取消',
@@ -299,7 +299,7 @@ const { mutate: unlinkDevice } = useMutation({
<NCard v-if="showCard" hoverable size="small"> <NCard v-if="showCard" hoverable size="small">
<template #header> <template #header>
<NFlex align="center"> <NFlex align="center">
<span>电路状态</span> <span>空开状态</span>
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => reboot()">
<template #trigger> <template #trigger>
<NButton secondary size="small" :loading="rebooting">重合闸</NButton> <NButton secondary size="small" :loading="rebooting">重合闸</NButton>
@@ -324,7 +324,7 @@ const { mutate: unlinkDevice } = useMutation({
<span>{{ getCircuitStatusText(circuit) }}</span> <span>{{ getCircuitStatusText(circuit) }}</span>
</template> </template>
</NTag> </NTag>
<span>电路{{ circuitIndex + 1 }}</span> <span>空开{{ circuitIndex + 1 }}</span>
</NFlex> </NFlex>
<NFlex justify="end" align="center"> <NFlex justify="end" align="center">
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })"> <NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
@@ -332,7 +332,7 @@ const { mutate: unlinkDevice } = useMutation({
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" /> <NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
</template> </template>
<template #default> <template #default>
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span> <span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}空开{{ circuitIndex + 1 }}吗?</span>
</template> </template>
</NPopconfirm> </NPopconfirm>
</NFlex> </NFlex>
@@ -60,7 +60,7 @@ const { mutate: linkPortToDevice, isPending: linking } = useMutation({
const upperDeviceDbId = ndmDevice.value.id; const upperDeviceDbId = ndmDevice.value.id;
if (!upperDeviceDbId) throw new Error('本设备没有ID'); if (!upperDeviceDbId) throw new Error('本设备没有ID');
if (circuitIndex.value === undefined) throw new Error('该电路不存在'); if (circuitIndex.value === undefined) throw new Error('该空开不存在');
if (!lowerDevice.value) throw new Error('请选择要关联的设备'); if (!lowerDevice.value) throw new Error('请选择要关联的设备');
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType); const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
@@ -195,7 +195,7 @@ const onCancel = () => {
<template #header> <template #header>
<span>{{ ndmDevice.name }}</span> <span>{{ ndmDevice.name }}</span>
<span> - </span> <span> - </span>
<span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span> <span>空开{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
<span> - </span> <span> - </span>
<span>关联设备</span> <span>关联设备</span>
</template> </template>
@@ -31,7 +31,7 @@ const { ndmDevice, station } = toRefs(props);
const showDetailModal = ref(false); const showDetailModal = ref(false);
const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [ const detailTableColumns: DataTableColumns<SecurityBoxCircuitRowData> = [
{ title: '电路序号', key: 'number' }, { title: '空开序号', key: 'number' },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
@@ -98,7 +98,7 @@ const tableColumns: DataTableColumns<SecurityBoxRuntimeRowData> = [
}, },
// { title: '开关状态', key: 'switches' }, // { title: '开关状态', key: 'switches' },
{ {
title: '电路状态', title: '空开状态',
key: 'circuits', key: 'circuits',
render(rowData) { render(rowData) {
const { info } = rowData.diagInfo; const { info } = rowData.diagInfo;
@@ -1,8 +1,9 @@
import ServerAlive from './server-alive.vue'; import ServerAlive from './server-alive.vue';
import ServerCard from './server-card.vue'; import ServerCard from './server-card.vue';
import ServerCurrentDiag from './server-current-diag.vue'; import ServerCurrentDiag from './server-current-diag.vue';
import ServerHighAvailable from './server-high-available.vue';
import ServerHistoryDiag from './server-history-diag.vue'; import ServerHistoryDiag from './server-history-diag.vue';
import ServerStreamPush from './server-stream-push.vue'; import ServerStreamPush from './server-stream-push.vue';
import ServerUpdate from './server-update.vue'; import ServerUpdate from './server-update.vue';
export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate, ServerStreamPush }; export { ServerAlive, ServerCard, ServerCurrentDiag, ServerHighAvailable, ServerHistoryDiag, ServerUpdate, ServerStreamPush };
@@ -23,8 +23,11 @@ const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query'; const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query';
const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query'; const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: isMediaServerAlive } = useQuery({ const { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -34,7 +37,7 @@ const { data: isMediaServerAlive } = useQuery({
}, },
}); });
const { data: isSipServerAlive } = useQuery({ const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer), enabled: computed(() => activeRequests.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis'; import { type NdmServerDiagInfo, type NdmServerResultVO, type Station } from '@/apis';
import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerStreamPush } from '@/components'; import { DeviceHardwareCard, DeviceHeaderCard, ServerAlive, ServerHighAvailable, ServerStreamPush } from '@/components';
import destr from 'destr'; import destr from 'destr';
import { NFlex } from 'naive-ui'; import { NFlex } from 'naive-ui';
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
@@ -27,6 +27,7 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template> <template>
<NFlex vertical> <NFlex vertical>
<ServerHighAvailable :ndm-device="ndmDevice" :station="station" />
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" /> <DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" /> <DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<ServerAlive :ndm-device="ndmDevice" :station="station" /> <ServerAlive :ndm-device="ndmDevice" :station="station" />
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { getHighAvailableApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NAlert, NCard, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
station: Station;
}>();
const settingStore = useSettingStore();
const { activeRequests } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const isVideoServer = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer);
const SERVER_HIGH_AVAILABLE_QUERY_KEY = 'server-high-available-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: highAvailable } = useQuery({
queryKey: computed(() => [SERVER_HIGH_AVAILABLE_QUERY_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && isVideoServer.value),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const highAvailable = await getHighAvailableApi({ stationCode: station.value.code, signal });
return highAvailable;
},
});
watch(activeRequests, (active) => {
if (!active) {
queryClient.cancelQueries({ queryKey: [SERVER_HIGH_AVAILABLE_QUERY_KEY] });
}
});
const showCard = computed(() => {
const { pyip: physicalIp } = highAvailable.value ?? {};
const ipAddressMatched = physicalIp === ndmDevice.value.ipAddress;
return isVideoServer.value && ipAddressMatched;
});
</script>
<template>
<NAlert v-if="showCard && !!highAvailable" :bordered="false" type="success">
<template #header>
<NFlex :align="'center'">
<div>正在提供服务</div>
<NFlex :align="'center'" style="font-size: smaller">
<div>虚拟IP: {{ highAvailable.vip }}</div>
<div>启用时间: {{ highAvailable.changeDate }}</div>
</NFlex>
</NFlex>
</template>
</NAlert>
</template>
<style scoped></style>
@@ -25,8 +25,10 @@ const showCard = computed(() => deviceType.value === DEVICE_TYPE_LITERALS.ndmMed
const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query'; const SERVER_STREAM_PUSH_KEY = 'server-stream-push-query';
const deviceUniqueKey = computed(() => [station.value.code, ndmDevice.value.id]);
const { data: streamPushes } = useQuery({ const { data: streamPushes } = useQuery({
queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]), queryKey: computed(() => [SERVER_STREAM_PUSH_KEY, deviceUniqueKey.value, ndmDevice.value.lastDiagTime]),
enabled: computed(() => activeRequests.value && showCard.value), enabled: computed(() => activeRequests.value && showCard.value),
refetchInterval: 30 * 1000, refetchInterval: 30 * 1000,
gcTime: 0, gcTime: 0,
@@ -1,3 +1,9 @@
<script lang="ts">
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
return `${stationCode ?? ''}-${device?.id ?? ''}`;
};
</script>
<script setup lang="ts"> <script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis'; import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables'; import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
@@ -110,7 +116,7 @@ watchImmediate(selectedDeviceType, (newDeviceType) => {
} }
}); });
const selectedKeys = computed(() => (selectedDevice.value?.id ? [selectedDevice.value.id] : undefined)); const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => { watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) { if (device && code) {
onSelectDevice(device, code); onSelectDevice(device, code);
@@ -316,26 +322,26 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
key: stationCode, key: stationCode,
prefix: () => renderStationNodePrefix(station), prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0), suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((nvrCluster) => { children: nvrClusters.map<TreeOption>((cluster) => {
return { return {
label: `${nvrCluster.name}`, label: `${cluster.name}`,
key: nvrCluster.id ?? `${nvrCluster.name}`, key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode), prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${nvrCluster.ipAddress}`, suffix: () => `${cluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((nvr) => { children: nvrSingletons.map<TreeOption>((device) => {
return { return {
label: `${nvr.name}`, label: `${device.name}`,
key: nvr.id ?? `${nvr.name}`, key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(nvr, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${nvr.ipAddress}`, suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode, stationCode,
device: nvr, device: device,
}; };
}), }),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode, stationCode,
device: nvrCluster, device: cluster,
}; };
}), }),
stationCode, stationCode,
@@ -352,7 +358,7 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const device = dev as NdmDeviceResultVO; const device = dev as NdmDeviceResultVO;
return { return {
label: `${device.name}`, label: `${device.name}`,
key: `${device.name}${device.ipAddress}`, key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
@@ -383,16 +389,16 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length), suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((device) => { children: clusters.map<TreeOption>((cluster) => {
return { return {
label: `${device.name}`, label: `${cluster.name}`,
key: `${device.name}${device.ipAddress}`, key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${cluster.ipAddress}`,
children: singletons.map<TreeOption>((device) => { children: singletons.map<TreeOption>((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: `${device.name}${device.ipAddress}`, key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
stationCode, stationCode,
@@ -400,7 +406,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
}; };
}), }),
stationCode, stationCode,
device, device: cluster,
}; };
}), }),
stationCode, stationCode,
@@ -414,7 +420,7 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
children: stationDevices[deviceType].map<TreeOption>((device) => { children: stationDevices[deviceType].map<TreeOption>((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: `${device.name}${device.ipAddress}`, key: createDeviceNodeKey(stationCode, device),
prefix: () => renderDeviceNodePrefix(device, stationCode), prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
stationCode, stationCode,
@@ -462,6 +468,7 @@ const onFoldDeviceTree = () => {
}; };
const onLocateDeviceTree = async () => { const onLocateDeviceTree = async () => {
if (!selectedStationCode.value) return; if (!selectedStationCode.value) return;
const stationCode = selectedStationCode.value;
if (!selectedDevice.value) return; if (!selectedDevice.value) return;
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType); const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
if (!deviceType) return; if (!deviceType) return;
@@ -473,24 +480,24 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType; activeTab.value = deviceType;
// 展开选择的车站 // 展开选择的车站
expandedKeys.value.push(selectedStationCode.value); expandedKeys.value.push(stationCode);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点 // 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) { if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const stationDevices = lineDevices.value[selectedStationCode.value]; const stationDevices = lineDevices.value[stationCode];
if (stationDevices) { if (stationDevices) {
const selectedNvr = selectedDevice.value as NdmNvrResultVO; const selectedNvr = selectedDevice.value as NdmNvrResultVO;
if (!isNvrCluster(selectedNvr)) { if (!isNvrCluster(selectedNvr)) {
const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr]; const nvrs = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? '')); const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? ''));
expandedKeys.value.push(...clusters.map((nvr) => `${nvr.id}`)); expandedKeys.value.push(...clusters.map((nvr) => createDeviceNodeKey(stationCode, nvr)));
} }
} }
} }
// 等待设备树展开完成,滚动到选择的设备 // 等待设备树展开完成,滚动到选择的设备
await nextTick(); await nextTick();
deviceTreeInst.value.scrollTo({ key: `${selectedDevice.value.id}`, behavior: 'smooth' }); deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' });
animated.value = true; animated.value = true;
}; };
+11
View File
@@ -136,6 +136,17 @@ export const unwrapResponse = <T>(resp: HttpResponse<T>) => {
return data; return data;
}; };
export const unwrapNullableResponse = <T>(resp: HttpResponse<T>) => {
const [err, data, result] = resp;
if (err) throw err;
if (result) {
const { isSuccess, path, msg, errorMsg } = result;
if (!isSuccess) throw new Error(`${path ? `${path}: ` : ''}${msg || errorMsg || '请求失败'}`);
}
if (data === undefined) throw new Error('响应数据不存在');
return data;
};
// 针对没有数据的响应,直接判断是否存在错误 // 针对没有数据的响应,直接判断是否存在错误
export const unwrapVoidResponse = (resp: HttpResponse<void>) => { export const unwrapVoidResponse = (resp: HttpResponse<void>) => {
const [err, , result] = resp; const [err, , result] = resp;