From 30cc596ed8b965bd1109330ecbdaf25803cb3778 Mon Sep 17 00:00:00 2001 From: yinjianm Date: Sun, 19 Apr 2026 04:54:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E4=BC=98=E5=8C=96=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=9B=91=E6=8E=A7=E9=BB=98=E8=AE=A4=E6=A6=82=E8=A7=88?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 CPU 和网络的历史图表直接整合进默认概览,保留常驻资源卡片, 并移除底部重复的图表区,减少侧栏中的重复信息。 --- .helloagents/CHANGELOG.md | 4 +- .helloagents/modules/frontend.md | 4 +- .../frontend/src/components/StatusMonitor.vue | 155 +++++++++++++++++- 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index c9a964f..9008d0a 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -2,9 +2,9 @@ ## [Unreleased] -- **[frontend]**: 删除状态监控默认视图中的 CPU 使用率卡与网络速度卡,保留内存 / 磁盘 / 进程管理作为常驻概览,减少右侧窄栏中的重复信息 — by yinjianm +- **[frontend]**: 移除状态监控底部重复的 CPU / 网络 `chart-panel` 图表区,保留默认概览中的 CPU 与网络卡片,只收掉重复展示 — by yinjianm - 类型: 快速修改(无方案包) - - 文件: packages/frontend/src/components/StatusMonitor.vue:60-168,276-280,282-325 + - 文件: packages/frontend/src/components/StatusMonitor.vue:184-189 - **[frontend]**: 将状态监控中的 CPU 卡片升级为总 CPU `canvas` 历史图 + 每核心实时条卡,并在极窄侧栏下自动切换为单列布局 — by yinjianm - 方案: [202604190351_status-monitor-cpu-total-and-per-core](archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/) diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index f19d504..b1a867c 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -58,8 +58,8 @@ ### 状态监控卡片 **条件**: 用户在 `/workspace` 右侧状态监控面板查看服务器资源状态。 -**行为**: `StatusMonitor.vue` 当前已从通用卡片栅格重排为更接近参考图的窄屏监控结构:顶部改为成对的信息条;默认概览区当前仅保留内存、磁盘和进程管理三个常驻模块,不再默认展示 CPU 使用率卡与网络速度卡,以减少右侧窄栏里的重复信息密度。其中内存卡片会在容器宽度大于等于 250px 时维持环形概览与统计块的高密度横向布局,仅在低于 250px 时切为手机式竖排;磁盘模块继续展示设备视觉块与紧凑磁盘摘要;默认视图底部继续保留“进程管理”概览与高占用进程预览,并通过“查看全部”打开 `ProcessManagerModal.vue`。其中进程统计当前已整体收纳到模块标题区右侧的一组 `monitor-module__pill` 胶囊中,统一展示“总数 / 运行中 / 休眠中”,不再额外保留独立摘要行,以减少默认卡片的纵向占用。该 modal 继续采用深色控制台式表格布局,支持搜索 PID / 用户 / 命令、自动刷新、手动刷新,以及对单个进程执行“结束”或“强制结束”操作,并通过当前活动 SSH 会话的 `wsManager` 与后端 `process:list` / `process:signal` 消息交互;当前还支持点击 `PID / USER / STATE / CPU / MEM / START / COMMAND` 表头做本地升降序排序,并为激活列显示方向标记,同时给右上角关闭按钮预留了独立安全区,避免与刷新区过近。 -**结果**: 前端状态监控形成了“更贴近参考图的默认小屏监控 + 独立进程管理页”的双层结构:默认面板保持了侧栏内更紧凑的三块常驻概览,不再重复显示 CPU 和网络卡片;完整进程管理继续独立存在,不挤占侧栏本体;进入进程管理详细视图后,也能更快按字段维度筛查进程。 +**行为**: `StatusMonitor.vue` 当前已从通用卡片栅格重排为更接近参考图的窄屏监控结构:顶部改为成对的信息条;默认概览区继续保留 CPU、内存、网络、磁盘和进程管理五块常驻模块,其中 CPU 卡片使用 `StatusMonitorCpuHistoryChart.vue` 展示总 CPU 历史曲线,并以紧凑网格展示每个核心的实时条卡;网络卡片继续使用 `StatusMonitorNetworkHistoryChart.vue` 展示最近网络历史,并保留下方统计表。为避免与默认概览重复,底部独立 `StatusCharts.vue` 图表区不再挂载。内存卡片会在容器宽度大于等于 250px 时维持环形概览与统计块的高密度横向布局,仅在低于 250px 时切为手机式竖排;磁盘模块继续展示设备视觉块与紧凑磁盘摘要;默认视图底部继续保留“进程管理”概览与高占用进程预览,并通过“查看全部”打开 `ProcessManagerModal.vue`。其中进程统计当前已整体收纳到模块标题区右侧的一组 `monitor-module__pill` 胶囊中,统一展示“总数 / 运行中 / 休眠中”,不再额外保留独立摘要行,以减少默认卡片的纵向占用。该 modal 继续采用深色控制台式表格布局,支持搜索 PID / 用户 / 命令、自动刷新、手动刷新,以及对单个进程执行“结束”或“强制结束”操作,并通过当前活动 SSH 会话的 `wsManager` 与后端 `process:list` / `process:signal` 消息交互;当前还支持点击 `PID / USER / STATE / CPU / MEM / START / COMMAND` 表头做本地升降序排序,并为激活列显示方向标记,同时给右上角关闭按钮预留了独立安全区,避免与刷新区过近。 +**结果**: 前端状态监控形成了“更贴近参考图的默认小屏监控 + 独立进程管理页”的双层结构:默认面板保留 CPU 和网络等常驻概览卡,同时移除了底部重复图表区,减少重复信息;完整进程管理继续独立存在,不挤占侧栏本体;进入进程管理详细视图后,也能更快按字段维度筛查进程。 ### 快捷指令拖拽排序 **条件**: 用户在 Workbench 的快捷指令视图中浏览分组或扁平命令列表,且当前未启用搜索过滤。 diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index 76a0722..0785ba1 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -57,6 +57,38 @@ +
+
+
+ {{ t('statusMonitor.cpuLabel') }} +
{{ t('statusMonitor.cpuUsageTitle') }}
+
+ {{ displayCpuCores }} +
+ +
+ + +
+
+
+
+ {{ item.label }} + {{ item.value }} +
+
+ +
+
+
+
+
+
+
@@ -91,6 +123,50 @@
+
+
+
+ {{ t('statusMonitor.networkLabel') }} +
{{ t('statusMonitor.networkLabel') }}
+
+ {{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }} +
+ +
+ + +
+
+ {{ networkInterfaceDisplay }} + {{ t('statusMonitor.downloadLabel') }} / {{ t('statusMonitor.uploadLabel') }} +
+
+ + {{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }} + {{ t('statusMonitor.totalTrafficLabel') }} +
+ +
+
+ + + {{ item.label }} + + {{ item.value }} + {{ item.totalValue }} +
+
+
+
+
+
@@ -181,12 +257,6 @@
- { const currentSessionState = computed(() => (props.activeSessionId ? sessions.value.get(props.activeSessionId) : null)); const currentServerStatus = computed(() => currentSessionState.value?.statusMonitorManager?.serverStatus?.value ?? null); +const currentCpuHistory = computed(() => currentSessionState.value?.statusMonitorManager?.cpuHistory?.value ?? Array(24).fill(null)); +const currentNetRxHistory = computed(() => currentSessionState.value?.statusMonitorManager?.netRxHistory?.value ?? Array(24).fill(null)); +const currentNetTxHistory = computed(() => currentSessionState.value?.statusMonitorManager?.netTxHistory?.value ?? Array(24).fill(null)); + const displayMemoryPercent = computed(() => clampPercent(currentServerStatus.value?.memPercent)); const displayDiskPercent = computed(() => clampPercent(currentServerStatus.value?.diskPercent)); const currentStatusError = computed(() => currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null); @@ -279,6 +354,23 @@ const displayCpuCores = computed(() => { const displayOsName = computed(() => (currentServerStatus.value?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable')); const networkInterfaceDisplay = computed(() => currentServerStatus.value?.netInterface || t('statusMonitor.notAvailable')); +const formatBytesPerSecond = (bytes?: number): string => { + if (bytes === undefined || bytes === null || isNaN(bytes)) return t('statusMonitor.notAvailable'); + if (bytes < 1024) return `${bytes} ${t('statusMonitor.bytesPerSecond')}`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} ${t('statusMonitor.kiloBytesPerSecond')}`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} ${t('statusMonitor.megaBytesPerSecond')}`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} ${t('statusMonitor.gigaBytesPerSecond')}`; +}; + +const formatBytes = (bytes?: number): string => { + if (bytes === undefined || bytes === null || isNaN(bytes)) return t('statusMonitor.notAvailable'); + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} ${t('statusMonitor.megaBytes')}`; + if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} ${t('statusMonitor.gigaBytes')}`; + return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1)} TB`; +}; + const formatCompactBytes = (bytes?: number): string => { if (bytes === undefined || bytes === null || isNaN(bytes)) return t('statusMonitor.notAvailable'); if (bytes < 1024) return `${bytes.toFixed(1)} B`; @@ -414,6 +506,55 @@ const systemCardMetaItems = computed(() => [ { key: 'uptime', label: t('statusMonitor.uptimeLabel'), value: uptimeDisplay.value }, ]); +const cpuCoreItems = computed(() => { + const rawPercents = currentServerStatus.value?.cpuCorePercents; + const fallbackCoreCount = (() => { + const currentCores = currentServerStatus.value?.cpuCores; + if (typeof currentCores !== 'number' || !Number.isFinite(currentCores) || currentCores <= 0) { + return 0; + } + return Math.round(currentCores); + })(); + + const normalizedPercents = Array.isArray(rawPercents) && rawPercents.length > 0 + ? rawPercents + : Array.from({ length: fallbackCoreCount }, () => 0); + + return normalizedPercents.map((percent, index) => { + const clampedPercent = clampPercent(percent); + return { + key: `cpu-core-${index + 1}`, + label: t('statusMonitor.cpuCoreLabel', { index: index + 1 }), + value: `${Math.round(clampedPercent)}%`, + percent: clampedPercent, + }; + }); +}); + +const networkFlowItems = computed(() => [ + { + key: 'download', + label: t('statusMonitor.downloadLabel'), + value: formatBytesPerSecond(currentServerStatus.value?.netRxRate), + totalValue: formatBytes(currentServerStatus.value?.netRxTotalBytes), + tone: 'down', + icon: 'fa-arrow-down', + }, + { + key: 'upload', + label: t('statusMonitor.uploadLabel'), + value: formatBytesPerSecond(currentServerStatus.value?.netTxRate), + totalValue: formatBytes(currentServerStatus.value?.netTxTotalBytes), + tone: 'up', + icon: 'fa-arrow-up', + }, +]); + +const networkRateUnitLabel = computed(() => { + const maxRate = Math.max(currentServerStatus.value?.netRxRate ?? 0, currentServerStatus.value?.netTxRate ?? 0); + return maxRate >= 1024 * 1024 ? 'MB/s' : 'KB/s'; +}); + const diskDeviceAccent = computed(() => { const raw = currentServerStatus.value?.diskDevice;