diff --git a/src/apis/model/biz/entity/index.ts b/src/apis/model/biz/entity/index.ts index b75a782..5e55031 100644 --- a/src/apis/model/biz/entity/index.ts +++ b/src/apis/model/biz/entity/index.ts @@ -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; diff --git a/src/apis/model/common/import-msg.ts b/src/apis/model/common/import-msg.ts new file mode 100644 index 0000000..69318bd --- /dev/null +++ b/src/apis/model/common/import-msg.ts @@ -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; +} diff --git a/src/apis/model/common/index.ts b/src/apis/model/common/index.ts new file mode 100644 index 0000000..c65eff6 --- /dev/null +++ b/src/apis/model/common/index.ts @@ -0,0 +1 @@ +export * from './import-msg'; diff --git a/src/apis/model/index.ts b/src/apis/model/index.ts index 10cbc67..2171903 100644 --- a/src/apis/model/index.ts +++ b/src/apis/model/index.ts @@ -1,3 +1,4 @@ export * from './base'; export * from './biz'; +export * from './common'; export * from './system'; diff --git a/src/apis/request/biz/composed/delete-device.ts b/src/apis/request/biz/composed/delete-device.ts new file mode 100644 index 0000000..c2077ce --- /dev/null +++ b/src/apis/request/biz/composed/delete-device.ts @@ -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); +}; diff --git a/src/apis/request/biz/composed/export-device.ts b/src/apis/request/biz/composed/export-device.ts new file mode 100644 index 0000000..0033661 --- /dev/null +++ b/src/apis/request/biz/composed/export-device.ts @@ -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, 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); +}; diff --git a/src/apis/request/biz/composed/import-device.ts b/src/apis/request/biz/composed/import-device.ts new file mode 100644 index 0000000..de2a34f --- /dev/null +++ b/src/apis/request/biz/composed/import-device.ts @@ -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); +}; diff --git a/src/apis/request/biz/composed/index.ts b/src/apis/request/biz/composed/index.ts index ce99900..ebe61f1 100644 --- a/src/apis/request/biz/composed/index.ts +++ b/src/apis/request/biz/composed/index.ts @@ -1,2 +1,5 @@ +export * from './delete-device'; export * from './detail-device'; +export * from './export-device'; +export * from './import-device'; export * from './probe-device'; diff --git a/src/components/device/device-tree/device-tree.vue b/src/components/device/device-tree/device-tree.vue index 1b1965d..b62e4d5 100644 --- a/src/components/device/device-tree/device-tree.vue +++ b/src/components/device/device-tree/device-tree.vue @@ -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(() => [ + { + 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>(() => { +const lineDeviceTreeData = computed>(() => { const treeData: Record = {}; deviceTabPanes.forEach(({ name: paneName /* , tab: paneTab */ }) => { treeData[paneName] = stations.value.map((station) => { @@ -217,6 +309,8 @@ const lineDeviceTreeData = computed>(() => { device: nvrCluster, }; }), + stationCode, + deviceType: activeTab.value, }; } return { @@ -237,6 +331,8 @@ const lineDeviceTreeData = computed>(() => { device, }; }) ?? [], + stationCode, + deviceType: activeTab.value, }; }); }); @@ -278,6 +374,8 @@ const stationDeviceTreeData = computed(() => { device, }; }), + stationCode, + deviceType, }; } return { @@ -294,6 +392,8 @@ const stationDeviceTreeData = computed(() => { device, }; }), + stationCode, + deviceType, }; }); }); @@ -370,7 +470,13 @@ async function scrollDeviceTreeToSelectedDevice() { -
+
+ + diff --git a/src/composables/device/index.ts b/src/composables/device/index.ts index 95da067..5b525eb 100644 --- a/src/composables/device/index.ts +++ b/src/composables/device/index.ts @@ -1 +1,3 @@ +export * from './use-device-management'; +export * from './use-device-selection'; export * from './use-device-tree'; diff --git a/src/composables/device/use-device-management.ts b/src/composables/device/use-device-management.ts new file mode 100644 index 0000000..6545b0a --- /dev/null +++ b/src/composables/device/use-device-management.ts @@ -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 = { + 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 = { + 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((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, + }; +}; diff --git a/src/composables/device/use-device-selection.ts b/src/composables/device/use-device-selection.ts new file mode 100644 index 0000000..6751666 --- /dev/null +++ b/src/composables/device/use-device-selection.ts @@ -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(); + const selectedDeviceType = ref(); + const selectedDevice = ref(); + + 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, + }; +}; diff --git a/src/composables/device/use-device-tree.ts b/src/composables/device/use-device-tree.ts index 3443606..d460389 100644 --- a/src/composables/device/use-device-tree.ts +++ b/src/composables/device/use-device-tree.ts @@ -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(); - const selectedDeviceType = ref(); - const selectedDevice = ref(); - - 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, }; };