# 文件树(类 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:` / `index:` / `asset:` **新增/改造文件** - [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` - [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] 起拖行在选择集中 → 拖整个选择集;否则仅拖当前行并先切为单选 - [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` 上传并作为“真实文件”附件挂到该页面下 - [ ] 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] 多选后右键任一已选行 → 点击“删除到垃圾桶”,应删除整个选择集(而不是仅最后右键项)