feat: 设备树添加管理功能
- 新增设备导入、导出、删除功能及相关API - 封装设备管理逻辑,拆分设备选择与设备管理逻辑 - 添加右键菜单支持设备管理操作
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user