feat(frontend): 优化状态监控默认概览布局

将 CPU 和网络的历史图表直接整合进默认概览,保留常驻资源卡片,
并移除底部重复的图表区,减少侧栏中的重复信息。
This commit is contained in:
yinjianm
2026-04-19 04:54:56 +08:00
parent d300566f89
commit 30cc596ed8
3 changed files with 152 additions and 11 deletions
+2 -2
View File
@@ -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/)
+2 -2
View File
@@ -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 的快捷指令视图中浏览分组或扁平命令列表,且当前未启用搜索过滤。
@@ -57,6 +57,38 @@
</div>
</header>
<section class="monitor-module monitor-module--usage">
<div class="monitor-module__heading">
<div>
<span class="monitor-module__eyebrow">{{ t('statusMonitor.cpuLabel') }}</span>
<h5 class="monitor-module__title">{{ t('statusMonitor.cpuUsageTitle') }}</h5>
</div>
<span class="monitor-module__pill">{{ displayCpuCores }}</span>
</div>
<div class="module-split module-split--cpu">
<StatusMonitorCpuHistoryChart :cpu-history="currentCpuHistory" />
<div class="cpu-core-grid">
<article
v-for="item in cpuCoreItems"
:key="item.key"
class="usage-lane usage-lane--cpu"
>
<div class="usage-lane__content">
<div class="usage-lane__meta">
<span class="usage-lane__label">{{ item.label }}</span>
<span class="usage-lane__value-inline">{{ item.value }}</span>
</div>
<div class="usage-lane__track">
<span class="usage-lane__fill" :style="{ width: `${item.percent}%` }"></span>
</div>
</div>
</article>
</div>
</div>
</section>
<div class="monitor-module-grid">
<section class="monitor-module monitor-module--memory">
<div class="monitor-module__heading">
@@ -91,6 +123,50 @@
</div>
</section>
<section class="monitor-module monitor-module--network">
<div class="monitor-module__heading">
<div>
<span class="monitor-module__eyebrow">{{ t('statusMonitor.networkLabel') }}</span>
<h5 class="monitor-module__title">{{ t('statusMonitor.networkLabel') }}</h5>
</div>
<span class="monitor-module__pill">{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }}</span>
</div>
<div class="module-split module-split--network">
<StatusMonitorNetworkHistoryChart
:download-history="currentNetRxHistory"
:upload-history="currentNetTxHistory"
/>
<div class="network-table">
<div class="network-table__header">
<span>{{ networkInterfaceDisplay }}</span>
<span>{{ t('statusMonitor.downloadLabel') }} / {{ t('statusMonitor.uploadLabel') }}</span>
</div>
<div class="network-table__columns">
<span></span>
<span>{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitLabel }) }}</span>
<span>{{ t('statusMonitor.totalTrafficLabel') }}</span>
</div>
<div class="network-stat-stack">
<article
v-for="item in networkFlowItems"
:key="item.key"
:class="['network-stat', `network-stat--${item.tone}`]"
>
<span class="network-stat__label">
<i :class="['fas', item.icon]"></i>
<span>{{ item.label }}</span>
</span>
<span class="network-stat__value">{{ item.value }}</span>
<span class="network-stat__total">{{ item.totalValue }}</span>
</article>
</div>
</div>
</div>
</section>
<section class="monitor-module monitor-module--disk">
<div class="monitor-module__heading">
<div>
@@ -181,12 +257,6 @@
</section>
</div>
<StatusCharts
v-if="activeSessionId && currentServerStatus"
class="status-monitor__charts"
:server-status="currentServerStatus"
:active-session-id="activeSessionId"
/>
</section>
<ProcessManagerModal
@@ -202,7 +272,8 @@ 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 StatusCharts from './StatusCharts.vue';
import StatusMonitorCpuHistoryChart from './StatusMonitorCpuHistoryChart.vue';
import StatusMonitorNetworkHistoryChart from './StatusMonitorNetworkHistoryChart.vue';
import { useSessionStore } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store';
import { useConnectionsStore } from '../stores/connections.store';
@@ -242,6 +313,10 @@ const clampPercent = (value?: number): number => {
const currentSessionState = computed(() => (props.activeSessionId ? sessions.value.get(props.activeSessionId) : null));
const currentServerStatus = computed<ServerStatus | null>(() => currentSessionState.value?.statusMonitorManager?.serverStatus?.value ?? null);
const currentCpuHistory = computed<readonly (number | null)[]>(() => currentSessionState.value?.statusMonitorManager?.cpuHistory?.value ?? Array(24).fill(null));
const currentNetRxHistory = computed<readonly (number | null)[]>(() => currentSessionState.value?.statusMonitorManager?.netRxHistory?.value ?? Array(24).fill(null));
const currentNetTxHistory = computed<readonly (number | null)[]>(() => 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<string | null>(() => 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<MonitorOverviewItem[]>(() => [
{ 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;