# 文件树（类 VSCode Explorer）实现顺序 + 对应文件 + 必测用例

> 目标：在「文件模式」页面树中实现接近 VSCode Explorer 的体验：点击行高亮选择（支持多选/范围选）、`Ctrl+C/Ctrl+V` 复制粘贴（真实文件/文件夹复制）、拖拽移动/按修饰键拖拽复制。
>
> 非目标：键盘导航（↑↓←→）与编辑器正文内复制粘贴行为。

## 0. 参考代码位置（VSCode）

说明：VSCode Explorer 不是独立 React 组件，依赖 Workbench 服务体系；这里用于“行为/结构参考”，不直接搬运实现。

- [x] VSCode 参考仓库：`cankao/vscode`
- [x] 关键入口与文件（详见 `design/vscodetree`）：
  - [x] `cankao/vscode/src/vs/workbench/contrib/files/browser/files.contribution.ts`
  - [x] `cankao/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts`
  - [x] `cankao/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts`
  - [x] `cankao/vscode/src/vs/base/common/resourceTree.ts`
  - [x] `cankao/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts`

## 1. MNOTE 当前实现位置（改造落点）

- [x] 文件树（文件模式）：`wolai-frontend/src/components/sidebar/file-tree.tsx`
- [x] 侧边栏状态/数据/选择/复制/拖拽集成：`wolai-frontend/src/components/sidebar/sidebar.tsx`
- [x] 文档树扁平化：`wolai-frontend/src/lib/sidebar-tree.ts`（已有 `flattenDocumentTree`）
- [x] 复制页面（保留单页复制）：`wolai-frontend/src/app/api/documents/duplicate/route.ts`
- [x] 递归复制入口：`wolai-frontend/src/app/api/documents/copy-tree/route.ts`
- [x] 附件 copy/move/delete/rename：`wolai-frontend/src/app/api/media/batch/route.ts`

## 2. 实现顺序（建议小步提交，每步能回归）

### Step 0：建立“可见行模型”（TreeRow）——多选/复制/拖拽的共同基础

**要做**
- [x] 抽象 `visibleRows: FileTreeRow[]`（按展开状态 flatten：doc + index.md + assets + 子 doc）
- [x] `rowId` 全局唯一：`doc:<id>` / `index:<docId>` / `asset:<assetId>`

**新增/改造文件**
- [x] `wolai-frontend/src/lib/file-tree/types.ts`
- [x] `wolai-frontend/src/lib/file-tree/rows.ts`

**必测用例（Vitest：纯函数）**
- [x] `buildVisibleRows` 顺序稳定（展开/折叠后符合预期）
- [x] `rowId` 唯一且可逆解析

---

### Step 1：选择模型（Selection Model）——对齐 VSCode 的点击/多选/范围选/右键语义

**要做**
- [x] 去除复选框，改为“点击行高亮选择”
- [x] 多选覆盖 doc/index.md/asset 同一集合
- [x] 状态字段：
  - [x] `selectedRowIds: Set<string>`
  - [x] `anchorRowId: string | null`（Shift 范围起点）
  - [x] `focusedRowId: string | null`（最后交互行，用于粘贴目标推断）

**新增文件**
- [x] `wolai-frontend/src/lib/file-tree/selection.ts`（把交互写成纯函数，便于测）

**必测用例（Vitest：纯函数）**
- [x] 单击：清空并仅选中当前；更新 anchor/focus
- [x] Ctrl/Cmd+单击：切换选中；不清空其他
- [x] Shift+单击：按 `visibleRows` 做区间选择（覆盖式）
- [x] 右键：未选中项右键 → 先切为单选再弹菜单；已选中项 → 保持多选集合
- [x] 空白处单击清空选择

---

### Step 2：`Ctrl+C / Ctrl+V`（仅文件树区域生效）

**要做**
- [x] 只在“文件树区域激活”时响应 `Ctrl+C/V`，不影响编辑器输入框/正文
- [x] 复制时写入自定义剪贴板 payload（同时提供内存 fallback）
- [x] 粘贴目标推断：
  - [x] focused 为 doc → 贴入其下
  - [x] focused 为 index/asset → 贴入其所属 doc
  - [x] 无 focused → fallback activeDocId

**新增文件**
- [x] `wolai-frontend/src/lib/file-tree/clipboard.ts`

**必测用例（Vitest）**
- [x] payload 版本/字段校验（`type/version/action/rowIds`）
- [x] 目标推断 3 分支覆盖（doc / index|asset / null）
- [x] 在 `input/textarea/contenteditable` 内不拦截 `Ctrl+C/V`

---

### Step 3：“真实文件/文件夹复制”后端能力（对齐 VSCode 语义）

**要做**
- [x] doc（文件夹/页面）复制：支持递归复制其内容（子页面 + 正文 + 附件）
- [x] asset（真实文件）复制：复制存储对象并创建新记录（`media/batch copy` + 重名策略）
- [x] 命名冲突：自动生成不冲突名称（文件夹与文件分别处理）
- [x] 虚拟附件（mindmap.json 等）不参与复制/拖拽（仅保留打开/下载逻辑）

**新增/改造 API**
- [x] 新增：`wolai-frontend/src/app/api/documents/copy-tree/route.ts`（递归复制入口，返回新旧 id 映射）
- [x] 保持：`wolai-frontend/src/app/api/documents/duplicate/route.ts`（现有单页复制不破坏）
- [x] 补齐：`wolai-frontend/src/app/api/media/upload/route.ts`（写入 `bucket/storage_path`，便于后续 copy/move）
- [x] 补齐：`wolai-frontend/src/app/api/media/batch/route.ts`（copy/move 支持 `storage_path` 或可解析的 `file_url`；重名策略）

**纯函数工具与测试**
- [x] 命名：`wolai-frontend/src/lib/file-tree/naming.ts`
- [x] 测试：`wolai-frontend/src/lib/file-tree/naming.test.ts`

---

### Step 4：拖拽移动/复制（含多选拖拽）

**要做**
- [x] 拖拽默认移动；按修饰键（`Alt`）为复制（浏览器层面 dropEffect 已设置；Alt-copy 建议人工补测一次）
- [x] 起拖行在选择集中 → 拖整个选择集；否则仅拖当前行并先切为单选
- [ ] 拖拽悬停反馈（drop feedback）：
  - [ ] 悬停到“收起的 doc（文件夹）行”时，仅该行变灰
  - [ ] 悬停到“展开的 doc（文件夹）行”时，该 doc 的可见子节点范围一起变灰（类似 VSCode Explorer）
- [x] drop 目标：
  - [x] drop 到 doc 行：贴入其下
  - [x] drop 到 index/asset 行：等同贴入其所属 doc
  - [x] 禁止把 doc 拖到自身或后代（含多选去重/去后代）
- [x] 与“拖拽上传文件到页面”的 drop 行为区分（payload 不同）

**新增文件**
- [x] `wolai-frontend/src/lib/file-tree/dnd.ts`
- [x] `wolai-frontend/src/lib/file-tree/asset.ts`（判断“真实文件”附件）

**必测用例（Vitest：纯函数）**
- [x] `inferDropTargetDocId`：doc / index|asset / null 推断正确（`wolai-frontend/src/lib/file-tree/dnd.test.ts`）
- [x] `isInvalidDocDrop`：自拖/后代拖拦截（`wolai-frontend/src/lib/file-tree/dnd.test.ts`）
- [x] `isRealFileAsset`：排除 mindmap 等虚拟附件（`wolai-frontend/src/lib/file-tree/asset.test.ts`）

## 3. 手工验收（每步至少跑一遍）

- [x] 点击行高亮；active（当前打开页）与 selected（选中）同时可识别
- [x] `Ctrl/Cmd+单击` 多选；`Shift+单击` 连续范围选；右键语义正确
- [ ] `Ctrl+C/Ctrl+V`：仅树区域生效；编辑器正文不受影响（MCP 按键模拟不稳定，建议人工复核一次）
- [x] 复制 doc：`/api/documents/copy-tree` 已验证返回 200（页面端 `fetch`）
- [ ] 复制 asset：生成新的存储对象（不是引用），新文件可独立下载（需要至少 1 个真实附件用例）
- [x] 拖拽移动 doc：已通过页面拖拽触发 `/api/documents/move` 并验证 200
- [x] 从系统拖拽文件到文件树：落点为目标页面（doc 行 / index 行 / asset 行），触发 `/api/media/upload` 上传并作为“真实文件”附件挂到该页面下
- [ ] 拖拽悬停反馈：收起 doc 行仅该行变灰；展开 doc 行则其可见范围一起变灰
- [ ] 从系统拖拽单个文件：只创建 1 份附件记录（不应出现两个同名文件）
- [ ] 从系统拖拽文件到“当前打开的页面”：主编辑区自动插入 1 个附件/媒体块（可在页面正文中看到）
- [ ] Alt+拖拽复制：逻辑已接入（`event.altKey`），需要人工补测一次（MCP 暂无法“按住 Alt 拖拽”）

---

### Step 5：多选删除（右键删除作用于选择集）

**要做**
- [x] 右键删除：若右键对象在 `selectedRowIds` 内，则对整个选择集执行删除
- [x] index 行视作其所属页面（去重后再删）
- [x] 页面删除：移动到垃圾桶（复用 `/api/documents/delete`）
- [x] 附件删除：沿用当前逻辑（`/api/media/batch delete`；mindmap 走 `/api/mindmap/:docId`）
- [x] 若页面被删除，则跳过其页面内已选附件删除（避免重复/无效操作）

**新增/改造文件**
- [x] `wolai-frontend/src/lib/file-tree/delete.ts`
- [x] `wolai-frontend/src/components/sidebar/sidebar.tsx`

**必测用例（Vitest：纯函数）**
- [x] 选中父/子页面时，仅删除父页面（去后代）
- [x] index 行与 doc 行同时选中时，页面 id 去重
- [x] 页面被删时，其下已选附件不参与附件删除

**手工验收**
- [x] 多选后右键任一已选行 → 点击“删除到垃圾桶”，应删除整个选择集（而不是仅最后右键项）
- [ ] 多选包含「页面 + 附件」时：无论最后右键落在页面行还是附件行，“删除”都应按选择集执行
