Files
mnote/recycle/design/file-tree-vscode-checklist.md
T
2026-04-13 19:21:42 +08:00

8.8 KiB
Raw Blame History

文件树(类 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.ts
    • cankao/vscode/src/vs/workbench/contrib/files/browser/views/explorerView.ts
    • cankao/vscode/src/vs/workbench/contrib/files/common/explorerModel.ts
    • cankao/vscode/src/vs/base/common/resourceTree.ts
    • cankao/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/renamewolai-frontend/src/app/api/media/batch/route.ts

2. 实现顺序(建议小步提交,每步能回归)

Step 0:建立“可见行模型”(TreeRow)——多选/复制/拖拽的共同基础

要做

  • 抽象 visibleRows: FileTreeRow[](按展开状态 flattendoc + index.md + assets + 子 doc
  • rowId 全局唯一:doc:<id> / index:<docId> / asset:<assetId>

新增/改造文件

  • wolai-frontend/src/lib/file-tree/types.ts
  • wolai-frontend/src/lib/file-tree/rows.ts

必测用例(Vitest:纯函数)

  • buildVisibleRows 顺序稳定(展开/折叠后符合预期)
  • rowId 唯一且可逆解析

Step 1:选择模型(Selection Model)——对齐 VSCode 的点击/多选/范围选/右键语义

要做

  • 去除复选框,改为“点击行高亮选择”
  • 多选覆盖 doc/index.md/asset 同一集合
  • 状态字段:
    • selectedRowIds: Set<string>
    • anchorRowId: string | nullShift 范围起点)
    • focusedRowId: string | null(最后交互行,用于粘贴目标推断)

新增文件

  • wolai-frontend/src/lib/file-tree/selection.ts(把交互写成纯函数,便于测)

必测用例(Vitest:纯函数)

  • 单击:清空并仅选中当前;更新 anchor/focus
  • Ctrl/Cmd+单击:切换选中;不清空其他
  • Shift+单击:按 visibleRows 做区间选择(覆盖式)
  • 右键:未选中项右键 → 先切为单选再弹菜单;已选中项 → 保持多选集合
  • 空白处单击清空选择

Step 2Ctrl+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.tscopy/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.ts
  • wolai-frontend/src/lib/file-tree/asset.ts(判断“真实文件”附件)

必测用例(Vitest:纯函数)

  • inferDropTargetDocIddoc / 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 deletemindmap 走 /api/mindmap/:docId
  • 若页面被删除,则跳过其页面内已选附件删除(避免重复/无效操作)

新增/改造文件

  • wolai-frontend/src/lib/file-tree/delete.ts
  • wolai-frontend/src/components/sidebar/sidebar.tsx

必测用例(Vitest:纯函数)

  • 选中父/子页面时,仅删除父页面(去后代)
  • index 行与 doc 行同时选中时,页面 id 去重
  • 页面被删时,其下已选附件不参与附件删除

手工验收

  • 多选后右键任一已选行 → 点击“删除到垃圾桶”,应删除整个选择集(而不是仅最后右键项)
  • 多选包含「页面 + 附件」时:无论最后右键落在页面行还是附件行,“删除”都应按选择集执行