Files
ndm-web-client/src/components/device-page/device-card/current-diag-card/switch-port-card.vue
2025-09-26 19:01:48 +08:00

461 lines
14 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">
/**
* 交换机端口布局组件
*
* 功能描述:
* 1. 解析端口名称格式x/y/z按槽位x/y进行分组
* 2. 实现两行纵向优先的Grid布局显示端口
* 3. 提供端口状态的可视化指示(在线/离线/未知)
* 4. 通过悬停弹窗显示端口详细信息和速率数据
*
* 技术特点:
* - 基于 Vue 3 Composition API
* - 使用 NaiveUI 组件库
* - 支持亮色/暗色主题切换
* - 响应式数据处理和布局
*/
import { getPortStatusVal, transformPortSpeed } from '../../helper';
import type { NdmSwitchPortInfo } from '@/apis/domains';
import { NCard, NGrid, NGridItem, NPopover } from 'naive-ui';
import { computed, toRefs } from 'vue';
/**
* 组件属性定义
*
* @param portInfoList 端口信息列表
* 数据格式NdmSwitchPortInfo[]
* 每个端口包含portName, upDown, inFlow, outFlow, inBytes, outBytes 等字段
*
* 端口名称格式:"x/y/z"
* - x: 槽位号
* - y: 子卡号
* - z: 接口序号
*
* 示例数据:
* [
* { portName: "1/0/1", upDown: 1, inFlow: 1024, outFlow: 2048, ... },
* { portName: "1/0/2", upDown: 0, inFlow: 0, outFlow: 0, ... },
* { portName: "2/0/1", upDown: 2, inFlow: 512, outFlow: 1024, ... }
* ]
*/
const props = defineProps<{
portInfoList: NdmSwitchPortInfo[];
}>();
const { portInfoList } = toRefs(props);
/**
* 解析端口名称并按槽位分组
*
* 示例输入数据:
* portInfoList = [
* { portName: "1/0/3", upDown: 1, ... },
* { portName: "1/0/1", upDown: 0, ... },
* { portName: "2/0/2", upDown: 1, ... },
* { portName: "1/1/1", upDown: 2, ... },
* { portName: "2/0/1", upDown: 1, ... }
* ]
*/
const groupedPorts = computed(() => {
// 第一步:创建分组容器
// 使用 Map 数据结构存储分组结果key 为槽位标识value 为端口数组
const groups = new Map<string, NdmSwitchPortInfo[]>();
// 第二步:遍历所有端口,按槽位进行分组
portInfoList.value.forEach((port) => {
// 解析端口名称格式 "x/y/z" -> ["x", "y", "z"]
// 例如:"1/0/3" -> ["1", "0", "3"]
const parts = port.portName.split('/');
if (parts.length >= 3) {
// 组合槽位键:槽位号/子卡号 (parts[0]/parts[1])
// 例如:"1" + "/" + "0" = "1/0"
const slotKey = `${parts[0]}/${parts[1]}`;
// 如果该槽位组不存在,创建新的空数组
if (!groups.has(slotKey)) {
groups.set(slotKey, []);
}
// 将端口添加到对应的槽位组中
groups.get(slotKey)!.push(port);
}
});
// 此时 groups 的结构示例:
// Map {
// "1/0" => [{ portName: "1/0/3" }, { portName: "1/0/1" }],
// "2/0" => [{ portName: "2/0/2" }, { portName: "2/0/1" }],
// "1/1" => [{ portName: "1/1/1" }]
// }
// 第三步:对槽位组进行排序并处理每组内的端口排序
return Array.from(groups.entries()) // 转换为数组:[["1/0", [...]], ["2/0", [...]], ["1/1", [...]]]
.sort(([a], [b]) => {
// 解析槽位键进行数值排序
// 例如:"1/0" -> [1, 0], "2/0" -> [2, 0]
const [slotA, subA] = a.split('/').map(Number);
const [slotB, subB] = b.split('/').map(Number);
// 首先按槽位号排序,如果相同则按子卡号排序
// 排序结果:"1/0" -> "1/1" -> "2/0"
return slotA - slotB || subA - subB;
})
.map(([slotKey, ports]) => ({
slotKey, // 槽位标识,如 "1/0"
ports: ports.sort((a, b) => {
// 对每个槽位组内的端口按接口序号排序
// 提取接口序号:"1/0/3" -> "3" -> 3
const portNumA = parseInt(a.portName.split('/')[2]);
const portNumB = parseInt(b.portName.split('/')[2]);
// 按接口序号升序排列端口1 -> 端口2 -> 端口3
return portNumA - portNumB;
}),
}));
// 最终返回结果示例:
// [
// {
// slotKey: "1/0",
// ports: [{ portName: "1/0/1" }, { portName: "1/0/3" }]
// },
// {
// slotKey: "1/1",
// ports: [{ portName: "1/1/1" }]
// },
// {
// slotKey: "2/0",
// ports: [{ portName: "2/0/1" }, { portName: "2/0/2" }]
// }
// ]
});
/**
* 获取端口状态样式类
*
* @param port 端口信息对象
* @returns 对应的CSS类名
*
* 状态映射:
* - upDown = 1: 端口启用 -> 'port-up' (绿色)
* - upDown = 2: 端口禁用 -> 'port-down' (红色)
* - 其他值: 状态未知 -> 'port-unknown' (黄色)
*
* 示例:
* getPortStatusClass({upDown: 1}) -> 'port-up'
* getPortStatusClass({upDown: 2}) -> 'port-down'
* getPortStatusClass({upDown: 0}) -> 'port-unknown'
*/
const getPortStatusClass = (port: NdmSwitchPortInfo) => {
if (port.upDown === 1) {
return 'port-up';
} else if (port.upDown === 2) {
return 'port-down';
}
return 'port-unknown';
};
</script>
<template>
<!-- 主容器使用 NaiveUI 卡片组件包装整个端口布局 -->
<NCard v-if="portInfoList.length > 0" size="small" title="端口数据" hoverable>
<div class="switch-port-layout">
<!--
槽位组遍历根据 groupedPorts 计算属性渲染每个槽位组
数据结构示例
groupedPorts = [
{ slotKey: "1/0", ports: [port1, port2, ...] },
{ slotKey: "1/1", ports: [port3, port4, ...] },
{ slotKey: "2/0", ports: [port5, port6, ...] }
]
-->
<div v-for="group in groupedPorts" :key="group.slotKey" class="slot-group">
<!-- 槽位标题显示槽位标识和端口数量统计 -->
<div class="slot-header">
<span class="slot-title">槽位 {{ group.slotKey }}</span>
<span class="port-count">({{ group.ports.length }}个端口)</span>
</div>
<NGrid :cols="Math.ceil(group.ports.length / 2)" :x-gap="8" :y-gap="8" class="port-grid">
<!--
布局映射逻辑实现两行纵向优先的Grid布局
示例假设有6个端口 [port1, port2, port3, port4, port5, port6]
portIndex: 0 1 2 3 4 5
gridRow: 1 2 1 2 1 2 ((portIndex % 2) + 1)
gridColumn: 1 1 2 2 3 3 (Math.floor(portIndex / 2) + 1)
最终布局效果
第1行port1 port3 port5
第2行port2 port4 port6
这样实现了纵向优先填充的两行布局
-->
<NGridItem
v-for="(port, portIndex) in group.ports"
:key="port.portName"
:style="{
gridRow: (portIndex % 2) + 1, // 奇偶索引分别占第1、2行
gridColumn: Math.floor(portIndex / 2) + 1, // 每两个端口占一列
}"
>
<!--
端口信息弹窗使用 NaiveUI Popover 组件显示详细信息
触发方式hover悬停触发
位置top在端口项上方显示
-->
<NPopover trigger="hover" placement="top">
<template #trigger>
<!--
端口项显示
- 动态应用状态样式类port-up/port-down/port-unknown
- 显示端口接口序号从完整端口名 x/y/z 中提取 z 部分
- 状态指示器圆点
-->
<div class="port-item" :class="getPortStatusClass(port)">
<div class="port-name">{{ port.portName.split('/')[2] }}</div>
<div class="port-status-indicator"></div>
</div>
</template>
<!--
弹窗内容显示端口的详细信息
信息包括
1. 完整端口名称
2. 端口状态启用/禁用/未知
3. 上行速率使用 transformPortSpeed 函数计算
4. 下行速率使用 transformPortSpeed 函数计算
5. 总速率使用 transformPortSpeed 函数计算
-->
<div class="port-popover-content">
<div class="port-info-row">
<span class="label">端口:</span>
<span class="value">{{ port.portName }}</span>
</div>
<div class="port-info-row">
<span class="label">状态:</span>
<span class="value" :class="getPortStatusClass(port)">{{ getPortStatusVal(port) }}</span>
</div>
<div class="port-info-row">
<span class="label">上行速率:</span>
<span class="value">{{ transformPortSpeed(port, 'in') }}</span>
</div>
<div class="port-info-row">
<span class="label">下行速率:</span>
<span class="value">{{ transformPortSpeed(port, 'out') }}</span>
</div>
<div class="port-info-row">
<span class="label">总速率:</span>
<span class="value">{{ transformPortSpeed(port, 'total') }}</span>
</div>
</div>
</NPopover>
</NGridItem>
</NGrid>
</div>
</div>
</NCard>
</template>
<style scoped lang="scss">
/*
* 交换机端口布局样式
*
* 设计理念:
* 1. 简约清晰的视觉风格
* 2. 直观的端口状态指示
* 3. 良好的交互体验
* 4. 亮色/暗色模式适配
*/
.switch-port-layout {
/* 槽位组容器 - 每个槽位之间保持适当间距 */
.slot-group {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0; // 最后一个槽位组不需要下边距
}
}
/* 槽位标题头部 - 显示槽位信息和端口数量 */
.slot-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color); // 使用主题变量适配亮暗模式
.slot-title {
font-weight: 600;
font-size: 14px;
color: var(--text-color-1); // 主要文本颜色
}
.port-count {
font-size: 12px;
color: var(--text-color-3); // 次要文本颜色
}
}
/* 端口网格容器 - 限制最大宽度防止过度拉伸 */
.port-grid {
max-width: 100%;
}
/*
* 端口项样式 - 核心视觉元素
*
* 尺寸设计48x36px适合显示端口号和状态指示器
* 状态指示:通过边框颜色和圆点颜色区分端口状态
* 交互效果:悬停时轻微上移和阴影效果
*/
.port-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 48px; // 固定宽度确保布局一致性
height: 36px; // 固定高度确保视觉平衡
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--card-color);
cursor: pointer;
transition: all 0.2s ease; // 平滑的交互动画
/* 悬停效果 - 提升交互体验 */
&:hover {
transform: translateY(-1px); // 轻微上移
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 添加阴影
}
/* 端口名称显示 - 只显示接口序号部分 */
.port-name {
font-size: 11px;
font-weight: 500;
color: var(--text-color-2);
line-height: 1; // 紧凑行高
}
/* 端口状态指示器 - 小圆点显示状态 */
.port-status-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
margin-top: 2px;
}
/* 端口启用状态 - 绿色主题 */
&.port-up {
border-color: #18a058; // NaiveUI 成功色
.port-status-indicator {
background-color: #18a058;
}
&:hover {
background-color: rgba(24, 160, 88, 0.05); // 浅绿色背景
}
}
/* 端口禁用状态 - 红色主题 */
&.port-down {
border-color: #d03050; // NaiveUI 错误色
.port-status-indicator {
background-color: #d03050;
}
&:hover {
background-color: rgba(208, 48, 80, 0.05); // 浅红色背景
}
}
/* 端口未知状态 - 黄色主题 */
&.port-unknown {
border-color: #f0a020; // NaiveUI 警告色
.port-status-indicator {
background-color: #f0a020;
}
&:hover {
background-color: rgba(240, 160, 32, 0.05); // 浅黄色背景
}
}
}
}
/*
* 端口信息弹窗内容样式
*
* 功能:显示端口的详细信息,包括状态和速率数据
* 布局:标签-值的两列布局,右对齐数值便于对比
*/
.port-popover-content {
min-width: 160px; // 确保弹窗有足够宽度显示内容
.port-info-row {
display: flex;
justify-content: space-between; // 标签和值分别左右对齐
align-items: center;
margin-bottom: 6px;
&:last-child {
margin-bottom: 0; // 最后一行不需要下边距
}
/* 信息标签样式 - 统一的次要文本样式 */
.label {
font-size: 12px;
color: var(--text-color-3); // 次要文本颜色
margin-right: 12px;
}
/* 信息值样式 - 突出显示的数据 */
.value {
font-size: 12px;
font-weight: 500; // 稍微加粗突出数值
color: var(--text-color-2);
/* 状态值的颜色区分 - 与端口项状态颜色保持一致 */
&.port-up {
color: #18a058; // 启用状态:绿色
}
&.port-down {
color: #d03050; // 禁用状态:红色
}
&.port-unknown {
color: #f0a020; // 未知状态:黄色
}
}
}
}
/*
* 暗色模式适配
*
* 针对系统暗色模式的特殊样式调整
* 主要调整阴影效果,使其在暗色背景下更加明显
*/
@media (prefers-color-scheme: dark) {
.switch-port-layout {
.port-item {
&:hover {
/* 暗色模式下使用白色阴影替代黑色阴影 */
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.1);
}
}
}
}
</style>