feat: 设备关联与解除关联
- 支持配置交换机端口的下游关联设备 - 支持配置安防箱电路的下游关联设备 - 支持解除关联 - 删除设备时校验是否存在上/下游设备
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
export * from './diag';
|
export * from './diag';
|
||||||
|
export * from './link-description';
|
||||||
export * from './station';
|
export * from './station';
|
||||||
|
|||||||
10
src/apis/domain/biz/link-description/index.ts
Normal file
10
src/apis/domain/biz/link-description/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NdmCameraLinkDescription } from './ndm-camera-link-description';
|
||||||
|
import type { NdmSecurityBoxLinkDescription } from './ndm-security-box-link-description';
|
||||||
|
import type { NdmSwitchLinkDescription } from './ndm-switch-link-description';
|
||||||
|
|
||||||
|
export * from './link-description';
|
||||||
|
export * from './ndm-camera-link-description';
|
||||||
|
export * from './ndm-security-box-link-description';
|
||||||
|
export * from './ndm-switch-link-description';
|
||||||
|
|
||||||
|
export type NdmDeviceLinkDescription = NdmCameraLinkDescription | NdmSecurityBoxLinkDescription | NdmSwitchLinkDescription;
|
||||||
5
src/apis/domain/biz/link-description/link-description.ts
Normal file
5
src/apis/domain/biz/link-description/link-description.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { DeviceStoreIndex } from '@/apis';
|
||||||
|
|
||||||
|
export interface LinkDescription {
|
||||||
|
upstream?: DeviceStoreIndex[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { LinkDescription } from './link-description';
|
||||||
|
|
||||||
|
export interface NdmCameraLinkDescription extends LinkDescription {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { DeviceStoreIndex } from '@/apis';
|
||||||
|
import type { LinkDescription } from './link-description';
|
||||||
|
|
||||||
|
export interface NdmSecurityBoxLinkDescription extends LinkDescription {
|
||||||
|
downstream?: {
|
||||||
|
[circuitIndex: number]: DeviceStoreIndex;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { DeviceStoreIndex } from '@/apis';
|
||||||
|
import type { LinkDescription } from './link-description';
|
||||||
|
|
||||||
|
export interface NdmSwitchLinkDescription extends LinkDescription {
|
||||||
|
downstream?: {
|
||||||
|
[portName: string]: DeviceStoreIndex;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,13 @@ import type {
|
|||||||
NdmVideoServerResultVO,
|
NdmVideoServerResultVO,
|
||||||
Station,
|
Station,
|
||||||
} from '@/apis';
|
} from '@/apis';
|
||||||
import { DEVICE_TYPE_LITERALS } from '@/enums';
|
import { DEVICE_TYPE_LITERALS, type DeviceType } from '@/enums';
|
||||||
|
|
||||||
|
export interface DeviceStoreIndex {
|
||||||
|
stationCode: Station['code'];
|
||||||
|
deviceType: DeviceType;
|
||||||
|
deviceDbId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StationDevices {
|
export interface StationDevices {
|
||||||
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: NdmAlarmHostResultVO[];
|
[DEVICE_TYPE_LITERALS.ndmAlarmHost]: NdmAlarmHostResultVO[];
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { detailDeviceApi, probeDeviceApi, type NdmDeviceResultVO, type Station } from '@/apis';
|
import { detailDeviceApi, probeDeviceApi, type LinkDescription, type NdmDeviceResultVO, type Station } from '@/apis';
|
||||||
import { DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enums';
|
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
||||||
|
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, tryGetDeviceType } from '@/enums';
|
||||||
import { useDeviceStore } from '@/stores';
|
import { useDeviceStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
|
import { ApiOutlined, ReloadOutlined } from '@vicons/antd';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
|
import destr from 'destr';
|
||||||
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
|
import { NButton, NCard, NFlex, NIcon, NTag, NTooltip } from 'naive-ui';
|
||||||
import { computed, onBeforeUnmount, ref, toRefs } from 'vue';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
ndmDevice: NdmDeviceResultVO;
|
ndmDevice: NdmDeviceResultVO;
|
||||||
@@ -18,6 +21,9 @@ defineSlots<{
|
|||||||
'append-info': () => any;
|
'append-info': () => any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
const { ndmDevice, station } = toRefs(props);
|
const { ndmDevice, station } = toRefs(props);
|
||||||
|
|
||||||
const type = computed(() => {
|
const type = computed(() => {
|
||||||
@@ -30,6 +36,44 @@ const status = computed(() => ndmDevice.value.deviceStatus);
|
|||||||
const ipAddr = computed(() => ndmDevice.value.ipAddress ?? '-');
|
const ipAddr = computed(() => ndmDevice.value.ipAddress ?? '-');
|
||||||
const gbCode = computed(() => Reflect.get(ndmDevice.value, 'gbCode') as string | undefined);
|
const gbCode = computed(() => Reflect.get(ndmDevice.value, 'gbCode') as string | undefined);
|
||||||
|
|
||||||
|
const linkDescription = computed(() => {
|
||||||
|
const result = destr<any>(ndmDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result as LinkDescription;
|
||||||
|
});
|
||||||
|
|
||||||
|
const upperDevices = computed(() => {
|
||||||
|
const devices: NdmDeviceResultVO[] = [];
|
||||||
|
if (!linkDescription.value) return devices;
|
||||||
|
if (!linkDescription.value.upstream) return devices;
|
||||||
|
linkDescription.value.upstream.forEach((deviceStoreIndex) => {
|
||||||
|
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
|
||||||
|
const stationDevices = lineDevices.value[stationCode];
|
||||||
|
if (!stationDevices) return;
|
||||||
|
const classified = stationDevices[deviceType];
|
||||||
|
const device = classified.find((device) => device.id === deviceDbId);
|
||||||
|
if (device) devices.push(device);
|
||||||
|
});
|
||||||
|
return devices.sort((aDevice, bDevice) => {
|
||||||
|
// 按在DEVICE_TYPE_LITERALS中的顺序排序
|
||||||
|
const aDeviceType = tryGetDeviceType(aDevice.deviceType);
|
||||||
|
const bDeviceType = tryGetDeviceType(bDevice.deviceType);
|
||||||
|
if (!aDeviceType || !bDeviceType) return 0;
|
||||||
|
const deviceTypes = Object.values(DEVICE_TYPE_LITERALS);
|
||||||
|
return deviceTypes.indexOf(aDeviceType) - deviceTypes.indexOf(bDeviceType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取从父组件注入的 `selectDevice` 函数
|
||||||
|
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
|
||||||
|
// 跳转到上游设备
|
||||||
|
const navigateToUpperDevice = (upperDevice: NdmDeviceResultVO) => {
|
||||||
|
if (!!selectDeviceFn && !!selectDeviceFn.value) {
|
||||||
|
selectDeviceFn.value(upperDevice, station.value.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const canOpenMgmtPage = computed(() => {
|
const canOpenMgmtPage = computed(() => {
|
||||||
return Object.keys(ndmDevice.value).includes('manageUrl');
|
return Object.keys(ndmDevice.value).includes('manageUrl');
|
||||||
});
|
});
|
||||||
@@ -96,15 +140,14 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<NCard hoverable size="small">
|
<NCard hoverable size="small">
|
||||||
<template #header>
|
<template #header>
|
||||||
|
<NFlex vertical>
|
||||||
<NFlex align="center">
|
<NFlex align="center">
|
||||||
<NTag v-if="status === '10'" size="small" type="success">在线</NTag>
|
<NTag v-if="status === '10'" size="small" type="success">在线</NTag>
|
||||||
<NTag v-else-if="status === '20'" size="small" type="error">离线</NTag>
|
<NTag v-else-if="status === '20'" size="small" type="error">离线</NTag>
|
||||||
<NTag v-else size="small" type="warning">-</NTag>
|
<NTag v-else size="small" type="warning">-</NTag>
|
||||||
<div>{{ name }}</div>
|
<div>{{ name }}</div>
|
||||||
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
|
<NButton v-if="canOpenMgmtPage" ghost size="tiny" type="default" :focusable="false" @click="onClickOpenMgmtPage">管理</NButton>
|
||||||
</NFlex>
|
<div style="margin-left: auto">
|
||||||
</template>
|
|
||||||
<template #header-extra>
|
|
||||||
<NTooltip v-if="canProbe" trigger="hover">
|
<NTooltip v-if="canProbe" trigger="hover">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
|
<NButton size="small" quaternary circle :loading="probing" @click="() => probeDevice()">
|
||||||
@@ -129,6 +172,16 @@ onBeforeUnmount(() => {
|
|||||||
<span>刷新设备</span>
|
<span>刷新设备</span>
|
||||||
</template>
|
</template>
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
|
<div v-if="upperDevices.length > 0" style="font-size: 0.85rem">
|
||||||
|
<span>上游设备:</span>
|
||||||
|
<template v-for="(device, index) in upperDevices" :key="index">
|
||||||
|
<span style="text-decoration: underline; cursor: pointer" @click="() => navigateToUpperDevice(device)">{{ device.name }}</span>
|
||||||
|
<span v-if="index < upperDevices.length - 1">、</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div style="font-size: small; color: #666">
|
<div style="font-size: small; color: #666">
|
||||||
|
|||||||
@@ -4,7 +4,20 @@ import DeviceHeaderCard from './device-header-card.vue';
|
|||||||
import NvrDiskCard from './nvr-disk-card.vue';
|
import NvrDiskCard from './nvr-disk-card.vue';
|
||||||
import NvrRecordCard from './nvr-record-card.vue';
|
import NvrRecordCard from './nvr-record-card.vue';
|
||||||
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
|
import SecurityBoxCircuitCard from './security-box-circuit-card.vue';
|
||||||
|
import SecurityBoxCircuitLinkModal from './security-box-circuit-link-modal.vue';
|
||||||
import SecurityBoxEnvCard from './security-box-env-card.vue';
|
import SecurityBoxEnvCard from './security-box-env-card.vue';
|
||||||
import SwitchPortCard from './switch-port-card.vue';
|
import SwitchPortCard from './switch-port-card.vue';
|
||||||
|
import SwitchPortLinkModal from './switch-port-link-modal.vue';
|
||||||
|
|
||||||
export { DeviceCommonCard, DeviceHardwareCard, DeviceHeaderCard, NvrDiskCard, NvrRecordCard, SecurityBoxCircuitCard, SecurityBoxEnvCard, SwitchPortCard };
|
export {
|
||||||
|
DeviceCommonCard,
|
||||||
|
DeviceHardwareCard,
|
||||||
|
DeviceHeaderCard,
|
||||||
|
NvrDiskCard,
|
||||||
|
NvrRecordCard,
|
||||||
|
SecurityBoxCircuitCard,
|
||||||
|
SecurityBoxCircuitLinkModal,
|
||||||
|
SecurityBoxEnvCard,
|
||||||
|
SwitchPortCard,
|
||||||
|
SwitchPortLinkModal,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,28 +1,53 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { probeDeviceApi, rebootSecurityBoxApi, turnCitcuitStatusApi, type NdmSecurityBoxCircuit, type NdmSecurityBoxResultVO, type Station } from '@/apis';
|
import {
|
||||||
|
detailDeviceApi,
|
||||||
|
probeDeviceApi,
|
||||||
|
rebootSecurityBoxApi,
|
||||||
|
turnCitcuitStatusApi,
|
||||||
|
updateDeviceApi,
|
||||||
|
type LinkDescription,
|
||||||
|
type NdmDeviceResultVO,
|
||||||
|
type NdmSecurityBoxCircuit,
|
||||||
|
type NdmSecurityBoxLinkDescription,
|
||||||
|
type NdmSecurityBoxResultVO,
|
||||||
|
type Station,
|
||||||
|
} from '@/apis';
|
||||||
|
import { SecurityBoxCircuitLinkModal } from '@/components';
|
||||||
|
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
||||||
|
import { useDeviceStore, useSettingStore } from '@/stores';
|
||||||
import { parseErrorFeedback } from '@/utils';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { PoweroffOutlined } from '@vicons/antd';
|
import { PoweroffOutlined } from '@vicons/antd';
|
||||||
import { watchImmediate } from '@vueuse/core';
|
import { watchImmediate } from '@vueuse/core';
|
||||||
import { NButton, NCard, NDescriptions, NDescriptionsItem, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type TagProps } from 'naive-ui';
|
import { isCancel } from 'axios';
|
||||||
import { computed, ref, toRefs } from 'vue';
|
import destr from 'destr';
|
||||||
|
import { cloneDeep, isFunction } from 'es-toolkit';
|
||||||
|
import { NButton, NCard, NDescriptions, NDescriptionsItem, NDropdown, NFlex, NIcon, NPopconfirm, NPopover, NSwitch, NTag, useThemeVars, type DropdownOption, type TagProps } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, inject, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
circuits?: NdmSecurityBoxCircuit[];
|
|
||||||
ndmDevice: NdmSecurityBoxResultVO;
|
ndmDevice: NdmSecurityBoxResultVO;
|
||||||
station: Station;
|
station: Station;
|
||||||
|
circuits?: NdmSecurityBoxCircuit[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
const { circuits, ndmDevice, station } = toRefs(props);
|
const deviceStore = useDeviceStore();
|
||||||
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { offlineDev } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { ndmDevice, station, circuits } = toRefs(props);
|
||||||
|
|
||||||
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
|
const showCard = computed(() => !!circuits.value && circuits.value.length > 0);
|
||||||
|
|
||||||
const boxCircuits = ref<NdmSecurityBoxCircuit[]>([]);
|
const localCircuits = ref<NdmSecurityBoxCircuit[]>([]);
|
||||||
|
|
||||||
watchImmediate(circuits, (newCircuits) => {
|
watchImmediate(circuits, (newCircuits) => {
|
||||||
boxCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
|
localCircuits.value = newCircuits?.map((circuit) => ({ ...circuit })) ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
|
const getCircuitStatusTagType = (circuit: NdmSecurityBoxCircuit): TagProps['type'] => {
|
||||||
@@ -40,22 +65,34 @@ const getCircuitStatusClassName = (circuit: NdmSecurityBoxCircuit) => {
|
|||||||
return status === 0 ? 'circuit-off' : status === 1 ? 'circuit-on' : 'circuit-unknown';
|
return status === 0 ? 'circuit-off' : status === 1 ? 'circuit-on' : 'circuit-unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
|
||||||
const { mutate: turnStatus, isPending: turning } = useMutation({
|
const { mutate: turnStatus, isPending: turning } = useMutation({
|
||||||
mutationFn: async (params: { circuitIndex: number; newStatus: boolean }) => {
|
mutationFn: async (params: { circuitIndex: number; newStatus: boolean }) => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
const { circuitIndex, newStatus } = params;
|
const { circuitIndex, newStatus } = params;
|
||||||
if (!ndmDevice.value.ipAddress) {
|
if (!ndmDevice.value.ipAddress) {
|
||||||
throw new Error('设备IP地址不存在');
|
throw new Error('设备IP地址不存在');
|
||||||
}
|
}
|
||||||
const status = newStatus ? 1 : 0;
|
const status = newStatus ? 1 : 0;
|
||||||
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode: station.value.code });
|
const stationCode = station.value.code;
|
||||||
await probeDeviceApi(ndmDevice.value, { stationCode: station.value.code });
|
const signal = abortController.value.signal;
|
||||||
|
await turnCitcuitStatusApi(ndmDevice.value.ipAddress, circuitIndex, status, { stationCode, signal });
|
||||||
|
await probeDeviceApi(ndmDevice.value, { stationCode, signal });
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
onSuccess: (status, { circuitIndex }) => {
|
onSuccess: (status, { circuitIndex }) => {
|
||||||
const circuit = boxCircuits.value.at(circuitIndex);
|
window.$loadingBar.finish();
|
||||||
|
const circuit = localCircuits.value.at(circuitIndex);
|
||||||
if (circuit) circuit.status = status;
|
if (circuit) circuit.status = status;
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
window.$message.error(errorFeedback);
|
window.$message.error(errorFeedback);
|
||||||
@@ -64,15 +101,173 @@ const { mutate: turnStatus, isPending: turning } = useMutation({
|
|||||||
|
|
||||||
const { mutate: reboot, isPending: rebooting } = useMutation({
|
const { mutate: reboot, isPending: rebooting } = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
if (!ndmDevice.value.ipAddress) {
|
if (!ndmDevice.value.ipAddress) {
|
||||||
throw new Error('设备IP地址不存在');
|
throw new Error('设备IP地址不存在');
|
||||||
}
|
}
|
||||||
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode: station.value.code });
|
|
||||||
|
const stationCode = station.value.code;
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
await rebootSecurityBoxApi(ndmDevice.value.ipAddress, { stationCode, signal });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
window.$loadingBar.finish();
|
||||||
window.$message.success('设备重启成功');
|
window.$message.success('设备重启成功');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const upperDeviceLinkDescription = computed(() => {
|
||||||
|
const result = destr<any>(ndmDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result as NdmSecurityBoxLinkDescription;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLowerDeviceByCircuitIndex = (circuitIndex: number) => {
|
||||||
|
if (!upperDeviceLinkDescription.value) return null;
|
||||||
|
const downstream = upperDeviceLinkDescription.value.downstream;
|
||||||
|
if (!downstream) return null;
|
||||||
|
const deviceStoreIndex = downstream[circuitIndex];
|
||||||
|
if (!deviceStoreIndex) return null;
|
||||||
|
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
|
||||||
|
const stationDevices = lineDevices.value[stationCode];
|
||||||
|
if (!stationDevices) return null;
|
||||||
|
const devices = stationDevices[deviceType];
|
||||||
|
const device = devices.find((device) => device.id === deviceDbId);
|
||||||
|
if (!device) return null;
|
||||||
|
return device;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取从父组件注入的selectDevice函数
|
||||||
|
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
|
||||||
|
// 跳转到下游设备
|
||||||
|
const navigateToLowerDevice = (circuitIndex: number) => {
|
||||||
|
const lowerDevice = getLowerDeviceByCircuitIndex(circuitIndex);
|
||||||
|
if (!lowerDevice) return;
|
||||||
|
if (!!selectDeviceFn && !!selectDeviceFn.value) {
|
||||||
|
selectDeviceFn.value(lowerDevice, station.value.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const contextmenu = ref<{ x: number; y: number; circuitIndex?: number }>({ x: 0, y: 0 });
|
||||||
|
const showContextmenu = ref(false);
|
||||||
|
const contextmenuOptions = computed<DropdownOption[]>(() => [
|
||||||
|
{
|
||||||
|
label: '关联设备',
|
||||||
|
key: 'link-device',
|
||||||
|
onSelect: () => {
|
||||||
|
showContextmenu.value = false;
|
||||||
|
showModal.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '解除关联',
|
||||||
|
key: 'unlink-device',
|
||||||
|
onSelect: () => {
|
||||||
|
showContextmenu.value = false;
|
||||||
|
const circuitIndex = contextmenu.value.circuitIndex;
|
||||||
|
if (circuitIndex === undefined) return;
|
||||||
|
const lowerDevice = getLowerDeviceByCircuitIndex(circuitIndex);
|
||||||
|
if (!lowerDevice) return;
|
||||||
|
window.$dialog.warning({
|
||||||
|
title: '确认解除关联吗?',
|
||||||
|
content: `将解除【电路${circuitIndex + 1}】与【${lowerDevice.name}】的关联关系。`,
|
||||||
|
style: { width: '600px' },
|
||||||
|
contentStyle: { height: '60px' },
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确认',
|
||||||
|
onNegativeClick: () => {
|
||||||
|
window.$dialog.destroyAll();
|
||||||
|
},
|
||||||
|
onPositiveClick: () => {
|
||||||
|
window.$dialog.destroyAll();
|
||||||
|
unlinkDevice({ circuitIndex, lowerDevice });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||||
|
const onSelect = option['onSelect'];
|
||||||
|
if (isFunction(onSelect)) {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onContextmenu = (payload: PointerEvent, circuitIndex: number) => {
|
||||||
|
payload.stopPropagation();
|
||||||
|
payload.preventDefault();
|
||||||
|
const { clientX, clientY } = payload;
|
||||||
|
contextmenu.value = { x: clientX, y: clientY, circuitIndex };
|
||||||
|
showContextmenu.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: unlinkDevice } = useMutation({
|
||||||
|
mutationFn: async (params: { circuitIndex: number; lowerDevice: NdmDeviceResultVO }) => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
|
const { circuitIndex, lowerDevice } = params;
|
||||||
|
|
||||||
|
if (!upperDeviceLinkDescription.value) return;
|
||||||
|
|
||||||
|
// 1.从下游设备的linkDescription的upstream字段中删除当前上游设备
|
||||||
|
const modifiedLowerDevice = cloneDeep(lowerDevice);
|
||||||
|
// 解除关联时下游设备的linkDescription一定存在
|
||||||
|
const modifiedLowerDeviceLinkDescription = destr<LinkDescription>(modifiedLowerDevice.linkDescription);
|
||||||
|
// upstream字段存在时才进行删除操作
|
||||||
|
if (modifiedLowerDeviceLinkDescription.upstream) {
|
||||||
|
const index = modifiedLowerDeviceLinkDescription.upstream.findIndex((deviceStoreIndex) => deviceStoreIndex.deviceDbId === ndmDevice.value.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
modifiedLowerDeviceLinkDescription.upstream.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 2. 修改上游设备的linkDescription的downstream字段
|
||||||
|
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
|
||||||
|
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
|
||||||
|
delete modifiedUpperLinkDescription.downstream?.[circuitIndex];
|
||||||
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
||||||
|
|
||||||
|
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
||||||
|
if (offlineDev.value) {
|
||||||
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
|
}
|
||||||
|
const stationCode = station.value.code;
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
window.$loadingBar.finish();
|
||||||
|
window.$message.success('解除成功');
|
||||||
|
if (!data) return;
|
||||||
|
const { upperDevice, lowerDevice } = data;
|
||||||
|
if (!!upperDevice && !!lowerDevice) {
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...upperDevice });
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const errorFeedback = parseErrorFeedback(error);
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
window.$message.error(errorFeedback);
|
window.$message.error(errorFeedback);
|
||||||
@@ -94,11 +289,11 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
|
|||||||
</NFlex>
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div style="display: grid" :style="{ 'grid-template-columns': `repeat(${Math.min(boxCircuits.length, 4)}, 1fr)` }">
|
<div style="display: grid" :style="{ 'grid-template-columns': `repeat(${Math.min(localCircuits.length, 4)}, 1fr)` }">
|
||||||
<template v-for="(circuit, index) in boxCircuits" :key="index">
|
<template v-for="(circuit, circuitIndex) in localCircuits" :key="circuitIndex">
|
||||||
<NPopover :delay="300">
|
<NPopover :delay="300">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NFlex justify="center" align="center" :size="0">
|
<div style="display: flex; justify-content: center; align-items: center" @contextmenu="(payload) => onContextmenu(payload, circuitIndex)">
|
||||||
<NFlex vertical class="pointer-cursor circuit" style="padding: 12px" :class="getCircuitStatusClassName(circuit)">
|
<NFlex vertical class="pointer-cursor circuit" style="padding: 12px" :class="getCircuitStatusClassName(circuit)">
|
||||||
<NFlex align="center">
|
<NFlex align="center">
|
||||||
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
|
<NTag class="pointer-cursor" size="small" :type="getCircuitStatusTagType(circuit)">
|
||||||
@@ -109,25 +304,31 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
|
|||||||
<span>{{ getCircuitStatusText(circuit) }}</span>
|
<span>{{ getCircuitStatusText(circuit) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</NTag>
|
</NTag>
|
||||||
<span>电路{{ index + 1 }}</span>
|
<span>电路{{ circuitIndex + 1 }}</span>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
<NFlex justify="end" align="center">
|
<NFlex justify="end" align="center">
|
||||||
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: index, newStatus: circuit.status !== 1 })">
|
<NPopconfirm :positive-text="'确认'" :negative-text="'取消'" @positive-click="() => turnStatus({ circuitIndex: circuitIndex, newStatus: circuit.status !== 1 })">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
|
<NSwitch size="small" :value="circuit.status === 1" :loading="turning" />
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ index + 1 }}吗?</span>
|
<span>确定要{{ circuit.status === 1 ? '关闭' : '开启' }}电路{{ circuitIndex + 1 }}吗?</span>
|
||||||
</template>
|
</template>
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFlex>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<NDescriptions bordered size="small" label-placement="left" :column="1">
|
<NDescriptions bordered size="small" label-placement="left" :column="1">
|
||||||
<NDescriptionsItem label="电压">{{ circuit.voltage }}V</NDescriptionsItem>
|
<NDescriptionsItem label="电压">{{ circuit.voltage }}V</NDescriptionsItem>
|
||||||
<NDescriptionsItem label="电流">{{ circuit.current }}A</NDescriptionsItem>
|
<NDescriptionsItem label="电流">{{ circuit.current }}A</NDescriptionsItem>
|
||||||
|
<NDescriptionsItem label="关联设备">
|
||||||
|
<span v-if="getLowerDeviceByCircuitIndex(circuitIndex)" style="text-decoration: underline; cursor: pointer" @click="() => navigateToLowerDevice(circuitIndex)">
|
||||||
|
{{ getLowerDeviceByCircuitIndex(circuitIndex)?.name || '-' }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</NDescriptionsItem>
|
||||||
</NDescriptions>
|
</NDescriptions>
|
||||||
</template>
|
</template>
|
||||||
</NPopover>
|
</NPopover>
|
||||||
@@ -135,6 +336,19 @@ const { mutate: reboot, isPending: rebooting } = useMutation({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
|
<SecurityBoxCircuitLinkModal v-model:show="showModal" :ndm-device="ndmDevice" :station="station" :circuit-index="contextmenu.circuitIndex" />
|
||||||
|
|
||||||
|
<NDropdown
|
||||||
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
:show="showContextmenu"
|
||||||
|
:x="contextmenu.x"
|
||||||
|
:y="contextmenu.y"
|
||||||
|
:options="contextmenuOptions"
|
||||||
|
@select="onSelectDropdownOption"
|
||||||
|
@clickoutside="() => (showContextmenu = false)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { detailDeviceApi, updateDeviceApi, type NdmCameraLinkDescription, type NdmDeviceResultVO, type NdmSecurityBoxLinkDescription, type NdmSecurityBoxResultVO, type Station } from '@/apis';
|
||||||
|
import { DeviceTree } from '@/components';
|
||||||
|
import { tryGetDeviceType } from '@/enums';
|
||||||
|
import { useDeviceStore, useSettingStore } from '@/stores';
|
||||||
|
import { parseErrorFeedback } from '@/utils';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { cloneDeep } from 'es-toolkit';
|
||||||
|
import { NButton, NFlex, NModal } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
ndmDevice: NdmSecurityBoxResultVO;
|
||||||
|
station: Station;
|
||||||
|
circuitIndex?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const show = defineModel<boolean>('show', { default: false });
|
||||||
|
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { offlineDev } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { ndmDevice, station, circuitIndex } = toRefs(props);
|
||||||
|
|
||||||
|
const upperDeviceLinkDescription = computed(() => {
|
||||||
|
const result = destr<any>(ndmDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result as NdmSecurityBoxLinkDescription;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lowerDevice = ref<NdmDeviceResultVO>();
|
||||||
|
const lowerDeviceLinkDescription = computed<NdmCameraLinkDescription | null>(() => {
|
||||||
|
if (!lowerDevice.value) return null;
|
||||||
|
const result = destr<any>(lowerDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const onAfterSelectDevice = (device: NdmDeviceResultVO) => {
|
||||||
|
lowerDevice.value = device;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
|
||||||
|
const { mutate: linkPortToDevice, isPending: linking } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
|
const upperDeviceType = tryGetDeviceType(ndmDevice.value.deviceType);
|
||||||
|
if (!upperDeviceType) throw new Error('本设备类型未知');
|
||||||
|
const upperDeviceDbId = ndmDevice.value.id;
|
||||||
|
if (!upperDeviceDbId) throw new Error('本设备没有ID');
|
||||||
|
|
||||||
|
if (circuitIndex.value === undefined) throw new Error('该电路不存在');
|
||||||
|
|
||||||
|
if (!lowerDevice.value) throw new Error('请选择要关联的设备');
|
||||||
|
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
|
||||||
|
if (!lowerDeviceType) throw new Error('该设备类型未知');
|
||||||
|
const lowerDeviceDbId = lowerDevice.value?.id;
|
||||||
|
if (!lowerDeviceDbId) throw new Error('该设备没有ID');
|
||||||
|
|
||||||
|
// 0. 检查上游设备的linkDescription的downstream字段是否存在某个端口已经关联下游设备
|
||||||
|
const duplicated = Object.entries(upperDeviceLinkDescription.value?.downstream ?? {}).find(([, deviceStoreIndex]) => {
|
||||||
|
return deviceStoreIndex.deviceDbId === lowerDeviceDbId;
|
||||||
|
});
|
||||||
|
if (duplicated) {
|
||||||
|
const [portName] = duplicated;
|
||||||
|
throw new Error(`该设备已关联到端口${portName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 修改上游设备的linkDescription的downstream字段
|
||||||
|
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
|
||||||
|
let modifiedUpperDeviceLinkDescription: NdmSecurityBoxLinkDescription;
|
||||||
|
if (!upperDeviceLinkDescription.value) {
|
||||||
|
modifiedUpperDeviceLinkDescription = {
|
||||||
|
downstream: {
|
||||||
|
[circuitIndex.value]: {
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: lowerDeviceType,
|
||||||
|
deviceDbId: lowerDeviceDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
modifiedUpperDeviceLinkDescription = {
|
||||||
|
...upperDeviceLinkDescription.value,
|
||||||
|
downstream: {
|
||||||
|
...upperDeviceLinkDescription.value.downstream,
|
||||||
|
[circuitIndex.value]: {
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: lowerDeviceType,
|
||||||
|
deviceDbId: lowerDeviceDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 2. 修改下游设备的linkDescription的upstream字段
|
||||||
|
const modifiedLowerDevice = cloneDeep(lowerDevice.value);
|
||||||
|
let modifiedLowerDeviceLinkDescription: NdmCameraLinkDescription;
|
||||||
|
if (!lowerDeviceLinkDescription.value) {
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
upstream: [
|
||||||
|
{
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const upstream = cloneDeep(lowerDeviceLinkDescription.value.upstream);
|
||||||
|
if (!upstream) {
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
...lowerDeviceLinkDescription.value,
|
||||||
|
upstream: [
|
||||||
|
{
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const deviceStoreIndex = upstream.find((deviceStoreIndex) => deviceStoreIndex.deviceDbId === upperDeviceDbId);
|
||||||
|
if (!deviceStoreIndex) {
|
||||||
|
upstream.push({
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
...lowerDeviceLinkDescription.value,
|
||||||
|
upstream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
||||||
|
if (offlineDev.value) {
|
||||||
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
|
}
|
||||||
|
const stationCode = station.value.code;
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
|
||||||
|
},
|
||||||
|
onSuccess: ({ upperDevice, lowerDevice }) => {
|
||||||
|
show.value = false;
|
||||||
|
window.$loadingBar.finish();
|
||||||
|
window.$message.success('关联成功');
|
||||||
|
if (!!upperDevice && !!lowerDevice) {
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...upperDevice });
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLink = () => {
|
||||||
|
linkPortToDevice();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
show.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :content-style="{ height: '100%', overflow: 'hidden' }" @close="onCancel" @esc="onCancel">
|
||||||
|
<template #header>
|
||||||
|
<span>{{ ndmDevice.name }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>电路{{ circuitIndex ? circuitIndex + 1 : '-' }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>关联设备</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<DeviceTree :station="station" :events="['select']" :device-prefix-label="'选择'" @after-select-device="onAfterSelectDevice" />
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<NFlex justify="end">
|
||||||
|
<NButton size="small" quaternary @click="onCancel">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :disabled="!lowerDevice" :loading="linking" @click="onLink">关联</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,16 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmSwitchPortInfo } from '@/apis';
|
import { detailDeviceApi, updateDeviceApi, type LinkDescription, type NdmDeviceResultVO, type NdmSwitchLinkDescription, type NdmSwitchPortInfo, type NdmSwitchResultVO, type Station } from '@/apis';
|
||||||
|
import { SwitchPortLinkModal } from '@/components';
|
||||||
|
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
||||||
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
|
import { getPortStatusValue, transformPortSpeed } from '@/helpers';
|
||||||
import { NCard, NDescriptions, NDescriptionsItem, NPopover, useThemeVars } from 'naive-ui';
|
import { useDeviceStore, useSettingStore } from '@/stores';
|
||||||
import { computed, toRefs } from 'vue';
|
import { parseErrorFeedback } from '@/utils';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { cloneDeep, isFunction } from 'es-toolkit';
|
||||||
|
import { NCard, NDescriptions, NDescriptionsItem, NDropdown, NPopover, useThemeVars, type DropdownOption } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, inject, onBeforeUnmount, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
ndmDevice: NdmSwitchResultVO;
|
||||||
|
station: Station;
|
||||||
ports?: NdmSwitchPortInfo[];
|
ports?: NdmSwitchPortInfo[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
const { ports } = toRefs(props);
|
const deviceStore = useDeviceStore();
|
||||||
|
const { lineDevices } = storeToRefs(deviceStore);
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { offlineDev } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { ndmDevice, station, ports } = toRefs(props);
|
||||||
|
|
||||||
const showCard = computed(() => !!ports.value);
|
const showCard = computed(() => !!ports.value);
|
||||||
|
|
||||||
@@ -58,6 +75,158 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
|
|||||||
}
|
}
|
||||||
return 'port-unknown';
|
return 'port-unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upperDeviceLinkDescription = computed(() => {
|
||||||
|
const result = destr<any>(ndmDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result as NdmSwitchLinkDescription;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLowerDeviceByPort = (port: NdmSwitchPortInfo) => {
|
||||||
|
const portName = port.portName;
|
||||||
|
if (!upperDeviceLinkDescription.value) return null;
|
||||||
|
const downstream = upperDeviceLinkDescription.value.downstream;
|
||||||
|
if (!downstream) return null;
|
||||||
|
const deviceStoreIndex = downstream[portName];
|
||||||
|
if (!deviceStoreIndex) return null;
|
||||||
|
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
|
||||||
|
const stationDevices = lineDevices.value[stationCode];
|
||||||
|
if (!stationDevices) return null;
|
||||||
|
const devices = stationDevices[deviceType];
|
||||||
|
const device = devices.find((device) => device.id === deviceDbId);
|
||||||
|
if (!device) return null;
|
||||||
|
return device;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取从父组件注入的selectDevice函数
|
||||||
|
const selectDeviceFn = inject(SELECT_DEVICE_FN_INJECTION_KEY);
|
||||||
|
// 跳转到下游设备
|
||||||
|
const navigateToLowerDevice = (port: NdmSwitchPortInfo) => {
|
||||||
|
const lowerDevice = getLowerDeviceByPort(port);
|
||||||
|
if (!lowerDevice) return;
|
||||||
|
if (!!selectDeviceFn && !!selectDeviceFn.value) {
|
||||||
|
selectDeviceFn.value(lowerDevice, station.value.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const contextmenu = ref<{ x: number; y: number; port?: NdmSwitchPortInfo }>({ x: 0, y: 0 });
|
||||||
|
const showContextmenu = ref(false);
|
||||||
|
const contextmenuOptions = computed<DropdownOption[]>(() => [
|
||||||
|
{
|
||||||
|
label: '关联设备',
|
||||||
|
key: 'link-device',
|
||||||
|
onSelect: () => {
|
||||||
|
showContextmenu.value = false;
|
||||||
|
showModal.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '解除关联',
|
||||||
|
key: 'unlink-device',
|
||||||
|
onSelect: () => {
|
||||||
|
showContextmenu.value = false;
|
||||||
|
const port = contextmenu.value.port;
|
||||||
|
if (!port) return;
|
||||||
|
const lowerDevice = getLowerDeviceByPort(port);
|
||||||
|
if (!lowerDevice) return;
|
||||||
|
window.$dialog.warning({
|
||||||
|
title: '确认解除关联吗?',
|
||||||
|
content: `将解除【${port.portName}】与【${lowerDevice.name}】的关联关系。`,
|
||||||
|
style: { width: '600px' },
|
||||||
|
contentStyle: { height: '60px' },
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确认',
|
||||||
|
onNegativeClick: () => {
|
||||||
|
window.$dialog.destroyAll();
|
||||||
|
},
|
||||||
|
onPositiveClick: () => {
|
||||||
|
window.$dialog.destroyAll();
|
||||||
|
unlinkDevice({ port, lowerDevice });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
|
||||||
|
const onSelect = option['onSelect'];
|
||||||
|
if (isFunction(onSelect)) {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onContextmenu = (payload: PointerEvent, port: NdmSwitchPortInfo) => {
|
||||||
|
payload.stopPropagation();
|
||||||
|
payload.preventDefault();
|
||||||
|
const { clientX, clientY } = payload;
|
||||||
|
contextmenu.value = { x: clientX, y: clientY, port };
|
||||||
|
showContextmenu.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
const { mutate: unlinkDevice } = useMutation({
|
||||||
|
mutationFn: async (params: { port: NdmSwitchPortInfo; lowerDevice: NdmDeviceResultVO }) => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
|
const { port, lowerDevice } = params;
|
||||||
|
|
||||||
|
if (!upperDeviceLinkDescription.value) return;
|
||||||
|
|
||||||
|
// 1. 从下游设备的linkDescription的upstream字段中删除当前上游设备
|
||||||
|
const modifiedLowerDevice = cloneDeep(lowerDevice);
|
||||||
|
// 解除关联时下游设备的linkDescription一定存在
|
||||||
|
const modifiedLowerDeviceLinkDescription = destr<LinkDescription>(modifiedLowerDevice.linkDescription);
|
||||||
|
// upstream字段存在时才进行删除操作
|
||||||
|
if (modifiedLowerDeviceLinkDescription.upstream) {
|
||||||
|
const index = modifiedLowerDeviceLinkDescription.upstream.findIndex((deviceStoreIndex) => deviceStoreIndex.deviceDbId === ndmDevice.value.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
modifiedLowerDeviceLinkDescription.upstream.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 2. 修改上游设备的linkDescription的downstream字段
|
||||||
|
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
|
||||||
|
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
|
||||||
|
delete modifiedUpperLinkDescription.downstream?.[port.portName];
|
||||||
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
|
||||||
|
|
||||||
|
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
||||||
|
if (offlineDev.value) {
|
||||||
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
|
}
|
||||||
|
const stationCode = station.value.code;
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
window.$loadingBar.finish();
|
||||||
|
window.$message.success('解除成功');
|
||||||
|
if (!data) return;
|
||||||
|
const { upperDevice, lowerDevice } = data;
|
||||||
|
if (!!upperDevice && !!lowerDevice) {
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...upperDevice });
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
abortController.value.abort();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -77,7 +246,12 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
|
|||||||
<NPopover :delay="300">
|
<NPopover :delay="300">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<!-- 最外层div宽度100% -->
|
<!-- 最外层div宽度100% -->
|
||||||
<div class="port" style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer" :class="getPortClassName(port)">
|
<div
|
||||||
|
class="port"
|
||||||
|
style="height: 40px; box-sizing: border-box; display: flex; cursor: pointer"
|
||||||
|
:class="getPortClassName(port)"
|
||||||
|
@contextmenu="(payload) => onContextmenu(payload, port)"
|
||||||
|
>
|
||||||
<!-- 将端口号和状态指示器包裹起来 用于居中布局 -->
|
<!-- 将端口号和状态指示器包裹起来 用于居中布局 -->
|
||||||
<div style="margin: auto; display: flex; flex-direction: column; align-items: center">
|
<div style="margin: auto; display: flex; flex-direction: column; align-items: center">
|
||||||
<div style="font-size: xx-small">{{ index }}</div>
|
<div style="font-size: xx-small">{{ index }}</div>
|
||||||
@@ -92,6 +266,12 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
|
|||||||
<NDescriptionsItem label="上行速率">{{ transformPortSpeed(port, 'in') }}</NDescriptionsItem>
|
<NDescriptionsItem label="上行速率">{{ transformPortSpeed(port, 'in') }}</NDescriptionsItem>
|
||||||
<NDescriptionsItem label="下行速率">{{ transformPortSpeed(port, 'out') }}</NDescriptionsItem>
|
<NDescriptionsItem label="下行速率">{{ transformPortSpeed(port, 'out') }}</NDescriptionsItem>
|
||||||
<NDescriptionsItem label="总速率">{{ transformPortSpeed(port, 'total') }}</NDescriptionsItem>
|
<NDescriptionsItem label="总速率">{{ transformPortSpeed(port, 'total') }}</NDescriptionsItem>
|
||||||
|
<NDescriptionsItem label="关联设备">
|
||||||
|
<span v-if="getLowerDeviceByPort(port)" style="text-decoration: underline; cursor: pointer" @click="() => navigateToLowerDevice(port)">
|
||||||
|
{{ getLowerDeviceByPort(port)?.name || '-' }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</NDescriptionsItem>
|
||||||
</NDescriptions>
|
</NDescriptions>
|
||||||
</template>
|
</template>
|
||||||
</NPopover>
|
</NPopover>
|
||||||
@@ -101,6 +281,19 @@ const getPortClassName = (port: NdmSwitchPortInfo) => {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
|
<SwitchPortLinkModal v-model:show="showModal" :ndm-device="ndmDevice" :station="station" :port="contextmenu.port" />
|
||||||
|
|
||||||
|
<NDropdown
|
||||||
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
:show="showContextmenu"
|
||||||
|
:x="contextmenu.x"
|
||||||
|
:y="contextmenu.y"
|
||||||
|
:options="contextmenuOptions"
|
||||||
|
@select="onSelectDropdownOption"
|
||||||
|
@clickoutside="() => (showContextmenu = false)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
detailDeviceApi,
|
||||||
|
updateDeviceApi,
|
||||||
|
type NdmCameraLinkDescription,
|
||||||
|
type NdmDeviceResultVO,
|
||||||
|
type NdmSwitchLinkDescription,
|
||||||
|
type NdmSwitchPortInfo,
|
||||||
|
type NdmSwitchResultVO,
|
||||||
|
type Station,
|
||||||
|
} from '@/apis';
|
||||||
|
import { DeviceTree } from '@/components';
|
||||||
|
import { tryGetDeviceType } from '@/enums';
|
||||||
|
import { useDeviceStore, useSettingStore } from '@/stores';
|
||||||
|
import { parseErrorFeedback } from '@/utils';
|
||||||
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
|
import { isCancel } from 'axios';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { cloneDeep } from 'es-toolkit';
|
||||||
|
import { NButton, NFlex, NModal } from 'naive-ui';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, ref, toRefs } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
ndmDevice: NdmSwitchResultVO;
|
||||||
|
station: Station;
|
||||||
|
port?: NdmSwitchPortInfo;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const show = defineModel<boolean>('show', { default: false });
|
||||||
|
|
||||||
|
const deviceStore = useDeviceStore();
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const { offlineDev } = storeToRefs(settingStore);
|
||||||
|
|
||||||
|
const { ndmDevice, station, port } = toRefs(props);
|
||||||
|
|
||||||
|
const upperDeviceLinkDescription = computed(() => {
|
||||||
|
const result = destr<any>(ndmDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result as NdmSwitchLinkDescription;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lowerDevice = ref<NdmDeviceResultVO>();
|
||||||
|
const lowerDeviceLinkDescription = computed<NdmCameraLinkDescription | NdmSwitchLinkDescription | null>(() => {
|
||||||
|
if (!lowerDevice.value) return null;
|
||||||
|
const result = destr<any>(lowerDevice.value.linkDescription);
|
||||||
|
if (!result) return null;
|
||||||
|
if (typeof result !== 'object') return null;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const onAfterSelectDevice = (device: NdmDeviceResultVO) => {
|
||||||
|
lowerDevice.value = device;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortController = ref<AbortController>(new AbortController());
|
||||||
|
|
||||||
|
const { mutate: linkPortToDevice, isPending: linking } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
|
||||||
|
window.$loadingBar.start();
|
||||||
|
|
||||||
|
const upperDeviceType = tryGetDeviceType(ndmDevice.value.deviceType);
|
||||||
|
if (!upperDeviceType) throw new Error('本设备类型未知');
|
||||||
|
const upperDeviceDbId = ndmDevice.value.id;
|
||||||
|
if (!upperDeviceDbId) throw new Error('本设备没有ID');
|
||||||
|
|
||||||
|
const { portName } = port.value ?? {};
|
||||||
|
if (!portName) throw new Error('该端口没有名称');
|
||||||
|
|
||||||
|
if (!lowerDevice.value) throw new Error('请选择要关联的设备');
|
||||||
|
const lowerDeviceType = tryGetDeviceType(lowerDevice.value?.deviceType);
|
||||||
|
if (!lowerDeviceType) throw new Error('该设备类型未知');
|
||||||
|
const lowerDeviceDbId = lowerDevice.value?.id;
|
||||||
|
if (!lowerDeviceDbId) throw new Error('该设备没有ID');
|
||||||
|
|
||||||
|
// 0. 检查上游设备的linkDescription的downstream字段是否存在某个端口已经关联下游设备
|
||||||
|
const duplicated = Object.entries(upperDeviceLinkDescription.value?.downstream ?? {}).find(([, deviceStoreIndex]) => {
|
||||||
|
return deviceStoreIndex.deviceDbId === lowerDeviceDbId;
|
||||||
|
});
|
||||||
|
if (duplicated) {
|
||||||
|
const [portName] = duplicated;
|
||||||
|
throw new Error(`该设备已关联到端口${portName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 修改上游设备的linkDescription的downstream字段
|
||||||
|
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
|
||||||
|
let modifiedUpperDeviceLinkDescription: NdmSwitchLinkDescription;
|
||||||
|
if (!upperDeviceLinkDescription.value) {
|
||||||
|
modifiedUpperDeviceLinkDescription = {
|
||||||
|
downstream: {
|
||||||
|
[portName]: {
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: lowerDeviceType,
|
||||||
|
deviceDbId: lowerDeviceDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
modifiedUpperDeviceLinkDescription = {
|
||||||
|
...upperDeviceLinkDescription.value,
|
||||||
|
downstream: {
|
||||||
|
...upperDeviceLinkDescription.value.downstream,
|
||||||
|
[portName]: {
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: lowerDeviceType,
|
||||||
|
deviceDbId: lowerDeviceDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 2. 修改下游设备的linkDescription的upstream字段
|
||||||
|
const modifiedLowerDevice = cloneDeep(lowerDevice.value);
|
||||||
|
let modifiedLowerDeviceLinkDescription: NdmSwitchLinkDescription | NdmCameraLinkDescription;
|
||||||
|
if (!lowerDeviceLinkDescription.value) {
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
upstream: [
|
||||||
|
{
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const upstream = cloneDeep(lowerDeviceLinkDescription.value.upstream);
|
||||||
|
if (!upstream) {
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
...lowerDeviceLinkDescription.value,
|
||||||
|
upstream: [
|
||||||
|
{
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const deviceStoreIndex = upstream.find((deviceStoreIndex) => deviceStoreIndex.deviceDbId === upperDeviceDbId);
|
||||||
|
if (!deviceStoreIndex) {
|
||||||
|
upstream.push({
|
||||||
|
stationCode: station.value.code,
|
||||||
|
deviceType: upperDeviceType,
|
||||||
|
deviceDbId: upperDeviceDbId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
modifiedLowerDeviceLinkDescription = {
|
||||||
|
...lowerDeviceLinkDescription.value,
|
||||||
|
upstream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifiedLowerDevice.linkDescription = JSON.stringify(modifiedLowerDeviceLinkDescription);
|
||||||
|
|
||||||
|
// 3. 发起update请求并获取最新的设备详情(离线模式下直接修改本地数据)
|
||||||
|
if (offlineDev.value) {
|
||||||
|
return { upperDevice: modifiedUpperDevice, lowerDevice: modifiedLowerDevice };
|
||||||
|
}
|
||||||
|
const stationCode = station.value.code;
|
||||||
|
const signal = abortController.value.signal;
|
||||||
|
await updateDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
await updateDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
const latestUpperDevice = await detailDeviceApi(modifiedUpperDevice, { stationCode, signal });
|
||||||
|
const latestLowerDevice = await detailDeviceApi(modifiedLowerDevice, { stationCode, signal });
|
||||||
|
return { upperDevice: latestUpperDevice, lowerDevice: latestLowerDevice };
|
||||||
|
},
|
||||||
|
onSuccess: ({ upperDevice, lowerDevice }) => {
|
||||||
|
show.value = false;
|
||||||
|
window.$loadingBar.finish();
|
||||||
|
window.$message.success('关联成功');
|
||||||
|
if (!!upperDevice && !!lowerDevice) {
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...upperDevice });
|
||||||
|
deviceStore.patchDevice(station.value.code, { ...lowerDevice });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
window.$loadingBar.error();
|
||||||
|
if (isCancel(error)) return;
|
||||||
|
console.error(error);
|
||||||
|
const errorFeedback = parseErrorFeedback(error);
|
||||||
|
window.$message.error(errorFeedback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLink = () => {
|
||||||
|
linkPortToDevice();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
abortController.value.abort();
|
||||||
|
show.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal v-model:show="show" preset="card" style="width: 600px; height: 600px" :content-style="{ height: '100%', overflow: 'hidden' }" @close="onCancel" @esc="onCancel">
|
||||||
|
<template #header>
|
||||||
|
<span>{{ ndmDevice.name }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>{{ port?.portName }}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>关联设备</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<DeviceTree :station="station" :events="['select']" :device-prefix-label="'选择'" @after-select-device="onAfterSelectDevice" />
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<NFlex justify="end">
|
||||||
|
<NButton size="small" quaternary @click="onCancel">取消</NButton>
|
||||||
|
<NButton size="small" type="primary" :disabled="!lowerDevice" :loading="linking" @click="onLink">关联</NButton>
|
||||||
|
</NFlex>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="ts"></style>
|
||||||
@@ -49,7 +49,7 @@ const circuits = computed(() => lastDiagInfo.value?.info?.at(0)?.circuits);
|
|||||||
<DeviceCommonCard :common-info="commonInfo" />
|
<DeviceCommonCard :common-info="commonInfo" />
|
||||||
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
||||||
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
|
<SecurityBoxEnvCard :fan-speeds="fanSpeeds" :temperature="temperature" :humidity="humidity" :switches="switches" />
|
||||||
<SecurityBoxCircuitCard :circuits="circuits" :ndm-device="ndmDevice" :station="station" />
|
<SecurityBoxCircuitCard :ndm-device="ndmDevice" :station="station" :circuits="circuits" />
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const ports = computed(() => lastDiagInfo.value?.info?.portInfoList);
|
|||||||
<NFlex vertical>
|
<NFlex vertical>
|
||||||
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
|
||||||
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" />
|
||||||
<SwitchPortCard :ports="ports" />
|
<SwitchPortCard :ndm-device="ndmDevice" :station="station" :ports="ports" />
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
|
import { deleteDeviceApi, exportDeviceApi, importDeviceApi, type ImportMsg, type NdmDeviceLinkDescription, type NdmDevicePageQuery, type PageParams, type Station } from '@/apis';
|
||||||
import { useStationDevicesMutation } from '@/composables';
|
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';
|
||||||
@@ -6,6 +6,7 @@ import { downloadByData, parseErrorFeedback } from '@/utils';
|
|||||||
import { useMutation } from '@tanstack/vue-query';
|
import { useMutation } from '@tanstack/vue-query';
|
||||||
import { isCancel } from 'axios';
|
import { isCancel } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import destr from 'destr';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { h, onBeforeUnmount } from 'vue';
|
import { h, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
@@ -152,6 +153,32 @@ export const useDeviceManagement = () => {
|
|||||||
|
|
||||||
window.$loadingBar.start();
|
window.$loadingBar.start();
|
||||||
|
|
||||||
|
// 检查要删除的设备是否存在关联设备
|
||||||
|
const stationDevices = lineDevices.value[stationCode];
|
||||||
|
if (!!stationDevices) {
|
||||||
|
const classified = stationDevices[deviceType];
|
||||||
|
if (!!classified) {
|
||||||
|
const device = classified.find((device) => device.id === id);
|
||||||
|
if (!!device) {
|
||||||
|
const maybeLinkDescription = destr<any>(device.linkDescription);
|
||||||
|
if (!!maybeLinkDescription && typeof maybeLinkDescription === 'object') {
|
||||||
|
const linkDescription = maybeLinkDescription as NdmDeviceLinkDescription;
|
||||||
|
// 只要有上游或下游设备,就不能删除
|
||||||
|
const { upstream } = linkDescription;
|
||||||
|
if (!!upstream && upstream.length > 0) {
|
||||||
|
throw new Error('该设备存在关联的上游设备,无法删除');
|
||||||
|
}
|
||||||
|
if ('downstream' in linkDescription) {
|
||||||
|
const { downstream } = linkDescription;
|
||||||
|
if (!!downstream && Object.keys(downstream).length > 0) {
|
||||||
|
throw new Error('该设备存在关联的下游设备,无法删除');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await deleteDeviceApi(deviceType, id, { stationCode, signal });
|
return await deleteDeviceApi(deviceType, id, { stationCode, signal });
|
||||||
},
|
},
|
||||||
onSuccess: (_, { stationCode, signal }) => {
|
onSuccess: (_, { stationCode, signal }) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './injection';
|
||||||
export * from './java';
|
export * from './java';
|
||||||
export * from './mutation';
|
export * from './mutation';
|
||||||
export * from './query';
|
export * from './query';
|
||||||
|
|||||||
5
src/constants/injection.ts
Normal file
5
src/constants/injection.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { UseDeviceTreeReturn } from '@/composables';
|
||||||
|
import { createInjectionKey } from '@/utils';
|
||||||
|
import { type Ref } from 'vue';
|
||||||
|
|
||||||
|
export const SELECT_DEVICE_FN_INJECTION_KEY = createInjectionKey<Ref<UseDeviceTreeReturn['selectDevice'] | undefined>>('select-device-fn');
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NdmDeviceResultVO, Station } from '@/apis';
|
import type { NdmDeviceResultVO, Station } from '@/apis';
|
||||||
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
|
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
|
||||||
|
import type { UseDeviceSelectionReturn } from '@/composables';
|
||||||
|
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
|
||||||
import { useStationStore } from '@/stores';
|
import { useStationStore } from '@/stores';
|
||||||
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
|
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
const stationStore = useStationStore();
|
const stationStore = useStationStore();
|
||||||
const { stations } = storeToRefs(stationStore);
|
const { stations } = storeToRefs(stationStore);
|
||||||
@@ -12,6 +14,13 @@ const { stations } = storeToRefs(stationStore);
|
|||||||
const selectedStation = ref<Station>();
|
const selectedStation = ref<Station>();
|
||||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||||
|
|
||||||
|
// 获取设备树暴露出来的 `selectDevice` 函数,并将其提供给子组件
|
||||||
|
const selectDeviceFn = ref<UseDeviceSelectionReturn['selectDevice']>();
|
||||||
|
const onExposeSelectDeviceFn: DeviceTreeProps['onExposeSelectDeviceFn'] = (fn) => {
|
||||||
|
selectDeviceFn.value = fn;
|
||||||
|
};
|
||||||
|
provide(SELECT_DEVICE_FN_INJECTION_KEY, selectDeviceFn);
|
||||||
|
|
||||||
const onAfterSelectDevice: DeviceTreeProps['onAfterSelectDevice'] = (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 +30,7 @@ const onAfterSelectDevice: DeviceTreeProps['onAfterSelectDevice'] = (device, sta
|
|||||||
<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 :events="['select', 'manage']" :sync-route="true" :device-prefix-label="'查看'" @after-select-device="onAfterSelectDevice" />
|
<DeviceTree :events="['select', 'manage']" :sync-route="true" :device-prefix-label="'查看'" @expose-select-device-fn="onExposeSelectDeviceFn" @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