8.8 KiB
8.8 KiB
文件树(类 VSCode Explorer)实现顺序 + 对应文件 + 必测用例
目标:在「文件模式」页面树中实现接近 VSCode Explorer 的体验:点击行高亮选择(支持多选/范围选)、
Ctrl+C/Ctrl+V复制粘贴(真实文件/文件夹复制)、拖拽移动/按修饰键拖拽复制。非目标:键盘导航(↑↓←→)与编辑器正文内复制粘贴行为。
0. 参考代码位置(VSCode)
说明:VSCode Explorer 不是独立 React 组件,依赖 Workbench 服务体系;这里用于“行为/结构参考”,不直接搬运实现。
- VSCode 参考仓库:
cankao/vscode - 关键入口与文件(详见
design/vscodetree):cankao/vscode/src/vs/workbench/contrib/files/browser/files.contribution.tscankao/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.tscankao/vscode/src/vs/workbench/contrib/files/common/explorerModel.tscankao/vscode/src/vs/base/common/resourceTree.tscankao/vscode/src/vs/workbench/contrib/files/browser/fileCommands.ts
1. MNOTE 当前实现位置(改造落点)
- 文件树(文件模式):
wolai-frontend/src/components/sidebar/file-tree.tsx - 侧边栏状态/数据/选择/复制/拖拽集成:
wolai-frontend/src/components/sidebar/sidebar.tsx - 文档树扁平化:
wolai-frontend/src/lib/sidebar-tree.ts(已有flattenDocumentTree) - 复制页面(保留单页复制):
wolai-frontend/src/app/api/documents/duplicate/route.ts - 递归复制入口:
wolai-frontend/src/app/api/documents/copy-tree/route.ts - 附件 copy/move/delete/rename:
wolai-frontend/src/app/api/media/batch/route.ts
2. 实现顺序(建议小步提交,每步能回归)
Step 0:建立“可见行模型”(TreeRow)——多选/复制/拖拽的共同基础
要做
- 抽象
visibleRows: FileTreeRow[](按展开状态 flatten:doc + index.md + assets + 子 doc) rowId全局唯一:doc:<id>/index:<docId>/asset:<assetId>
新增/改造文件
wolai-frontend/src/lib/file-tree/types.tswolai-frontend/src/lib/file-tree/rows.ts
必测用例(Vitest:纯函数)
buildVisibleRows顺序稳定(展开/折叠后符合预期)rowId唯一且可逆解析
Step 1:选择模型(Selection Model)——对齐 VSCode 的点击/多选/范围选/右键语义
要做
- 去除复选框,改为“点击行高亮选择”
- 多选覆盖 doc/index.md/asset 同一集合
- 状态字段:
selectedRowIds: Set<string>anchorRowId: string | null(Shift 范围起点)focusedRowId: string | null(最后交互行,用于粘贴目标推断)
新增文件
wolai-frontend/src/lib/file-tree/selection.ts(把交互写成纯函数,便于测)
必测用例(Vitest:纯函数)
- 单击:清空并仅选中当前;更新 anchor/focus
- Ctrl/Cmd+单击:切换选中;不清空其他
- Shift+单击:按
visibleRows做区间选择(覆盖式) - 右键:未选中项右键 → 先切为单选再弹菜单;已选中项 → 保持多选集合
- 空白处单击清空选择
Step 2:Ctrl+C / Ctrl+V(仅文件树区域生效)
要做
- 只在“文件树区域激活”时响应
Ctrl+C/V,不影响编辑器输入框/正文 - 复制时写入自定义剪贴板 payload(同时提供内存 fallback)
- 粘贴目标推断:
- focused 为 doc → 贴入其下
- focused 为 index/asset → 贴入其所属 doc
- 无 focused → fallback activeDocId
新增文件
wolai-frontend/src/lib/file-tree/clipboard.ts
必测用例(Vitest)
- payload 版本/字段校验(
type/version/action/rowIds) - 目标推断 3 分支覆盖(doc / index|asset / null)
- 在
input/textarea/contenteditable内不拦截Ctrl+C/V
Step 3:“真实文件/文件夹复制”后端能力(对齐 VSCode 语义)
要做
- doc(文件夹/页面)复制:支持递归复制其内容(子页面 + 正文 + 附件)
- asset(真实文件)复制:复制存储对象并创建新记录(
media/batch copy+ 重名策略) - 命名冲突:自动生成不冲突名称(文件夹与文件分别处理)
- 虚拟附件(mindmap.json 等)不参与复制/拖拽(仅保留打开/下载逻辑)
新增/改造 API
- 新增:
wolai-frontend/src/app/api/documents/copy-tree/route.ts(递归复制入口,返回新旧 id 映射) - 保持:
wolai-frontend/src/app/api/documents/duplicate/route.ts(现有单页复制不破坏) - 补齐:
wolai-frontend/src/app/api/media/upload/route.ts(写入bucket/storage_path,便于后续 copy/move) - 补齐:
wolai-frontend/src/app/api/media/batch/route.ts(copy/move 支持storage_path或可解析的file_url;重名策略)
纯函数工具与测试
- 命名:
wolai-frontend/src/lib/file-tree/naming.ts - 测试:
wolai-frontend/src/lib/file-tree/naming.test.ts
Step 4:拖拽移动/复制(含多选拖拽)
要做
- 拖拽默认移动;按修饰键(
Alt)为复制(浏览器层面 dropEffect 已设置;Alt-copy 建议人工补测一次) - 起拖行在选择集中 → 拖整个选择集;否则仅拖当前行并先切为单选
- 拖拽悬停反馈(drop feedback):
- 悬停到“收起的 doc(文件夹)行”时,仅该行变灰
- 悬停到“展开的 doc(文件夹)行”时,该 doc 的可见子节点范围一起变灰(类似 VSCode Explorer)
- drop 目标:
- drop 到 doc 行:贴入其下
- drop 到 index/asset 行:等同贴入其所属 doc
- 禁止把 doc 拖到自身或后代(含多选去重/去后代)
- 与“拖拽上传文件到页面”的 drop 行为区分(payload 不同)
新增文件
wolai-frontend/src/lib/file-tree/dnd.tswolai-frontend/src/lib/file-tree/asset.ts(判断“真实文件”附件)
必测用例(Vitest:纯函数)
inferDropTargetDocId:doc / index|asset / null 推断正确(wolai-frontend/src/lib/file-tree/dnd.test.ts)isInvalidDocDrop:自拖/后代拖拦截(wolai-frontend/src/lib/file-tree/dnd.test.ts)isRealFileAsset:排除 mindmap 等虚拟附件(wolai-frontend/src/lib/file-tree/asset.test.ts)
3. 手工验收(每步至少跑一遍)
- 点击行高亮;active(当前打开页)与 selected(选中)同时可识别
Ctrl/Cmd+单击多选;Shift+单击连续范围选;右键语义正确Ctrl+C/Ctrl+V:仅树区域生效;编辑器正文不受影响(MCP 按键模拟不稳定,建议人工复核一次)- 复制 doc:
/api/documents/copy-tree已验证返回 200(页面端fetch) - 复制 asset:生成新的存储对象(不是引用),新文件可独立下载(需要至少 1 个真实附件用例)
- 拖拽移动 doc:已通过页面拖拽触发
/api/documents/move并验证 200 - 从系统拖拽文件到文件树:落点为目标页面(doc 行 / index 行 / asset 行),触发
/api/media/upload上传并作为“真实文件”附件挂到该页面下 - 拖拽悬停反馈:收起 doc 行仅该行变灰;展开 doc 行则其可见范围一起变灰
- 从系统拖拽单个文件:只创建 1 份附件记录(不应出现两个同名文件)
- 从系统拖拽文件到“当前打开的页面”:主编辑区自动插入 1 个附件/媒体块(可在页面正文中看到)
- Alt+拖拽复制:逻辑已接入(
event.altKey),需要人工补测一次(MCP 暂无法“按住 Alt 拖拽”)
Step 5:多选删除(右键删除作用于选择集)
要做
- 右键删除:若右键对象在
selectedRowIds内,则对整个选择集执行删除 - index 行视作其所属页面(去重后再删)
- 页面删除:移动到垃圾桶(复用
/api/documents/delete) - 附件删除:沿用当前逻辑(
/api/media/batch delete;mindmap 走/api/mindmap/:docId) - 若页面被删除,则跳过其页面内已选附件删除(避免重复/无效操作)
新增/改造文件
wolai-frontend/src/lib/file-tree/delete.tswolai-frontend/src/components/sidebar/sidebar.tsx
必测用例(Vitest:纯函数)
- 选中父/子页面时,仅删除父页面(去后代)
- index 行与 doc 行同时选中时,页面 id 去重
- 页面被删时,其下已选附件不参与附件删除
手工验收
- 多选后右键任一已选行 → 点击“删除到垃圾桶”,应删除整个选择集(而不是仅最后右键项)
- 多选包含「页面 + 附件」时:无论最后右键落在页面行还是附件行,“删除”都应按选择集执行