From d300566f894ee6bf342cfb76d3d6f7601106d1a9 Mon Sep 17 00:00:00 2001 From: yinjianm Date: Sun, 19 Apr 2026 04:44:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E7=B2=BE=E7=AE=80=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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认视图移除 CPU 使用率卡和网络速度卡,保留内存、 磁盘与进程管理作为常驻概览,以减少右侧窄栏中的重复 信息。 同时压缩 CPU/网络历史图的高度,避免在窄侧栏下撑开 布局。 --- .helloagents/CHANGELOG.md | 7 + .helloagents/modules/frontend.md | 4 +- .../frontend/src/components/StatusMonitor.vue | 218 +++++------------- .../StatusMonitorCpuHistoryChart.vue | 17 +- .../StatusMonitorNetworkHistoryChart.vue | 8 +- 5 files changed, 80 insertions(+), 174 deletions(-) diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index cfa5584..c9a964f 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +- **[frontend]**: 删除状态监控默认视图中的 CPU 使用率卡与网络速度卡,保留内存 / 磁盘 / 进程管理作为常驻概览,减少右侧窄栏中的重复信息 — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/frontend/src/components/StatusMonitor.vue:60-168,276-280,282-325 + - **[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/) @@ -97,6 +101,9 @@ - 方案: [202603250614_terminal-ansi-color-effects](archive/2026-03/202603250614_terminal-ansi-color-effects/) ### 快速修改 +- **[frontend]**: 将 CPU 历史图卡片从随父容器拉伸改为固定紧凑高度约 `188px`,并同步压缩标题区与 canvas 高度,避免在窄侧栏下被撑到约 `278px` — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue:185-245 - **[frontend]**: 将状态监控“进程管理”的“运行中 / 休眠中”也收纳进标题区胶囊组,和“总数”一起以内联小显示呈现,不再保留独立摘要行 - by yinjianm - 类型: 快速修改(无方案包) - 文件: packages/frontend/src/components/StatusMonitor.vue:217-230,571-575,875-880 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index eab146d..f19d504 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -58,8 +58,8 @@ ### 状态监控卡片 **条件**: 用户在 `/workspace` 右侧状态监控面板查看服务器资源状态。 -**行为**: `StatusMonitor.vue` 当前已从通用卡片栅格重排为更接近参考图的窄屏监控结构:顶部改为成对的信息条,资源概览改为带编号的紧凑使用率行,内存/网络/磁盘模块都采用明显的左右分区关系;其中内存卡片现在会在容器宽度大于等于 250px 时维持环形概览与统计块的高密度横向布局,仅在低于 250px 时切为手机式竖排;网络卡片则通过新增 `StatusMonitorNetworkHistoryChart.vue` 把原本的 SVG 趋势线替换为基于 Chart.js `canvas` 的最近 24 个采样点历史图,并进一步固定为“上方历史图 + 下方统计表”的纵向堆叠结构,不再切回左右双列,同时通过压缩图表 canvas 高度、网络表间距与统计项 padding,把整个网络模块约束在 `350px` 以内;CPU 卡片也已升级为 `StatusMonitorCpuHistoryChart.vue` 驱动的总 CPU `canvas` 历史图,并在图表旁/下方用紧凑网格展示每个核心的独立实时条卡,在容器宽度低于 250px 时自动退化为单列;磁盘模块继续展示设备视觉块与紧凑磁盘摘要;默认视图底部继续保留“进程管理”概览与高占用进程预览,并通过“查看全部”打开 `ProcessManagerModal.vue`。其中进程统计当前已整体收纳到模块标题区右侧的一组 `monitor-module__pill` 胶囊中,统一展示“总数 / 运行中 / 休眠中”,不再额外保留独立摘要行,以减少默认卡片的纵向占用。该 modal 继续采用深色控制台式表格布局,支持搜索 PID / 用户 / 命令、自动刷新、手动刷新,以及对单个进程执行“结束”或“强制结束”操作,并通过当前活动 SSH 会话的 `wsManager` 与后端 `process:list` / `process:signal` 消息交互;当前还支持点击 `PID / USER / STATE / CPU / MEM / START / COMMAND` 表头做本地升降序排序,并为激活列显示方向标记,同时给右上角关闭按钮预留了独立安全区,避免与刷新区过近。 -**结果**: 前端状态监控形成了“更贴近参考图的默认小屏监控 + 独立进程管理页”的双层结构:默认面板不仅保持了侧栏内的高密度布局,还允许用户直接在网络和 CPU 卡片里查看近期历史波动,并在 CPU 区快速识别负载集中在哪些核心;完整进程管理继续独立存在,不挤占侧栏本体;进入进程管理详细视图后,也能更快按字段维度筛查进程。 +**行为**: `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 和网络卡片;完整进程管理继续独立存在,不挤占侧栏本体;进入进程管理详细视图后,也能更快按字段维度筛查进程。 ### 快捷指令拖拽排序 **条件**: 用户在 Workbench 的快捷指令视图中浏览分组或扁平命令列表,且当前未启用搜索过滤。 diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index d2c8821..76a0722 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -57,38 +57,6 @@ -
-
-
- {{ t('statusMonitor.cpuLabel') }} -
{{ t('statusMonitor.cpuUsageTitle') }}
-
- {{ displayCpuCores }} -
- -
- - -
-
-
-
- {{ item.label }} - {{ item.value }} -
-
- -
-
-
-
-
-
-
@@ -123,50 +91,6 @@
-
-
-
- {{ 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 }} -
-
-
-
-
-
@@ -278,9 +202,7 @@ import { computed, ref, watch, type CSSProperties, type PropType } from 'vue'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; import ProcessManagerModal from './ProcessManagerModal.vue'; -import StatusMonitorCpuHistoryChart from './StatusMonitorCpuHistoryChart.vue'; import StatusCharts from './StatusCharts.vue'; -import StatusMonitorNetworkHistoryChart from './StatusMonitorNetworkHistoryChart.vue'; import { useSessionStore } from '../stores/session.store'; import { useSettingsStore } from '../stores/settings.store'; import { useConnectionsStore } from '../stores/connections.store'; @@ -320,11 +242,6 @@ const clampPercent = (value?: number): number => { 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 displayCpuPercent = computed(() => clampPercent(currentServerStatus.value?.cpuPercent)); const displayMemoryPercent = computed(() => clampPercent(currentServerStatus.value?.memPercent)); const displayDiskPercent = computed(() => clampPercent(currentServerStatus.value?.diskPercent)); const currentStatusError = computed(() => currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null); @@ -362,23 +279,6 @@ 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`; @@ -514,55 +414,6 @@ 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; @@ -1012,13 +863,18 @@ const copyIpToClipboard = async (ipAddress: string | null) => { .module-split--network { grid-template-columns: 1fr; + grid-template-rows: minmax(0, 146px) minmax(0, 1fr); + min-height: 0; align-content: start; gap: 8px; + overflow: hidden; } .monitor-module--network { + grid-template-rows: auto minmax(0, 1fr); max-height: 350px; gap: 8px; + overflow: hidden; } .memory-ring-panel, @@ -1138,14 +994,14 @@ const copyIpToClipboard = async (ipAddress: string | null) => { .network-table { display: grid; - gap: 6px; - height: auto; + grid-template-rows: auto auto minmax(0, 1fr); + gap: 4px; + min-height: 0; + height: 100%; padding: 8px 10px; + overflow: hidden; } -.network-table__header, -.network-table__columns, -.network-stat, .disk-summary-table__head, .disk-summary-table__row { display: flex; @@ -1155,46 +1011,73 @@ const copyIpToClipboard = async (ipAddress: string | null) => { } .network-table__header { - padding-bottom: 6px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + padding-bottom: 4px; border-bottom: 1px solid rgba(148, 163, 184, 0.1); + font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } .network-table__columns { + display: grid; + grid-template-columns: minmax(0, 0.78fr) repeat(2, minmax(0, 0.61fr)); + align-items: center; + gap: 6px; padding-top: 0; color: #9cb0c2; + font-size: 10px; font-weight: 700; } .network-stat-stack { - gap: 6px; + min-height: 0; + align-content: start; + gap: 4px; } .network-table__columns span, .network-stat span, .disk-summary-table__head span, .disk-summary-table__row span { - flex: 1 1 0; min-width: 0; } +.network-table__header span:first-child, .network-table__columns span:first-child, -.network-stat span:first-child { - flex-basis: 30%; +.network-stat__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.network-table__header span:last-child, +.network-table__columns span:not(:first-child), +.network-stat__value, +.network-stat__total { + justify-self: end; + text-align: right; } .network-stat { + display: grid; + grid-template-columns: minmax(0, 0.78fr) repeat(2, minmax(0, 0.61fr)); + align-items: center; + gap: 6px; border-radius: 10px; border: 1px solid rgba(148, 163, 184, 0.06); background: rgba(255, 255, 255, 0.03); - padding: 8px 10px; + padding: 6px 8px; } .network-stat__label { display: inline-flex; align-items: center; - gap: 6px; + gap: 5px; color: #d9e5f1; + font-size: 11px; } .network-stat__label i { @@ -1205,7 +1088,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { .network-stat__value, .network-stat__total { color: #f8fbff; - font-size: 13px; + font-size: 12px; font-weight: 800; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } @@ -1470,6 +1353,17 @@ const copyIpToClipboard = async (ipAddress: string | null) => { width: 100%; flex: none; } + + .monitor-module--network .network-table__header, + .monitor-module--network .network-table__columns, + .monitor-module--network .network-stat { + align-items: center; + } + + .monitor-module--network .network-table__columns span, + .monitor-module--network .network-stat span { + width: auto; + } } @container (max-width: 440px) { diff --git a/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue b/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue index e5116b5..04013da 100644 --- a/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue +++ b/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue @@ -184,15 +184,16 @@ onBeforeUnmount(() => {