refactor: 移除未使用的测试组件

This commit is contained in:
yangsy
2025-12-12 20:15:16 +08:00
parent d73861442c
commit 9ca5630c87

View File

@@ -1,475 +0,0 @@
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
import { isNvrCluster } from '@/helpers';
import { useDeviceStore, useStationStore } from '@/stores';
import { sleep } from '@/utils';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
NButton,
NDropdown,
NFlex,
NInput,
NRadio,
NRadioGroup,
NTab,
NTabs,
NTag,
NTree,
useThemeVars,
type DropdownOption,
type TagProps,
type TreeInst,
type TreeOption,
type TreeOverrideNodeClickBehavior,
type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{
station?: Station; // 支持渲染指定车站的设备树
}>();
const emit = defineEmits<{
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
}>();
const { station } = toRefs(props);
const themeVars = useThemeVars();
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
selectDevice(device, stationCode);
emit('selectDevice', device, stationCode);
};
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
routeDevice(device, stationCode, { path: '/device' });
emit('selectDevice', device, stationCode);
};
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
onMounted(() => {
initFromRoute(lineDevices.value);
});
// lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
initFromRoute(newLineDevices);
},
{
debounce: 500,
deep: true,
},
);
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 ? [selectedDevice.value.id] : undefined));
watch([selectedKeys, selectedDevice, selectedStationCode], ([, device, code]) => {
if (device && code) {
onSelectDevice(device, code);
}
});
const contextmenu = ref<{ x: number; y: number; stationCode?: Station['code']; deviceType: DeviceType | null }>({ x: 0, y: 0, deviceType: null });
const showContextmenu = ref(false);
const contextmenuOptions: DropdownOption[] = [
{
label: '导出设备',
key: 'export-device',
onSelect: () => {
// 需要拿到当前选中的设备类型和车站编号
const { stationCode, deviceType } = contextmenu.value;
console.log(stationCode, deviceType);
showContextmenu.value = false;
},
},
];
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];
if (isFunction(onSelect)) {
onSelect();
}
};
// ========== 设备树节点交互 ==========
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 string;
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
}
},
// TODO: 支持右键点击车站导出设备列表
onContextmenu: (payload) => {
payload.stopPropagation();
payload.preventDefault();
if (!option['device']) {
const { clientX, clientY } = payload;
const stationCode = option['stationCode'] as string;
const deviceType = option['deviceType'] as DeviceType;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType };
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: string) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
return h(
NButton,
{
text: true,
size: 'tiny',
type: 'info',
style: {
marginRight: 8,
} as CSSProperties,
onClick: (e: MouseEvent) => {
e.stopPropagation();
// 选择设备
// 区分是否需要跳转路由
if (!station.value) {
onSelectDevice(device, stationCode);
} else {
onRouteDevice(device, station.value.code);
}
},
},
() => '查看',
);
};
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<string, TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {};
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 nvrs = devices as NdmNvrResultVO[];
const nvrClusters: NdmNvrResultVO[] = [];
const nvrSingletons: NdmNvrResultVO[] = [];
for (const device of nvrs) {
if (isNvrCluster(device)) {
nvrClusters.push(device);
} else {
nvrSingletons.push(device);
}
}
return {
label: stationName,
key: stationCode,
prefix: () => renderStationNodePrefix(station),
suffix: () => renderIcmpStatistics(onlineDevices?.length ?? 0, offlineDevices?.length ?? 0, devices?.length ?? 0),
children: nvrClusters.map<TreeOption>((nvrCluster) => {
return {
label: `${nvrCluster.name}`,
key: nvrCluster.id ?? `${nvrCluster.name}`,
prefix: () => renderDeviceNodePrefix(nvrCluster, stationCode),
suffix: () => `${nvrCluster.ipAddress}`,
children: nvrSingletons.map<TreeOption>((nvr) => {
return {
label: `${nvr.name}`,
key: nvr.id ?? `${nvr.name}`,
prefix: () => renderDeviceNodePrefix(nvr, stationCode),
suffix: () => `${nvr.ipAddress}`,
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvr,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
stationCode,
device: nvrCluster,
};
}),
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: device.id ?? `${device.name}`,
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 nvrs = stationDevices[deviceType] as NdmNvrResultVO[];
const clusters = nvrs.filter((nvr) => isNvrCluster(nvr));
const singletons = nvrs.filter((nvr) => !isNvrCluster(nvr));
return {
label: `${DEVICE_TYPE_NAMES[deviceType]}`,
key: deviceType,
suffix: () => renderIcmpStatistics(onlineCount, offlineCount, nvrs.length),
children: clusters.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
children: singletons.map<TreeOption>((device) => {
return {
label: `${device.name}`,
key: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
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: device.id ?? `${device.name}`,
prefix: () => renderDeviceNodePrefix(device, stationCode),
suffix: () => `${device.ipAddress}`,
stationCode,
device,
};
}),
stationCode,
deviceType,
};
});
});
// ========== 设备树搜索 ==========
const searchInput = ref('');
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, status: statusInput.value });
});
const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, status } = destr<{ search: string; status: string }>(pattern);
const device = node['device'] as NdmDeviceResultVO | undefined;
const { name, ipAddress, deviceId, deviceStatus } = device ?? {};
const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search) || (deviceId ?? '').includes(search);
const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched;
};
// ========== 设备树交互 ==========
const expandedKeys = ref<string[]>();
const deviceTreeInst = useTemplateRef<TreeInst>('deviceTreeInst');
const onFoldDeviceTree = () => {
expandedKeys.value = [];
};
const onLocateDeviceTree = () => {
const stationCode = selectedStationCode.value;
const device = selectedDevice.value;
if (!stationCode || !device?.id) return;
const deviceTypeVal = tryGetDeviceType(device.deviceType);
if (!!deviceTypeVal) {
activeTab.value = deviceTypeVal;
}
const expanded = [stationCode];
if (activeTab.value === DEVICE_TYPE_LITERALS.ndmNvr) {
const nvrs = lineDevices.value[stationCode]?.[DEVICE_TYPE_LITERALS.ndmNvr];
if (nvrs) {
const clusterKeys = nvrs.filter((nvr) => !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress).map((nvr) => String(nvr.id));
expanded.push(...clusterKeys);
}
}
expandedKeys.value = expanded;
// 由于数据量大所以开启虚拟滚动,
// 但是无法知晓NTree内部的虚拟列表容器何时创建完成所以使用setTimeout延迟固定时间后执行滚动
scrollDeviceTreeToSelectedDevice();
};
async function scrollDeviceTreeToSelectedDevice() {
await sleep(350);
const inst = deviceTreeInst.value;
inst?.scrollTo({ key: selectedDevice?.value?.id ?? `${selectedDevice.value?.name}`, behavior: 'smooth' });
}
</script>
<template>
<div style="height: 100%; display: flex; flex-direction: column">
<!-- 搜索和筛选 -->
<div style="padding: 12px; flex: 0 0 auto">
<NInput v-model:value="searchInput" placeholder="搜索设备名称、设备ID或IP地址" clearable />
<NFlex align="center">
<NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio>
<NRadio value="10">在线</NRadio>
<NRadio value="20">离线</NRadio>
</NRadioGroup>
<NButton text size="tiny" type="info" @click="onFoldDeviceTree" style="margin-left: auto">收起</NButton>
<NButton text size="tiny" type="info" @click="onLocateDeviceTree">定位</NButton>
</NFlex>
</div>
<!-- 设备树 -->
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
<template v-if="!station">
<div style="height: 100%; flex: 0 0 auto">
<NTabs v-model:value="activeTab" animated 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'"
: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"
: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>