feat: enhance device tree

This commit is contained in:
yangsy
2025-08-18 02:16:41 +08:00
parent 9cf72a1734
commit 7de9d5ea5c
2 changed files with 104 additions and 36 deletions

View File

@@ -73,9 +73,8 @@ const treeData = computed<TreeOption[]>(() => {
key: type,
children: offlineDeviceList.map<TreeOption>((device) => ({
label: `${device.name}`,
key: device.deviceId,
key: device.id,
suffix: () => `${device.ipAddress ?? '未知IP地址'}`,
isLeaf: true,
device,
})),
};
@@ -85,7 +84,7 @@ const treeData = computed<TreeOption[]>(() => {
const nodeProps = ({ option }: { option: TreeOption }) => {
return {
ondblclick: () => {
if (option.isLeaf) {
if (option['device']) {
const queryControlStore = useQueryControlStore();
queryControlStore.enablePolling();
const device = option['device'] as NdmDeviceVO;

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import type { NdmDeviceResultVO, NdmDeviceVO } from '@/apis/models/device';
import type { NdmDeviceResultVO, NdmDeviceVO, NdmNvrResultVO } from '@/apis/models/device';
import { useLineDevicesQuery } from '@/composables/query/use-line-devices-query';
import { DeviceType, DeviceTypeName, type DeviceTypeCode, type DeviceTypeKey } from '@/enums/device-type';
import { useStationStore } from '@/stores/station';
import { ChevronBack } from '@vicons/ionicons5';
import {
NButton,
NButtonGroup,
NFlex,
NIcon,
NInput,
NLayout,
@@ -18,11 +21,11 @@ import {
NTabs,
NTag,
NTree,
type TreeInst,
type TreeOption,
type TreeOverrideNodeClickBehavior,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { h } from 'vue';
import { h, nextTick, onMounted, useTemplateRef } from 'vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter, type LocationQuery } from 'vue-router';
import { destr } from 'destr';
@@ -40,13 +43,6 @@ const deviceTabPanes = Object.keys(DeviceType).map((key) => {
};
});
const override: TreeOverrideNodeClickBehavior = ({ option }) => {
if (option.children) {
return 'toggleExpand';
}
return 'default';
};
// 获取车站和设备数据
const stationStore = useStationStore();
const { stationList } = storeToRefs(stationStore);
@@ -59,6 +55,62 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const devices = lineDevices.value?.[stationCode][paneName];
const onlineDevices = devices?.filter((device) => device.deviceStatus === '10');
const offlineDevices = devices?.filter((device) => device.deviceStatus === '20');
// 对于录像机需要根据clusterList字段以分号分隔设备IP进一步形成子树结构
if (paneName === DeviceType.Nvr) {
const nvrs = devices as NdmNvrResultVO[] | undefined;
return {
label: stationName,
key: stationCode,
suffix: () => `(${onlineDevices?.length ?? 0}/${offlineDevices?.length ?? 0}/${devices?.length ?? 0})`,
children: nvrs
?.filter((device) => {
const nvr = device as NdmNvrResultVO;
return !!nvr.clusterList?.trim() && nvr.clusterList !== nvr.ipAddress;
})
.map<TreeOption>((nvrCluster) => {
return {
label: `${nvrCluster.name}`,
key: nvrCluster.deviceId,
suffix: () => `${nvrCluster.ipAddress}`,
prefix: () => {
return h(
NTag,
{ type: nvrCluster.deviceStatus === '10' ? 'success' : nvrCluster.deviceStatus === '20' ? 'error' : 'warning', size: 'tiny' },
{
default: () => (nvrCluster.deviceStatus === '10' ? '在线' : nvrCluster.deviceStatus === '20' ? '离线' : '未知'),
},
);
},
children: nvrs
.filter((nvr) => {
return nvrCluster.clusterList?.includes(nvr.ipAddress ?? '');
})
.map<TreeOption>((nvr) => {
return {
label: `${nvr.name}`,
key: nvr.deviceId,
suffix: () => `${nvr.ipAddress}`,
prefix: () => {
return h(
NTag,
{ type: nvr.deviceStatus === '10' ? 'success' : nvr.deviceStatus === '20' ? 'error' : 'warning', size: 'tiny' },
{
default: () => (nvr.deviceStatus === '10' ? '在线' : nvr.deviceStatus === '20' ? '离线' : '未知'),
},
);
},
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
device: nvr,
stationCode,
};
}),
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
device: nvrCluster,
stationCode,
};
}),
};
}
return {
label: stationName,
key: stationCode,
@@ -76,8 +128,7 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
},
);
},
isLeaf: true,
// TODO: 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
device,
stationCode,
})),
@@ -90,7 +141,8 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const selectedTab = ref<DeviceTypeCode>(DeviceType.Camera);
const selectedKeys = ref<string[]>([]);
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRouteQuery = (query: LocationQuery) => {
// 从路由参数中设置选中的设备
const setFromRouteQuery = (query: LocationQuery) => {
const { stationCode, deviceType, deviceId } = query;
let stnCode = '';
let devType: DeviceTypeCode = DeviceType.Camera;
@@ -102,18 +154,20 @@ const initFromRouteQuery = (query: LocationQuery) => {
selectedKeys.value = [devId];
const stnDevices = lineDevices.value?.[stnCode];
const devices = stnDevices?.[devType];
// 如果没找到,那还是赋值为原来选择的设备
selectedDevice.value = devices?.find((device) => device.deviceId === deviceId) ?? selectedDevice.value;
};
watch(
() => route.query,
(query) => initFromRouteQuery(query),
{ immediate: true },
);
watch(lineDevices, () => initFromRouteQuery(route.query), { immediate: true });
// 页面加载时,需要设置选中的设备
onMounted(() => setFromRouteQuery(route.query));
// 页面加载时设备数据可能不存在,因此当设备数据更新时,需要重新设置选中的设备
watch(lineDevices, () => setFromRouteQuery(route.query), { immediate: true });
// 更改选择的设备类型时,更新路由查询参数
watch(selectedTab, (newTab) => router.replace({ query: { ...route.query, deviceType: newTab } }));
// 点击设备时更新路由查询参数
const nodeProps = ({ option }: { option: TreeOption }) => {
return {
ondblclick: () => {
if (option.isLeaf) {
onclick: () => {
if (option['device']) {
const device = option['device'] as NdmDeviceResultVO;
selectedDevice.value = device;
router.replace({
@@ -128,7 +182,6 @@ const nodeProps = ({ option }: { option: TreeOption }) => {
},
};
};
watch(selectedTab, (newTab) => router.replace({ query: { ...route.query, deviceType: newTab } }));
const searchInput = ref('');
const statusInput = ref('');
@@ -149,28 +202,44 @@ const searchFilter = (pattern: string, node: TreeOption): boolean => {
const statusMatched = status === '' || status === deviceStatus;
return searchMatched && statusMatched;
};
// 定位到选中的设备
const deviceTreeInsts = useTemplateRef<TreeInst[]>('deviceTreeInsts');
const onClickLocateDeviceTree = () => {
selectedTab.value = (selectedDevice.value?.deviceType ?? selectedTab.value) as DeviceTypeCode;
// 等待Tab切换完成后再执行滚动
nextTick(() => {
const inst = deviceTreeInsts.value?.at(0);
inst?.scrollTo({ key: selectedDevice.value?.deviceId, behavior: 'smooth' });
});
};
</script>
<template>
<NLayout has-sider style="height: 100%">
<NLayoutSider bordered :width="540" :collapsed-width="0" show-trigger="arrow-circle">
<NLayoutSider bordered :width="540" :collapsed-width="0" show-trigger="bar">
<div style="height: 100%; display: flex; flex-direction: column">
<div style="flex-shrink: 0; padding: 12px">
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" />
<NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio>
<NRadio value="10">在线</NRadio>
<NRadio value="20">线</NRadio>
</NRadioGroup>
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
<NFlex justify="space-between" align="center">
<NRadioGroup v-model:value="statusInput">
<NRadio value="">全部</NRadio>
<NRadio value="10">线</NRadio>
<NRadio value="20">离线</NRadio>
</NRadioGroup>
<NButtonGroup>
<NButton text size="tiny" type="info" @click="onClickLocateDeviceTree">定位</NButton>
</NButtonGroup>
</NFlex>
</div>
<div style="flex: 1; min-height: 0">
<NTabs v-model:value="selectedTab" animated type="line" placement="left" style="height: 100%">
<NTabPane v-for="pane in deviceTabPanes" :key="pane.name" :name="pane.name" :tab="pane.tab">
<NTree
:ref="'deviceTreeInsts'"
v-model:selected-keys="selectedKeys"
:data="lineDeviceTreeData[pane.name]"
:override-default-node-click-behavior="override"
:node-props="nodeProps"
:show-irrelevant-nodes="false"
:pattern="searchPattern"
@@ -187,7 +256,7 @@ const searchFilter = (pattern: string, node: TreeOption): boolean => {
</div>
</div>
</NLayoutSider>
<NLayoutContent>
<NLayoutContent :content-style="{ 'padding-left': '24px' }">
<NPageHeader v-if="route.query['from']" title="" @back="onClickBack">
<template #back>
<NIcon>
@@ -197,10 +266,10 @@ const searchFilter = (pattern: string, node: TreeOption): boolean => {
</template>
</NPageHeader>
<div>selectedKeys: {{ selectedKeys }}</div>
<NScrollbar style="width: 500px; height: 400px">
<div>selectedDevice:</div>
<NScrollbar style="width: 500px; height: 400px; border: solid 1px salmon">
<pre>{{ selectedDevice }}</pre>
</NScrollbar>
<!-- <pre style="width: 500px; height: 100%; overflow: scroll">{{ lineDevices }}</pre> -->
</NLayoutContent>
</NLayout>
</template>