refactor: 重构NVR集群处理逻辑,优化设备树展示

抽离NVR集群相关的公共工具函数,重构isNvrCluster的实现逻辑。修复原有设备树中NVR分组的逻辑错误,原本会将所有单机NVR添加到每个集群的子节点中,现在会正确将单机NVR分配到对应集群,未归属的单机NVR作为独立节点。简化设备树组件的代码,统一使用封装后的工具方法处理集群分组。修复设备定位时的集群匹配逻辑,使用更准确的IP包含判断。初始化设备树数据的默认空值,修正类型定义错误。
This commit is contained in:
yangsy
2026-05-20 21:18:54 +08:00
parent 0b39c9c602
commit ca7d6baa2e
2 changed files with 149 additions and 65 deletions
@@ -8,7 +8,7 @@ const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceRe
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';
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 { createNvrClusterRelationship, isNvrCluster, nvrInCluster } from '@/helpers';
import { useDeviceStore, usePermissionStore } from '@/stores'; import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchDebounced, watchImmediate } from '@vueuse/core'; import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr'; import destr from 'destr';
@@ -297,8 +297,18 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] }); return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
}; };
// 全线设备树 // 全线设备树
const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => { const lineDeviceTreeData = computed<Record<DeviceType, TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {}; const treeData: Record<DeviceType, TreeOption[]> = {
[DEVICE_TYPE_LITERALS.ndmCamera]: [],
[DEVICE_TYPE_LITERALS.ndmNvr]: [],
[DEVICE_TYPE_LITERALS.ndmSwitch]: [],
[DEVICE_TYPE_LITERALS.ndmDecoder]: [],
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: [],
[DEVICE_TYPE_LITERALS.ndmMediaServer]: [],
[DEVICE_TYPE_LITERALS.ndmVideoServer]: [],
[DEVICE_TYPE_LITERALS.ndmKeyboard]: [],
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: [],
};
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => { deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => { treeData[paneName] = stations.value.map<TreeOption>((station) => {
const { name: stationName, code: stationCode } = station; const { name: stationName, code: stationCode } = station;
@@ -307,28 +317,39 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20'); const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构 // 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) { if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = devices as NdmNvrResultVO[]; const nvrDevices = devices as NdmNvrResultVO[];
const nvrClusters: NdmNvrResultVO[] = [];
const nvrSingletons: NdmNvrResultVO[] = []; const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
for (const device of nvrs) {
if (isNvrCluster(device)) {
nvrClusters.push(device);
} else {
nvrSingletons.push(device);
}
}
return { return {
label: stationName, label: stationName,
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>((cluster) => { children: [
...nvrClusters.map((cluster) => {
return { return {
label: `${cluster.name}`, label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster), key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode), prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`, suffix: () => `${cluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((device) => { children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
return {
label: `${clusterNode.name}`,
key: createDeviceNodeKey(stationCode, clusterNode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
suffix: () => `${clusterNode.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: clusterNode,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: cluster,
};
}),
...nvrStandalones.map((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: createDeviceNodeKey(stationCode, device),
@@ -339,15 +360,12 @@ const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() =>
device: device, device: device,
}; };
}), }),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 ],
stationCode,
device: cluster,
};
}),
stationCode, stationCode,
deviceType: activeTab.value, deviceType: activeTab.value,
}; };
} }
// 非录像机设备
return { return {
label: stationName, label: stationName,
key: stationCode, key: stationCode,
@@ -382,20 +400,36 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length; const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length; const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) { if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = stationDevices[deviceType] as NdmNvrResultVO[]; const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr)); const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
return { return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length), suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length),
children: clusters.map<TreeOption>((cluster) => { children: [
...nvrClusters.map((cluster) => {
return { return {
label: `${cluster.name}`, label: `${cluster.name}`,
key: createDeviceNodeKey(stationCode, cluster), key: createDeviceNodeKey(stationCode, cluster),
prefix: () => renderDeviceNodePrefix(cluster, stationCode), prefix: () => renderDeviceNodePrefix(cluster, stationCode),
suffix: () => `${cluster.ipAddress}`, suffix: () => `${cluster.ipAddress}`,
children: singletons.map<TreeOption>((device) => { children: (nvrTreeMap.get(cluster.ipAddress ?? '') ?? []).map((clusterNode) => {
return {
label: `${clusterNode.name}`,
key: createDeviceNodeKey(stationCode, clusterNode),
prefix: () => renderDeviceNodePrefix(clusterNode, stationCode),
suffix: () => `${clusterNode.ipAddress}`,
stationCode,
device: clusterNode,
};
}),
stationCode,
device: cluster,
};
}),
...nvrStandalones.map((device) => {
return { return {
label: `${device.name}`, label: `${device.name}`,
key: createDeviceNodeKey(stationCode, device), key: createDeviceNodeKey(stationCode, device),
@@ -405,14 +439,12 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
device, device,
}; };
}), }),
stationCode, ],
device: cluster,
};
}),
stationCode, stationCode,
deviceType, deviceType,
}; };
} }
// 非录像机设备
return { return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`, label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType, key: deviceType,
@@ -488,9 +520,13 @@ const onLocateDeviceTree = async () => {
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 nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr) && nvr.clusterList?.includes(selectedNvr.clusterList ?? '')); const clusters = nvrDevices.filter((device) => {
expandedKeys.value.push(...clusters.map((nvr) => createDeviceNodeKey(stationCode, nvr))); if (!isNvrCluster(device)) return false;
const cluster = device;
return nvrInCluster(selectedNvr, cluster);
});
expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
} }
} }
} }
+51 -3
View File
@@ -1,8 +1,56 @@
import type { NdmNvrResultVO } from '@/apis'; import type { NdmNvrResultVO } from '@/apis';
// 解析 clusterList 字段
export const parseIpListFromClusterList = (nvr: NdmNvrResultVO) => {
const ipList = (nvr.clusterList ?? '').split(';');
return ipList.map((ip) => ip.trim()).filter((ip) => !!ip);
};
export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => { export const isNvrCluster = (maybeNvrCluster: NdmNvrResultVO) => {
const { ipAddress, clusterList } = maybeNvrCluster; const { ipAddress } = maybeNvrCluster;
if (!clusterList?.trim()) return false; const ipList = parseIpListFromClusterList(maybeNvrCluster);
if (clusterList === ipAddress) return false; if (ipList.length === 0) return false;
if (ipList.length === 1 && ipList.at(0) === ipAddress) return false;
return true; return true;
}; };
export const nvrInCluster = (nvr: NdmNvrResultVO, cluster: NdmNvrResultVO) => {
const { ipAddress } = nvr;
if (!ipAddress) return false;
const ipList = parseIpListFromClusterList(cluster);
return ipList.includes(ipAddress);
};
export const createNvrClusterRelationship = (nvrDevices: NdmNvrResultVO[]) => {
const nvrClusters = nvrDevices.filter((nvr) => isNvrCluster(nvr));
const nvrNotClusters = nvrDevices.filter((nvr) => !isNvrCluster(nvr));
const nodedNvrIpAddressSet = new Set<string | null>();
const nvrStandalones: NdmNvrResultVO[] = [];
const nvrTreeMap = new Map<string, NdmNvrResultVO[]>();
// 遍历所有非集群录像机,将它们分配到对应的录像机集群中
for (const nvr of nvrNotClusters) {
for (const cluster of nvrClusters) {
if (nvrInCluster(nvr, cluster)) {
if (!!cluster.ipAddress) {
// 写入录像机与集群的关系
nvrTreeMap.set(cluster.ipAddress, [...(nvrTreeMap.get(cluster.ipAddress) ?? []), nvr]);
// 记录已分配的录像机IP地址
nodedNvrIpAddressSet.add(nvr.ipAddress);
}
}
}
}
// 分配完成后,过滤出未分配的录像机,形成录像机单机列表
nvrNotClusters.forEach((device) => {
if (!nodedNvrIpAddressSet.has(device.ipAddress)) {
nvrStandalones.push(device);
}
});
return {
nvrClusters,
// nvrNotClusters,
nvrTreeMap,
nvrStandalones,
};
};