feat: migrate id and improve interaction

- use DB id of device to mark tree node key
- dblclick offlien device node to jump to DevicePage
- dblclick device node to view device detail
This commit is contained in:
yangsy
2025-08-18 11:05:26 +08:00
parent 104400500e
commit 4e14eb3590
2 changed files with 87 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { NInput, NModal, NTree } from 'naive-ui'; import { NButton, NInput, NModal, NTree } from 'naive-ui';
import { computed, ref, toRefs, watch } from 'vue'; import { computed, ref, toRefs, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import type { TreeOption, TreeOverrideNodeClickBehavior } from 'naive-ui'; import type { TreeOption, TreeOverrideNodeClickBehavior } from 'naive-ui';
@@ -17,6 +17,7 @@ import type {
import { useQueryControlStore } from '@/stores/query-control'; import { useQueryControlStore } from '@/stores/query-control';
import { DeviceType, DeviceTypeName } from '@/enums/device-type'; import { DeviceType, DeviceTypeName } from '@/enums/device-type';
import type { Station } from '@/apis/domains'; import type { Station } from '@/apis/domains';
import { h } from 'vue';
interface Props { interface Props {
station: Station; station: Station;
@@ -51,8 +52,8 @@ watch(show, (newValue) => {
const searchPattern = ref(''); const searchPattern = ref('');
const searchFilter: (pattern: string, node: TreeOption) => boolean = (pattern, node) => { const searchFilter: (pattern: string, node: TreeOption) => boolean = (pattern, node) => {
const device = node['device'] as NdmDeviceVO; const device = node['device'] as NdmDeviceVO | undefined;
const { name, ipAddress } = device; const { name, ipAddress } = device ?? {};
return (name ?? '').includes(pattern) || (ipAddress ?? '').includes(pattern); return (name ?? '').includes(pattern) || (ipAddress ?? '').includes(pattern);
}; };
@@ -75,12 +76,38 @@ const treeData = computed<TreeOption[]>(() => {
label: `${device.name}`, label: `${device.name}`,
key: device.id, key: device.id,
suffix: () => `${device.ipAddress ?? '未知IP地址'}`, suffix: () => `${device.ipAddress ?? '未知IP地址'}`,
prefix: () => {
return h(
NButton,
{
text: true,
size: 'tiny',
type: 'info',
onClick: () => {
const queryControlStore = useQueryControlStore();
queryControlStore.enablePolling();
const dev = device as NdmDeviceVO;
router.push({
path: '/device',
query: {
stationCode: station.value.code,
deviceType: dev.deviceType,
deviceDBId: dev.id,
from: route.path,
},
});
},
},
{ default: () => h('span', '查看') },
);
},
device, device,
})), })),
}; };
}); });
}); });
// 双击设备节点,跳转到`实时设备状`态页面
const nodeProps = ({ option }: { option: TreeOption }) => { const nodeProps = ({ option }: { option: TreeOption }) => {
return { return {
ondblclick: () => { ondblclick: () => {
@@ -93,7 +120,7 @@ const nodeProps = ({ option }: { option: TreeOption }) => {
query: { query: {
stationCode: station.value.code, stationCode: station.value.code,
deviceType: device.deviceType, deviceType: device.deviceType,
deviceId: device.deviceId, deviceDBId: device.id,
from: route.path, from: route.path,
}, },
}); });
@@ -103,7 +130,7 @@ const nodeProps = ({ option }: { option: TreeOption }) => {
}; };
const override: TreeOverrideNodeClickBehavior = ({ option }) => { const override: TreeOverrideNodeClickBehavior = ({ option }) => {
if (option.children) { if (!option['device']) {
return 'toggleExpand'; return 'toggleExpand';
} }
return 'default'; return 'default';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NdmDeviceResultVO, NdmDeviceVO, NdmNvrResultVO } from '@/apis/models/device'; import type { NdmDeviceResultVO, NdmNvrResultVO } from '@/apis/models/device';
import { useLineDevicesQuery } from '@/composables/query/use-line-devices-query'; import { useLineDevicesQuery } from '@/composables/query/use-line-devices-query';
import { DeviceType, DeviceTypeName, type DeviceTypeCode, type DeviceTypeKey } from '@/enums/device-type'; import { DeviceType, DeviceTypeName, type DeviceTypeCode, type DeviceTypeKey } from '@/enums/device-type';
import { useStationStore } from '@/stores/station'; import { useStationStore } from '@/stores/station';
@@ -21,6 +21,7 @@ import {
NTabs, NTabs,
NTag, NTag,
NTree, NTree,
type TagProps,
type TreeInst, type TreeInst,
type TreeOption, type TreeOption,
} from 'naive-ui'; } from 'naive-ui';
@@ -70,16 +71,10 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
.map<TreeOption>((nvrCluster) => { .map<TreeOption>((nvrCluster) => {
return { return {
label: `${nvrCluster.name}`, label: `${nvrCluster.name}`,
key: nvrCluster.deviceId, key: nvrCluster.id,
suffix: () => `${nvrCluster.ipAddress}`, suffix: () => `${nvrCluster.ipAddress}`,
prefix: () => { prefix: () => {
return h( return renderDeviceNodePrefix(nvrCluster, stationCode);
NTag,
{ type: nvrCluster.deviceStatus === '10' ? 'success' : nvrCluster.deviceStatus === '20' ? 'error' : 'warning', size: 'tiny' },
{
default: () => (nvrCluster.deviceStatus === '10' ? '在线' : nvrCluster.deviceStatus === '20' ? '离线' : '未知'),
},
);
}, },
children: nvrs children: nvrs
.filter((nvr) => { .filter((nvr) => {
@@ -88,16 +83,10 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
.map<TreeOption>((nvr) => { .map<TreeOption>((nvr) => {
return { return {
label: `${nvr.name}`, label: `${nvr.name}`,
key: nvr.deviceId, key: nvr.id,
suffix: () => `${nvr.ipAddress}`, suffix: () => `${nvr.ipAddress}`,
prefix: () => { prefix: () => {
return h( return renderDeviceNodePrefix(nvrCluster, stationCode);
NTag,
{ type: nvr.deviceStatus === '10' ? 'success' : nvr.deviceStatus === '20' ? 'error' : 'warning', size: 'tiny' },
{
default: () => (nvr.deviceStatus === '10' ? '在线' : nvr.deviceStatus === '20' ? '离线' : '未知'),
},
);
}, },
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
device: nvr, device: nvr,
@@ -117,16 +106,10 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
suffix: () => `(${onlineDevices?.length ?? 0}/${offlineDevices?.length ?? 0}/${devices?.length ?? 0})`, suffix: () => `(${onlineDevices?.length ?? 0}/${offlineDevices?.length ?? 0}/${devices?.length ?? 0})`,
children: lineDevices.value?.[stationCode][paneName].map<TreeOption>((device) => ({ children: lineDevices.value?.[stationCode][paneName].map<TreeOption>((device) => ({
label: `${device.name}`, label: `${device.name}`,
key: device.deviceId, key: device.id,
suffix: () => `${device.ipAddress}`, suffix: () => `${device.ipAddress}`,
prefix: () => { prefix: () => {
return h( return renderDeviceNodePrefix(device, stationCode);
NTag,
{ type: device.deviceStatus === '10' ? 'success' : device.deviceStatus === '20' ? 'error' : 'warning', size: 'tiny' },
{
default: () => (device.deviceStatus === '10' ? '在线' : device.deviceStatus === '20' ? '离线' : '未知'),
},
);
}, },
// 当选择设备时,能获取到设备的所有信息,以及设备所属的车站 // 当选择设备时,能获取到设备的所有信息,以及设备所属的车站
device, device,
@@ -137,25 +120,56 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
}); });
return treeData; return treeData;
}); });
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
const { deviceStatus } = device;
const tagType: TagProps['type'] = deviceStatus === '10' ? 'success' : deviceStatus === '20' ? 'error' : 'warning';
const tagText = device.deviceStatus === '10' ? '在线' : device.deviceStatus === '20' ? '离线' : '未知';
return h(NTag, { type: tagType, size: 'tiny' }, () => tagText);
};
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
return h(
NButton,
{
text: true,
size: 'tiny',
type: 'info',
onClick: () => {
selectedDevice.value = device; // 更新选中的设备
router.replace({
query: {
...route.query,
stationCode: stationCode,
deviceType: selectedTab.value,
deviceDBId: device.id,
},
});
},
},
() => '查看',
);
};
return h('div', [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)]);
};
const selectedTab = ref<DeviceTypeCode>(DeviceType.Camera); const selectedTab = ref<DeviceTypeCode>(DeviceType.Camera);
const selectedKeys = ref<string[]>([]); const selectedKeys = ref<string[]>();
const selectedDevice = ref<NdmDeviceResultVO>(); const selectedDevice = ref<NdmDeviceResultVO>();
// 从路由参数中设置选中的设备 // 从路由参数中设置选中的设备
const setFromRouteQuery = (query: LocationQuery) => { const setFromRouteQuery = (query: LocationQuery) => {
const { stationCode, deviceType, deviceId } = query; const { stationCode, deviceType, deviceDBId } = query;
let stnCode = ''; let stnCode = '';
let devType: DeviceTypeCode = DeviceType.Camera; let devType: DeviceTypeCode = DeviceType.Camera;
let devId = ''; let devDBId = '';
if (stationCode) stnCode = stationCode as string; if (stationCode) stnCode = stationCode as string;
if (deviceType) devType = deviceType as DeviceTypeCode; if (deviceType) devType = deviceType as DeviceTypeCode;
if (deviceId) devId = deviceId as string; if (deviceDBId) devDBId = deviceDBId as string;
selectedTab.value = devType; selectedTab.value = devType;
selectedKeys.value = [devId]; selectedKeys.value = [devDBId];
const stnDevices = lineDevices.value?.[stnCode]; const stnDevices = lineDevices.value?.[stnCode];
const devices = stnDevices?.[devType]; const devices = stnDevices?.[devType];
// 如果没找到,那还是赋值为原来选择的设备 // 如果没找到,那还是赋值为原来选择的设备
selectedDevice.value = devices?.find((device) => device.deviceId === deviceId) ?? selectedDevice.value; selectedDevice.value = devices?.find((device) => device.id === devDBId) ?? selectedDevice.value;
}; };
// 页面加载时,需要设置选中的设备 // 页面加载时,需要设置选中的设备
onMounted(() => setFromRouteQuery(route.query)); onMounted(() => setFromRouteQuery(route.query));
@@ -163,19 +177,20 @@ onMounted(() => setFromRouteQuery(route.query));
watch(lineDevices, () => setFromRouteQuery(route.query), { immediate: true }); watch(lineDevices, () => setFromRouteQuery(route.query), { immediate: true });
// 更改选择的设备类型时,更新路由查询参数 // 更改选择的设备类型时,更新路由查询参数
watch(selectedTab, (newTab) => router.replace({ query: { ...route.query, deviceType: newTab } })); watch(selectedTab, (newTab) => router.replace({ query: { ...route.query, deviceType: newTab } }));
// 击设备时更新路由查询参数 // 击设备时更新路由查询参数
const nodeProps = ({ option }: { option: TreeOption }) => { const nodeProps = ({ option }: { option: TreeOption }) => {
return { return {
onclick: () => { ondblclick: () => {
if (option['device']) { if (option['device']) {
const device = option['device'] as NdmDeviceResultVO; const device = option['device'] as NdmDeviceResultVO;
selectedDevice.value = device; selectedKeys.value = device.id ? [device.id] : undefined; // 更新选中的树节点
selectedDevice.value = device; // 更新选中的设备
router.replace({ router.replace({
query: { query: {
...route.query, ...route.query,
stationCode: option['stationCode'] as string, stationCode: option['stationCode'] as string,
deviceType: selectedTab.value, deviceType: selectedTab.value,
deviceId: device.deviceId, deviceDBId: device.id,
}, },
}); });
} }
@@ -196,7 +211,7 @@ const searchPattern = computed(() => {
}); });
const searchFilter = (pattern: string, node: TreeOption): boolean => { const searchFilter = (pattern: string, node: TreeOption): boolean => {
const { search, status } = destr<{ search: string; status: string }>(pattern); const { search, status } = destr<{ search: string; status: string }>(pattern);
const device = node['device'] as NdmDeviceVO | undefined; const device = node['device'] as NdmDeviceResultVO | undefined;
const { name, ipAddress, deviceStatus } = device ?? {}; const { name, ipAddress, deviceStatus } = device ?? {};
const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search); const searchMatched = (name ?? '').includes(search) || (ipAddress ?? '').includes(search);
const statusMatched = status === '' || status === deviceStatus; const statusMatched = status === '' || status === deviceStatus;
@@ -210,14 +225,14 @@ const onClickLocateDeviceTree = () => {
// 等待Tab切换完成后再执行滚动 // 等待Tab切换完成后再执行滚动
nextTick(() => { nextTick(() => {
const inst = deviceTreeInsts.value?.at(0); const inst = deviceTreeInsts.value?.at(0);
inst?.scrollTo({ key: selectedDevice.value?.deviceId, behavior: 'smooth' }); inst?.scrollTo({ key: selectedDevice.value?.id, behavior: 'smooth' });
}); });
}; };
</script> </script>
<template> <template>
<NLayout has-sider style="height: 100%"> <NLayout has-sider style="height: 100%">
<NLayoutSider bordered :width="540" :collapsed-width="0" show-trigger="bar"> <NLayoutSider bordered :width="600" :collapsed-width="0" show-trigger="bar">
<div style="height: 100%; display: flex; flex-direction: column"> <div style="height: 100%; display: flex; flex-direction: column">
<div style="flex-shrink: 0; padding: 12px"> <div style="flex-shrink: 0; padding: 12px">
<NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable /> <NInput v-model:value="searchInput" placeholder="搜索设备名称或IP地址" clearable />
@@ -256,7 +271,7 @@ const onClickLocateDeviceTree = () => {
</div> </div>
</div> </div>
</NLayoutSider> </NLayoutSider>
<NLayoutContent :content-style="{ 'padding-left': '24px' }"> <NLayoutContent :content-style="{ 'padding-left': '24px', 'padding-top': '6px' }">
<NPageHeader v-if="route.query['from']" title="" @back="onClickBack"> <NPageHeader v-if="route.query['from']" title="" @back="onClickBack">
<template #back> <template #back>
<NIcon> <NIcon>
@@ -266,7 +281,7 @@ const onClickLocateDeviceTree = () => {
</template> </template>
</NPageHeader> </NPageHeader>
<div>selectedKeys: {{ selectedKeys }}</div> <div>selectedKeys: {{ selectedKeys }}</div>
<div>selectedDevice:</div> <div>selectedDevice: {{ selectedDevice?.deviceId }}</div>
<NScrollbar style="width: 500px; height: 400px; border: solid 1px salmon"> <NScrollbar style="width: 500px; height: 400px; border: solid 1px salmon">
<pre>{{ selectedDevice }}</pre> <pre>{{ selectedDevice }}</pre>
</NScrollbar> </NScrollbar>