feat: 设备树添加管理功能

- 新增设备导入、导出、删除功能及相关API
- 封装设备管理逻辑,拆分设备选择与设备管理逻辑
- 添加右键菜单支持设备管理操作
This commit is contained in:
yangsy
2025-12-17 13:50:26 +08:00
parent 073a29a83a
commit 03d5fb3fcd
13 changed files with 576 additions and 109 deletions

View File

@@ -1,11 +1,14 @@
import type { Nullable } from '@/types';
import type { NdmAlarmHost } from './alarm';
import type { NdmSecurityBox, NdmSwitch } from './other';
import type { NdmNvr } from './storage';
import type { NdmAlarmHost, NdmAlarmHostPageQuery } from './alarm';
import type { NdmSecurityBox, NdmSecurityBoxPageQuery, NdmSwitch, NdmSwitchPageQuery } from './other';
import type { NdmNvr, NdmNvrPageQuery } from './storage';
import type {
NdmCamera,
NdmCameraPageQuery,
NdmDecoder,
NdmDecoderPageQuery,
NdmKeyboard,
NdmKeyboardPageQuery,
NdmMediaServer,
NdmMediaServerPageQuery,
NdmMediaServerResultVO,
@@ -19,6 +22,16 @@ import type {
} from './video';
export type NdmDevice = NdmAlarmHost | NdmCamera | NdmDecoder | NdmKeyboard | NdmMediaServer | NdmNvr | NdmSecurityBox | NdmSwitch | NdmVideoServer;
export type NdmDevicePageQuery =
| NdmAlarmHostPageQuery
| NdmCameraPageQuery
| NdmDecoderPageQuery
| NdmKeyboardPageQuery
| NdmMediaServerPageQuery
| NdmNvrPageQuery
| NdmSecurityBoxPageQuery
| NdmSwitchPageQuery
| NdmVideoServerPageQuery;
export type NdmDeviceResultVO = Nullable<NdmDevice>;

View File

@@ -0,0 +1,13 @@
export interface ImportMsg {
wrongLines: WrongLine[];
wrongNum: number;
updateNum: number;
insertNum: number;
unchangedNum: number;
total: number;
}
export interface WrongLine {
rowNum: number;
msg: string;
}

View File

@@ -0,0 +1 @@
export * from './import-msg';

View File

@@ -1,3 +1,4 @@
export * from './base';
export * from './biz';
export * from './common';
export * from './system';

View File

@@ -0,0 +1,32 @@
import {
deleteAlarmHostApi,
deleteCameraApi,
deleteDecoderApi,
deleteKeyboardApi,
deleteMediaServerApi,
deleteNvrApi,
deleteSecurityBoxApi,
deleteSwitchApi,
deleteVideoServerApi,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const deleteDeviceApi = async (deviceType: DeviceType, id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: deleteAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: deleteCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: deleteDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: deleteKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: deleteMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: deleteNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: deleteSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: deleteSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: deleteVideoServerApi,
};
const deleteApi = apiRecord[deviceType];
if (!deleteApi) throw new Error('接口不存在');
return deleteApi([id], options);
};

View File

@@ -0,0 +1,34 @@
import {
exportAlarmHostApi,
exportCameraApi,
exportDecoderApi,
exportKeyboardApi,
exportMediaServerApi,
exportNvrApi,
exportSecurityBoxApi,
exportSwitchApi,
exportVideoServerApi,
type NdmDevicePageQuery,
type PageParams,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const exportDeviceApi = async (deviceType: DeviceType, pageQuery: PageParams<NdmDevicePageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: exportAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: exportCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: exportDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: exportKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: exportMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: exportNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: exportSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: exportSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: exportVideoServerApi,
};
const exportApi = apiRecord[deviceType];
if (!exportApi) throw new Error('接口不存在');
return exportApi(pageQuery, options);
};

View File

@@ -0,0 +1,32 @@
import {
importAlarmHostApi,
importCameraApi,
importDecoderApi,
importKeyboardApi,
importMediaServerApi,
importNvrApi,
importSecurityBoxApi,
importSwitchApi,
importVideoServerApi,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
export const importDeviceApi = async (deviceType: DeviceType, file: File, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const apiRecord = {
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: importAlarmHostApi,
[DEVICE_TYPE_LITERALS.ndmCamera]: importCameraApi,
[DEVICE_TYPE_LITERALS.ndmDecoder]: importDecoderApi,
[DEVICE_TYPE_LITERALS.ndmKeyboard]: importKeyboardApi,
[DEVICE_TYPE_LITERALS.ndmMediaServer]: importMediaServerApi,
[DEVICE_TYPE_LITERALS.ndmNvr]: importNvrApi,
[DEVICE_TYPE_LITERALS.ndmSecurityBox]: importSecurityBoxApi,
[DEVICE_TYPE_LITERALS.ndmSwitch]: importSwitchApi,
[DEVICE_TYPE_LITERALS.ndmVideoServer]: importVideoServerApi,
};
const importApi = apiRecord[deviceType];
if (!importApi) throw new Error('接口不存在');
return importApi(file, options);
};

View File

@@ -1,2 +1,5 @@
export * from './delete-device';
export * from './detail-device';
export * from './export-device';
export * from './import-device';
export * from './probe-device';

View File

@@ -5,10 +5,12 @@ import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceT
import { isNvrCluster } from '@/helpers';
import { useDeviceStore, useStationStore } from '@/stores';
import { sleep } from '@/utils';
import { watchDebounced, watchImmediate } from '@vueuse/core';
import { watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
import {
NButton,
NDropdown,
NFlex,
NInput,
NRadio,
@@ -18,6 +20,7 @@ import {
NTag,
NTree,
useThemeVars,
type DropdownOption,
type TagProps,
type TreeInst,
type TreeOption,
@@ -25,7 +28,7 @@ import {
type TreeProps,
} from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, onMounted, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
import { computed, h, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
const props = defineProps<{
station?: Station; // 支持渲染指定车站的设备树
@@ -39,7 +42,19 @@ const { station } = toRefs(props);
const themeVars = useThemeVars();
const { selectedStationCode, selectedDeviceType, selectedDevice, initFromRoute, selectDevice, routeDevice } = useDeviceTree();
const {
// 设备选择
selectedStationCode,
selectedDeviceType,
selectedDevice,
selectDevice,
routeDevice,
// 设备管理
exportDevice,
exportDeviceTemplate,
importDevice,
deleteDevice,
} = useDeviceTree();
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
selectDevice(device, stationCode);
@@ -56,23 +71,6 @@ 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],
@@ -91,6 +89,90 @@ watch([selectedKeys, selectedDevice, selectedStationCode], ([, 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;
@@ -116,6 +198,16 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
}
}
},
onContextmenu: (payload) => {
payload.stopPropagation();
payload.preventDefault();
const { clientX, clientY } = payload;
const stationCode = option['stationCode'] as Station['code'];
const deviceType = option['deviceType'] as DeviceType | undefined;
const device = option['device'] as NdmDeviceResultVO | undefined;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
showContextmenu.value = true;
},
};
};
@@ -137,7 +229,7 @@ const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count:
')',
]);
};
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string) => {
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
return h(
NButton,
@@ -170,7 +262,7 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: string)
return h(NFlex, { size: 'small' }, { default: () => [renderViewDeviceButton(device, stationCode), renderDeviceStatusTag(device)] });
};
// 全线设备树
const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
const lineDeviceTreeData = computed<Record<Station['code'], TreeOption[]>>(() => {
const treeData: Record<string, TreeOption[]> = {};
deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => {
treeData[paneName] = stations.value.map<TreeOption>((station) => {
@@ -217,6 +309,8 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
device: nvrCluster,
};
}),
stationCode,
deviceType: activeTab.value,
};
}
return {
@@ -237,6 +331,8 @@ const lineDeviceTreeData = computed<Record<string, TreeOption[]>>(() => {
device,
};
}) ?? [],
stationCode,
deviceType: activeTab.value,
};
});
});
@@ -278,6 +374,8 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
device,
};
}),
stationCode,
deviceType,
};
}
return {
@@ -294,6 +392,8 @@ const stationDeviceTreeData = computed<TreeOption[]>(() => {
device,
};
}),
stationCode,
deviceType,
};
});
});
@@ -370,7 +470,13 @@ async function scrollDeviceTreeToSelectedDevice() {
</NFlex>
</div>
<!-- 设备树 -->
<div style="overflow: hidden; flex: 1 1 auto; display: flex">
<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" animated type="line" placement="left" style="height: 100%">
@@ -415,6 +521,17 @@ async function scrollDeviceTreeToSelectedDevice() {
</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>

View File

@@ -1 +1,3 @@
export * from './use-device-management';
export * from './use-device-selection';
export * from './use-device-tree';

View File

@@ -0,0 +1,184 @@
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
import { DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
import { useDeviceStore, useStationStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { h, onBeforeUnmount } from 'vue';
import { useStationDevicesMutation } from '../query';
import { isCancel } from 'axios';
export const useDeviceManagement = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const { mutate: refreshStationDevices } = useStationDevicesMutation();
// 导出设备
const { mutate: exportDevice } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) throw new Error(`该车站没有${deviceTypeName}`);
const devices = stationDevices[deviceType];
const pageQuery: PageParams<NdmDevicePageQuery> = {
model: {},
extra: {},
current: 1,
size: devices.length,
sort: 'id',
order: 'descending',
};
window.$loadingBar.start();
const data = await exportDeviceApi(deviceType, pageQuery, { stationCode, signal });
return data;
},
onSuccess: (data, { deviceType, stationCode }) => {
window.$loadingBar.finish();
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const stationName = stations.value.find((station) => station.code === stationCode)?.name ?? '';
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
downloadByData(data, `${stationName}_${deviceTypeName}列表_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 下载设备导入模板
// FIXME: 采用导出空列表的方案但是后端生成的xlsx中会多一行空行如果直接再导入该文件就会多导入一个空设备
const { mutate: exportDeviceTemplate } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const pageQuery: PageParams<NdmDevicePageQuery> = {
model: {},
extra: {},
current: 1,
size: 0,
sort: 'id',
order: 'descending',
};
window.$loadingBar.start();
const data = await exportDeviceApi(deviceType, pageQuery, { stationCode, signal });
return data;
},
onSuccess: (data, { deviceType, stationCode }) => {
window.$loadingBar.finish();
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
const stationName = stations.value.find((station) => station.code === stationCode)?.name ?? '';
const deviceTypeName = DEVICE_TYPE_NAMES[deviceType];
downloadByData(data, `${stationName}_${deviceTypeName}导入模板_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 导入设备
const { mutate: importDevice } = useMutation({
mutationFn: async (params: { deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { deviceType, stationCode, signal } = params;
const data = await new Promise<ImportMsg>((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.xlsx';
fileInput.click();
fileInput.onchange = async () => {
const file = fileInput.files?.[0];
// console.log(file);
if (!file) {
window.$message.error('导入失败');
return;
}
window.$loadingBar.start();
const data = await importDeviceApi(deviceType, file, { stationCode, signal });
resolve(data);
};
});
return data;
},
onSuccess: (data, { stationCode, signal }) => {
window.$loadingBar.finish();
window.$dialog.success({
title: '导入成功',
content: () => {
return h('div', {}, [
h('p', {}, `新增数据:${data.insertNum}`),
h('p', {}, `更新数据:${data.updateNum}`),
h('p', {}, `不变数据:${data.unchangedNum}`),
h('p', {}, `错误数据:${data.wrongNum}`),
data.wrongLines.map((line) => h('p', { style: { 'margin-left': '8px' } }, `${line.rowNum}行:${line.msg}`)),
]);
},
});
const station = stations.value.find((station) => station.code === stationCode);
if (station) {
refreshStationDevices({ station, signal });
}
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 删除设备
const { mutate: deleteDevice } = useMutation({
mutationFn: async (params: { id: string; deviceType: DeviceType; stationCode: Station['code']; signal?: AbortSignal }) => {
const { id, deviceType, stationCode, signal } = params;
window.$loadingBar.start();
return await deleteDeviceApi(deviceType, id, { stationCode, signal });
},
onSuccess: (_, { stationCode, signal }) => {
window.$loadingBar.finish();
window.$message.success('删除成功');
const station = stations.value.find((station) => station.code === stationCode);
if (station) {
refreshStationDevices({ station, signal });
}
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
onBeforeUnmount(() => {
window.$loadingBar.finish();
});
return {
exportDevice,
exportDeviceTemplate,
importDevice,
deleteDevice,
};
};

View File

@@ -0,0 +1,112 @@
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { watchDebounced } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export const useDeviceSelection = () => {
const route = useRoute();
const router = useRouter();
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
const selectedStationCode = ref<string>();
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
selectedStationCode.value = stationCode as string;
}
if (deviceType) {
selectedDeviceType.value = deviceType as DeviceType;
}
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value];
if (devices) {
const device = devices.find((device) => device.id === selectedDeviceDbId);
if (device) {
selectedDevice.value = device;
}
}
}
}
};
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
selectedDevice.value = device;
selectedStationCode.value = stationCode;
const deviceType = tryGetDeviceType(device.deviceType);
if (deviceType) {
selectedDeviceType.value = deviceType;
}
};
const routeDevice = (device: NdmDeviceResultVO, stationCode: string, to: { path: string }) => {
const deviceDbId = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
router.push({
path: to.path,
query: {
stationCode,
deviceType,
deviceDbId,
from: route.path,
},
});
};
const syncToRoute = () => {
const query = { ...route.query };
// 当选中的设备发生变化时删除from参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
delete query['from'];
}
if (selectedStationCode.value) {
query['stationCode'] = selectedStationCode.value;
}
if (selectedDeviceType.value) {
query['deviceType'] = selectedDeviceType.value;
}
if (selectedDevice.value?.id) {
query['deviceDbId'] = selectedDevice.value.id;
}
router.replace({ query });
};
watch(selectedDevice, syncToRoute);
// lineDevices是shallowRef因此需要深度侦听才能获取内部变化
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
watchDebounced(
lineDevices,
(newLineDevices) => {
initFromRoute(newLineDevices);
},
{
debounce: 500,
deep: true,
},
);
onMounted(() => {
initFromRoute(lineDevices.value);
});
return {
selectedStationCode,
selectedDeviceType,
selectedDevice,
initFromRoute,
selectDevice,
routeDevice,
};
};

View File

@@ -1,89 +1,12 @@
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
import { tryGetDeviceType, type DeviceType } from '@/enums';
import { ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useDeviceManagement } from './use-device-management';
import { useDeviceSelection } from './use-device-selection';
export const useDeviceTree = () => {
const route = useRoute();
const router = useRouter();
const selectedStationCode = ref<string>();
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
selectedStationCode.value = stationCode as string;
}
if (deviceType) {
selectedDeviceType.value = deviceType as DeviceType;
}
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
const selectedDeviceDbId = deviceDbId as string;
const stationDevices = lineDevices[selectedStationCode.value];
if (stationDevices) {
const devices = stationDevices[selectedDeviceType.value];
if (devices) {
const device = devices.find((device) => device.id === selectedDeviceDbId);
if (device) {
selectedDevice.value = device;
}
}
}
}
};
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
selectedDevice.value = device;
selectedStationCode.value = stationCode;
const deviceType = tryGetDeviceType(device.deviceType);
if (deviceType) {
selectedDeviceType.value = deviceType;
}
};
const routeDevice = (device: NdmDeviceResultVO, stationCode: string, to: { path: string }) => {
const deviceDbId = device.id;
const deviceType = tryGetDeviceType(device.deviceType);
router.push({
path: to.path,
query: {
stationCode,
deviceType,
deviceDbId,
from: route.path,
},
});
};
const syncToRoute = () => {
const query = { ...route.query };
// 当选中的设备发生变化时删除from参数
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
delete query['from'];
}
if (selectedStationCode.value) {
query['stationCode'] = selectedStationCode.value;
}
if (selectedDeviceType.value) {
query['deviceType'] = selectedDeviceType.value;
}
if (selectedDevice.value?.id) {
query['deviceDbId'] = selectedDevice.value.id;
}
router.replace({ query });
};
watch(selectedDevice, syncToRoute);
const deviceSelection = useDeviceSelection();
const deviceManagement = useDeviceManagement();
return {
selectedStationCode,
selectedDeviceType,
selectedDevice,
initFromRoute,
selectDevice,
routeDevice,
...deviceSelection,
...deviceManagement,
};
};