# Terminal 页重构设计文档

**日期**: 2026-06-02  
**版本**: v1.0  
**状态**: 待审批

---

## 1. 背景与目标

### 现状问题

`frontend/src/pages/TerminalPage.vue` 是一个 **1244 行的单文件组件**，包含：会话管理、WebSocket 生命周期、xterm.js 实例管理、命令栏、快捷命令、上下文菜单、键盘快捷键、服务器列表、Uptime 计时、Ping 检测等所有逻辑。

经过逐行审计，发现以下 **分级问题**：

#### 🔴 Critical（可能导致 Bug 或内存泄漏）

| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| C1 | **`markRaw` 缺失** | `TermSession.term/fitAddon/ws/resizeObserver` | Vue 对 xterm Terminal、WebSocket、ResizeObserver 进行深度代理，导致 xterm 内部状态被破坏，可能出现渲染异常、内存泄漏 |
| C2 | **全局 overlay 状态** | `showDisconnectOverlay`、`reconnectCountdown` | 这两个 ref 是全局单例，仅当 *当前活动会话* 断开时才更新。切换到断开的非活动会话时，不显示 overlay，用户以为连接正常 |
| C3 | **Uptime 计时器独立 Map** | `_uptimeTimers` | 模块级 Map 与 session 对象分离，`closeTab` 和 `onBeforeUnmount` 都需要单独清理；若路径不一致会造成 timer 泄漏 |

#### 🟡 Medium（架构质量问题）

| # | 问题 | 位置 |
|---|------|------|
| M1 | **单文件 1244 行** | TerminalPage.vue | 不可维护，无法单独测试 |
| M2 | **脆弱 DOM 查询** | `getCmdInputEl()` | 依赖 CSS class `.cmd-bar--tall .sh-field__input--multi`，class 名变动即失效 |
| M3 | **重复服务器选择器 UI** | 空状态内嵌列表 + `showServerDialog` 对话框 | 同一套 UI 实现了两遍，联动逻辑分散 |
| M4 | **会话 interface 类型混乱** | `TermSession` | 数据字段（serverId, cwd）与 UI 状态（status, uptime, pingMs）与原生对象（ws, term, fitAddon）混在一个 interface |

#### 🟢 Minor（体验问题）

| # | 问题 |
|---|------|
| E1 | 向同一服务器新建会话时直接 `switchTab`，没有给用户选择 |
| E2 | `sendCdToSession` 使用 200ms `setTimeout` 等 TERMINAL_INIT，时序不可靠 |
| E3 | Ctrl+= 和 Ctrl++ 两个快捷键重复注册 |
| E4 | 快捷命令加载失败时静默降级为内置命令，不通知用户 |

### 目标

1. 消除 C1-C3 三个 Critical 问题（安全稳定性保障）
2. 将 TerminalPage.vue 拆解为可独立维护的组件树
3. 提取可复用 Composable，可单独 Vitest 测试
4. 修复 M2/M3 架构问题
5. 修复 E1-E4 体验问题

---

## 2. 方案选型

### 方案 A：局部修补（不推荐）

只修 C1-C3，不重构架构。  
**缺点**：1244 行单文件继续增长，下次迭代仍然面对同样复杂度。

### 方案 B：完全拆分 + Composable + Pinia（推荐）

按职责提取 Composable，`TerminalPage.vue` 只负责组合，保留约 80 行模板 + 30 行脚本。

---

## 3. 选定方案：方案 B

### 3.1 目标文件树

```
frontend/src/
├── pages/
│   └── TerminalPage.vue              (~80 行，组合层)
├── composables/terminal/
│   ├── useTerminalSession.ts         (单会话 xterm+WS 生命周期)
│   ├── useTerminalSessions.ts        (多会话 Tab 管理)
│   ├── useTerminalSettings.ts        (fontSize/scrollback localStorage)
│   └── useTerminalCmdBar.ts          (命令输入 + 历史)
└── components/terminal/
    ├── TerminalTabBar.vue             (Tab 条)
    ├── TerminalToolbar.vue            (工具栏: 状态/字号/断开)
    ├── TerminalArea.vue               (xterm 挂载区 + overlay)
    ├── TerminalCmdBar.vue             (命令栏 + 快捷键面板)
    ├── TerminalQuickBar.vue           (快捷命令栏)
    └── TerminalServerPicker.vue       (服务器选择 — 复用于空状态和新建对话框)
```

### 3.2 核心数据模型重设计

```typescript
// 分离「纯数据」和「原生对象」
interface TermSessionData {
  id: string
  serverId: number
  serverName: string
  cwd: string
  cmdHistory: string[]
  cmdIdx: number
  reconnectAttempt: number
  manualDisconnect: boolean
  status: 'idle' | 'connecting' | 'connected' | 'error'
  // Per-session UI state (replaces global overlay)
  showOverlay: boolean
  overlayMsg: string
  reconnectCountdown: string
  uptime: string
  pingMs: number | null
  connectionStartTime: number
  pendingPath: string
  initialCdSent: boolean
}

interface TermSessionNative {
  id: string
  ws: WebSocket | null
  term: Terminal | null             // markRaw
  fitAddon: FitAddon | null         // markRaw
  resizeObserver: ResizeObserver | null  // markRaw
  reconnectTimer: ReturnType<typeof setTimeout> | null
  pingTimer: ReturnType<typeof setInterval> | null
  uptimeTimer: ReturnType<typeof setInterval> | null  // 移入 session，不再用外部 Map
}
```

> **关键**：`TermSessionNative` 中所有字段使用 `markRaw()`，不放入 Vue reactive 系统。  
> `TermSessionData` 放入 `ref<TermSessionData[]>`，驱动模板响应式。

### 3.3 Per-Session Overlay（修复 C2）

每个会话自带 `showOverlay / overlayMsg / reconnectCountdown`，`TerminalArea.vue` 根据 `activeSession.showOverlay` 渲染 overlay，切换 Tab 时自然显示/隐藏对应状态。

### 3.4 markRaw 应用（修复 C1）

```typescript
// useTerminalSession.ts
const native = {
  ws: null as WebSocket | null,
  term: markRaw(new Terminal(options)),
  fitAddon: markRaw(new FitAddon()),
  resizeObserver: markRaw(new ResizeObserver(...)),
}
```

原生对象不进入任何 `ref`/`reactive`，仅通过 session ID 从一个 `Map<string, TermSessionNative>` 查找。

### 3.5 服务器选择器去重（修复 M3）

`TerminalServerPicker.vue` 接受 `prop: mode = 'inline' | 'dialog'`，内部渲染列表，空状态和新建对话框均复用，消除重复代码。

### 3.6 getCmdInputEl 重构（修复 M2）

`TerminalCmdBar.vue` 直接持有 `textarea` ref，通过 `defineExpose({ getInputEl })` 或直接在组件内处理复制/粘贴，不再依赖全局 DOM 查询。

---

## 4. 拆分阶段

| 阶段 | 内容 | 风险 |
|------|------|------|
| **T1** | markRaw 修复（C1）+ Uptime 计时器内嵌（C3） | 低，仅内部实现变化 |
| **T2** | Per-session overlay（C2）+ 修复 E1/E2 | 中，需重构状态管理 |
| **T3** | 提取 Composables（4 个）| 中，逻辑搬移，行为不变 |
| **T4** | 拆分 Components（6 个）| 低，模板拆分 |
| **T5** | 修复 M2/M3（DOM查询+服务器选择器去重）| 低 |
| **T6** | 修复 E3/E4（快捷键去重+快捷命令错误提示）| 低 |

---

## 5. 接口设计（关键部分）

### `useTerminalSessions`

```typescript
function useTerminalSessions() {
  const sessions: Ref<TermSessionData[]>
  const activeSessionId: Ref<string>
  const activeSession: ComputedRef<TermSessionData | null>

  function newSession(serverId: number, opts?: { path?: string }): Promise<void>
  function closeTab(sessionId: string): Promise<void>
  function switchTab(sessionId: string): Promise<void>

  return { sessions, activeSessionId, activeSession, newSession, closeTab, switchTab }
}
```

### `useTerminalSession`

```typescript
function useTerminalSession(sessionData: TermSessionData, container: Ref<HTMLElement | null>) {
  function connect(): Promise<void>
  function disconnect(manual?: boolean): void
  function reconnect(): void
  function sendData(data: string): void
  function sendResize(cols: number, rows: number): void
  function fit(): void
  function dispose(): void

  return { connect, disconnect, reconnect, sendData, sendResize, fit, dispose }
}
```

---

## 6. 安全与性能约束

- 不改动 `server/api/webssh.py`（后端已符合要求）
- `markRaw` 修复后 xterm 对象不再被 Vue 深度观测，内存占用降低
- 多 Tab 场景下每个 Tab 的 Ping 计时器独立，不互相干扰

---

## 7. 验收标准

- [ ] Vue DevTools 中 xterm Terminal / WebSocket 对象不可见（markRaw 生效）
- [ ] 多 Tab 场景：切换到断开的 Tab 时显示 overlay
- [ ] 关闭最后一个 Tab：显示空状态服务器选择器（非断开 overlay）
- [ ] `TerminalPage.vue` 模板 ≤ 100 行，script ≤ 50 行
- [ ] Vitest 可对 `useTerminalSettings`、`useTerminalCmdBar` 单独测试
- [ ] 页面功能无回归：xterm 正常渲染、WS 连接、resize、reconnect、快捷命令
