feat: 设备树添加管理功能
- 新增设备导入、导出、删除功能及相关API - 封装设备管理逻辑,拆分设备选择与设备管理逻辑 - 添加右键菜单支持设备管理操作
This commit is contained in:
@@ -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>;
|
||||
|
||||
|
||||
13
src/apis/model/common/import-msg.ts
Normal file
13
src/apis/model/common/import-msg.ts
Normal 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;
|
||||
}
|
||||
1
src/apis/model/common/index.ts
Normal file
1
src/apis/model/common/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './import-msg';
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './base';
|
||||
export * from './biz';
|
||||
export * from './common';
|
||||
export * from './system';
|
||||
|
||||
32
src/apis/request/biz/composed/delete-device.ts
Normal file
32
src/apis/request/biz/composed/delete-device.ts
Normal 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);
|
||||
};
|
||||
34
src/apis/request/biz/composed/export-device.ts
Normal file
34
src/apis/request/biz/composed/export-device.ts
Normal 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);
|
||||
};
|
||||
32
src/apis/request/biz/composed/import-device.ts
Normal file
32
src/apis/request/biz/composed/import-device.ts
Normal 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);
|
||||
};
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './delete-device';
|
||||
export * from './detail-device';
|
||||
export * from './export-device';
|
||||
export * from './import-device';
|
||||
export * from './probe-device';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './use-device-management';
|
||||
export * from './use-device-selection';
|
||||
export * from './use-device-tree';
|
||||
|
||||
184
src/composables/device/use-device-management.ts
Normal file
184
src/composables/device/use-device-management.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
112
src/composables/device/use-device-selection.ts
Normal file
112
src/composables/device/use-device-selection.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user