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 +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,
};
};