ca7d6baa2e
抽离NVR集群相关的公共工具函数,重构isNvrCluster的实现逻辑。修复原有设备树中NVR分组的逻辑错误,原本会将所有单机NVR添加到每个集群的子节点中,现在会正确将单机NVR分配到对应集群,未归属的单机NVR作为独立节点。简化设备树组件的代码,统一使用封装后的工具方法处理集群分组。修复设备定位时的集群匹配逻辑,使用更准确的IP包含判断。初始化设备树数据的默认空值,修正类型定义错误。
668 lines
24 KiB
Vue
668 lines
24 KiB
Vue
<script lang="ts">
|
|
const createDeviceNodeKey = (stationCode?: Station['code'], device?: NdmDeviceResultVO) => {
|
|
return `${stationCode ?? ''}-${device?.id ?? ''}`;
|
|
};
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
|
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
|
|
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
|
|
import { createNvrClusterRelationship, isNvrCluster, nvrInCluster } from '@/helpers';
|
|
import { useDeviceStore, usePermissionStore } from '@/stores';
|
|
import { watchDebounced, watchImmediate } from '@vueuse/core';
|
|
import destr from 'destr';
|
|
import { isFunction } from 'es-toolkit';
|
|
import {
|
|
NButton,
|
|
NDropdown,
|
|
NFlex,
|
|
NGrid,
|
|
NGridItem,
|
|
NInput,
|
|
NRadio,
|
|
NRadioGroup,
|
|
NSelect,
|
|
NTab,
|
|
NTabs,
|
|
NTag,
|
|
NTree,
|
|
useThemeVars,
|
|
type DropdownOption,
|
|
type SelectOption,
|
|
type TagProps,
|
|
type TreeInst,
|
|
type TreeOption,
|
|
type TreeOverrideNodeClickBehavior,
|
|
type TreeProps,
|
|
} from 'naive-ui';
|
|
import { storeToRefs } from 'pinia';
|
|
import { computed, h, nextTick, onBeforeUnmount, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
|
|
|
const props = defineProps<{
|
|
/**
|
|
* 支持渲染指定车站的设备树
|
|
*/
|
|
station?: Station;
|
|
/**
|
|
* 允许的事件类型
|
|
*
|
|
* - `select`:允许选择设备
|
|
* - `manage`:允许右键菜单管理设备
|
|
*/
|
|
events?: ('select' | 'manage')[];
|
|
/**
|
|
* 是否同步路由参数
|
|
*/
|
|
syncRoute?: boolean;
|
|
/**
|
|
* 设备节点的前缀按钮文字
|
|
*/
|
|
devicePrefixLabel?: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
afterSelectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
|
|
exposeSelectDeviceFn: [selectDeviceFn: UseDeviceTreeReturn['selectDevice']];
|
|
}>();
|
|
|
|
const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
|
|
|
|
const themeVars = useThemeVars();
|
|
|
|
const { hasPermission } = usePermission();
|
|
|
|
const {
|
|
// 设备选择
|
|
selectedStationCode,
|
|
selectedDeviceType,
|
|
selectedDevice,
|
|
syncFromRoute,
|
|
syncToRoute,
|
|
selectDevice,
|
|
// 设备管理
|
|
exportDevice,
|
|
exportDeviceTemplate,
|
|
importDevice,
|
|
deleteDevice,
|
|
} = useDeviceTree();
|
|
|
|
// 将 `selectDevice` 函数暴露给父组件
|
|
emit('exposeSelectDeviceFn', selectDevice);
|
|
|
|
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
|
// 仅当事件列表包含 `select` 时才触发选择事件
|
|
if (!events.value) return;
|
|
if (!events.value.includes('select')) return;
|
|
|
|
selectDevice(device, stationCode);
|
|
emit('afterSelectDevice', device, stationCode);
|
|
};
|
|
|
|
const permissionStore = usePermissionStore();
|
|
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
|
|
|
const deviceStore = useDeviceStore();
|
|
const { lineDevices } = storeToRefs(deviceStore);
|
|
|
|
const deviceTabPanes = Object.values(DEVICE_TYPE_LITERALS).map((deviceType) => ({
|
|
name: deviceType,
|
|
tab: DEVICE_TYPE_NAMES[deviceType],
|
|
}));
|
|
const activeTab = ref<DeviceType>(deviceTabPanes.at(0)!.name);
|
|
watchImmediate(selectedDeviceType, (newDeviceType) => {
|
|
if (newDeviceType) {
|
|
activeTab.value = newDeviceType;
|
|
}
|
|
});
|
|
|
|
const selectedKeys = computed(() => (selectedDevice.value?.id ? [createDeviceNodeKey(selectedStationCode.value, selectedDevice.value)] : undefined));
|
|
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
|
|
if (device && code) {
|
|
onSelectDevice(device, code);
|
|
}
|
|
});
|
|
|
|
const abortController = ref(new AbortController());
|
|
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType?: DeviceType | null; device?: NdmDeviceResultVO }>({ x: 0, y: 0, deviceType: null });
|
|
const showContextmenu = ref(false);
|
|
const contextmenuOptions = computed<DropdownOption[]>(() => [
|
|
{
|
|
label: '导出设备',
|
|
key: 'export-device',
|
|
show: !!contextmenu.value.deviceType,
|
|
onSelect: () => {
|
|
const { stationCode, deviceType } = contextmenu.value;
|
|
// console.log(stationCode, deviceType);
|
|
showContextmenu.value = false;
|
|
if (!stationCode || !deviceType) return;
|
|
abortController.value.abort();
|
|
abortController.value = new AbortController();
|
|
exportDevice({ deviceType, stationCode, signal: abortController.value.signal });
|
|
},
|
|
},
|
|
{
|
|
label: '导入设备',
|
|
key: 'import-device',
|
|
show: !!contextmenu.value.deviceType,
|
|
onSelect: () => {
|
|
const { stationCode, deviceType } = contextmenu.value;
|
|
// console.log(stationCode, deviceType);
|
|
showContextmenu.value = false;
|
|
if (!stationCode || !deviceType) return;
|
|
abortController.value.abort();
|
|
abortController.value = new AbortController();
|
|
importDevice({ deviceType, stationCode, signal: abortController.value.signal });
|
|
},
|
|
},
|
|
{
|
|
label: '下载导入模板',
|
|
key: 'export-template',
|
|
// 导出模板功能有缺陷,暂时不展示
|
|
show: false,
|
|
onSelect: () => {
|
|
const { stationCode, deviceType } = contextmenu.value;
|
|
// console.log(stationCode, deviceType);
|
|
showContextmenu.value = false;
|
|
if (!stationCode || !deviceType) return;
|
|
abortController.value.abort();
|
|
abortController.value = new AbortController();
|
|
exportDeviceTemplate({ deviceType, stationCode, signal: abortController.value.signal });
|
|
},
|
|
},
|
|
{
|
|
label: '删除设备',
|
|
key: 'delete-device',
|
|
show: !!contextmenu.value.device,
|
|
onSelect: () => {
|
|
const { stationCode, device } = contextmenu.value;
|
|
// console.log(stationCode, device);
|
|
showContextmenu.value = false;
|
|
if (!stationCode || !device) return;
|
|
const id = device.id;
|
|
const deviceType = tryGetDeviceType(device.deviceType);
|
|
if (!id || !deviceType) return;
|
|
window.$dialog.destroyAll();
|
|
window.$dialog.warning({
|
|
title: '删除设备',
|
|
content: `确认删除设备 ${device.name || device.deviceId || device.id} 吗?`,
|
|
positiveText: '确认',
|
|
negativeText: '取消',
|
|
onPositiveClick: () => {
|
|
abortController.value.abort();
|
|
abortController.value = new AbortController();
|
|
deleteDevice({ id, deviceType, stationCode, signal: abortController.value.signal });
|
|
},
|
|
});
|
|
},
|
|
},
|
|
]);
|
|
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
|
const onSelect = option['onSelect'];
|
|
if (isFunction(onSelect)) {
|
|
onSelect();
|
|
}
|
|
};
|
|
onBeforeUnmount(() => {
|
|
abortController.value.abort();
|
|
});
|
|
|
|
// ========== 设备树节点交互 ==========
|
|
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
|
|
const hasChildren = (option.children?.length ?? 0) > 0;
|
|
const isDeviceNode = !!option['device'];
|
|
if (hasChildren || !isDeviceNode) {
|
|
return 'toggleExpand';
|
|
} else {
|
|
return 'none';
|
|
}
|
|
};
|
|
const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
|
|
return {
|
|
onDblclick: (payload) => {
|
|
if (option['device']) {
|
|
payload.stopPropagation();
|
|
|
|
const device = option['device'] as NdmDeviceResultVO;
|
|
const stationCode = option['stationCode'] as Station['code'];
|
|
|
|
onSelectDevice(device, stationCode);
|
|
}
|
|
},
|
|
onContextmenu: (payload) => {
|
|
payload.stopPropagation();
|
|
payload.preventDefault();
|
|
|
|
// 如果事件列表不包含 `manage`,则直接结束逻辑
|
|
if (!events.value?.includes('manage')) return;
|
|
|
|
const stationCode = option['stationCode'] as Station['code'];
|
|
|
|
// 仅当用户在该车站拥有操作权限时才显示右键菜单
|
|
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return;
|
|
|
|
const deviceType = option['deviceType'] as DeviceType | undefined;
|
|
const device = option['device'] as NdmDeviceResultVO | undefined;
|
|
const { clientX, clientY } = payload;
|
|
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
|
|
showContextmenu.value = true;
|
|
},
|
|
};
|
|
};
|
|
|
|
// ========== 设备树数据 ==========
|
|
const renderStationNodePrefix = (station: Station) => {
|
|
const { online } = station;
|
|
const tagType: TagProps['type'] = online ? 'success' : 'error';
|
|
const tagText = online ? '在线' : '离线';
|
|
return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
|
|
};
|
|
const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count: number) => {
|
|
return h('span', null, [
|
|
'(',
|
|
h('span', { style: { color: themeVars.value.successColor } }, `${onlineCount}`),
|
|
'/',
|
|
h('span', { style: { color: themeVars.value.errorColor } }, `${offlineCount}`),
|
|
'/',
|
|
`${count}`,
|
|
')',
|
|
]);
|
|
};
|
|
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
|
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
|
if (!devicePrefixLabel.value) return null;
|
|
return h(
|
|
NButton,
|
|
{
|
|
text: true,
|
|
size: 'tiny',
|
|
type: 'info',
|
|
style: {
|
|
marginRight: 8,
|
|
} as CSSProperties,
|
|
onClick: (e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
onSelectDevice(device, stationCode);
|
|
},
|
|
},
|
|
() => devicePrefixLabel.value,
|
|
);
|
|
};
|
|
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
|
|
const { deviceStatus } = device;
|
|
const color = deviceStatus === '10' ? themeVars.value.successColor : deviceStatus === '20' ? themeVars.value.errorColor : themeVars.value.warningColor;
|
|
return h('div', { style: { color } }, { default: () => '◉' });
|
|
};
|
|
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
|
|
};
|
|
// 全线设备树
|
|
const lineDeviceTreeData = computed<Record<DeviceType, 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 */ }) => {
|
|
treeData[paneName] = stations.value.map<TreeOption>((station) => {
|
|
const { name: stationName, code: stationCode } = station;
|
|
const devices = lineDevices.value[stationCode]?.[paneName] ?? ([] as NdmDeviceResultVO[]);
|
|
const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
|
|
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
|
|
// 对于录像机,需要根据clusterList字段以分号分隔设备IP,进一步形成子树结构
|
|
if (paneName === DEVICE_TYPE_LITERALS.ndmNvr) {
|
|
const nvrDevices = devices as NdmNvrResultVO[];
|
|
|
|
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
|
|
|
|
return {
|
|
label: stationName,
|
|
key: stationCode,
|
|
prefix: () => renderStationNodePrefix(station),
|
|
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
|
children: [
|
|
...nvrClusters.map((cluster) => {
|
|
return {
|
|
label: `${cluster.name}`,
|
|
key: createDeviceNodeKey(stationCode, cluster),
|
|
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
|
suffix: () => `${cluster.ipAddress}`,
|
|
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 {
|
|
label: `${device.name}`,
|
|
key: createDeviceNodeKey(stationCode, device),
|
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
|
suffix: () => `${device.ipAddress}`,
|
|
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
|
stationCode,
|
|
device: device,
|
|
};
|
|
}),
|
|
],
|
|
stationCode,
|
|
deviceType: activeTab.value,
|
|
};
|
|
}
|
|
// 非录像机设备
|
|
return {
|
|
label: stationName,
|
|
key: stationCode,
|
|
prefix: () => renderStationNodePrefix(station),
|
|
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
|
|
children:
|
|
lineDevices.value[stationCode]?.[paneName]?.map<TreeOption>((dev) => {
|
|
const device = dev as NdmDeviceResultVO;
|
|
return {
|
|
label: `${device.name}`,
|
|
key: createDeviceNodeKey(stationCode, device),
|
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
|
suffix: () => `${device.ipAddress}`,
|
|
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
|
|
stationCode,
|
|
device,
|
|
};
|
|
}) ?? [],
|
|
stationCode,
|
|
deviceType: activeTab.value,
|
|
};
|
|
});
|
|
});
|
|
return treeData;
|
|
});
|
|
// 车站设备树
|
|
const stationDeviceTreeData = computed<TreeOption[]>(() => {
|
|
const stationCode = station.value?.code;
|
|
if (!stationCode) return [];
|
|
return Object.values(DEVICE_TYPE_LITERALS).map<TreeOption>((deviceType) => {
|
|
const stationDevices = lineDevices.value[stationCode] ?? initStationDevices();
|
|
const onlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '10').length;
|
|
const offlineCount = stationDevices[deviceType].filter((device) => device.deviceStatus === '20').length;
|
|
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
|
const nvrDevices = stationDevices[deviceType] as NdmNvrResultVO[];
|
|
|
|
const { nvrClusters, nvrTreeMap, nvrStandalones } = createNvrClusterRelationship(nvrDevices);
|
|
|
|
return {
|
|
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
|
key: deviceType,
|
|
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrDevices.length),
|
|
children: [
|
|
...nvrClusters.map((cluster) => {
|
|
return {
|
|
label: `${cluster.name}`,
|
|
key: createDeviceNodeKey(stationCode, cluster),
|
|
prefix: () => renderDeviceNodePrefix(cluster, stationCode),
|
|
suffix: () => `${cluster.ipAddress}`,
|
|
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 {
|
|
label: `${device.name}`,
|
|
key: createDeviceNodeKey(stationCode, device),
|
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
|
suffix: () => `${device.ipAddress}`,
|
|
stationCode,
|
|
device,
|
|
};
|
|
}),
|
|
],
|
|
stationCode,
|
|
deviceType,
|
|
};
|
|
}
|
|
// 非录像机设备
|
|
return {
|
|
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
|
|
key: deviceType,
|
|
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, stationDevices[deviceType].length),
|
|
children: stationDevices[deviceType].map<TreeOption>((device) => {
|
|
return {
|
|
label: `${device.name}`,
|
|
key: createDeviceNodeKey(stationCode, device),
|
|
prefix: () => renderDeviceNodePrefix(device, stationCode),
|
|
suffix: () => `${device.ipAddress}`,
|
|
stationCode,
|
|
device,
|
|
};
|
|
}),
|
|
stationCode,
|
|
deviceType,
|
|
};
|
|
});
|
|
});
|
|
|
|
// ========== 设备树搜索 ==========
|
|
const searchInput = ref('');
|
|
const searchTypeOptions: SelectOption[] = [
|
|
{ label: '设备名称', value: 'name' },
|
|
{ label: 'IP地址', value: 'ipAddress' },
|
|
];
|
|
type SearchType = 'name' | 'ipAddress';
|
|
const typeInput = ref<SearchType>('name');
|
|
const statusInput = ref('');
|
|
// 设备树将搜索框、选择器以及单选框的值都交给NTree的pattern属性
|
|
// 但是如果一个车站下没有匹配的设备,那么这个车站节点也不会显示
|
|
const searchPattern = computed(() => {
|
|
const search = searchInput.value;
|
|
const status = statusInput.value;
|
|
if (!search && !status) return ''; // 如果pattern非空会导致NTree组件认为筛选完成,UI上发生全量匹配
|
|
return JSON.stringify({ search: searchInput.value, type: typeInput.value, status: statusInput.value });
|
|
});
|
|
const searchFilter = (pattern: string, node: TreeOption): boolean => {
|
|
const { search, type, status } = destr<{ search: string; type: SearchType; status: string }>(pattern);
|
|
const device = node['device'] as NdmDeviceResultVO | undefined;
|
|
const { deviceStatus } = device ?? {};
|
|
const searchMatched = !!device?.[type]?.includes(search);
|
|
const statusMatched = status === '' || status === deviceStatus;
|
|
return searchMatched && statusMatched;
|
|
};
|
|
|
|
// ========== 设备树交互 ==========
|
|
const animated = ref(true);
|
|
const expandedKeys = ref<string[]>([]);
|
|
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
|
|
const onFoldDeviceTree = () => {
|
|
expandedKeys.value = [];
|
|
};
|
|
const onLocateDeviceTree = async () => {
|
|
if (!selectedStationCode.value) return;
|
|
const stationCode = selectedStationCode.value;
|
|
if (!selectedDevice.value) return;
|
|
const deviceType = tryGetDeviceType(selectedDevice.value.deviceType);
|
|
if (!deviceType) return;
|
|
if (!deviceTreeInst.value) return;
|
|
|
|
animated.value = false;
|
|
|
|
// 定位设备类型
|
|
activeTab.value = deviceType;
|
|
|
|
// 展开选择的车站
|
|
expandedKeys.value.push(stationCode);
|
|
|
|
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
|
|
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
|
|
const stationDevices = lineDevices.value[stationCode];
|
|
if (stationDevices) {
|
|
const selectedNvr = selectedDevice.value as NdmNvrResultVO;
|
|
if (!isNvrCluster(selectedNvr)) {
|
|
const nvrDevices = stationDevices[DEVICE_TYPE_LITERALS.ndmNvr];
|
|
const clusters = nvrDevices.filter((device) => {
|
|
if (!isNvrCluster(device)) return false;
|
|
const cluster = device;
|
|
return nvrInCluster(selectedNvr, cluster);
|
|
});
|
|
expandedKeys.value.push(...clusters.map((cluster) => createDeviceNodeKey(stationCode, cluster)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 等待设备树展开完成,滚动到选择的设备
|
|
await nextTick();
|
|
deviceTreeInst.value.scrollTo({ key: createDeviceNodeKey(stationCode, selectedDevice.value), behavior: 'smooth' });
|
|
|
|
animated.value = true;
|
|
};
|
|
|
|
// 当选择的设备发生变化时,定位设备树,并同步选中状态到路由参数
|
|
// 暂时不考虑多次执行的问题,因为当选择的设备在设备树视口内时,不会发生滚动
|
|
watch(selectedDevice, async (newDevice, oldDevice) => {
|
|
if (!!station.value) return;
|
|
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>
|
|
|
|
<template>
|
|
<div style="height: 100%; display: flex; flex-direction: column">
|
|
<!-- 搜索和筛选 -->
|
|
<div style="padding: 12px; flex: 0 0 auto">
|
|
<NGrid :cols="10" :x-gap="8">
|
|
<NGridItem :span="7">
|
|
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
|
|
</NGridItem>
|
|
<NGridItem :span="3">
|
|
<NSelect v-model:value="typeInput" :options="searchTypeOptions" placeholder="搜索类型" />
|
|
</NGridItem>
|
|
</NGrid>
|
|
<NFlex align="center">
|
|
<NRadioGroup v-model:value="statusInput">
|
|
<NRadio value="">全部</NRadio>
|
|
<NRadio value="10">在线</NRadio>
|
|
<NRadio value="20">离线</NRadio>
|
|
</NRadioGroup>
|
|
<template v-if="!station">
|
|
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
|
|
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
|
|
</template>
|
|
</NFlex>
|
|
</div>
|
|
<!-- 设备树 -->
|
|
<div
|
|
style="overflow: hidden; flex: 1 1 auto; display: flex"
|
|
:style="{
|
|
// 当右键菜单显示时,禁用设备树的点击事件,避免在打开菜单时仍能点击设备树节点
|
|
'pointer-events': showContextmenu ? 'none' : 'auto',
|
|
}"
|
|
>
|
|
<template v-if="!station">
|
|
<div style="height: 100%; flex: 0 0 auto">
|
|
<NTabs v-model:value="activeTab" type="line" placement="left" style="height: 100%">
|
|
<NTab v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab"></NTab>
|
|
</NTabs>
|
|
</div>
|
|
<div style="min-width: 0; flex: 1 1 auto">
|
|
<NTree
|
|
style="height: 100%"
|
|
v-model:expanded-keys="expandedKeys"
|
|
block-line
|
|
block-node
|
|
show-line
|
|
virtual-scroll
|
|
:ref="'deviceTreeInst'"
|
|
:animated="animated"
|
|
:selected-keys="selectedKeys"
|
|
:data="lineDeviceTreeData[activeTab]"
|
|
:show-irrelevant-nodes="false"
|
|
:pattern="searchPattern"
|
|
:filter="searchFilter"
|
|
:override-default-node-click-behavior="override"
|
|
:node-props="nodeProps"
|
|
:default-expand-all="false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<NTree
|
|
style="height: 100%"
|
|
block-line
|
|
block-node
|
|
show-line
|
|
virtual-scroll
|
|
:data="stationDeviceTreeData"
|
|
:animated="animated"
|
|
:selected-keys="selectedKeys"
|
|
:show-irrelevant-nodes="false"
|
|
:pattern="searchPattern"
|
|
:filter="searchFilter"
|
|
:override-default-node-click-behavior="override"
|
|
:node-props="nodeProps"
|
|
:default-expand-all="false"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<NDropdown
|
|
placement="bottom-start"
|
|
trigger="manual"
|
|
:show="showContextmenu"
|
|
:x="contextmenu.x"
|
|
:y="contextmenu.y"
|
|
:options="contextmenuOptions"
|
|
@select="onSelectDropdownOption"
|
|
@clickoutside="() => (showContextmenu = false)"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped lang="scss"></style>
|