feat: device cards

This commit is contained in:
yangsy
2025-09-02 23:35:54 +08:00
parent ced07b17ff
commit 9bf8229bf6
16 changed files with 2211 additions and 11 deletions

View File

@@ -0,0 +1,517 @@
<script lang="ts">
const transformPortSpeed = (portInfo: NdmSwitchPortInfo, type: 'in' | 'out' | 'total'): string => {
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s'];
const { inBytes, lastInBytes, outBytes, lastOutBytes, inFlow, outFlow, flow } = portInfo;
let result: number = 0;
if (inFlow && outFlow && flow) {
if (type === 'in') {
result = inFlow;
}
if (type === 'out') {
result = outFlow;
}
if (type === 'total') {
result = flow;
}
} else {
let dInBytes = 0;
let dOutBytes = 0;
if (inBytes < lastInBytes) {
dInBytes = inBytes + JAVA_UNSIGNED_INTEGER_MAX_VALUE - lastInBytes;
} else {
dInBytes = inBytes - lastInBytes;
}
if (outBytes < lastOutBytes) {
dOutBytes = outBytes + JAVA_UNSIGNED_INTEGER_MAX_VALUE - lastOutBytes;
} else {
dOutBytes = outBytes - lastOutBytes;
}
if (type === 'in') {
result = dInBytes;
}
if (type === 'out') {
result = dOutBytes;
}
if (type === 'total') {
result = dInBytes + dOutBytes;
}
result /= NDM_SWITCH_PROBE_INTERVAL;
}
let index = 0;
while (result >= 1024 && index < units.length - 1) {
result /= 1024;
index++;
}
return `${result.toFixed(3)} ${units[index]}`;
};
</script>
<script setup lang="ts">
/**
* 交换机端口布局组件
*
* 功能描述:
* 1. 解析端口名称格式x/y/z按槽位x/y进行分组
* 2. 实现两行纵向优先的Grid布局显示端口
* 3. 提供端口状态的可视化指示(在线/离线/未知)
* 4. 通过悬停弹窗显示端口详细信息和速率数据
*
* 技术特点:
* - 基于 Vue 3 Composition API
* - 使用 NaiveUI 组件库
* - 支持亮色/暗色主题切换
* - 响应式数据处理和布局
*/
// 类型定义导入
import type { NdmSwitchPortInfo } from '@/apis/domains';
// 常量导入
import { JAVA_UNSIGNED_INTEGER_MAX_VALUE, NDM_SWITCH_PROBE_INTERVAL } from '@/constants';
// NaiveUI 组件导入
import { NCard, NGrid, NGridItem, NPopover } from 'naive-ui';
// Vue 3 响应式 API
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)">
{{ port.upDown === 1 ? '启用' : port.upDown === 2 ? '禁用' : '未知' }}
</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, 'in') }}</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>