diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 83e8701..68339c4 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -4,3 +4,4 @@ - 2026-03-25:初始化 `.helloagents/` 知识库骨架与首批模块文档,不代表源码功能变更。 - 2026-03-25:新增 GHCR Docker 发布 workflow,并将 `docker-compose.yml` 的三个业务镜像切换到 `ghcr.io/micah123321/*`。 +- 2026-03-25:`/workspace` 默认布局改为“左侧 Workbench + 中央视终端 + 右侧状态监控”,并在状态监控中新增开机累计上下行流量展示。 diff --git a/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/.status.json b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/.status.json new file mode 100644 index 0000000..12c6dab --- /dev/null +++ b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":8,"failed":0,"pending":0,"total":8,"done":8,"percent":100,"current":"已完成 workbench 工作台、累计流量监控和构建验证","updated_at":"2026-03-25 12:33:00"} diff --git a/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/proposal.md b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/proposal.md new file mode 100644 index 0000000..556f74f --- /dev/null +++ b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/proposal.md @@ -0,0 +1,70 @@ +# 变更提案: workspace-workbench-monitor + +## 元信息 +```yaml +类型: 功能增强 +方案类型: implementation +优先级: P1 +状态: 实施中 +状态说明: 已完成实现、构建验证通过,待归档 +创建: 2026-03-25 +``` + +--- + +## 1. 需求 + +### 背景 +当前 `/workspace` 主工作区把 `命令历史`、`文件管理`、`编辑器` 拆成多个独立 pane,占用了终端垂直空间;状态监控只展示瞬时上下行速率,没有展示系统自开机以来的累计流量。 + +### 目标 +- 仅改造 `/workspace` 主工作区布局。 +- 采用方案 B:左侧 `Workbench` Tab 容器承载 `历史命令 / 文件 / 编辑器`,中央终端保持完整高度,右侧保留状态监控。 +- 在右侧状态监控新增“开机累计上/下行流量”展示。 + +### 约束条件 +```yaml +范围约束: 只改 /workspace 主工作区,不调整其它页面和弹层体系 +架构约束: 延续现有 Vue 3 + Pinia + splitpanes 布局体系,不引入新布局库 +后端约束: 仅基于现有 /proc/net/dev 采集链路扩展累计流量字段 +部署约束: 当前只考虑 amd64,不涉及 Docker / GHCR 变更 +``` + +### 验收标准 +- [ ] `/workspace` 默认主布局变为“左侧 Workbench + 中央终端 + 右侧状态监控” +- [ ] Workbench 内可切换 `命令历史 / 文件 / 编辑器` 三个标签页 +- [ ] 右侧状态监控新增开机累计下载 / 上传流量显示 +- [ ] 前后端构建通过,类型链闭合 + +--- + +## 2. 方案 + +### 技术方案 +新增前端 `WorkspaceWorkbench.vue` 组件,作为新的布局 pane,在内部用 tab 容器组合 `CommandHistoryView`、`FileManager` 和 `FileEditorContainer`。布局层新增 `workbench` pane 类型,并把默认布局改成“三栏工作台”。为兼容已有保存布局,增加一次仅针对旧默认布局的轻量迁移逻辑。后端则在状态轮询时直接复用 `/proc/net/dev` 已解析的总字节数,新增 `netRxTotalBytes` / `netTxTotalBytes` 字段并透传到前端状态监控。 + +### 影响范围 +```yaml +涉及模块: + - frontend: /workspace 布局、状态监控、i18n、前端类型 + - backend: 状态监控服务累计流量字段 +预计变更文件: 10+ +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 已保存的旧布局不自动切到新工作台 | 中 | 增加仅针对旧默认布局的迁移逻辑,避免误伤自定义布局 | +| 文件管理器在 tab 切换后状态丢失 | 中 | Workbench 内使用 `v-show` 保持三个面板常驻 | +| 累计流量字段前后端类型不一致 | 低 | 同步更新后端接口、本地类型与状态监控展示 | + +--- + +## 3. 技术决策 + +### workspace-workbench-monitor#D001: 采用方案 B 的三栏工作台 +**日期**: 2026-03-25 +**状态**: ✅采纳 +**决策**: 左侧集中工作台、中央终端、右侧状态监控。 +**理由**: 保住终端主区高度,同时让文件/命令/编辑入口集中到同一容器,符合用户确认的 xterminal 参考方向。 +**影响**: 影响 `/workspace` 默认布局、布局配置器与状态监控展示逻辑。 diff --git a/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/tasks.md b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/tasks.md new file mode 100644 index 0000000..ce5a6c7 --- /dev/null +++ b/.helloagents/archive/2026-03/202603251200_workspace-workbench-monitor/tasks.md @@ -0,0 +1,58 @@ +# 任务清单: workspace-workbench-monitor + +```yaml +@feature: workspace-workbench-monitor +@created: 2026-03-25 +@status: completed +@mode: R3 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 8 | 0 | 0 | 8 | + +--- + +## 任务列表 + +### 1. 方案包与布局基础 + +- [√] 1.1 创建 `/workspace` Workbench 改造方案包并记录方案 B | depends_on: [] +- [√] 1.2 新增 `workbench` pane 类型与默认布局迁移逻辑 | depends_on: [1.1] + +### 2. 前端工作台布局 + +- [√] 2.1 新增 `WorkspaceWorkbench.vue`,整合命令历史 / 文件 / 编辑器 tab | depends_on: [1.2] +- [√] 2.2 接入 `LayoutRenderer`、`LayoutConfigurator`、设置侧栏宽度类型链 | depends_on: [2.1] + +### 3. 状态监控扩展 + +- [√] 3.1 后端状态监控新增累计上下行流量字段 | depends_on: [1.1] +- [√] 3.2 前端状态监控展示累计流量并补充 i18n | depends_on: [3.1] + +### 4. 验证与知识库同步 + +- [√] 4.1 运行前后端构建验证并记录结果 | depends_on: [2.2, 3.2] +- [√] 4.2 更新 `.helloagents` 模块文档与变更记录 | depends_on: [4.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-25 12:00 | 1.1 | 完成 | 创建 implementation 方案包,进入开发实施 | +| 2026-03-25 12:18 | 1.2 / 2.1 / 2.2 | 完成 | 新增 `workbench` pane、默认布局切换为三栏工作台,并为旧默认布局做轻量迁移 | +| 2026-03-25 12:20 | 3.1 / 3.2 | 完成 | 后端透出累计上下行字节数,前端状态监控新增“开机累计流量”展示 | +| 2026-03-25 12:31 | 4.1 | 完成 | 执行 `npm install` 后,前后端 build 均通过 | +| 2026-03-25 12:33 | 4.2 | 完成 | 更新模块文档、变更日志并准备归档 | + +--- + +## 执行备注 + +- 本次只改 `/workspace` 主工作区,不触碰其它页面和全局弹层。 +- 默认布局改造需要兼顾已有后端 / localStorage 布局数据,因此会做保守迁移而不是强制清空布局。 +- 首次构建前本机缺少依赖,补跑 `npm install` 后,`packages/backend` 与 `packages/frontend` 构建均通过。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 25710f4..341ef70 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -8,11 +8,13 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| | 202603250317 | ghcr-docker-publish | implementation | workspace-root | ghcr-docker-publish#D001 | ✅完成 | +| 202603251200 | workspace-workbench-monitor | implementation | frontend, backend | workspace-workbench-monitor#D001 | ✅完成 | ## 按月归档 ### 2026-03 - [202603250317_ghcr-docker-publish](./2026-03/202603250317_ghcr-docker-publish/) - 新增 GHCR 镜像发布 workflow 并切换 compose 镜像来源 +- [202603251200_workspace-workbench-monitor](./2026-03/202603251200_workspace-workbench-monitor/) - `/workspace` 改为三栏 Workbench 布局,并新增开机累计流量监控 ## 结果状态说明 - ✅ 完成 diff --git a/.helloagents/context.md b/.helloagents/context.md index 94f94bf..7f059ee 100644 --- a/.helloagents/context.md +++ b/.helloagents/context.md @@ -39,6 +39,7 @@ ### 核心功能 - 统一管理 SSH、SFTP、RDP、VNC 远程连接。 - 提供基于 Vue 3 的工作区、布局配置、主题定制和命令输入体验。 +- `/workspace` 默认采用“三栏工作台”:左侧 Workbench tab 容器、中央终端、右侧状态监控。 - 提供认证、2FA、Passkey、Captcha、IP 白名单/黑名单、通知和审计能力。 - 支持远程文件管理、在线编辑、快速命令、命令历史和 SSH 挂起会话。 - 使用独立 `remote-gateway` 服务为远程桌面连接生成加密令牌并对接 `guacd`。 @@ -89,7 +90,7 @@ | 约束 | 原因 | 决策来源 | |------|------|---------| -| 暂无已归档方案包约束 | 知识库于 2026-03-25 首次初始化,后续决策应沉淀到 plan/archive | N/A | +| `/workspace` 默认主布局采用“Workbench + 终端 + 状态监控”三栏结构 | 统一终端主区高度,并将命令历史 / 文件 / 编辑器集中到左侧工作台 | workspace-workbench-monitor#D001 | ## 6. 已知技术债务(可选) diff --git a/.helloagents/modules/backend.md b/.helloagents/modules/backend.md index 913ac6f..b227d6a 100644 --- a/.helloagents/modules/backend.md +++ b/.helloagents/modules/backend.md @@ -39,6 +39,11 @@ **行为**: 按 `controller/service/repository/routes` 的分层模式组织连接、通知、设置、快速命令、主题等功能。 **结果**: 新增后端能力时应优先延续现有业务域目录结构,而不是在入口文件堆叠逻辑。 +### 状态监控 +**条件**: 前端工作区通过 WebSocket 订阅服务器状态。 +**行为**: `StatusMonitorService` 通过 SSH 读取 `free`、`df`、`/proc/stat` 与 `/proc/net/dev`,同时计算瞬时网速与默认网卡自开机以来的累计上下行字节数。 +**结果**: 前端状态监控既能展示实时速率,也能展示“开机累计流量”,后续扩展监控字段时应优先复用现有 SSH 采集链路。 + ## 依赖关系 ```yaml diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index 9bb179d..21ed0ed 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -36,8 +36,8 @@ ### 工作区交互 **条件**: 用户进入 `/workspace` 或相关管理页面。 -**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控。 -**结果**: 页面逻辑分散在 `views/`、`components/`、`stores/` 与 `composables/`,改动时应优先在对应层级定位。 +**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合命令历史、文件管理和编辑器。 +**结果**: 页面逻辑分散在 `views/`、`components/`、`stores/` 与 `composables/`,其中布局调整优先落在 `layout.store.ts`、`LayoutRenderer.vue` 与 `WorkspaceWorkbench.vue`。 ## 依赖关系 diff --git a/packages/backend/src/services/status-monitor.service.ts b/packages/backend/src/services/status-monitor.service.ts index 2e4af19..6fd00d9 100644 --- a/packages/backend/src/services/status-monitor.service.ts +++ b/packages/backend/src/services/status-monitor.service.ts @@ -18,6 +18,8 @@ interface ServerStatus { cpuModel?: string; netRxRate?: number; // Bytes per second netTxRate?: number; // Bytes per second + netRxTotalBytes?: number; // Bytes since boot + netTxTotalBytes?: number; // Bytes since boot netInterface?: string; osName?: string; loadAvg?: number[]; // 系统平均负载 [1min, 5min, 15min] @@ -318,6 +320,8 @@ export class StatusMonitorService { status.netInterface = defaultInterface; const currentRx = currentStats[defaultInterface].rx_bytes; const currentTx = currentStats[defaultInterface].tx_bytes; + status.netRxTotalBytes = currentRx; + status.netTxTotalBytes = currentTx; const prevStats = previousNetStats.get(sessionId); if (prevStats && prevStats.timestamp < timestamp) { diff --git a/packages/frontend/src/components/LayoutConfigurator.vue b/packages/frontend/src/components/LayoutConfigurator.vue index 43976b9..b45859b 100644 --- a/packages/frontend/src/components/LayoutConfigurator.vue +++ b/packages/frontend/src/components/LayoutConfigurator.vue @@ -163,6 +163,7 @@ const paneLabels = computed(() => ({ // Assuming labels might depend on i18n commandBar: t('layout.pane.commandBar', '命令栏'), fileManager: t('layout.pane.fileManager', '文件管理器'), editor: t('layout.pane.editor', '编辑器'), + workbench: t('layout.pane.workbench', '工作台'), statusMonitor: t('layout.pane.statusMonitor', '状态监视器'), commandHistory: t('layout.pane.commandHistory', '命令历史'), quickCommands: t('layout.pane.quickCommands', '快捷指令'), diff --git a/packages/frontend/src/components/LayoutRenderer.vue b/packages/frontend/src/components/LayoutRenderer.vue index 0b682be..0ec01e6 100644 --- a/packages/frontend/src/components/LayoutRenderer.vue +++ b/packages/frontend/src/components/LayoutRenderer.vue @@ -88,6 +88,7 @@ const componentMap: Record = { commandBar: defineAsyncComponent(() => import('./CommandInputBar.vue')), fileManager: defineAsyncComponent(() => import('./FileManager.vue')), editor: defineAsyncComponent(() => import('./FileEditorContainer.vue')), + workbench: defineAsyncComponent(() => import('./WorkspaceWorkbench.vue')), statusMonitor: defineAsyncComponent(() => import('./StatusMonitor.vue')), commandHistory: defineAsyncComponent(() => import('../views/CommandHistoryView.vue')), quickCommands: defineAsyncComponent(() => import('../views/QuickCommandsView.vue')), @@ -131,6 +132,7 @@ const paneLabels = computed(() => ({ commandBar: t('layout.pane.commandBar', '命令栏'), fileManager: t('layout.pane.fileManager', '文件管理器'), editor: t('layout.pane.editor', '编辑器'), + workbench: t('layout.pane.workbench', '工作台'), statusMonitor: t('layout.pane.statusMonitor', '状态监视器'), commandHistory: t('layout.pane.commandHistory', '命令历史'), quickCommands: t('layout.pane.quickCommands', '快捷指令'), @@ -186,6 +188,21 @@ const componentProps = computed(() => { class: 'pane-content', // --- 移除事件转发 --- }; + case 'workbench': + return { + tabs: props.editorTabs, + activeTabId: props.activeEditorTabId, + sessionId: currentActiveSession?.sessionId ?? props.activeSessionId, + instanceId: props.layoutNode.id || `workbench-main-${props.activeSessionId ?? 'unknown'}`, + dbConnectionId: currentActiveSession?.connectionId ?? null, + wsDeps: currentActiveSession ? { + sendMessage: currentActiveSession.wsManager.sendMessage, + onMessage: currentActiveSession.wsManager.onMessage, + isConnected: currentActiveSession.wsManager.isConnected, + isSftpReady: currentActiveSession.wsManager.isSftpReady + } : null, + class: 'flex flex-col flex-grow h-full overflow-hidden', + }; case 'commandBar': return { class: 'pane-content', @@ -240,6 +257,21 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | ' ...baseProps, // --- 移除事件转发 --- }; + case 'workbench': + return { + ...baseProps, + tabs: editorTabsFromStore.value, + activeTabId: activeEditorTabIdFromStore.value, + sessionId: activeSession.value?.sessionId ?? props.activeSessionId, + instanceId: side === 'left' ? 'workbench-sidebar-left' : 'workbench-sidebar-right', + dbConnectionId: activeSession.value?.connectionId ?? null, + wsDeps: activeSession.value ? { + sendMessage: activeSession.value.wsManager.sendMessage, + onMessage: activeSession.value.wsManager.onMessage, + isConnected: activeSession.value.wsManager.isConnected, + isSftpReady: activeSession.value.wsManager.isSftpReady + } : null, + }; case 'fileManager': // Only provide props if there's an active session if (activeSession.value) { @@ -357,6 +389,7 @@ const getIconClasses = (paneName: PaneName): string[] => { case 'quickCommands': return ['fas', 'fa-bolt']; case 'dockerManager': return ['fab', 'fa-docker']; // Use 'fab' for Docker case 'editor': return ['fas', 'fa-file-alt']; + case 'workbench': return ['fas', 'fa-table-columns']; case 'statusMonitor': return ['fas', 'fa-tachometer-alt']; case 'suspendedSshSessions': return ['fas', 'fa-pause-circle']; // 图标:暂停圈 // Add other specific icons here if needed diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index 0a1f092..4977b89 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -143,6 +143,21 @@ +
+ +
+ + + {{ t('statusMonitor.downloadLabel') }} + {{ formatBytes(currentServerStatus?.netRxTotalBytes) }} + + + + {{ t('statusMonitor.uploadLabel') }} + {{ formatBytes(currentServerStatus?.netTxTotalBytes) }} + +
+
@@ -161,6 +176,7 @@ import { storeToRefs } from 'pinia'; // 导入 storeToRefs import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store import { useConnectionsStore } from '../stores/connections.store'; // 导入连接 store import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // + 导入通知 store +import type { ServerStatus } from '../types/server.types'; const { t } = useI18n(); const sessionStore = useSessionStore(); @@ -173,24 +189,6 @@ const isSwitchingSession = ref(false); const formatPercentageText = (percentage: number): string => `${Math.round(percentage)}%`; -interface ServerStatus { - cpuPercent?: number; - memPercent?: number; - memUsed?: number; // MB - memTotal?: number; // MB - swapPercent?: number; - swapUsed?: number; // MB - swapTotal?: number; // MB - diskPercent?: number; - diskUsed?: number; // KB - diskTotal?: number; // KB - cpuModel?: string; - netRxRate?: number; // 字节/秒 - netTxRate?: number; // 字节/秒 - netInterface?: string; - osName?: string; -} - // --- Props --- const props = defineProps({ activeSessionId: { @@ -276,6 +274,15 @@ const formatBytesPerSecond = (bytes?: number): string => { 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 formatKbToGb = (kb?: number): string => { if (kb === undefined || kb === null) return t('statusMonitor.notAvailable'); if (kb === 0) return `0.0 ${t('statusMonitor.gigaBytes')}`; diff --git a/packages/frontend/src/components/WorkspaceWorkbench.vue b/packages/frontend/src/components/WorkspaceWorkbench.vue new file mode 100644 index 0000000..4c411e8 --- /dev/null +++ b/packages/frontend/src/components/WorkspaceWorkbench.vue @@ -0,0 +1,165 @@ + + + diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index fe4889f..28313c4 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -583,6 +583,9 @@ "swapLabel": "Swap:", "diskLabel": "Disk:", "networkLabel": "Network", + "totalTrafficLabel": "Traffic Since Boot", + "downloadLabel": "Download", + "uploadLabel": "Upload", "notAvailable": "N/A", "bytesPerSecond": "B/s", "kiloBytesPerSecond": "KB/s", @@ -1250,10 +1253,11 @@ "pane": { "connections": "Connections", "terminal": "Terminal", - "commandBar": "Command Bar", - "fileManager": "File Manager", - "editor": "Editor", - "statusMonitor": "Status Monitor", + "commandBar": "Command Bar", + "fileManager": "File Manager", + "editor": "Editor", + "workbench": "Workbench", + "statusMonitor": "Status Monitor", "commandHistory": "Command History", "quickCommands": "Quick Commands", "dockerManager": "Docker Manager", @@ -1262,14 +1266,28 @@ "panes": { "suspendedSshSessions": "Suspended Session Manager" }, - "noActiveSession": { - "title": "No Active Session", - "message": "Please connect to a session first", - "fileManagerSidebar": "File Manager requires an active session", - "statusMonitorSidebar": "Status Monitor requires an active session" - } - }, - "header": { + "noActiveSession": { + "title": "No Active Session", + "message": "Please connect to a session first", + "fileManagerSidebar": "File Manager requires an active session", + "statusMonitorSidebar": "Status Monitor requires an active session" + } + }, + "workspace": { + "noActiveSession": "No active session", + "workbench": { + "title": "Workbench", + "label": "Workspace", + "noSession": "No active session", + "fileManagerHint": "Activate an SSH session to browse remote files.", + "tabs": { + "files": "Files", + "history": "History", + "editor": "Editor" + } + } + }, + "header": { "hide": "Hide", "show": "Show Top Navigation" }, diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 17e81d9..62710b7 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -555,6 +555,7 @@ "dockerManager": "Docker マネージャー", "editor": "エディター", "fileManager": "ファイルマネージャー", + "workbench": "ワークベンチ", "quickCommands": "クイックコマンド", "statusMonitor": "ステータスモニター", "terminal": "ターミナル", @@ -564,6 +565,20 @@ "suspendedSshSessions": "中断されたセッション管理者" } }, + "workspace": { + "noActiveSession": "アクティブなセッションはありません", + "workbench": { + "title": "Workbench", + "label": "ワークスペース", + "noSession": "アクティブなセッションはありません", + "fileManagerHint": "SSH セッションを有効にするとリモートファイルを参照できます。", + "tabs": { + "files": "ファイル", + "history": "履歴", + "editor": "エディター" + } + } + }, "layoutConfigurator": { "availablePanes": "利用可能なパネル", "confirmClearLayout": "レイアウト全体をクリアしますか?すべてのパネルが利用可能なリストに戻ります。", @@ -1205,6 +1220,9 @@ "megaBytesPerSecond": "MB/秒", "memoryLabel": "メモリ:", "networkLabel": "ネットワーク", + "totalTrafficLabel": "起動後の累計トラフィック", + "downloadLabel": "受信", + "uploadLabel": "送信", "notAvailable": "N/A", "osLabel": "OS:", "swapLabel": "スワップ:", @@ -1612,4 +1630,4 @@ "copiedSuccess": "パスがクリップボードにコピーされました", "copiedError": "パスのコピーに失敗しました" } -} \ No newline at end of file +} diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 6b9ed6e..31abed8 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -583,6 +583,9 @@ "swapLabel": "Swap:", "diskLabel": "磁盘:", "networkLabel": "网络", + "totalTrafficLabel": "开机累计流量", + "downloadLabel": "下行", + "uploadLabel": "上行", "notAvailable": "N/A", "bytesPerSecond": "B/s", "kiloBytesPerSecond": "KB/s", @@ -1254,10 +1257,11 @@ "pane": { "connections": "连接列表", "terminal": "终端", - "commandBar": "命令栏", - "fileManager": "文件管理器", - "editor": "编辑器", - "statusMonitor": "状态监视器", + "commandBar": "命令栏", + "fileManager": "文件管理器", + "editor": "编辑器", + "workbench": "工作台", + "statusMonitor": "状态监视器", "commandHistory": "命令历史", "quickCommands": "快捷指令", "dockerManager": "Docker 管理器", @@ -1266,14 +1270,28 @@ "panes": { "suspendedSshSessions": "挂起会话管理器" }, - "noActiveSession": { - "title": "无活动会话", - "message": "请先连接一个会话", - "fileManagerSidebar": "文件管理器需要活动会话", - "statusMonitorSidebar": "状态监视器需要活动会话" - } - }, - "header": { + "noActiveSession": { + "title": "无活动会话", + "message": "请先连接一个会话", + "fileManagerSidebar": "文件管理器需要活动会话", + "statusMonitorSidebar": "状态监视器需要活动会话" + } + }, + "workspace": { + "noActiveSession": "没有活动的会话", + "workbench": { + "title": "Workbench", + "label": "工作台", + "noSession": "未激活会话", + "fileManagerHint": "激活一个 SSH 会话后即可浏览远程文件。", + "tabs": { + "files": "文件", + "history": "历史命令", + "editor": "编辑器" + } + } + }, + "header": { "hide": "隐藏", "show": "显示顶部导航" }, diff --git a/packages/frontend/src/stores/layout.store.ts b/packages/frontend/src/stores/layout.store.ts index f6d428a..b60fc10 100644 --- a/packages/frontend/src/stores/layout.store.ts +++ b/packages/frontend/src/stores/layout.store.ts @@ -3,7 +3,7 @@ import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'; import apiClient from '../utils/apiClient'; // 定义所有可用面板的名称 -export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager' | 'suspendedSshSessions'; +export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'workbench' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager' | 'suspendedSshSessions'; // 定义布局节点接口 export interface LayoutNode { @@ -25,7 +25,38 @@ function generateId(): string { return Math.random().toString(36).substring(2, 15); } -// 定义默认布局结构 (根据用户提供的配置更新,但使用 generateId) +function isPaneNode(node: LayoutNode | undefined | null, component: PaneName): boolean { + return node?.type === 'pane' && node.component === component; +} + +function isLegacyDefaultLayout(node: LayoutNode | null): boolean { + if (!node || node.type !== 'container' || node.direction !== 'horizontal' || !node.children || node.children.length !== 3) { + return false; + } + + const [leftColumn, centerColumn, rightColumn] = node.children; + + return Boolean( + leftColumn?.type === 'container' && + leftColumn.direction === 'vertical' && + leftColumn.children?.length === 3 && + isPaneNode(leftColumn.children[0], 'statusMonitor') && + isPaneNode(leftColumn.children[1], 'commandHistory') && + isPaneNode(leftColumn.children[2], 'quickCommands') && + centerColumn?.type === 'container' && + centerColumn.direction === 'vertical' && + centerColumn.children?.length === 3 && + isPaneNode(centerColumn.children[0], 'terminal') && + isPaneNode(centerColumn.children[1], 'commandBar') && + isPaneNode(centerColumn.children[2], 'fileManager') && + rightColumn?.type === 'container' && + rightColumn.direction === 'vertical' && + rightColumn.children?.length === 1 && + isPaneNode(rightColumn.children[0], 'editor') + ); +} + +// 定义默认布局结构 const getDefaultLayout = (): LayoutNode => ({ id: generateId(), // Generate new ID type: "container", @@ -33,69 +64,35 @@ const getDefaultLayout = (): LayoutNode => ({ children: [ { id: generateId(), // Generate new ID - type: "container", - direction: "vertical", - children: [ - { - id: generateId(), // Generate new ID - type: "pane", - component: "statusMonitor", - size: 44.56372126372345 // 使用用户提供的 size - }, - { - id: generateId(), // Generate new ID - type: "pane", - component: "commandHistory", - size: 26.235651482670775 // 使用用户提供的 size - }, - { - id: generateId(), // Generate new ID - type: "pane", - component: "quickCommands", - size: 29.200627253605774 // 使用用户提供的 size - } - ], - size: 14.59006012147659 // 使用用户提供的 size + type: "pane", + component: "workbench", + size: 23 }, { id: generateId(), // Generate new ID type: "container", direction: "vertical", - size: 58.02787988626151, // 使用用户提供的 size + size: 57, children: [ { id: generateId(), // Generate new ID type: "pane", component: "terminal", - size: 59.94833664833884 // 使用用户提供的 size + size: 94 }, { id: generateId(), // Generate new ID type: "pane", component: "commandBar", - size: 5 // 使用用户提供的 size - }, - { - id: generateId(), // Generate new ID - type: "pane", - component: "fileManager", - size: 35.05166335166116 // 使用用户提供的 size + size: 6 } ] }, { id: generateId(), // Generate new ID - type: "container", - direction: "vertical", - size: 27.3820599922619, // 使用用户提供的 size - children: [ - { - id: generateId(), // Generate new ID - type: "pane", - component: "editor", - size: 100 // 使用用户提供的 size - } - ] + type: "pane", + component: "statusMonitor", + size: 20 } ] }); @@ -165,7 +162,7 @@ export const useLayoutStore = defineStore('layout', () => { // 所有理论上可用的面板名称 const allPossiblePanes: Ref = ref([ 'connections', 'terminal', 'commandBar', 'fileManager', - 'editor', 'statusMonitor', 'commandHistory', 'quickCommands', + 'editor', 'workbench', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager', 'suspendedSshSessions' // <-- 添加新的挂起 SSH 会话视图 ]); // 控制布局(Header/Footer)可见性的状态 @@ -201,6 +198,18 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { return node; } +function normalizeLoadedLayout(node: LayoutNode | null): LayoutNode | null { + const layoutWithIds = ensureNodeIds(node); + if (!layoutWithIds) return null; + + if (isLegacyDefaultLayout(layoutWithIds)) { + console.log('[Layout Store] Detected legacy workspace default layout, migrating to workbench layout.'); + return ensureNodeIds(getDefaultLayout()); + } + + return layoutWithIds; +} + // --- Actions --- // 初始化布局和侧栏配置 async function initializeLayout() { @@ -220,7 +229,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { if (response.data) { console.log('[Layout Store] Step 1: Backend returned data.'); // +++ 在赋值前确保 ID 存在 +++ - loadedLayout = ensureNodeIds(response.data); + loadedLayout = normalizeLoadedLayout(response.data); layoutLoadedFromBackend = true; console.log('[Layout Store] Step 1: Layout processed with ensureNodeIds.'); // 更新 localStorage (使用处理过的布局) @@ -270,13 +279,13 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { const parsedLayout = JSON.parse(savedLayout) as LayoutNode; console.log('[Layout Store] Step 3: Parsed layout from localStorage.'); // +++ 在赋值前确保 ID 存在 +++ - loadedLayout = ensureNodeIds(parsedLayout); + loadedLayout = normalizeLoadedLayout(parsedLayout); console.log('[Layout Store] Step 3: Layout processed with ensureNodeIds.'); } else { // 4. 如果 localStorage 也没有,使用默认主布局 console.log('[Layout Store] Step 4: No layout in localStorage. Applying default.'); // +++ 确保默认布局也有 ID (虽然 getDefaultLayout 内部会生成) +++ - loadedLayout = ensureNodeIds(getDefaultLayout()); + loadedLayout = normalizeLoadedLayout(getDefaultLayout()); console.log('[Layout Store] Step 4: Default layout processed with ensureNodeIds.'); } } catch (error) { @@ -284,7 +293,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { // Fallback to default if error and loadedLayout is still null if (!loadedLayout) { console.log('[Layout Store] Step 3/4: Applying default layout due to error.'); - loadedLayout = ensureNodeIds(getDefaultLayout()); + loadedLayout = normalizeLoadedLayout(getDefaultLayout()); } } } @@ -326,7 +335,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null { // Final check (主要是为了调试,可以简化或移除) if (!layoutTree.value) { console.error('[Layout Store] FATAL: layoutTree is STILL null after all attempts! Applying default as last resort.'); - layoutTree.value = ensureNodeIds(getDefaultLayout()); + layoutTree.value = normalizeLoadedLayout(getDefaultLayout()); } if (!sidebarPanes.value || !Array.isArray(sidebarPanes.value.left) || !Array.isArray(sidebarPanes.value.right)) { console.warn('[Layout Store] Final Check: Sidebar panes invalid. Applying default.'); diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index 6c89b6b..ff40ce2 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -162,7 +162,7 @@ export const useSettingsStore = defineStore('settings', () => { // Load and parse sidebar pane widths const defaultPaneWidth = '350px'; // +++ Ensure PaneName type is available or define it here +++ - const knownPanes: PaneName[] = ['connections', 'fileManager', 'editor', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager']; // Add all possible sidebar panes + const knownPanes: PaneName[] = ['connections', 'fileManager', 'editor', 'workbench', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager']; // Add all possible sidebar panes let loadedWidths: Record = {}; try { if (settings.value.sidebarPaneWidths) { diff --git a/packages/frontend/src/types/server.types.ts b/packages/frontend/src/types/server.types.ts index 19dbb2d..14b542d 100644 --- a/packages/frontend/src/types/server.types.ts +++ b/packages/frontend/src/types/server.types.ts @@ -13,6 +13,8 @@ export interface ServerStatus { swapTotal?: number; // MB netRxRate?: number; // Bytes/sec netTxRate?: number; // Bytes/sec + netRxTotalBytes?: number; // Bytes since boot + netTxTotalBytes?: number; // Bytes since boot netInterface?: string; osName?: string; }