Files
ndm-web-platform/src/components/device/device-card/components/current-diag/switch-port-card.vue

354 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
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 { useDeviceStore, useSettingStore } from '@/stores';
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<{
ndmDevice: NdmSwitchResultVO;
station: Station;
ports?: NdmSwitchPortInfo[];
}>();
const themeVars = useThemeVars();
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 switchSlots = computed(() => {
// 解析端口名称,将端口按槽位进行分组
const groupMap = new Map<string, NdmSwitchPortInfo[]>();
ports.value?.forEach((port) => {
const parts = port.portName.split('/');
if (parts.length >= 3) {
const slotName = `${parts[0]}/${parts[1]}`;
if (!groupMap.has(slotName)) {
groupMap.set(slotName, []);
}
groupMap.get(slotName)!.push(port);
}
});
// 将Map转换为entries
const entries = Array.from(groupMap.entries());
// 按槽位进行排序
const sorted = entries.sort(([aSlotName], [bSlotName]) => {
const [mainA = 0, subA = 0] = aSlotName.split('/').map((value) => parseInt(value));
const [mainB = 0, subB = 0] = bSlotName.split('/').map((value) => parseInt(value));
return mainA - mainB || subA - subB;
});
// 按端口号进行排序
return sorted.map(([slotName, ports]) => ({
slotName,
ports: ports.sort((portA, portB) => {
const portNumA = parseInt(portA.portName.split('/').at(2) ?? '0');
const portNumB = parseInt(portB.portName.split('/').at(2) ?? '0');
return portNumA - portNumB;
}),
}));
});
const getPortClassName = (port: NdmSwitchPortInfo) => {
if (port.upDown === 1) {
return 'port-up';
}
if (port.upDown === 2) {
return 'port-down';
}
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) => {
if (!upperDeviceLinkDescription.value) return null;
const downstream = upperDeviceLinkDescription.value.downstream;
if (!downstream) return null;
const deviceStoreIndex = downstream[port.portName];
if (!deviceStoreIndex) return null;
const { stationCode, deviceType, deviceDbId } = deviceStoreIndex;
const stationDevices = lineDevices.value[stationCode];
if (!stationDevices) return null;
const devices = stationDevices[deviceType];
const lowerDevice = devices.find((device) => device.id === deviceDbId);
if (!lowerDevice) {
// 下游设备不存在时解除关联
const modifiedUpperDevice = cloneDeep(ndmDevice.value);
const modifiedUpperLinkDescription = cloneDeep(upperDeviceLinkDescription.value);
delete modifiedUpperLinkDescription.downstream?.[port.portName];
modifiedUpperDevice.linkDescription = JSON.stringify(modifiedUpperLinkDescription);
// 不需要等待异步
const stationCode = station.value.code;
updateDeviceApi(modifiedUpperDevice, { stationCode }).then(() => {
detailDeviceApi(modifiedUpperDevice, { stationCode }).then((upperDevice) => {
if (!upperDevice) return;
deviceStore.patchDevice(stationCode, { ...upperDevice });
});
});
return null;
}
return lowerDevice;
};
// 获取从父组件注入的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);
// TODO: 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>
<template>
<NCard v-if="showCard" hoverable size="small">
<template #header>
<span>端口数据</span>
</template>
<template #default>
<!-- 遍历所有槽位 -->
<template v-for="{ slotName, ports } in switchSlots" :key="slotName">
<div style="padding: 8px 0">
<span>{{ slotName }}{{ ports.length }}个端口</span>
<!-- 端口布局 2 至少12列 纵向排序 -->
<div style="display: grid; grid-template-rows: repeat(2, auto); grid-auto-flow: column" :style="{ 'grid-template-columns': `repeat(${Math.max(ports.length / 2, 12)}, 1fr)` }">
<template v-for="(port, index) in ports" :key="port.portName">
<!-- 端口 -->
<NPopover :delay="300">
<template #trigger>
<!-- 最外层div宽度100% -->
<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="font-size: xx-small">{{ index }}</div>
<div class="indicator" style="width: 8px; height: 8px; border-radius: 50%" :class="getPortClassName(port)"></div>
</div>
</div>
</template>
<template #default>
<NDescriptions bordered size="small" label-placement="left" :column="1">
<NDescriptionsItem label="端口名称">{{ port.portName }}</NDescriptionsItem>
<NDescriptionsItem label="状态">{{ getPortStatusValue(port) }}</NDescriptionsItem>
<NDescriptionsItem label="上行速率">{{ transformPortSpeed(port, 'in') }}</NDescriptionsItem>
<NDescriptionsItem label="下行速率">{{ transformPortSpeed(port, 'out') }}</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>
</template>
</NPopover>
</template>
</div>
</div>
</template>
</template>
</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>
<style scoped lang="scss">
.port {
&.port-up {
&:hover {
background-color: #18a05816;
}
}
&.port-down {
&:hover {
background-color: #d0305016;
}
}
&.port-unknown {
&:hover {
background-color: #f0a02016;
}
}
&:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
box-shadow: v-bind('themeVars.boxShadow1');
}
.indicator {
&.port-up {
background-color: #18a058;
}
&.port-down {
background-color: #d03050;
}
&.port-unknown {
background-color: #f0a020;
}
}
}
</style>