219 lines
8.4 KiB
Markdown
219 lines
8.4 KiB
Markdown
|
|
# Terminal 页重构技术计划
|
|||
|
|
|
|||
|
|
**日期**: 2026-06-02
|
|||
|
|
**对应设计文档**: `docs/design/specs/2026-06-02-terminal-refactor-design.md`
|
|||
|
|
**阶段编号**: T1 ~ T6
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 涉及文件清单
|
|||
|
|
|
|||
|
|
### 新建文件
|
|||
|
|
| 文件 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `frontend/src/composables/terminal/useTerminalSession.ts` | 单会话 xterm + WS 生命周期 |
|
|||
|
|
| `frontend/src/composables/terminal/useTerminalSessions.ts` | 多会话 Tab 管理 |
|
|||
|
|
| `frontend/src/composables/terminal/useTerminalSettings.ts` | fontSize/scrollback localStorage 持久化 |
|
|||
|
|
| `frontend/src/composables/terminal/useTerminalCmdBar.ts` | 命令输入 + 历史记录 |
|
|||
|
|
| `frontend/src/components/terminal/TerminalTabBar.vue` | Tab 条 |
|
|||
|
|
| `frontend/src/components/terminal/TerminalToolbar.vue` | 工具栏(状态/字号/断开) |
|
|||
|
|
| `frontend/src/components/terminal/TerminalArea.vue` | xterm 挂载区 + per-session overlay |
|
|||
|
|
| `frontend/src/components/terminal/TerminalCmdBar.vue` | 命令栏 + 快捷键面板 |
|
|||
|
|
| `frontend/src/components/terminal/TerminalQuickBar.vue` | 快捷命令栏 |
|
|||
|
|
| `frontend/src/components/terminal/TerminalServerPicker.vue` | 服务器选择(inline + dialog 两种模式) |
|
|||
|
|
|
|||
|
|
### 修改文件
|
|||
|
|
| 文件 | 改动 |
|
|||
|
|
|------|------|
|
|||
|
|
| `frontend/src/pages/TerminalPage.vue` | 瘦身至 ~130 行,仅组合上述组件 |
|
|||
|
|
|
|||
|
|
### 不改动文件
|
|||
|
|
| 文件 | 原因 |
|
|||
|
|
|------|------|
|
|||
|
|
| `server/api/webssh.py` | 后端逻辑已完善,协议不变 |
|
|||
|
|
| `frontend/src/composables/useTerminalQuickCommands.ts` | 已结构良好,T6 仅小调整 |
|
|||
|
|
| `frontend/src/components/ShellHighlightField.vue` | 无需变更 |
|
|||
|
|
| `frontend/src/utils/wsUrl.ts` | 无需变更 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 实施步骤
|
|||
|
|
|
|||
|
|
### T1:markRaw 修复 + Uptime 计时器内嵌(Critical 修复)
|
|||
|
|
|
|||
|
|
**目标**:解决 C1(Vue 代理 xterm/WebSocket)和 C3(Uptime timer 外部 Map)
|
|||
|
|
|
|||
|
|
**步骤**:
|
|||
|
|
1. 在 `TerminalPage.vue` 中,将 `TermSession` interface 拆分:
|
|||
|
|
- 新增 `TermSessionNative` type,字段:`ws / term / fitAddon / resizeObserver / reconnectTimer / pingTimer / uptimeTimer`
|
|||
|
|
- 新增 module-level `const nativeMap = new Map<string, TermSessionNative>()`
|
|||
|
|
- 从 `TermSession` interface 中删除上述字段
|
|||
|
|
2. 所有原生对象创建时包裹 `markRaw()`:
|
|||
|
|
- `term = markRaw(new Terminal(...))`
|
|||
|
|
- `fitAddon = markRaw(new FitAddon())`
|
|||
|
|
- `resizeObserver = markRaw(new ResizeObserver(...))`
|
|||
|
|
- WebSocket 无需 markRaw(已在 connectSession 内部使用,不进 reactive)
|
|||
|
|
3. 将 `_uptimeTimers` Map 删除,uptimeTimer 改存入 `nativeMap[sessionId].uptimeTimer`
|
|||
|
|
4. 更新 `closeTab` 和 `onBeforeUnmount` 中的清理逻辑,从 `nativeMap` 获取 native 对象后清理
|
|||
|
|
|
|||
|
|
**验收**:Vue DevTools → Components 面板,`sessions` ref 中不出现 Terminal/WebSocket/FitAddon 对象
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### T2:Per-Session Overlay(Critical 修复)
|
|||
|
|
|
|||
|
|
**目标**:解决 C2(全局 overlay 状态)
|
|||
|
|
|
|||
|
|
**步骤**:
|
|||
|
|
1. 在 `TermSession` interface 中新增字段:
|
|||
|
|
```typescript
|
|||
|
|
showOverlay: boolean // 替代全局 showDisconnectOverlay
|
|||
|
|
overlayMsg: string // 替代全局 disconnectMsg
|
|||
|
|
reconnectCountdown: string // 移入 session
|
|||
|
|
```
|
|||
|
|
2. 删除全局 `showDisconnectOverlay`、`disconnectMsg`、`reconnectCountdown` refs
|
|||
|
|
3. 在 `connectSession`、`startReconnect`、`disconnect`、`closeTab` 中,将原来操作全局 ref 的代码改为操作 `s.showOverlay / s.overlayMsg / s.reconnectCountdown`
|
|||
|
|
4. 模板中的 `<v-overlay>` 绑定改为 `activeSession?.showOverlay`
|
|||
|
|
5. 关闭最后一个 Tab 时,不显示 overlay,让空状态区域自然显示(删除 `showDisconnectOverlay = true` 的 closeTab 逻辑)
|
|||
|
|
|
|||
|
|
**验收**:
|
|||
|
|
- 打开 2 个会话,让会话 2 断开,切换到会话 2,overlay 显示
|
|||
|
|
- 关闭最后一个会话,出现空状态服务器选择器,不出现 overlay
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### T3:提取 Composables
|
|||
|
|
|
|||
|
|
**目标**:解决 M1(单文件过大),提取 4 个 Composable
|
|||
|
|
|
|||
|
|
#### T3-A:`useTerminalSettings`
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 仅管理 fontSize / scrollback 的 localStorage 持久化
|
|||
|
|
export function useTerminalSettings() {
|
|||
|
|
const fontSize = ref(...)
|
|||
|
|
const scrollback = ref(...)
|
|||
|
|
function changeFontSize(delta: number): void
|
|||
|
|
function changeScrollback(val: number): void
|
|||
|
|
return { fontSize, scrollback, scrollbackOptions, changeFontSize, changeScrollback }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### T3-B:`useTerminalCmdBar`
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 管理命令输入、历史记录
|
|||
|
|
export function useTerminalCmdBar(getActiveSession: () => TermSession | null) {
|
|||
|
|
const cmdInput = ref('')
|
|||
|
|
function sendCmd(): void
|
|||
|
|
function cmdHistoryUp(): void
|
|||
|
|
function cmdHistoryDown(): void
|
|||
|
|
function sendCtrl(key: string): void
|
|||
|
|
function sendKey(key: string): void
|
|||
|
|
return { cmdInput, sendCmd, cmdHistoryUp, cmdHistoryDown, sendCtrl, sendKey }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### T3-C:`useTerminalSessions`
|
|||
|
|
|
|||
|
|
核心:多 Tab 管理,包含 `sessions / activeSessionId / activeSession / newSession / closeTab / switchTab / applyRouteQuery`
|
|||
|
|
|
|||
|
|
#### T3-D:`useTerminalSession`
|
|||
|
|
|
|||
|
|
核心:单会话连接 + 重连 + ping + uptime,参数为 `sessionData` ref 和 `nativeMap`
|
|||
|
|
|
|||
|
|
**注意**:T3-C/D 是最大改动,需完整测试后再提交
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### T4:拆分 Components
|
|||
|
|
|
|||
|
|
**目标**:消除模板耦合,每个组件职责单一
|
|||
|
|
|
|||
|
|
| 组件 | Props | Emits |
|
|||
|
|
|------|-------|-------|
|
|||
|
|
| `TerminalTabBar` | `sessions, activeSessionId` | `switch, close, new` |
|
|||
|
|
| `TerminalToolbar` | `session, fontSize, scrollback` | `font-change, scrollback-change, disconnect, fullscreen` |
|
|||
|
|
| `TerminalArea` | `sessions, activeSessionId` | `context-menu` |
|
|||
|
|
| `TerminalCmdBar` | `disabled` | `send, ctrl, key` |
|
|||
|
|
| `TerminalQuickBar` | `commands` | `exec, manage` |
|
|||
|
|
| `TerminalServerPicker` | `servers, mode` | `select` |
|
|||
|
|
|
|||
|
|
`TerminalPage.vue` 仅负责组合,约 80 行模板 + 30 行 setup。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### T5:修复 M2(DOM 查询) + M3(服务器选择器去重)
|
|||
|
|
|
|||
|
|
**M2 修复**:
|
|||
|
|
- `TerminalCmdBar.vue` 组件内部声明 `const inputRef = ref<HTMLTextAreaElement | null>(null)`
|
|||
|
|
- 将 `<ShellHighlightField>` 替换为绑定 ref 的原生 textarea(或 ShellHighlightField 补充 expose)
|
|||
|
|
- 删除 `getCmdInputEl()` 函数
|
|||
|
|
|
|||
|
|
**M3 修复**:
|
|||
|
|
- 新建 `TerminalServerPicker.vue`,props: `mode: 'inline' | 'dialog'`
|
|||
|
|
- 空状态区域使用 `<TerminalServerPicker mode="inline">`
|
|||
|
|
- 新建会话对话框内容使用 `<TerminalServerPicker mode="dialog">`
|
|||
|
|
- 删除模板中的重复列表代码
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### T6:Minor 体验修复
|
|||
|
|
|
|||
|
|
**E1 修复**(向同一服务器新建会话):
|
|||
|
|
- `newSession` 中检测到同 serverId 时,弹出 snackbar:「会话已存在,已切换过去。如需新建请关闭原会话。」
|
|||
|
|
- 提供一个可选 `opts.forceNew: boolean` 参数供未来扩展
|
|||
|
|
|
|||
|
|
**E2 修复**(初始 cd 指令时序):
|
|||
|
|
- 将 `sendCdToSession` 从 `setTimeout(200)` 改为在 `TERMINAL_INIT` 消息处理中直接发送,不依赖超时
|
|||
|
|
|
|||
|
|
**E3 修复**(重复快捷键):
|
|||
|
|
- 删除 `onKeydown` 中重复的 `e.key === '='` 分支,只保留 `e.key === '+'`(标准 Chromium 行为)
|
|||
|
|
|
|||
|
|
**E4 修复**(快捷命令加载失败提示):
|
|||
|
|
- `loadQuickCmds` catch 块改为:`snackbar('快捷命令加载失败,使用内置命令', 'warning')` 后再赋值内置
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 依赖与配置变更
|
|||
|
|
|
|||
|
|
- 无新 npm 包依赖
|
|||
|
|
- 无 Vue Router / Pinia 结构变更
|
|||
|
|
- xterm 版本不变(@xterm/xterm)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 测试要点
|
|||
|
|
|
|||
|
|
| 场景 | 验证方式 |
|
|||
|
|
|------|----------|
|
|||
|
|
| markRaw 生效 | Vue DevTools sessions ref 无 Terminal 实例 |
|
|||
|
|
| 多 Tab 断开 overlay | 手动断网,切换 Tab,overlay 正确显示 |
|
|||
|
|
| 最后 Tab 关闭 | 显示空状态选择器,不显示 overlay |
|
|||
|
|
| xterm resize | 拖拽侧栏/改字号,xterm 正确填充容器 |
|
|||
|
|
| reconnect 指数退避 | 断连后等待,验证 1s/2s/4s 重连间隔 |
|
|||
|
|
| 命令历史 | 发送命令,↑/↓ 正确回溯 |
|
|||
|
|
| 快捷命令 | 点击执行,命令发送到活动终端 |
|
|||
|
|
| pendingPath 跳转 | `/terminal?server_id=1&path=/var/log` 打开后自动 cd |
|
|||
|
|
| T1-T6 全部功能回归 | 手动端到端测试所有功能 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 回滚方式
|
|||
|
|
|
|||
|
|
全程在功能分支 `refactor/terminal` 开发,main 分支不受影响。
|
|||
|
|
任何阶段出现严重问题,直接 `git checkout main -- frontend/src/pages/TerminalPage.vue` 回滚到重构前。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 执行顺序建议
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
T1(markRaw + timer, 1h) → T2(per-session overlay, 1h)
|
|||
|
|
→ T6(minor fixes, 0.5h)
|
|||
|
|
→ T3(composables, 2h)→ T4(components, 2h)→ T5(dom + picker, 0.5h)
|
|||
|
|
→ 全量测试 → changelog → 部署
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
建议 T1+T2+T6 作为第一批次(安全稳定性),T3+T4+T5 作为第二批次(架构重构)分两次 PR 合并。
|