461 lines
14 KiB
Vue
461 lines
14 KiB
Vue
<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>
|