feat: 扩展设备树功能
- 支持控制是否同步路由参数 - 支持配置允许的事件类型 (select/manage) - 支持自定义设备节点前缀按钮文字 - 支持向外暴露设备选择逻辑 - 不再封装跳转设备逻辑,由外部实现 - 在车站模式下也支持选择设备
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
|
||||||
import { useDeviceTree } from '@/composables';
|
import { useDeviceTree, type UseDeviceTreeReturn } from '@/composables';
|
||||||
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
|
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
|
||||||
import { isNvrCluster } from '@/helpers';
|
import { isNvrCluster } from '@/helpers';
|
||||||
import { useDeviceStore, useStationStore } from '@/stores';
|
import { useDeviceStore, useStationStore } from '@/stores';
|
||||||
@@ -30,14 +30,33 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
import { computed, h, nextTick, onBeforeUnmount, ref, toRefs, useTemplateRef, watch, type CSSProperties } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
station?: Station; // 支持渲染指定车站的设备树
|
/**
|
||||||
|
* 支持渲染指定车站的设备树
|
||||||
|
*/
|
||||||
|
station?: Station;
|
||||||
|
/**
|
||||||
|
* 允许的事件类型
|
||||||
|
*
|
||||||
|
* - `select`:允许选择设备
|
||||||
|
* - `manage`:允许右键菜单管理设备
|
||||||
|
*/
|
||||||
|
events?: ('select' | 'manage')[];
|
||||||
|
/**
|
||||||
|
* 是否同步路由参数
|
||||||
|
*/
|
||||||
|
syncRoute?: boolean;
|
||||||
|
/**
|
||||||
|
* 设备节点的前缀按钮文字
|
||||||
|
*/
|
||||||
|
devicePrefixLabel?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
selectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
|
afterSelectDevice: [device: NdmDeviceResultVO, stationCode: Station['code']];
|
||||||
|
exposeSelectDeviceFn: [selectDeviceFn: UseDeviceTreeReturn['selectDevice']];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { station } = toRefs(props);
|
const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
@@ -47,22 +66,25 @@ const {
|
|||||||
selectedDeviceType,
|
selectedDeviceType,
|
||||||
selectedDevice,
|
selectedDevice,
|
||||||
selectDevice,
|
selectDevice,
|
||||||
routeDevice,
|
|
||||||
// 设备管理
|
// 设备管理
|
||||||
exportDevice,
|
exportDevice,
|
||||||
exportDeviceTemplate,
|
exportDeviceTemplate,
|
||||||
importDevice,
|
importDevice,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
} = useDeviceTree();
|
} = useDeviceTree({
|
||||||
|
syncRoute: computed(() => !!syncRoute.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 `selectDevice` 函数暴露给父组件
|
||||||
|
emit('exposeSelectDeviceFn', selectDevice);
|
||||||
|
|
||||||
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||||
selectDevice(device, stationCode);
|
// 仅当事件列表包含 `select` 时才触发选择事件
|
||||||
emit('selectDevice', device, stationCode);
|
if (!events.value) return;
|
||||||
};
|
if (!events.value.includes('select')) return;
|
||||||
|
|
||||||
const onRouteDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
selectDevice(device, stationCode);
|
||||||
routeDevice(device, stationCode, { path: '/device' });
|
emit('afterSelectDevice', device, stationCode);
|
||||||
emit('selectDevice', device, stationCode);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const stationStore = useStationStore();
|
||||||
@@ -187,20 +209,20 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
|
|||||||
onDblclick: (payload) => {
|
onDblclick: (payload) => {
|
||||||
if (option['device']) {
|
if (option['device']) {
|
||||||
payload.stopPropagation();
|
payload.stopPropagation();
|
||||||
|
|
||||||
const device = option['device'] as NdmDeviceResultVO;
|
const device = option['device'] as NdmDeviceResultVO;
|
||||||
const stationCode = option['stationCode'] as Station['code'];
|
const stationCode = option['stationCode'] as Station['code'];
|
||||||
// 区分是否需要跳转路由
|
|
||||||
// 当 props.station 存在时,说明当前是单独渲染车站的设备树,需要跳转路由到设备诊断页面
|
onSelectDevice(device, stationCode);
|
||||||
if (!station.value) {
|
|
||||||
onSelectDevice(device, stationCode);
|
|
||||||
} else {
|
|
||||||
onRouteDevice(device, stationCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onContextmenu: (payload) => {
|
onContextmenu: (payload) => {
|
||||||
payload.stopPropagation();
|
payload.stopPropagation();
|
||||||
payload.preventDefault();
|
payload.preventDefault();
|
||||||
|
|
||||||
|
// 仅当事件列表包含 `manage` 时才显示右键菜单
|
||||||
|
if (!events.value?.includes('manage')) return;
|
||||||
|
|
||||||
const { clientX, clientY } = payload;
|
const { clientX, clientY } = payload;
|
||||||
const stationCode = option['stationCode'] as Station['code'];
|
const stationCode = option['stationCode'] as Station['code'];
|
||||||
const deviceType = option['deviceType'] as DeviceType | undefined;
|
const deviceType = option['deviceType'] as DeviceType | undefined;
|
||||||
@@ -231,6 +253,7 @@ const renderIcmpStatistics = (onlineCount: number, offlineCount: number, count:
|
|||||||
};
|
};
|
||||||
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||||
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
|
const renderViewDeviceButton = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||||
|
if (!devicePrefixLabel.value) return null;
|
||||||
return h(
|
return h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
@@ -242,17 +265,11 @@ const renderDeviceNodePrefix = (device: NdmDeviceResultVO, stationCode: Station[
|
|||||||
} as CSSProperties,
|
} as CSSProperties,
|
||||||
onClick: (e: MouseEvent) => {
|
onClick: (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 选择设备
|
|
||||||
// 区分是否需要跳转路由
|
onSelectDevice(device, stationCode);
|
||||||
// 当 props.station 存在时,说明当前是单独渲染车站的设备树,需要跳转路由到设备诊断页面
|
|
||||||
if (!station.value) {
|
|
||||||
onSelectDevice(device, stationCode);
|
|
||||||
} else {
|
|
||||||
onRouteDevice(device, stationCode);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
() => '查看',
|
() => devicePrefixLabel.value,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
|
const renderDeviceStatusTag = (device: NdmDeviceResultVO) => {
|
||||||
@@ -529,6 +546,7 @@ watch(selectedDevice, async () => {
|
|||||||
virtual-scroll
|
virtual-scroll
|
||||||
:data="stationDeviceTreeData"
|
:data="stationDeviceTreeData"
|
||||||
:animated="animated"
|
:animated="animated"
|
||||||
|
:selected-keys="selectedKeys"
|
||||||
:show-irrelevant-nodes="false"
|
:show-irrelevant-nodes="false"
|
||||||
:pattern="searchPattern"
|
:pattern="searchPattern"
|
||||||
:filter="searchFilter"
|
:filter="searchFilter"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Station } from '@/apis';
|
import type { Station } from '@/apis';
|
||||||
import { DeviceTree } from '@/components';
|
import { DeviceTree, type DeviceTreeProps } from '@/components';
|
||||||
|
import { tryGetDeviceType } from '@/enums';
|
||||||
import { NModal } from 'naive-ui';
|
import { NModal } from 'naive-ui';
|
||||||
import { toRefs } from 'vue';
|
import { toRefs } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
station?: Station;
|
station?: Station;
|
||||||
@@ -10,13 +12,30 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const show = defineModel<boolean>('show', { default: false });
|
const show = defineModel<boolean>('show', { default: false });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { station } = toRefs(props);
|
const { station } = toRefs(props);
|
||||||
|
|
||||||
|
const onAfterSelectDevice: DeviceTreeProps['onAfterSelectDevice'] = (device, stationCode) => {
|
||||||
|
const deviceDbId = device.id;
|
||||||
|
const deviceType = tryGetDeviceType(device.deviceType);
|
||||||
|
router.push({
|
||||||
|
path: '/device',
|
||||||
|
query: {
|
||||||
|
stationCode,
|
||||||
|
deviceType,
|
||||||
|
deviceDbId,
|
||||||
|
fromPage: route.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :title="`${station?.name} - 设备详情`" :content-style="{ height: '100%', overflow: 'hidden' }">
|
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :title="`${station?.name} - 设备详情`" :content-style="{ height: '100%', overflow: 'hidden' }">
|
||||||
<template #default>
|
<template #default>
|
||||||
<DeviceTree :station="station" />
|
<DeviceTree :station="station" :events="['select', 'manage']" :device-prefix-label="'查看'" @after-select-device="onAfterSelectDevice" />
|
||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
|
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
|
||||||
|
import { useStationDevicesMutation } from '@/composables';
|
||||||
import { DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
|
import { DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
|
||||||
import { useDeviceStore, useStationStore } from '@/stores';
|
import { useDeviceStore, useStationStore } from '@/stores';
|
||||||
import { downloadByData, parseErrorFeedback } from '@/utils';
|
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { h, onBeforeUnmount } from 'vue';
|
import { h, onBeforeUnmount } from 'vue';
|
||||||
import { useStationDevicesMutation } from '../query';
|
|
||||||
import { isCancel } from 'axios';
|
|
||||||
|
|
||||||
export const useDeviceManagement = () => {
|
export const useDeviceManagement = () => {
|
||||||
const stationStore = useStationStore();
|
const stationStore = useStationStore();
|
||||||
@@ -182,3 +182,5 @@ export const useDeviceManagement = () => {
|
|||||||
deleteDevice,
|
deleteDevice,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UseDeviceManagementReturn = ReturnType<typeof useDeviceManagement>;
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
|
import type { LineDevices, NdmDeviceResultVO, Station } from '@/apis';
|
||||||
import { tryGetDeviceType, type DeviceType } from '@/enums';
|
import { tryGetDeviceType, type DeviceType } from '@/enums';
|
||||||
import { useDeviceStore } from '@/stores';
|
import { useDeviceStore } from '@/stores';
|
||||||
import { watchDebounced } from '@vueuse/core';
|
import { watchDebounced } from '@vueuse/core';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
export const useDeviceSelection = () => {
|
export const useDeviceSelection = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
|
||||||
|
const { syncRoute } = options ?? {};
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
const { lineDevices } = storeToRefs(deviceStore);
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
const selectedStationCode = ref<string>();
|
const selectedStationCode = ref<Station['code']>();
|
||||||
const selectedDeviceType = ref<DeviceType>();
|
const selectedDeviceType = ref<DeviceType>();
|
||||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||||
|
|
||||||
const initFromRoute = (lineDevices: LineDevices) => {
|
const initFromRoute = (lineDevices: LineDevices) => {
|
||||||
const { stationCode, deviceType, deviceDbId } = route.query;
|
const { stationCode, deviceType, deviceDbId } = route.query;
|
||||||
if (stationCode) {
|
if (stationCode) {
|
||||||
selectedStationCode.value = stationCode as string;
|
selectedStationCode.value = stationCode as Station['code'];
|
||||||
}
|
}
|
||||||
if (deviceType) {
|
if (deviceType) {
|
||||||
selectedDeviceType.value = deviceType as DeviceType;
|
selectedDeviceType.value = deviceType as DeviceType;
|
||||||
@@ -40,7 +42,7 @@ export const useDeviceSelection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
|
const selectDevice = (device: NdmDeviceResultVO, stationCode: Station['code']) => {
|
||||||
selectedDevice.value = device;
|
selectedDevice.value = device;
|
||||||
selectedStationCode.value = stationCode;
|
selectedStationCode.value = stationCode;
|
||||||
const deviceType = tryGetDeviceType(device.deviceType);
|
const deviceType = tryGetDeviceType(device.deviceType);
|
||||||
@@ -49,20 +51,6 @@ export const useDeviceSelection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
fromPage: route.path,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncToRoute = () => {
|
const syncToRoute = () => {
|
||||||
const query = { ...route.query };
|
const query = { ...route.query };
|
||||||
// 当选中的设备发生变化时,删除fromPage参数
|
// 当选中的设备发生变化时,删除fromPage参数
|
||||||
@@ -81,14 +69,20 @@ export const useDeviceSelection = () => {
|
|||||||
router.replace({ query });
|
router.replace({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(selectedDevice, syncToRoute);
|
watch(selectedDevice, () => {
|
||||||
|
if (toValue(syncRoute)) {
|
||||||
|
syncToRoute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
// lineDevices是shallowRef,因此需要深度侦听才能获取内部变化,
|
||||||
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
// 而单纯的深度侦听又可能会引发性能问题,因此尝试使用防抖侦听
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
lineDevices,
|
lineDevices,
|
||||||
(newLineDevices) => {
|
(newLineDevices) => {
|
||||||
initFromRoute(newLineDevices);
|
if (toValue(syncRoute)) {
|
||||||
|
initFromRoute(newLineDevices);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
debounce: 500,
|
debounce: 500,
|
||||||
@@ -97,7 +91,9 @@ export const useDeviceSelection = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initFromRoute(lineDevices.value);
|
if (toValue(syncRoute)) {
|
||||||
|
initFromRoute(lineDevices.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -107,6 +103,7 @@ export const useDeviceSelection = () => {
|
|||||||
|
|
||||||
initFromRoute,
|
initFromRoute,
|
||||||
selectDevice,
|
selectDevice,
|
||||||
routeDevice,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UseDeviceSelectionReturn = ReturnType<typeof useDeviceSelection>;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import type { MaybeRefOrGetter } from 'vue';
|
||||||
import { useDeviceManagement } from './use-device-management';
|
import { useDeviceManagement } from './use-device-management';
|
||||||
import { useDeviceSelection } from './use-device-selection';
|
import { useDeviceSelection } from './use-device-selection';
|
||||||
|
|
||||||
export const useDeviceTree = () => {
|
export const useDeviceTree = (options?: { syncRoute?: MaybeRefOrGetter<boolean> }) => {
|
||||||
const deviceSelection = useDeviceSelection();
|
const { syncRoute } = options ?? {};
|
||||||
|
|
||||||
|
const deviceSelection = useDeviceSelection({ syncRoute });
|
||||||
const deviceManagement = useDeviceManagement();
|
const deviceManagement = useDeviceManagement();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -10,3 +13,5 @@ export const useDeviceTree = () => {
|
|||||||
...deviceManagement,
|
...deviceManagement,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UseDeviceTreeReturn = ReturnType<typeof useDeviceTree>;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const { stations } = storeToRefs(stationStore);
|
|||||||
const selectedStation = ref<Station>();
|
const selectedStation = ref<Station>();
|
||||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||||
|
|
||||||
const onSelectDevice: DeviceTreeProps['onSelectDevice'] = (device, stationCode) => {
|
const onAfterSelectDevice: DeviceTreeProps['onAfterSelectDevice'] = (device, stationCode) => {
|
||||||
selectedDevice.value = device;
|
selectedDevice.value = device;
|
||||||
selectedStation.value = stations.value.find((station) => station.code === stationCode);
|
selectedStation.value = stations.value.find((station) => station.code === stationCode);
|
||||||
};
|
};
|
||||||
@@ -21,7 +21,7 @@ const onSelectDevice: DeviceTreeProps['onSelectDevice'] = (device, stationCode)
|
|||||||
<template>
|
<template>
|
||||||
<NLayout has-sider style="height: 100%">
|
<NLayout has-sider style="height: 100%">
|
||||||
<NLayoutSider bordered :width="600" :collapsed-width="0" show-trigger="bar">
|
<NLayoutSider bordered :width="600" :collapsed-width="0" show-trigger="bar">
|
||||||
<DeviceTree @select-device="onSelectDevice" />
|
<DeviceTree :events="['select', 'manage']" :sync-route="true" :device-prefix-label="'查看'" @after-select-device="onAfterSelectDevice" />
|
||||||
</NLayoutSider>
|
</NLayoutSider>
|
||||||
<NLayoutContent :content-style="{ padding: '8px 8px 8px 24px' }">
|
<NLayoutContent :content-style="{ padding: '8px 8px 8px 24px' }">
|
||||||
<template v-if="selectedStation && selectedDevice">
|
<template v-if="selectedStation && selectedDevice">
|
||||||
|
|||||||
Reference in New Issue
Block a user