# 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 合并。
