diff --git a/TRACKING.md b/TRACKING.md index 452d5464..b62af9e1 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -7,9 +7,16 @@ - 先落文档、测试用例和进度跟踪文件。 - 再实施 `/editor/canvas` 的 Lovart 风格画布交互、缩放二级菜单、吸附线、元数据窗口、真实生成 / 修改入口和工程 / 画布持久化。 +## 拆分口径 + +- `docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md` 中的二十七个“阶段模块”是原始计划项,用来描述目标边界。 +- 本文件验证记录里的“执行批次”按每次实际修改、验证、提交和推送递增,包含原始计划项落地、计划外回归修复、组件复用、测试迁移和后续模型收口。 +- 因此“第四十一执行批次”不代表原始计划有四十一个阶段;它表示已完成并推送的第四十一次前端拆分相关增量。 +- 当前原始二十七个计划项已经覆盖到第二十七阶段模块,后续执行批次属于对已拆模块的深化收口或回归修复。 + ## 进度 -| 阶段 | 状态 | 记录 | +| 工作项 | 状态 | 记录 | | --- | --- | --- | | 文档 | 已完成 | 已补领域词、技术方案和后端表 / API 边界。 | | 测试用例 | 已完成 | 已补前端交互测试和 editor project client 测试。 | @@ -110,52 +117,53 @@ - 2026-06-14 组件复用修正:画布图片 hover 尺寸标签改为复用 `PlatformPillBadge tone="lightOverlay"`,局部 CSS 只保留定位和深色覆盖,不再重复维护 badge 的圆角、字号和基础排版;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformPillBadge.test.tsx`、`npm run typecheck`。 - 2026-06-14 组件复用修正:生成跟随框的关闭按钮改为复用 `PlatformIconButton variant="surfaceFloating"`,编辑器薄包装 `EditorIconButton` 增加 variant 透传,删除局部关闭按钮基础 chrome;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformIconButton.test.tsx`、`npm run typecheck`。 - 2026-06-16 编辑器回归修正:工程 / 素材 / 上传等编辑器请求恢复全局 401 / 403 登录弹窗;未登录上传会先弹登录并在登录后续传;画布背景入口恢复为 `画布背景设置` 面板,支持预设色、自定义颜色、HEX 输入、非法值不应用、恢复默认和 Escape 关闭。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/services/image-editor/editorProjectClient.test.ts`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器 smoke:`http://127.0.0.1:10006/editor/canvas` 未登录打开 `账号入口`,登录后上传素材成功,背景面板打开后点击“暖灰”使画布背景变为 `rgb(243, 240, 234)`。 -- 2026-06-17 前端拆分第一阶段:新增 `ImageCanvasEditorTypes`、`ImageCanvasEditorModel`、`ImageCanvasGenerationModel` 和 `ImageCanvasExportModel`,把类型、画布快照 / 吸附 / 背景、生成输入快照和导出元数据规则从 `ImageCanvasEditorView` 抽出;新增模型层单测,主视图从 8286 行降至 7054 行。 +- 2026-06-17 前端拆分第一执行批次:新增 `ImageCanvasEditorTypes`、`ImageCanvasEditorModel`、`ImageCanvasGenerationModel` 和 `ImageCanvasExportModel`,把类型、画布快照 / 吸附 / 背景、生成输入快照和导出元数据规则从 `ImageCanvasEditorView` 抽出;新增模型层单测,主视图从 8286 行降至 7054 行。 - 2026-06-17 浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录打开工程和未登录上传均弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开 `画布背景设置` 面板,点击 `暖灰` 后画布背景为 `rgb(243, 240, 234)`;登录开发账号后上传图片成功进入 `项目素材`,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第二阶段:新增 `ImageCanvasSidebarView`,把素材 / 图层共用左侧整合面板从主视图抽出;上传链路、登录弹窗、素材拖到画布、持久化、图层历史和右键菜单状态机仍保留在主视图,避免过度拆分。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 +- 2026-06-17 前端拆分第二执行批次:新增 `ImageCanvasSidebarView`,把素材 / 图层共用左侧整合面板从主视图抽出;上传链路、登录弹窗、素材拖到画布、持久化、图层历史和右键菜单状态机仍保留在主视图,避免过度拆分。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 - 2026-06-17 侧栏拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;`画布背景色` 打开 `画布背景设置` dialog,包含预设、自定义颜色、HEX 和恢复默认;使用临时开发账号登录后上传图片成功进入 `项目素材`,点击素材可添加到画布,切换 `图层` 侧栏后能看到同一图片图层,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第三阶段:新增 `ImageCanvasStageView`,把画布工作区视觉树、图层渲染、生成占位框、右键菜单、左下 dock、小地图和底部 AI 工具栏从主视图抽出;拖拽 / 缩放、历史、上传、登录、生成提交、素材持久化和右键命令仍保留在主视图,避免拆散状态机。 +- 2026-06-17 前端拆分第三执行批次:新增 `ImageCanvasStageView`,把画布工作区视觉树、图层渲染、生成占位框、右键菜单、左下 dock、小地图和底部 AI 工具栏从主视图抽出;拖拽 / 缩放、历史、上传、登录、生成提交、素材持久化和右键命令仍保留在主视图,避免拆散状态机。 - 2026-06-17 舞台拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开完整 `画布背景设置` dialog,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;使用临时开发账号密码登录后上传 `smoke.png` 成功进入 `项目素材`,点击素材添加到画布,切换 `图层` 后显示同一图层,图片浮动工具栏、小地图和 `AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第四阶段:新增 `ImageCanvasGenerationComposerView`,把生成图片、生成规范、生成角色形象、生成图标素材、快速编辑、角色动画和修改图片弹窗从主视图抽出;生成提交、上传 input、引用选择、占位框拖拽、结果回写、历史和画布状态机仍保留在主视图。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 +- 2026-06-17 前端拆分第四执行批次:新增 `ImageCanvasGenerationComposerView`,把生成图片、生成规范、生成角色形象、生成图标素材、快速编辑、角色动画和修改图片弹窗从主视图抽出;生成提交、上传 input、引用选择、占位框拖拽、结果回写、历史和画布状态机仍保留在主视图。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 - 2026-06-17 生成面板拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空浏览器数据后未登录刷新弹出 `账号入口`;关闭登录后 `画布背景色` 打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 跟随对话框,`AI画布工具栏` 保持可见;使用临时开发账号密码登录后上传素材成功,点击素材可添加到画布,切换 `图层` 面板可看到对应图层。 -- 2026-06-17 前端拆分第五阶段:新增 `ImageCanvasLayerCommandModel`,把右键图层目标解析、复制 / 粘贴 / 创建副本、层级移动、分组 / 解组、显隐、锁定、翻转和删除的数据规则从主视图抽出;主视图只保留历史、选中态、菜单关闭、元数据清理和导出下载副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasLayerCommandModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景入口打开完整 `画布背景设置` 面板;登录后上传素材成功,点击素材可加入画布,图片右键打开 `图片功能面板`,创建副本、水平翻转、锁定和隐藏均生效,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第六阶段:新增 `ImageCanvasInteractionModel`,把适合视图、中心缩放、普通滚轮纵向滚动、Ctrl / Cmd 滚轮缩放、坐标换算、框选命中、平移、生成占位框拖拽、图层拖拽吸附、小地图投影、小地图点击定位和小地图拖拽视图移动的纯规则从主视图抽出;主视图保留事件、pointer capture、history、生成对象回写、选中态和状态更新。验证命令:`npm run test -- src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材、画布图层、小地图和 `AI画布工具栏` 保持可见,Ctrl 滚轮从 110% 缩放到 121%,普通滚轮不改变缩放,浏览器控制台无 passive wheel 错误。 +- 2026-06-17 前端拆分第五执行批次:新增 `ImageCanvasLayerCommandModel`,把右键图层目标解析、复制 / 粘贴 / 创建副本、层级移动、分组 / 解组、显隐、锁定、翻转和删除的数据规则从主视图抽出;主视图只保留历史、选中态、菜单关闭、元数据清理和导出下载副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasLayerCommandModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景入口打开完整 `画布背景设置` 面板;登录后上传素材成功,点击素材可加入画布,图片右键打开 `图片功能面板`,创建副本、水平翻转、锁定和隐藏均生效,`AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第六执行批次:新增 `ImageCanvasInteractionModel`,把适合视图、中心缩放、普通滚轮纵向滚动、Ctrl / Cmd 滚轮缩放、坐标换算、框选命中、平移、生成占位框拖拽、图层拖拽吸附、小地图投影、小地图点击定位和小地图拖拽视图移动的纯规则从主视图抽出;主视图保留事件、pointer capture、history、生成对象回写、选中态和状态更新。验证命令:`npm run test -- src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材、画布图层、小地图和 `AI画布工具栏` 保持可见,Ctrl 滚轮从 110% 缩放到 121%,普通滚轮不改变缩放,浏览器控制台无 passive wheel 错误。 - 2026-06-17 新增素材持久化修正:素材库图片、上传到画布、生成图、修改图和图标素材加入画布时会先用当前图层快照更新本地画布,再在资源创建完成后立刻保存带真实 `resourceId` 的 layout,避免资源创建异步返回时把空 `layers` 写回工程。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录弹出 `账号入口`,登录后上传素材、点击素材加入画布并刷新,画布图片和 `AI画布工具栏` 均保持可见。 -- 2026-06-17 前端拆分第七阶段:新增 `useCanvasHistory`,把画布历史快照、撤销、重做、历史栈长度限制和 `canUndo` / `canRedo` 派生状态从主视图抽出;主视图只在具体动作前捕获历史,并注入恢复快照后的菜单 / hover / 框选 / 拖拽清理。验证命令:`npm run test -- src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 -- 2026-06-17 前端拆分第八阶段:新增 `useImageCanvasProjectPersistence`,把项目加载、`projectId` 状态、未就绪资源队列、工程资源创建、资源创建后即时保存和 450ms 自动保存从主视图抽出;新增 hook 单测锁定新增图层资源创建后保存真实 `resourceId` 的 layout。验证命令:`npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 -- 2026-06-17 前端拆分第九阶段:新增 `useCanvasGenerationDialogs`,把画布生成对象的 active / inactive 注册表、归档、激活、按 id 更新 / 删除、按图层清理和生成中最新占位框查询从主视图抽出;主视图继续保留生成提交、结果落图、quick edit 和跨图层副作用。同步把 `画布背景设置` 调整为 Lovart 式紧凑色板弹层。验证命令:`npm run test -- src/components/image-editor/useCanvasGenerationDialogs.test.tsx src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录弹出 `账号入口`,关闭后点击 `画布背景色` 显示色域、色相条、圆形预设和 HEX 输入,点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 对话框,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第十阶段:新增 `useImageCanvasAssetLibrary`,把账号级素材库加载、文件夹新建 / 折叠 / 重命名 / 删除、素材重命名 / 删除、素材选择模式、框选、多选删除、素材拖到文件夹和素材库 401 登录弹窗从主视图抽出;主视图继续保留上传读取、上传进度、拖到画布坐标、画布图层创建和工程资源持久化。新增 hook 单测覆盖素材库归一化、401 登录、新建文件夹临时 id 替换、素材移动、删除回调和多选删除。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景面板点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后生成占位和 `生成图片` 对话框出现且 `AI画布工具栏` 保持可见;登录临时开发账号后上传图片成功进入 `项目素材`,点击素材加入画布,切换 `图层` 可看到对应图层,控制台无前端 error。 -- 2026-06-17 前端拆分第十一阶段:新增 `ImageCanvasFileModel` 和 `useImageCanvasUploadWorkflow`,把隐藏上传 input、上传目标分发、未登录续传、上传占位卡片、素材落库、拖到画布建层、生成参考图上传从主视图抽出;主视图保留画布 drop 外层判断和项目资源持久化注入。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,登录临时开发账号后 `画布背景设置` 面板点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后显示 `Image Generator` 占位框和 `生成图片` 对话框且 `AI画布工具栏` 保持可见;上传图片后素材数增加,点击素材加入画布,切换 `图层` 面板可看到 2 个图层,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第十二阶段:新增 `ImageCanvasGenerationLayerModel`,把普通生图、修改图片、快速编辑和图标素材批量生成结果落画布的图层 id、临时 resourceId、标题、位置、原始分辨率尺寸、zIndex、source metadata、源图关联和 `generationInputs` 纯规则从主视图抽出;主视图继续负责 API 提交、生成对象状态、资源持久化、选中态、侧栏和适合视图副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后 `画布背景设置` 面板保留色相 / 自定义颜色 / 预设 / HEX / 恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后显示 `Image Generator` 占位框和 `生成图片` 对话框且 `AI画布工具栏` 保持可见;真实上传图片后素材数从 2 增至 3,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第十三阶段:新增 `useImageCanvasAssetExportWorkflow`,把画布素材导出状态、单图右键导出、整包 ZIP 组包、图片去重、读取失败记录、metadata / manifest 和下载链接副作用从主视图抽出;主视图保留右键目标解析和状态提示渲染。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后下载按钮启用,点击后触发真实下载 `未命名画布-画布素材-20260617.zip` 并显示导出状态;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `生成图片` 对话框出现且 `AI画布工具栏` 保持可见,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第十四阶段:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。 -- 2026-06-17 前端拆分第十五阶段:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第七执行批次:新增 `useCanvasHistory`,把画布历史快照、撤销、重做、历史栈长度限制和 `canUndo` / `canRedo` 派生状态从主视图抽出;主视图只在具体动作前捕获历史,并注入恢复快照后的菜单 / hover / 框选 / 拖拽清理。验证命令:`npm run test -- src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 +- 2026-06-17 前端拆分第八执行批次:新增 `useImageCanvasProjectPersistence`,把项目加载、`projectId` 状态、未就绪资源队列、工程资源创建、资源创建后即时保存和 450ms 自动保存从主视图抽出;新增 hook 单测锁定新增图层资源创建后保存真实 `resourceId` 的 layout。验证命令:`npm run test -- src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 +- 2026-06-17 前端拆分第九执行批次:新增 `useCanvasGenerationDialogs`,把画布生成对象的 active / inactive 注册表、归档、激活、按 id 更新 / 删除、按图层清理和生成中最新占位框查询从主视图抽出;主视图继续保留生成提交、结果落图、quick edit 和跨图层副作用。同步把 `画布背景设置` 调整为 Lovart 式紧凑色板弹层。验证命令:`npm run test -- src/components/image-editor/useCanvasGenerationDialogs.test.tsx src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录弹出 `账号入口`,关闭后点击 `画布背景色` 显示色域、色相条、圆形预设和 HEX 输入,点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 对话框,`AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第十执行批次:新增 `useImageCanvasAssetLibrary`,把账号级素材库加载、文件夹新建 / 折叠 / 重命名 / 删除、素材重命名 / 删除、素材选择模式、框选、多选删除、素材拖到文件夹和素材库 401 登录弹窗从主视图抽出;主视图继续保留上传读取、上传进度、拖到画布坐标、画布图层创建和工程资源持久化。新增 hook 单测覆盖素材库归一化、401 登录、新建文件夹临时 id 替换、素材移动、删除回调和多选删除。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景面板点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后生成占位和 `生成图片` 对话框出现且 `AI画布工具栏` 保持可见;登录临时开发账号后上传图片成功进入 `项目素材`,点击素材加入画布,切换 `图层` 可看到对应图层,控制台无前端 error。 +- 2026-06-17 前端拆分第十一执行批次:新增 `ImageCanvasFileModel` 和 `useImageCanvasUploadWorkflow`,把隐藏上传 input、上传目标分发、未登录续传、上传占位卡片、素材落库、拖到画布建层、生成参考图上传从主视图抽出;主视图保留画布 drop 外层判断和项目资源持久化注入。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,登录临时开发账号后 `画布背景设置` 面板点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后显示 `Image Generator` 占位框和 `生成图片` 对话框且 `AI画布工具栏` 保持可见;上传图片后素材数增加,点击素材加入画布,切换 `图层` 面板可看到 2 个图层,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第十二执行批次:新增 `ImageCanvasGenerationLayerModel`,把普通生图、修改图片、快速编辑和图标素材批量生成结果落画布的图层 id、临时 resourceId、标题、位置、原始分辨率尺寸、zIndex、source metadata、源图关联和 `generationInputs` 纯规则从主视图抽出;主视图继续负责 API 提交、生成对象状态、资源持久化、选中态、侧栏和适合视图副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后 `画布背景设置` 面板保留色相 / 自定义颜色 / 预设 / HEX / 恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后显示 `Image Generator` 占位框和 `生成图片` 对话框且 `AI画布工具栏` 保持可见;真实上传图片后素材数从 2 增至 3,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第十三执行批次:新增 `useImageCanvasAssetExportWorkflow`,把画布素材导出状态、单图右键导出、整包 ZIP 组包、图片去重、读取失败记录、metadata / manifest 和下载链接副作用从主视图抽出;主视图保留右键目标解析和状态提示渲染。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后下载按钮启用,点击后触发真实下载 `未命名画布-画布素材-20260617.zip` 并显示导出状态;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `生成图片` 对话框出现且 `AI画布工具栏` 保持可见,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第十四执行批次:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。 +- 2026-06-17 前端拆分第十五执行批次:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。 - 2026-06-17 上传鉴权回归修正:普通素材上传入口在未登录时先打开 `账号入口`,不再先弹系统文件选择器;登录后用户再次点击上传即可打开文件选择器,避免浏览器拦截登录后异步触发的系统选择器。拖拽 / 已选中文件的续传逻辑仍保留,角色 / 图标生成参考图仍作为本地引用上传,不强制登录。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,`画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;未登录点击侧栏上传直接弹登录,不出现文件选择器;登录后再次点击上传可以打开文件选择器并上传成功,素材计数从 4 增至 5,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第十六阶段:新增 `useImageCanvasEditorChrome`,把项目标题 / 重命名、侧栏开关、当前工具、缩放菜单、背景设置、小地图和背景 HEX 状态从主视图抽出;主视图继续保留上传 / 生成 / 键盘 Escape 的跨工作流编排。新增 hook 单测覆盖重命名、鉴权登录、背景色输入、面板开关和工具状态;主视图从 2039 行降至 1966 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 -- 2026-06-17 前端拆分第十七阶段:新增 `useImageCanvasViewportControls`,把视口状态、画布尺寸、小地图投影、适合视图、中心缩放、滚轮语义、坐标换算和小地图移动从主视图抽出;主视图继续保留图层拖拽、框选、生成占位拖拽、上传 drop 和历史触发时机。验证命令:`npm run test -- src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏可见;普通滚轮不改变缩放,Ctrl 滚轮从 `100%` 到 `110%`;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第十八阶段:新增 `useImageCanvasStageInteractions`,把画布舞台 pointer 状态机、选择 / 框选、多选拖拽、生成占位拖拽、抓手 / Space 临时抓手 / 中键平移、小地图 click / drag 分流和吸附线状态从主视图抽出;主视图继续保留上传 drop、右键菜单、生成提交、项目持久化和工具栏动作分流。新增 hook 单测覆盖多选拖拽、框选、临时抓手、生成占位和小地图分流;主视图从 1802 行降至 1452 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新和未登录上传均弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏保持可见;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;普通滚轮不改变缩放,Ctrl 滚轮从 `146%` 到 `161%`;抓手 / 文字 / 选择工具可连续切换;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,关闭对话框后占位图保留,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第十九阶段:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第二十阶段:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。 -- 2026-06-17 前端拆分第二十一阶段:新增 `useImageCanvasKeyboardShortcuts`,把 Ctrl / Cmd + Z 撤销、Ctrl / Cmd + Shift + Z 重做、Shift 状态、Backspace / Delete 删除、Escape 关闭临时面板和 Space 临时抓手从主视图抽出;主视图继续注入图层删除、生成对话框、快速编辑和 chrome 面板 setter。新增 hook 单测覆盖输入框忽略快捷键、删除选中图层、删除生成占位、Escape 保留生成中面板、Space 和 Shift;主视图从 1337 行降至 1250 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录直接弹出 `账号入口` 且未抢跑 `/api/editor/*`,登录后 `/api/editor/assets/library` 和 `/api/editor/projects/recent` 为 200,`AI画布工具栏` 与 `画布面板入口` 可见,viewport 背景为 `rgb(248, 250, 252)` 且 `background-image: none`;按住 Space 从 `文字工具` 临时切到 `抓手工具`,松开恢复 `文字工具`;`画布背景设置` 点击 `暖灰` 后背景变为 `rgb(243, 240, 234)`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 同时可见,登录后控制台无前端 error。 -- 2026-06-17 前端拆分第二十二阶段:新增 `useImageCanvasAssetPointerDragBridge`,把素材卡片 pointer 拖拽到画布 / 文件夹的全局监听、拖拽激活、画布 drop 提示、文件夹高亮、移动素材、加入画布和点击抑制从主视图抽出;主视图继续负责素材库事实、画布建层、历史和工程资源持久化。同步恢复 `账号入口` 认证弹窗 portal 渲染,避免全屏画布遮住登录弹窗;`画布背景设置` 调整为独立白色浮层,包含当前色、色域、色相、自定义颜色、预设网格、HEX 输入和恢复默认。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 显示当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;上传图片进入 `项目素材`,真实 pointer 拖拽素材到画布后图层数从 0 到 1 且 `AI画布工具栏` 保持可见;新建 `SmokeFolder` 后真实 pointer 拖拽素材到文件夹,`项目素材` 变 0、`SmokeFolder` 变 1,素材执行移动而非拷贝。控制台仅有未登录 refresh 401,登录后编辑器 API 均为 200。 -- 2026-06-17 前端拆分第二十三阶段:新增 `ImageCanvasOverlayModel`,把生成输入框锚定、图标素材生成面板宽度、选中图片工具栏边界、快速编辑面板和角色动画面板定位从主视图抽出为纯模型;主视图继续保留生成 / quick edit / 角色动画状态机和舞台编排。新增模型单测覆盖锚定优先级、生成中隐藏、icon 宽度、工具栏 clamp、quick edit / 角色动画边界和生成 dialog 模式识别;主视图从 1182 行降至 1133 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开显示 `账号入口`,登录后 `画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;素材库已有素材真实 pointer 拖入画布后图层数从 0 到 1,工具栏保持可见,截图留存于 `output/playwright/editor-overlay-model-regression-20260617.png`。 -- 2026-06-17 前端拆分第二十四阶段:新增 `useImageCanvasAssetCanvasBridge`,把删除素材清理画布图层、素材加入画布、素材 pointer 拖入画布 / 文件夹、画布 drag/drop 分流和文件拖入画布参数组装从主视图抽成素材到画布桥接 hook;主视图继续保留素材库事实、上传读取、工程资源持久化和历史触发。新增 hook 单测覆盖素材建层、pointer drop 入画布和删除素材清理关联图层 / 选中态;主视图从 1133 行降至 1086 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 是完整设置面板,包含当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,HEX 输入 `#ffffff` 后变为白色,按 Escape 关闭面板;登录后上传图片请求 `/api/editor/assets` 返回 200,素材库出现上传素材,`AI画布工具栏` 保持可见。 -- 2026-06-17 前端拆分第二十五阶段:新增 `ImageCanvasStageControllerModel` 和 `useImageCanvasStageController`,把舞台派生状态、生成 / 选中浮层位置、右键菜单目标、空白画布右键、图层右键和清空画布焦点从主视图抽出;主视图继续保留工具切换、上传 / 生成入口、图层命令、项目持久化和舞台 pointer 状态机。新增模型和 hook 单测覆盖选中 / 生成锚定、右键菜单位置、显示 / 解锁判断、清空焦点和空白 / 图层右键菜单;主视图从 1086 行降至 993 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/routing/appPageRoutes.test.ts`、`npm run typecheck`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` dialog,包含色相、自定义颜色、预设、HEX 和恢复默认;点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-stage-controller-smoke-20260617.png`。 -- 2026-06-17 前端拆分第二十六阶段:新增 `ImageCanvasTopbarView`,把返回项目入口、项目标题展示 / 重命名表单、下载画布素材按钮和导出状态提示从主视图抽出;主视图继续保留 chrome hook、项目持久化、导出工作流和实际导出副作用。新增组件单测覆盖返回入口、标题编辑入口、重命名提交 / 取消、导出按钮禁用 / 启用和导出状态提示;主视图从 993 行降至 905 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasTopbarView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,顶栏返回项目入口、项目名、`画布` 标签和下载按钮均可见;关闭登录后打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-topbar-smoke-20260617.png`。 -- 2026-06-17 前端拆分第二十七阶段:新增 `useImageCanvasGenerationSurface`,把生成 Composer JSX、生成工具切换分流、普通生图 / 图标生成 / 快速编辑 / 角色动画浮层定位从主视图抽出;`useImageCanvasGenerationWorkflow` 继续负责生成状态机和真实 API 提交。同步移除 `ImageCanvasStageControllerModel` 中重复的生成锚点 / Composer 位置派生,避免舞台控制器和生成表面重复持有生成浮层职责;主视图从 905 行降至 793 行。rebase 到远端 `支持规范参考图输入` 后,生成表面继续透传角色形象规范和常规参考图入口,并把左下 dock / 底部工具栏层级提到 Composer 之上,避免生成输入框盖住常用工具和背景面板。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,关闭登录后 `画布背景设置` 保持完整面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,再点击 `生成角色形象` 能打开包含 `角色形象规范` 和 `常规参考图` 的对话框,控制台仅有未登录 refresh 401。 +- 2026-06-17 前端拆分第十六执行批次:新增 `useImageCanvasEditorChrome`,把项目标题 / 重命名、侧栏开关、当前工具、缩放菜单、背景设置、小地图和背景 HEX 状态从主视图抽出;主视图继续保留上传 / 生成 / 键盘 Escape 的跨工作流编排。新增 hook 单测覆盖重命名、鉴权登录、背景色输入、面板开关和工具状态;主视图从 2039 行降至 1966 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 +- 2026-06-17 前端拆分第十七执行批次:新增 `useImageCanvasViewportControls`,把视口状态、画布尺寸、小地图投影、适合视图、中心缩放、滚轮语义、坐标换算和小地图移动从主视图抽出;主视图继续保留图层拖拽、框选、生成占位拖拽、上传 drop 和历史触发时机。验证命令:`npm run test -- src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏可见;普通滚轮不改变缩放,Ctrl 滚轮从 `100%` 到 `110%`;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第十八执行批次:新增 `useImageCanvasStageInteractions`,把画布舞台 pointer 状态机、选择 / 框选、多选拖拽、生成占位拖拽、抓手 / Space 临时抓手 / 中键平移、小地图 click / drag 分流和吸附线状态从主视图抽出;主视图继续保留上传 drop、右键菜单、生成提交、项目持久化和工具栏动作分流。新增 hook 单测覆盖多选拖拽、框选、临时抓手、生成占位和小地图分流;主视图从 1802 行降至 1452 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新和未登录上传均弹出 `账号入口`,登录后素材 / 画布 / 小地图和底部工具栏保持可见;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;普通滚轮不改变缩放,Ctrl 滚轮从 `146%` 到 `161%`;抓手 / 文字 / 选择工具可连续切换;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,关闭对话框后占位图保留,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第十九执行批次:新增 `useImageCanvasCanvasDropWorkflow`,把画布区域 drag over / drag leave / drop 分流从主视图抽出,覆盖素材库图片拖入画布、本地文件拖入画布、无关拖拽不拦截、默认文件夹选择和画布遮罩清理;主视图继续注入素材建层、文件上传、drop 点换算和素材移动高亮清理。新增 hook 单测覆盖拖拽入画布细节,主视图从 1452 行降至 1405 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材 / 画布 / 小地图和底部工具栏可见,真实鼠标拖拽素材库图片到画布时出现 `添加到画布` 遮罩,松手后画布图层数量从 4 增至 5;`画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;抓手工具可切回选择工具,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第二十执行批次:新增 `ImageCanvasMetadataModalView`,把图片信息弹窗从主视图抽出,承载图片类型、生成输入、参考图、模型、分辨率、Provider、Task 和 Object 信息渲染;主视图只保留 `metadataLayer` 状态和关闭回调。同步修复未登录进入编辑器时项目 / 素材接口抢跑 401、`重置画布视图` 点击事件误传给适合视图函数的问题。新增组件单测覆盖生成图 metadata、上传图 fallback 和关闭回调,新增 hook / 主视图测试覆盖未登录不请求受保护素材 / 工程数据和重置按钮回归;主视图从 1405 行降至 1337 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后直接弹出 `账号入口`,且未登录状态下没有发起 `/api/editor/*` 请求;登录临时开发账号后 `重置画布视图` 无控制台错误,`画布背景设置` 保持 Lovart 式白色浮层,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`,上传素材可加入画布,右上角图片信息按钮可打开不透明白底元数据弹窗,关闭后 `AI画布工具栏` 仍可见。 +- 2026-06-17 前端拆分第二十一执行批次:新增 `useImageCanvasKeyboardShortcuts`,把 Ctrl / Cmd + Z 撤销、Ctrl / Cmd + Shift + Z 重做、Shift 状态、Backspace / Delete 删除、Escape 关闭临时面板和 Space 临时抓手从主视图抽出;主视图继续注入图层删除、生成对话框、快速编辑和 chrome 面板 setter。新增 hook 单测覆盖输入框忽略快捷键、删除选中图层、删除生成占位、Escape 保留生成中面板、Space 和 Shift;主视图从 1337 行降至 1250 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录直接弹出 `账号入口` 且未抢跑 `/api/editor/*`,登录后 `/api/editor/assets/library` 和 `/api/editor/projects/recent` 为 200,`AI画布工具栏` 与 `画布面板入口` 可见,viewport 背景为 `rgb(248, 250, 252)` 且 `background-image: none`;按住 Space 从 `文字工具` 临时切到 `抓手工具`,松开恢复 `文字工具`;`画布背景设置` 点击 `暖灰` 后背景变为 `rgb(243, 240, 234)`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 同时可见,登录后控制台无前端 error。 +- 2026-06-17 前端拆分第二十二执行批次:新增 `useImageCanvasAssetPointerDragBridge`,把素材卡片 pointer 拖拽到画布 / 文件夹的全局监听、拖拽激活、画布 drop 提示、文件夹高亮、移动素材、加入画布和点击抑制从主视图抽出;主视图继续负责素材库事实、画布建层、历史和工程资源持久化。同步恢复 `账号入口` 认证弹窗 portal 渲染,避免全屏画布遮住登录弹窗;`画布背景设置` 调整为独立白色浮层,包含当前色、色域、色相、自定义颜色、预设网格、HEX 输入和恢复默认。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 显示当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;上传图片进入 `项目素材`,真实 pointer 拖拽素材到画布后图层数从 0 到 1 且 `AI画布工具栏` 保持可见;新建 `SmokeFolder` 后真实 pointer 拖拽素材到文件夹,`项目素材` 变 0、`SmokeFolder` 变 1,素材执行移动而非拷贝。控制台仅有未登录 refresh 401,登录后编辑器 API 均为 200。 +- 2026-06-17 前端拆分第二十三执行批次:新增 `ImageCanvasOverlayModel`,把生成输入框锚定、图标素材生成面板宽度、选中图片工具栏边界、快速编辑面板和角色动画面板定位从主视图抽出为纯模型;主视图继续保留生成 / quick edit / 角色动画状态机和舞台编排。新增模型单测覆盖锚定优先级、生成中隐藏、icon 宽度、工具栏 clamp、quick edit / 角色动画边界和生成 dialog 模式识别;主视图从 1182 行降至 1133 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasKeyboardShortcuts.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/useImageCanvasViewportControls.test.tsx src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/useCanvasHistory.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录打开显示 `账号入口`,登录后 `画布背景设置` 点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;素材库已有素材真实 pointer 拖入画布后图层数从 0 到 1,工具栏保持可见,截图留存于 `output/playwright/editor-overlay-model-regression-20260617.png`。 +- 2026-06-17 前端拆分第二十四执行批次:新增 `useImageCanvasAssetCanvasBridge`,把删除素材清理画布图层、素材加入画布、素材 pointer 拖入画布 / 文件夹、画布 drag/drop 分流和文件拖入画布参数组装从主视图抽成素材到画布桥接 hook;主视图继续保留素材库事实、上传读取、工程资源持久化和历史触发。新增 hook 单测覆盖素材建层、pointer drop 入画布和删除素材清理关联图层 / 选中态;主视图从 1133 行降至 1086 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 是完整设置面板,包含当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,HEX 输入 `#ffffff` 后变为白色,按 Escape 关闭面板;登录后上传图片请求 `/api/editor/assets` 返回 200,素材库出现上传素材,`AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第二十五执行批次:新增 `ImageCanvasStageControllerModel` 和 `useImageCanvasStageController`,把舞台派生状态、生成 / 选中浮层位置、右键菜单目标、空白画布右键、图层右键和清空画布焦点从主视图抽出;主视图继续保留工具切换、上传 / 生成入口、图层命令、项目持久化和舞台 pointer 状态机。新增模型和 hook 单测覆盖选中 / 生成锚定、右键菜单位置、显示 / 解锁判断、清空焦点和空白 / 图层右键菜单;主视图从 1086 行降至 993 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/routing/appPageRoutes.test.ts`、`npm run typecheck`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` dialog,包含色相、自定义颜色、预设、HEX 和恢复默认;点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-stage-controller-smoke-20260617.png`。 +- 2026-06-17 前端拆分第二十六执行批次:新增 `ImageCanvasTopbarView`,把返回项目入口、项目标题展示 / 重命名表单、下载画布素材按钮和导出状态提示从主视图抽出;主视图继续保留 chrome hook、项目持久化、导出工作流和实际导出副作用。新增组件单测覆盖返回入口、标题编辑入口、重命名提交 / 取消、导出按钮禁用 / 启用和导出状态提示;主视图从 993 行降至 905 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasTopbarView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,顶栏返回项目入口、项目名、`画布` 标签和下载按钮均可见;关闭登录后打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-topbar-smoke-20260617.png`。 +- 2026-06-17 前端拆分第二十七执行批次:新增 `useImageCanvasGenerationSurface`,把生成 Composer JSX、生成工具切换分流、普通生图 / 图标生成 / 快速编辑 / 角色动画浮层定位从主视图抽出;`useImageCanvasGenerationWorkflow` 继续负责生成状态机和真实 API 提交。同步移除 `ImageCanvasStageControllerModel` 中重复的生成锚点 / Composer 位置派生,避免舞台控制器和生成表面重复持有生成浮层职责;主视图从 905 行降至 793 行。rebase 到远端 `支持规范参考图输入` 后,生成表面继续透传角色形象规范和常规参考图入口,并把左下 dock / 底部工具栏层级提到 Composer 之上,避免生成输入框盖住常用工具和背景面板。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,关闭登录后 `画布背景设置` 保持完整面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,再点击 `生成角色形象` 能打开包含 `角色形象规范` 和 `常规参考图` 的对话框,控制台仅有未登录 refresh 401。 - 2026-06-17 上传侧栏回归修正:上传工作流移除上传到画布后强制切换 `图层` 侧栏的副作用,保留新增素材卡、创建画布图层和选中新图层。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 登录后点击 `上传到项目素材` 上传图片,左侧仍显示 `素材` 且 `打开素材` 为 pressed;把图片文件 drop 到 `画布工作区` 后素材库和画布图层均出现 `canvas-drop-sidebar-smoke.png`,新图层被选中,`打开素材=true`、`打开图层=false`,`AI画布工具栏` 保持可见,登录后控制台无 error。 -- 2026-06-17 前端拆分第二十八阶段:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasGenerationImageOptionsView`、`ImageCanvasSpecGenerationPanelView`、`ImageCanvasIconSpritesheetComposerView`、`ImageCanvasQuickEditPanelView` 和 `ImageCanvasCharacterAnimationPanelView`,把图片生成参数、生成规范、图标素材生成、快速编辑图片和角色动画面板从 Composer 内联 JSX 抽出;Composer 降至 707 行,继续保留生成模式分流、角色引用菜单 portal 和修改图片弹窗编排。新增子视图单测覆盖参数切换、规范表单、图标规范菜单 / 描述列表、快速编辑和角色动画状态。验证命令:`npm run test -- src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录显示 `账号入口`,关闭后点击 `生成工具` 能看到 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏`;点击 `生成角色形象` 能看到 `生成角色形象` 面板和 `角色形象规范`;点击 `生成规范` 后选择 `角色形象规范` 能打开 `生成规范` 面板并显示 `提交生成规范`;点击 `生成图标素材` 能打开 `生成图标素材` 面板、图标规范操作和素材描述列表,控制台仅有未登录 refresh 401。 -- 2026-06-17 前端拆分第二十九阶段:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasBottomToolbarView`、`ImageCanvasPanelDockView`、`ImageCanvasContextMenusView` 和 `ImageCanvasSelectedLayerToolbarView`,把底部 AI 工具栏、左下缩放 / 背景 / 小地图控制坞、画布 / 图片右键菜单和选中图片浮动工具栏从 StageView 内联 JSX 抽出;StageView 降至 538 行,继续保留画布世界、图层和生成占位渲染,所有 pointer / 多选 / 拖拽 / 生成状态机仍在既有 hook 中。新增子视图单测覆盖工具切换、缩放 / 背景 / 小地图控制、右键菜单命令和选中图片工具栏接线。验证命令:`npm run test -- src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录弹出 `账号入口`,关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框、`画布面板入口` 和 `AI画布工具栏` 均可见;点击 `生成规范` 后页面级 `生成规范类型` 菜单可见。控制台仅有预期的未登录 `/api/auth/refresh` 401,截图留存于 `output/playwright/editor-stage-view-split-smoke-20260617.png`。 -- 2026-06-17 前端拆分第三十阶段:新增 `ImageCanvasEditorShellView`,把编辑器最外层 section、隐藏上传 input、素材拖拽预览、侧栏 / 顶栏 / 舞台 / 元数据弹窗组合从 `ImageCanvasEditorView` 抽成页面壳;主视图继续保留所有 hook 状态编排、上传 / 生成 / 拖拽 / 项目持久化和跨模块副作用,只把已经组装好的 `sidebarProps`、`topbarProps`、`stageProps` 和 `metadataProps` 交给 shell 渲染。新增 shell 单测覆盖上传 input、拖拽预览坐标兜底、顶栏 / 舞台 / 工具栏 / 元数据弹窗装配;主视图降至 777 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`,关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`返回项目页面`、`画布面板入口` 和 `AI画布工具栏` 均可见;点击 `生成工具` 后 `Image Generator` 和 `生成图片` 对话框可见;点击 `生成规范` 后页面级 `生成规范类型` 菜单可见。控制台仅有预期的未登录 `/api/auth/refresh` 401,截图留存于 `output/playwright/editor-shell-split-smoke-20260617.png`。 -- 2026-06-17 前端拆分第三十一阶段:继续收口 `ImageCanvasSidebarView`,新增 `ImageCanvasAssetLibraryPanelView` 和 `ImageCanvasLayerPanelView`,把素材库文件夹 / 拖拽上传 / 素材选择模式与图层列表 / 右键入口拆成两个完整 surface;侧栏外壳只保留标题、计数和当前 tab 分流。同步把 `ImageCanvasEditorView.test.tsx` 中纯侧栏输入框 chrome 断言迁到 `ImageCanvasSidebarView.test.tsx`,保留主视图重命名、建文件夹、上传、删除、API 调用和画布集成断言。验证命令:`npm run test -- src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "switches the shared sidebar between assets and layers"`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 -- 2026-06-17 前端拆分第三十二阶段:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasBasicGenerationComposerView`、`ImageCanvasCharacterGenerationComposerView` 和 `ImageCanvasEditGenerationModalView`,把普通生图跟随框、角色形象生成面板和修改图片弹窗从 Composer 内联 JSX 中抽出;Composer 降至 312 行,只保留生成模式分流、portal 菜单和各面板装配。新增三组子视图单测覆盖普通生图 prompt / 参考图 / 提交 / 关闭、角色参考图菜单 / 状态恢复 / 提交、修改图片弹窗提示词 / 失败 / 关闭。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,`AI画布工具栏` 仍可见;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。 -- 2026-06-17 前端拆分第三十三阶段:继续收口 `ImageCanvasAssetLibraryPanelView`,新增 `ImageCanvasAssetFolderSectionView` 和 `ImageCanvasAssetRowView`,把素材库文件夹头 / 文件夹 drop 区域、素材卡片 / 上传进度 / 重命名 / 选择模式从素材库父面板中拆成两个完整 surface;素材库父面板降至 279 行,只保留素材列表容器、新建文件夹表单、批量操作栏和框选遮罩。新增素材行单测覆盖普通点击加入画布、选择模式改为选中、重命名 Enter 提交和上传中禁用 / 进度显示。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;默认素材栏显示 `项目素材` 文件夹、上传入口和底部 `AI画布工具栏`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。 -- 2026-06-17 前端拆分第三十四阶段:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasWorldView`,把画布世界表面、吸附参考线、可见图层排序 / 悬浮 / 选中 / 锁定 / 生成态、元数据角标、框选矩形、生成占位框和浮动生成状态从 StageView 内联 JSX 中抽出;StageView 降至 324 行,继续保留 viewport 宿主、drop overlay、左下 dock、底部工具栏、右键菜单和选中图片工具栏装配。新增 world view 单测覆盖隐藏图层过滤、悬浮尺寸、生成态、元数据按钮、吸附线、框选矩形和生成占位框事件。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第三十五阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationSubmissionModel`,把普通生图、修改图片、规范生成和角色形象生成的请求 payload、标准化 prompt、结果图层标题 / assetKind 和 generationInputs 快照构建从 workflow hook 中抽成纯模型;workflow hook 保留对话状态、真实 API 调用、图片引用解析、结果落图、选中和 fit 副作用,避免拆散生成生命周期。新增模型单测覆盖普通生图、修改图、带参考图的规范生成、带规范 / 常规参考图的角色生成和缺失源图异常;`useImageCanvasGenerationWorkflow` 从 1167 行降至 1104 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第三十六阶段:继续收口 `useImageCanvasUploadWorkflow`,新增 `ImageCanvasUploadModel`,把上传目标文件夹解析、上传中素材占位卡、上传到画布的临时图层、无效 drop 坐标兜底和图片真实尺寸回填坐标计算从 hook 中抽成纯模型;upload workflow hook 保留登录恢复、文件读取、真实素材创建 API、上传进度状态和生成面板参考图写入副作用。新增模型单测覆盖文件夹兜底、占位素材、画布落点、非法坐标兜底和真实尺寸修正;`useImageCanvasUploadWorkflow` 从 546 行降至 510 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框、`上传到项目素材` 入口和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第三十七阶段:继续收口 `useImageCanvasAssetLibrary`,新增 `ImageCanvasAssetLibraryModel`,把素材分组、可选择素材筛选、全选状态、素材 / 文件夹重命名、文件夹折叠、本地新建文件夹占位、持久化文件夹替换、本地删除素材、删除文件夹回默认文件夹、选择集合切换、批量删除和本地移动素材到文件夹从 hook 中抽成纯模型;asset library hook 继续保留加载账号素材库、后端 CRUD 调用、登录弹窗、DOM 框选和素材拖拽命中生命周期。新增模型单测覆盖分组 / 选择、重命名 / 折叠 / 本地文件夹、本地文件夹持久化替换、删除文件夹回默认文件夹、全选 / 批量删除和本地移动;`useImageCanvasAssetLibrary` 从 609 行降至 573 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;切回 `打开素材` 后侧栏显示 `素材` 且 `上传到项目素材` 入口可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第三十八阶段:继续收口 `useImageCanvasGenerationWorkflow`,扩展 `ImageCanvasGenerationSubmissionModel`,把图标素材批量生成的规范校验 / 描述清洗 / 请求 payload / generationInputs,以及角色动画生成的 prompt 清洗 / objectKey 优先源图 / 尺寸 / 价格 / 模型参数从 workflow hook 中抽成纯模型;workflow hook 继续保留对话状态、真实 API 调用、生成结果落图、失败恢复和角色动画面板生命周期。新增模型单测覆盖图标缺少规范、图标空描述、图标描述 trim / 参考快照,以及角色动画 trim、objectKey 源图和价格计算;`useImageCanvasGenerationWorkflow` 从 1104 行降至 1075 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;点击 `生成图标素材` 后 `Icon Generator` 占位和 `生成图标素材` 面板可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第三十九阶段:开始拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorView.test-utils` 共享默认工程 / 素材库 mock、认证上下文、pointer / dataTransfer / deferred helper 和生命周期 setup;新增 `ImageCanvasEditorAssetsIntegration.test.tsx`,把素材库默认文件夹去重、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 拖拽移动、多文件上传、未登录续传、上传占位、401 登录、素材选择模式、删除素材同步清理画布图层、素材框选、画布 drop 上传和素材库拖入画布等集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 4817 行降至 3741 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第四十阶段:继续拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorGenerationIntegration.test.tsx`,把画布生图占位 / 生成提交、生成错误与未登录、规范生成、角色形象生成、图标素材生成、角色动画、快速编辑、生成图元数据和修改结果等生成相关集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 3741 行降至 1419 行,主测试保留工程加载、导出、画布基础交互、右键菜单、侧栏、缩放、小地图、工具切换、吸附和历史。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第四十一阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationDialogModel`,把普通生图 / 规范 / 角色形象 / 图标素材生成对话框草稿、修改图片 / 快速编辑 / 角色动画面板草稿、角色 / 图标参考选择、规范表单更新、图标描述更新、角色动画时长更新以及生成器失焦 / 关闭规则从 hook 中抽成纯模型;workflow hook 保留真实 API 调用、生成结果落画布、侧栏 / 工具 / 选中态副作用和错误回写。新增模型单测覆盖各类草稿、失败态清理、引用选择、描述限制、动画时长和 composer 可见性;`useImageCanvasGenerationWorkflow.ts` 从 1075 行降至 870 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 -- 2026-06-17 前端拆分第四十二阶段:继续收口 `useImageCanvasStageInteractions`,新增 `ImageCanvasStageInteractionModel`,把 pointer button / client / id 归一化、画布框选初始状态、抓手平移拖拽状态、多选图层选择与图层拖拽初始状态、生成器 composer 随图层点击显隐、生成占位拖拽状态、小地图拖拽状态和拖拽阈值从 hook 中抽成纯模型;stage hook 保留 DOM 事件拦截、pointer capture / release、React 状态写入、拖拽移动执行和回调副作用。新增模型单测覆盖 pointer 兜底、中键识别、框选 / 平移状态、多选 toggle、组拖拽初始层快照、生成器 composer 规则、生成占位状态和小地图 click / drag 分流;`useImageCanvasStageInteractions.ts` 从 610 行降至 521 行。只读子代理复核结论:当前没有同一轮必须顺手拆的第二个明显大块,`ImageCanvasEditorView.tsx` 已主要是组合层,generation / asset / upload hooks 剩余复杂度多为异步编排或持久化副作用,后续应随具体需求再拆,避免过度碎片化。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasStageInteractions.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第二十八执行批次:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasGenerationImageOptionsView`、`ImageCanvasSpecGenerationPanelView`、`ImageCanvasIconSpritesheetComposerView`、`ImageCanvasQuickEditPanelView` 和 `ImageCanvasCharacterAnimationPanelView`,把图片生成参数、生成规范、图标素材生成、快速编辑图片和角色动画面板从 Composer 内联 JSX 抽出;Composer 降至 707 行,继续保留生成模式分流、角色引用菜单 portal 和修改图片弹窗编排。新增子视图单测覆盖参数切换、规范表单、图标规范菜单 / 描述列表、快速编辑和角色动画状态。验证命令:`npm run test -- src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录显示 `账号入口`,关闭后点击 `生成工具` 能看到 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏`;点击 `生成角色形象` 能看到 `生成角色形象` 面板和 `角色形象规范`;点击 `生成规范` 后选择 `角色形象规范` 能打开 `生成规范` 面板并显示 `提交生成规范`;点击 `生成图标素材` 能打开 `生成图标素材` 面板、图标规范操作和素材描述列表,控制台仅有未登录 refresh 401。 +- 2026-06-17 前端拆分第二十九执行批次:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasBottomToolbarView`、`ImageCanvasPanelDockView`、`ImageCanvasContextMenusView` 和 `ImageCanvasSelectedLayerToolbarView`,把底部 AI 工具栏、左下缩放 / 背景 / 小地图控制坞、画布 / 图片右键菜单和选中图片浮动工具栏从 StageView 内联 JSX 抽出;StageView 降至 538 行,继续保留画布世界、图层和生成占位渲染,所有 pointer / 多选 / 拖拽 / 生成状态机仍在既有 hook 中。新增子视图单测覆盖工具切换、缩放 / 背景 / 小地图控制、右键菜单命令和选中图片工具栏接线。验证命令:`npm run test -- src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录弹出 `账号入口`,关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框、`画布面板入口` 和 `AI画布工具栏` 均可见;点击 `生成规范` 后页面级 `生成规范类型` 菜单可见。控制台仅有预期的未登录 `/api/auth/refresh` 401,截图留存于 `output/playwright/editor-stage-view-split-smoke-20260617.png`。 +- 2026-06-17 前端拆分第三十执行批次:新增 `ImageCanvasEditorShellView`,把编辑器最外层 section、隐藏上传 input、素材拖拽预览、侧栏 / 顶栏 / 舞台 / 元数据弹窗组合从 `ImageCanvasEditorView` 抽成页面壳;主视图继续保留所有 hook 状态编排、上传 / 生成 / 拖拽 / 项目持久化和跨模块副作用,只把已经组装好的 `sidebarProps`、`topbarProps`、`stageProps` 和 `metadataProps` 交给 shell 渲染。新增 shell 单测覆盖上传 input、拖拽预览坐标兜底、顶栏 / 舞台 / 工具栏 / 元数据弹窗装配;主视图降至 777 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`,关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`返回项目页面`、`画布面板入口` 和 `AI画布工具栏` 均可见;点击 `生成工具` 后 `Image Generator` 和 `生成图片` 对话框可见;点击 `生成规范` 后页面级 `生成规范类型` 菜单可见。控制台仅有预期的未登录 `/api/auth/refresh` 401,截图留存于 `output/playwright/editor-shell-split-smoke-20260617.png`。 +- 2026-06-17 前端拆分第三十一执行批次:继续收口 `ImageCanvasSidebarView`,新增 `ImageCanvasAssetLibraryPanelView` 和 `ImageCanvasLayerPanelView`,把素材库文件夹 / 拖拽上传 / 素材选择模式与图层列表 / 右键入口拆成两个完整 surface;侧栏外壳只保留标题、计数和当前 tab 分流。同步把 `ImageCanvasEditorView.test.tsx` 中纯侧栏输入框 chrome 断言迁到 `ImageCanvasSidebarView.test.tsx`,保留主视图重命名、建文件夹、上传、删除、API 调用和画布集成断言。验证命令:`npm run test -- src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx -t "switches the shared sidebar between assets and layers"`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +- 2026-06-17 前端拆分第三十二执行批次:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasBasicGenerationComposerView`、`ImageCanvasCharacterGenerationComposerView` 和 `ImageCanvasEditGenerationModalView`,把普通生图跟随框、角色形象生成面板和修改图片弹窗从 Composer 内联 JSX 中抽出;Composer 降至 312 行,只保留生成模式分流、portal 菜单和各面板装配。新增三组子视图单测覆盖普通生图 prompt / 参考图 / 提交 / 关闭、角色参考图菜单 / 状态恢复 / 提交、修改图片弹窗提示词 / 失败 / 关闭。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,`AI画布工具栏` 仍可见;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。 +- 2026-06-17 前端拆分第三十三执行批次:继续收口 `ImageCanvasAssetLibraryPanelView`,新增 `ImageCanvasAssetFolderSectionView` 和 `ImageCanvasAssetRowView`,把素材库文件夹头 / 文件夹 drop 区域、素材卡片 / 上传进度 / 重命名 / 选择模式从素材库父面板中拆成两个完整 surface;素材库父面板降至 279 行,只保留素材列表容器、新建文件夹表单、批量操作栏和框选遮罩。新增素材行单测覆盖普通点击加入画布、选择模式改为选中、重命名 Enter 提交和上传中禁用 / 进度显示。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;默认素材栏显示 `项目素材` 文件夹、上传入口和底部 `AI画布工具栏`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。 +- 2026-06-17 前端拆分第三十四执行批次:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasWorldView`,把画布世界表面、吸附参考线、可见图层排序 / 悬浮 / 选中 / 锁定 / 生成态、元数据角标、框选矩形、生成占位框和浮动生成状态从 StageView 内联 JSX 中抽出;StageView 降至 324 行,继续保留 viewport 宿主、drop overlay、左下 dock、底部工具栏、右键菜单和选中图片工具栏装配。新增 world view 单测覆盖隐藏图层过滤、悬浮尺寸、生成态、元数据按钮、吸附线、框选矩形和生成占位框事件。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十五执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationSubmissionModel`,把普通生图、修改图片、规范生成和角色形象生成的请求 payload、标准化 prompt、结果图层标题 / assetKind 和 generationInputs 快照构建从 workflow hook 中抽成纯模型;workflow hook 保留对话状态、真实 API 调用、图片引用解析、结果落图、选中和 fit 副作用,避免拆散生成生命周期。新增模型单测覆盖普通生图、修改图、带参考图的规范生成、带规范 / 常规参考图的角色生成和缺失源图异常;`useImageCanvasGenerationWorkflow` 从 1167 行降至 1104 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十六执行批次:继续收口 `useImageCanvasUploadWorkflow`,新增 `ImageCanvasUploadModel`,把上传目标文件夹解析、上传中素材占位卡、上传到画布的临时图层、无效 drop 坐标兜底和图片真实尺寸回填坐标计算从 hook 中抽成纯模型;upload workflow hook 保留登录恢复、文件读取、真实素材创建 API、上传进度状态和生成面板参考图写入副作用。新增模型单测覆盖文件夹兜底、占位素材、画布落点、非法坐标兜底和真实尺寸修正;`useImageCanvasUploadWorkflow` 从 546 行降至 510 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框、`上传到项目素材` 入口和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十七执行批次:继续收口 `useImageCanvasAssetLibrary`,新增 `ImageCanvasAssetLibraryModel`,把素材分组、可选择素材筛选、全选状态、素材 / 文件夹重命名、文件夹折叠、本地新建文件夹占位、持久化文件夹替换、本地删除素材、删除文件夹回默认文件夹、选择集合切换、批量删除和本地移动素材到文件夹从 hook 中抽成纯模型;asset library hook 继续保留加载账号素材库、后端 CRUD 调用、登录弹窗、DOM 框选和素材拖拽命中生命周期。新增模型单测覆盖分组 / 选择、重命名 / 折叠 / 本地文件夹、本地文件夹持久化替换、删除文件夹回默认文件夹、全选 / 批量删除和本地移动;`useImageCanvasAssetLibrary` 从 609 行降至 573 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;切回 `打开素材` 后侧栏显示 `素材` 且 `上传到项目素材` 入口可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十八执行批次:继续收口 `useImageCanvasGenerationWorkflow`,扩展 `ImageCanvasGenerationSubmissionModel`,把图标素材批量生成的规范校验 / 描述清洗 / 请求 payload / generationInputs,以及角色动画生成的 prompt 清洗 / objectKey 优先源图 / 尺寸 / 价格 / 模型参数从 workflow hook 中抽成纯模型;workflow hook 继续保留对话状态、真实 API 调用、生成结果落图、失败恢复和角色动画面板生命周期。新增模型单测覆盖图标缺少规范、图标空描述、图标描述 trim / 参考快照,以及角色动画 trim、objectKey 源图和价格计算;`useImageCanvasGenerationWorkflow` 从 1104 行降至 1075 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;点击 `生成图标素材` 后 `Icon Generator` 占位和 `生成图标素材` 面板可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十九执行批次:开始拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorView.test-utils` 共享默认工程 / 素材库 mock、认证上下文、pointer / dataTransfer / deferred helper 和生命周期 setup;新增 `ImageCanvasEditorAssetsIntegration.test.tsx`,把素材库默认文件夹去重、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 拖拽移动、多文件上传、未登录续传、上传占位、401 登录、素材选择模式、删除素材同步清理画布图层、素材框选、画布 drop 上传和素材库拖入画布等集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 4817 行降至 3741 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第四十执行批次:继续拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorGenerationIntegration.test.tsx`,把画布生图占位 / 生成提交、生成错误与未登录、规范生成、角色形象生成、图标素材生成、角色动画、快速编辑、生成图元数据和修改结果等生成相关集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 3741 行降至 1419 行,主测试保留工程加载、导出、画布基础交互、右键菜单、侧栏、缩放、小地图、工具切换、吸附和历史。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第四十一执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationDialogModel`,把普通生图 / 规范 / 角色形象 / 图标素材生成对话框草稿、修改图片 / 快速编辑 / 角色动画面板草稿、角色 / 图标参考选择、规范表单更新、图标描述更新、角色动画时长更新以及生成器失焦 / 关闭规则从 hook 中抽成纯模型;workflow hook 保留真实 API 调用、生成结果落画布、侧栏 / 工具 / 选中态副作用和错误回写。新增模型单测覆盖各类草稿、失败态清理、引用选择、描述限制、动画时长和 composer 可见性;`useImageCanvasGenerationWorkflow.ts` 从 1075 行降至 870 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第四十二执行批次:继续收口 `useImageCanvasStageInteractions`,新增 `ImageCanvasStageInteractionModel`,把 pointer button / client / id 归一化、画布框选初始状态、抓手平移拖拽状态、多选图层选择与图层拖拽初始状态、生成器 composer 随图层点击显隐、生成占位拖拽状态、小地图拖拽状态和拖拽阈值从 hook 中抽成纯模型;stage hook 保留 DOM 事件拦截、pointer capture / release、React 状态写入、拖拽移动执行和回调副作用。新增模型单测覆盖 pointer 兜底、中键识别、框选 / 平移状态、多选 toggle、组拖拽初始层快照、生成器 composer 规则、生成占位状态和小地图 click / drag 分流;`useImageCanvasStageInteractions.ts` 从 610 行降至 521 行。只读子代理复核结论:当前没有同一轮必须顺手拆的第二个明显大块,`ImageCanvasEditorView.tsx` 已主要是组合层,generation / asset / upload hooks 剩余复杂度多为异步编排或持久化副作用,后续应随具体需求再拆,避免过度碎片化。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasStageInteractions.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第四十三执行批次:继续收口 `useImageCanvasGenerationWorkflow`,新增 `useImageCanvasGenerationSubmissionWorkflow`,把普通生图 / 修改图 / 图标素材批量生成 / 快速编辑 / 角色动画的提交 API、提交中 / 失败 / 完成状态回写、生成结果落画布、选中图层、切换图层侧栏、适合视图和 last image model 记忆从入口 workflow 中抽成生成提交流水线 hook;原 workflow 保留生成入口、菜单开关、画布选取引用、表单字段更新、生成器关闭和删除图层后的状态清理,避免把 UI 选择态和提交态混在一起。新增 hook 单测覆盖 quick edit 成功 / 失败、edit 成功清理 modal、生成使用最新移动占位、icon 缺规范校验、icon 成功批量落图并记住模型、角色动画 completed / failed 状态机;同步修复新测试文件的 mock 隔离,避免 `vi.clearAllMocks()` 清空并行集成测试的调用记录;`useImageCanvasGenerationWorkflow.ts` 从 870 行降至 499 行。统一验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasStageInteractionModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/useImageCanvasStageInteractions.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx new file mode 100644 index 00000000..1269eda8 --- /dev/null +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -0,0 +1,578 @@ +/* @vitest-environment jsdom */ + +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useRef, useState } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { + CanvasGenerationDialogState, + CanvasLayer, + CanvasTool, + CharacterAnimationPanelState, + GenerateDialogState, + QuickEditPanelState, + SidebarPanel, +} from './ImageCanvasEditorTypes'; +import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs'; +import { createQuickEditPanelDraft } from './ImageCanvasGenerationDialogModel'; +import { useImageCanvasGenerationSubmissionWorkflow } from './useImageCanvasGenerationSubmissionWorkflow'; + +const resolveEditorImageReferenceDataUrlMock = vi.hoisted(() => vi.fn()); +const editEditorImageMock = vi.hoisted(() => vi.fn()); +const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn()); +const generateEditorIconSpritesheetMock = vi.hoisted(() => vi.fn()); +const generateEditorImageMock = vi.hoisted(() => vi.fn()); + +vi.mock('../../services/image-editor/editorImageReference', () => ({ + resolveEditorImageReferenceDataUrl: resolveEditorImageReferenceDataUrlMock, +})); + +vi.mock('../../services/image-editor/editorProjectClient', async () => { + const actual = await vi.importActual< + typeof import('../../services/image-editor/editorProjectClient') + >('../../services/image-editor/editorProjectClient'); + return { + ...actual, + editEditorImage: editEditorImageMock, + generateEditorCharacterAnimation: generateEditorCharacterAnimationMock, + generateEditorIconSpritesheet: generateEditorIconSpritesheetMock, + generateEditorImage: generateEditorImageMock, + }; +}); + +function createLayer(overrides: Partial = {}): CanvasLayer { + return { + id: 'layer-source', + resourceId: 'resource-source', + title: '源图', + src: 'https://assets.example.test/source.png', + x: 120, + y: 140, + width: 320, + height: 240, + originalWidth: 1024, + originalHeight: 768, + zIndex: 2, + sourceType: 'uploaded', + ...overrides, + }; +} + +function createGenerated(overrides = {}) { + return { + imageSrc: 'data:image/png;base64,generated', + width: 1024, + height: 1024, + sourceType: 'generated' as const, + prompt: '生成提示词', + actualPrompt: '生成提示词', + model: 'gpt-image-2', + provider: 'VectorEngine', + taskId: 'task-generated', + ...overrides, + }; +} + +function createIconResult(name: string, imageSrc: string) { + return { + name, + imageSrc, + width: 128, + height: 128, + }; +} + +function SubmissionWorkflowHarness({ + initialDialog = null, + initialQuickEditPanel = null, + initialCharacterAnimationPanel = null, + initialLayers = [createLayer()], +}: { + initialDialog?: GenerateDialogState | null; + initialQuickEditPanel?: QuickEditPanelState | null; + initialCharacterAnimationPanel?: CharacterAnimationPanelState | null; + initialLayers?: CanvasLayer[]; +}) { + const [layers, setLayers] = useState(initialLayers); + const [activeTool, setActiveTool] = useState('generate'); + const [activeSidebarPanel, setActiveSidebarPanel] = + useState('assets'); + const [selectedLayerId, setSelectedLayerId] = useState(null); + const [quickEditPanel, setQuickEditPanel] = + useState(initialQuickEditPanel); + const [characterAnimationPanel, setCharacterAnimationPanel] = + useState( + initialCharacterAnimationPanel, + ); + const rememberedImageModelRef = useRef(null); + const fitLayersMockRef = useRef(vi.fn()); + const layerCounterRef = useRef(0); + const dialogs = useCanvasGenerationDialogs(); + + const sourceLayer = layers[0] ?? null; + const quickEditSourceLayer = quickEditPanel + ? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ?? + null) + : null; + const characterAnimationSourceLayer = characterAnimationPanel + ? (layers.find( + (layer) => layer.id === characterAnimationPanel.sourceLayerId, + ) ?? null) + : null; + + const workflow = useImageCanvasGenerationSubmissionWorkflow({ + layers, + canvasSize: { width: 900, height: 640 }, + viewport: { x: 10, y: 20, scale: 2 }, + layerCounterRef, + quickEditPanel, + quickEditSourceLayer, + setQuickEditPanel, + characterAnimationPanel, + characterAnimationSourceLayer, + setCharacterAnimationPanel, + setGenerateDialog: dialogs.setGenerateDialog, + updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById, + removeCanvasGenerationDialogById: dialogs.removeCanvasGenerationDialogById, + getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder, + appendCanvasLayersWithResources: (nextLayers) => + setLayers((currentLayers) => [...currentLayers, ...nextLayers]), + selectSingleLayer: setSelectedLayerId, + fitLayers: fitLayersMockRef.current, + setActiveTool, + setActiveSidebarPanel, + rememberImageModel: (imageModel) => { + rememberedImageModelRef.current = imageModel; + }, + }); + + const activeDialog = dialogs.generateDialog; + const activeCanvasDialog = + activeDialog && 'id' in activeDialog + ? (activeDialog as CanvasGenerationDialogState) + : null; + + return ( +
+ {activeTool} + {activeSidebarPanel ?? '-'} + {selectedLayerId ?? '-'} + + {layers + .map( + (layer) => + `${layer.id}:${layer.title}:${layer.sourceResourceId ?? '-'}:${layer.assetKind ?? '-'}`, + ) + .join('|')} + + + {activeDialog + ? `${activeDialog.mode}:${activeDialog.status}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.generatedLayerId ?? '-'}:${activeDialog.placeholder ? 'placeholder' : '-'}:${activeDialog.errorMessage ?? '-'}` + : '-'} + + + {quickEditPanel + ? `${quickEditPanel.sourceLayerId}:${quickEditPanel.status}:${quickEditPanel.prompt || '-'}:${quickEditPanel.errorMessage ?? '-'}` + : '-'} + + + {characterAnimationPanel + ? `${characterAnimationPanel.sourceLayerId}:${characterAnimationPanel.status}:${characterAnimationPanel.promptText || '-'}:${characterAnimationPanel.errorMessage ?? '-'}:${characterAnimationPanel.result ? 'result' : '-'}` + : '-'} + + + {fitLayersMockRef.current.mock.calls.length} + + + {rememberedImageModelRef.current ?? '-'} + + + + + + + + + +
+ ); +} + +describe('useImageCanvasGenerationSubmissionWorkflow', () => { + beforeEach(() => { + resolveEditorImageReferenceDataUrlMock.mockReset(); + editEditorImageMock.mockReset(); + generateEditorCharacterAnimationMock.mockReset(); + generateEditorIconSpritesheetMock.mockReset(); + generateEditorImageMock.mockReset(); + resolveEditorImageReferenceDataUrlMock.mockImplementation( + async (src: string) => `data:image/png;base64,${src.split('/').pop()}`, + ); + }); + + it('submits quick edits, clears the panel, and fits the source plus result', async () => { + generateEditorImageMock.mockResolvedValueOnce( + createGenerated({ prompt: '快速修图' }), + ); + render(); + + fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' })); + fireEvent.click(screen.getByRole('button', { name: '填写快速编辑' })); + fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' })); + + expect(screen.getByTestId('quick-edit').textContent).toBe( + 'layer-source:generating:快速修图:-', + ); + await waitFor(() => { + expect(generateEditorImageMock).toHaveBeenCalledWith({ + prompt: '快速修图', + size: '1024x768', + kind: 'quick-edit', + model: 'gemini-3.1-flash-image-preview', + referenceImageSrcs: ['data:image/png;base64,source.png'], + }); + }); + await waitFor(() => { + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-quick-edit-1:源图 快速编辑:resource-source:-', + ); + }); + expect(screen.getByTestId('quick-edit').textContent).toBe('-'); + expect(screen.getByTestId('tool').textContent).toBe('select'); + expect(screen.getByTestId('sidebar').textContent).toBe('layers'); + expect(screen.getByTestId('selected').textContent).toBe( + 'layer-quick-edit-1', + ); + expect(screen.getByTestId('fit-count').textContent).toBe('1'); + }); + + it('keeps failed quick edit panels visible with the normalized prompt', async () => { + generateEditorImageMock.mockRejectedValueOnce(new Error('修图失败')); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' })); + + await waitFor(() => { + expect(screen.getByTestId('quick-edit').textContent).toBe( + 'layer-source:failed:快速编辑图片:修图失败', + ); + }); + }); + + it('submits edit dialogs beside the source and clears the modal state', async () => { + editEditorImageMock.mockResolvedValueOnce( + createGenerated({ prompt: '修改当前图片' }), + ); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + + expect(screen.getByTestId('dialog').textContent).toBe( + 'edit:generating:open:-:-:-', + ); + await waitFor(() => { + expect(editEditorImageMock).toHaveBeenCalledWith({ + prompt: '修改当前图片', + sourceImageSrc: 'data:image/png;base64,source.png', + }); + }); + await waitFor(() => { + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-edit-1:源图 修改结果:resource-source:-', + ); + }); + expect(screen.getByTestId('dialog').textContent).toBe('-'); + expect(screen.getByTestId('tool').textContent).toBe('select'); + expect(screen.getByTestId('fit-count').textContent).toBe('1'); + }); + + it('uses the latest moved canvas placeholder when a generation completes', async () => { + generateEditorImageMock.mockResolvedValueOnce( + createGenerated({ width: 512, height: 512 }), + ); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '移动占位' })); + fireEvent.click(screen.getByRole('button', { name: '提交当前生成' })); + + await waitFor(() => { + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-generated-1:生成图片 1:-:-', + ); + }); + expect(screen.getByTestId('dialog').textContent).toBe( + 'generate:idle:open:layer-generated-1:-:-', + ); + }); + + it('validates icon submission before calling the API', async () => { + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交图标' })); + + expect(generateEditorIconSpritesheetMock).not.toHaveBeenCalled(); + await waitFor(() => { + expect(screen.getByTestId('dialog').textContent).toContain( + '请选择图标素材规范', + ); + }); + }); + + it('submits icon spritesheets, trims descriptions, and remembers the model', async () => { + generateEditorIconSpritesheetMock.mockResolvedValueOnce({ + prompt: '图标素材', + actualPrompt: '图标素材', + model: 'gpt-image-2', + provider: 'VectorEngine', + taskId: 'task-icons', + iconImageSrcs: [ + createIconResult('返回按钮', 'data:image/png;base64,back'), + createIconResult('设置按钮', 'data:image/png;base64,setting'), + ], + }); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '设置初始对话' })); + fireEvent.click(screen.getByRole('button', { name: '提交图标' })); + + await waitFor(() => { + expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith( + expect.objectContaining({ + referenceImageSrc: 'data:image/png;base64,spec', + iconDescriptions: ['返回按钮', '设置按钮'], + model: 'gpt-image-2', + }), + ); + }); + await waitFor(() => { + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-icon-1:返回按钮:-:icon', + ); + }); + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-icon-2:设置按钮:-:icon', + ); + expect(screen.getByTestId('selected').textContent).toBe('layer-icon-1'); + expect(screen.getByTestId('dialog').textContent).toBe('-'); + expect(screen.getByTestId('remembered-model').textContent).toBe( + 'gpt-image-2', + ); + }); + + it('moves character animation panels from generating to completed', async () => { + generateEditorCharacterAnimationMock.mockResolvedValueOnce({ + frames: [{ imageSrc: 'data:image/png;base64,frame' }], + previewSrc: 'data:image/png;base64,preview', + taskId: 'animation-task', + }); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '提交角色动画' })); + + expect(screen.getByTestId('character-animation').textContent).toBe( + 'character-layer:generating:跑步:-:-', + ); + await waitFor(() => { + expect(generateEditorCharacterAnimationMock).toHaveBeenCalledWith( + expect.objectContaining({ + sourceLayerId: 'character-layer', + sourceImageSrc: 'generated/character.png', + promptText: '跑步', + model: 'seedance2.0', + }), + ); + }); + await waitFor(() => { + expect(screen.getByTestId('character-animation').textContent).toBe( + 'character-layer:completed:跑步:-:result', + ); + }); + }); + + it('uses a fallback error message for failed character animation submissions', async () => { + generateEditorCharacterAnimationMock.mockRejectedValueOnce(new Error('')); + render( + , + ); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: '提交角色动画' })); + }); + + await waitFor(() => { + expect(screen.getByTestId('character-animation').textContent).toBe( + 'character-layer:failed:-:生成角色动画失败:-', + ); + }); + }); +}); diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts new file mode 100644 index 00000000..443babc9 --- /dev/null +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -0,0 +1,508 @@ +import { + type Dispatch, + type MutableRefObject, + type SetStateAction, + useCallback, + useMemo, +} from 'react'; + +import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; +import { + editEditorImage, + generateEditorCharacterAnimation, + generateEditorIconSpritesheet, + generateEditorImage, +} from '../../services/image-editor/editorProjectClient'; +import type { + CanvasGenerationDialogState, + CanvasGenerationInputs, + CanvasLayer, + CanvasTool, + CanvasViewport, + CharacterAnimationPanelState, + GenerateDialogState, + QuickEditPanelState, + SidebarPanel, +} from './ImageCanvasEditorTypes'; +import { + createGeneratedResultLayer, + createIconSpritesheetResultLayers, + createQuickEditResultLayer, +} from './ImageCanvasGenerationLayerModel'; +import { + buildEditGenerationInputs, + isCanvasGenerationDialog, + resolveImageGenerationErrorMessage, +} from './ImageCanvasGenerationModel'; +import { + buildCharacterAnimationSubmissionPlan, + buildIconSpritesheetGenerationSubmissionPlan, + buildImageGenerationSubmissionPlan, +} from './ImageCanvasGenerationSubmissionModel'; + +type CanvasSize = { width: number; height: number }; + +type CanvasGenerationDialogUpdater = ( + dialog: CanvasGenerationDialogState, +) => CanvasGenerationDialogState | null; + +type GenerationSubmissionWorkflowOptions = { + layers: CanvasLayer[]; + canvasSize: CanvasSize; + viewport: CanvasViewport; + layerCounterRef: MutableRefObject; + quickEditPanel: QuickEditPanelState | null; + quickEditSourceLayer: CanvasLayer | null; + setQuickEditPanel: Dispatch>; + characterAnimationPanel: CharacterAnimationPanelState | null; + characterAnimationSourceLayer: CanvasLayer | null; + setCharacterAnimationPanel: Dispatch< + SetStateAction + >; + setGenerateDialog: Dispatch>; + updateCanvasGenerationDialogById: ( + dialogId: string, + updater: CanvasGenerationDialogUpdater, + ) => void; + removeCanvasGenerationDialogById: (dialogId: string) => void; + getGeneratingDialogPlaceholder: ( + dialog: GenerateDialogState, + ) => GenerateDialogState['placeholder']; + appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void; + selectSingleLayer: (layerId: string | null) => void; + fitLayers: (targetLayers?: CanvasLayer[]) => void; + setActiveTool: Dispatch>; + setActiveSidebarPanel: Dispatch>; + rememberImageModel: (imageModel: string) => void; +}; + +export function useImageCanvasGenerationSubmissionWorkflow({ + layers, + canvasSize, + viewport, + layerCounterRef, + quickEditPanel, + quickEditSourceLayer, + setQuickEditPanel, + characterAnimationPanel, + characterAnimationSourceLayer, + setCharacterAnimationPanel, + setGenerateDialog, + updateCanvasGenerationDialogById, + removeCanvasGenerationDialogById, + getGeneratingDialogPlaceholder, + appendCanvasLayersWithResources, + selectSingleLayer, + fitLayers, + setActiveTool, + setActiveSidebarPanel, + rememberImageModel, +}: GenerationSubmissionWorkflowOptions) { + const addGeneratedResultLayer = useCallback( + ( + generated: Parameters[0]['generated'], + options: { + sourceLayer?: CanvasLayer; + frame?: GenerateDialogState['placeholder']; + assetKind?: CanvasLayer['assetKind']; + title?: string; + dialogId?: string; + generationInputs?: CanvasGenerationInputs; + } = {}, + ) => { + layerCounterRef.current += 1; + const generatedIndex = layerCounterRef.current; + const nextLayer = createGeneratedResultLayer({ + generated, + generatedIndex, + canvasSize, + viewport, + sourceLayer: options.sourceLayer, + frame: options.frame, + assetKind: options.assetKind, + title: options.title, + generationInputs: options.generationInputs, + }); + + appendCanvasLayersWithResources([nextLayer]); + selectSingleLayer(nextLayer.id); + setActiveSidebarPanel('layers'); + if (options.sourceLayer) { + setGenerateDialog(null); + setActiveTool('select'); + } else if (options.dialogId) { + updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => + currentDialog.mode === 'character' || currentDialog.mode === 'icon' + ? null + : { + ...currentDialog, + status: 'idle', + composerOpen: true, + generatedLayerId: nextLayer.id, + placeholder: undefined, + errorMessage: undefined, + }, + ); + } + if (options.sourceLayer) { + fitLayers([options.sourceLayer, nextLayer]); + } + }, + [ + appendCanvasLayersWithResources, + canvasSize, + fitLayers, + layerCounterRef, + selectSingleLayer, + setActiveSidebarPanel, + setActiveTool, + setGenerateDialog, + updateCanvasGenerationDialogById, + viewport, + ], + ); + + const addQuickEditResultLayer = useCallback( + ( + generated: Parameters[0]['generated'], + sourceLayer: CanvasLayer, + generationInputs: CanvasGenerationInputs, + ) => { + layerCounterRef.current += 1; + const generatedIndex = layerCounterRef.current; + const nextLayer = createQuickEditResultLayer({ + generated, + generatedIndex, + sourceLayer, + generationInputs, + }); + + appendCanvasLayersWithResources([nextLayer]); + selectSingleLayer(nextLayer.id); + setActiveSidebarPanel('layers'); + setQuickEditPanel(null); + setActiveTool('select'); + fitLayers([sourceLayer, nextLayer]); + }, + [ + appendCanvasLayersWithResources, + fitLayers, + layerCounterRef, + selectSingleLayer, + setActiveSidebarPanel, + setActiveTool, + setQuickEditPanel, + ], + ); + + const addIconSpritesheetResultLayers = useCallback( + ( + generated: Parameters< + typeof createIconSpritesheetResultLayers + >[0]['generated'], + iconResults: Parameters< + typeof createIconSpritesheetResultLayers + >[0]['iconResults'], + generationInputs: CanvasGenerationInputs, + frame?: GenerateDialogState['placeholder'], + dialogId?: string, + ) => { + const startIndex = layerCounterRef.current + 1; + const nextLayers = createIconSpritesheetResultLayers({ + generated, + iconResults, + startIndex, + canvasSize, + viewport, + generationInputs, + frame, + }); + + if (!nextLayers.length) { + return; + } + layerCounterRef.current += nextLayers.length; + appendCanvasLayersWithResources(nextLayers); + selectSingleLayer(nextLayers[0]?.id ?? null); + setActiveSidebarPanel('layers'); + if (dialogId) { + removeCanvasGenerationDialogById(dialogId); + } + setActiveTool('select'); + }, + [ + appendCanvasLayersWithResources, + canvasSize, + layerCounterRef, + removeCanvasGenerationDialogById, + selectSingleLayer, + setActiveSidebarPanel, + setActiveTool, + viewport, + ], + ); + + const submitIconSpritesheetGeneration = useCallback( + async (dialog: GenerateDialogState) => { + if (dialog.mode !== 'icon') { + return; + } + const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null; + const setSubmittingIconDialog = ( + nextDialog: CanvasGenerationDialogState, + ) => { + updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog); + }; + const submissionPlan = + buildIconSpritesheetGenerationSubmissionPlan(dialog); + if (!submissionPlan.ok) { + if (canvasDialog) { + setSubmittingIconDialog({ + ...canvasDialog, + status: 'failed', + composerOpen: true, + errorMessage: submissionPlan.errorMessage, + }); + } + return; + } + + if (!canvasDialog) { + return; + } + + setSubmittingIconDialog({ + ...canvasDialog, + iconDescriptions: submissionPlan.iconDescriptions, + status: 'generating', + composerOpen: false, + errorMessage: undefined, + }); + + try { + const generated = await generateEditorIconSpritesheet( + submissionPlan.input, + ); + rememberImageModel(submissionPlan.rememberImageModel); + addIconSpritesheetResultLayers( + generated, + generated.iconImageSrcs, + submissionPlan.generationInputs, + getGeneratingDialogPlaceholder(dialog), + canvasDialog.id, + ); + } catch (error) { + setSubmittingIconDialog({ + ...canvasDialog, + iconDescriptions: submissionPlan.iconDescriptions, + status: 'failed', + composerOpen: true, + errorMessage: resolveImageGenerationErrorMessage(error), + }); + } + }, + [ + addIconSpritesheetResultLayers, + getGeneratingDialogPlaceholder, + rememberImageModel, + updateCanvasGenerationDialogById, + ], + ); + + const submitQuickEdit = useCallback(async () => { + if (!quickEditPanel || !quickEditSourceLayer) { + return; + } + + const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片'; + setQuickEditPanel({ + ...quickEditPanel, + prompt: normalizedPrompt, + status: 'generating', + errorMessage: undefined, + }); + + try { + const referenceImageSrc = await resolveEditorImageReferenceDataUrl( + quickEditSourceLayer.src, + ); + const generated = await generateEditorImage({ + prompt: normalizedPrompt, + size: quickEditPanel.size, + kind: 'quick-edit', + model: quickEditPanel.model, + referenceImageSrcs: [referenceImageSrc], + }); + addQuickEditResultLayer( + generated, + quickEditSourceLayer, + buildEditGenerationInputs( + '快速编辑提示词', + normalizedPrompt, + quickEditSourceLayer, + ), + ); + } catch (error) { + setQuickEditPanel({ + ...quickEditPanel, + prompt: normalizedPrompt, + status: 'failed', + errorMessage: resolveImageGenerationErrorMessage(error), + }); + } + }, [ + addQuickEditResultLayer, + quickEditPanel, + quickEditSourceLayer, + setQuickEditPanel, + ]); + + const submitImageGeneration = useCallback( + async (dialog: GenerateDialogState) => { + const normalizedPrompt = + dialog.prompt.trim() || + (dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片'); + const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null; + if (canvasDialog) { + updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({ + ...currentDialog, + prompt: normalizedPrompt, + status: 'generating', + composerOpen: false, + })); + } else { + setGenerateDialog({ + ...dialog, + prompt: normalizedPrompt, + status: 'generating', + composerOpen: dialog.mode === 'edit', + }); + } + + try { + const submissionPlan = buildImageGenerationSubmissionPlan({ + dialog, + layers, + nextGeneratedIndex: layerCounterRef.current + 1, + }); + if (submissionPlan.kind === 'edit') { + const referenceImageSrc = await resolveEditorImageReferenceDataUrl( + submissionPlan.sourceLayer.src, + ); + const generated = await editEditorImage({ + prompt: submissionPlan.normalizedPrompt, + sourceImageSrc: referenceImageSrc, + }); + addGeneratedResultLayer(generated, { + sourceLayer: submissionPlan.sourceLayer, + generationInputs: submissionPlan.generationInputs, + }); + } else { + const generated = await generateEditorImage(submissionPlan.input); + if (submissionPlan.rememberImageModel) { + rememberImageModel(submissionPlan.rememberImageModel); + } + addGeneratedResultLayer(generated, { + frame: getGeneratingDialogPlaceholder(dialog), + assetKind: submissionPlan.result.assetKind, + title: submissionPlan.result.title, + dialogId: canvasDialog?.id, + generationInputs: submissionPlan.result.generationInputs, + }); + } + } catch (error) { + if (canvasDialog) { + updateCanvasGenerationDialogById(canvasDialog.id, () => ({ + ...canvasDialog, + prompt: normalizedPrompt, + status: 'failed', + composerOpen: true, + errorMessage: resolveImageGenerationErrorMessage(error), + })); + } else { + setGenerateDialog({ + ...dialog, + prompt: normalizedPrompt, + status: 'failed', + composerOpen: true, + errorMessage: resolveImageGenerationErrorMessage(error), + }); + } + } + }, + [ + addGeneratedResultLayer, + getGeneratingDialogPlaceholder, + layerCounterRef, + layers, + rememberImageModel, + setGenerateDialog, + updateCanvasGenerationDialogById, + ], + ); + + const submitCharacterAnimation = useCallback(async () => { + if (!characterAnimationPanel || !characterAnimationSourceLayer) { + return; + } + const submissionPlan = buildCharacterAnimationSubmissionPlan({ + panel: characterAnimationPanel, + sourceLayer: characterAnimationSourceLayer, + }); + const nextPanel = { + ...characterAnimationPanel, + promptText: submissionPlan.promptText, + status: 'generating' as const, + errorMessage: undefined, + result: undefined, + }; + setCharacterAnimationPanel(nextPanel); + + try { + const result = await generateEditorCharacterAnimation( + submissionPlan.input, + ); + setCharacterAnimationPanel((currentPanel) => + currentPanel + ? { + ...currentPanel, + status: 'completed', + result, + } + : currentPanel, + ); + } catch (error) { + setCharacterAnimationPanel((currentPanel) => + currentPanel + ? { + ...currentPanel, + status: 'failed', + errorMessage: + error instanceof Error && error.message.trim() + ? error.message + : '生成角色动画失败', + } + : currentPanel, + ); + } + }, [ + characterAnimationPanel, + characterAnimationSourceLayer, + setCharacterAnimationPanel, + ]); + + return useMemo( + () => ({ + submitIconSpritesheetGeneration, + submitQuickEdit, + submitImageGeneration, + submitCharacterAnimation, + }), + [ + submitCharacterAnimation, + submitIconSpritesheetGeneration, + submitImageGeneration, + submitQuickEdit, + ], + ); +} diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index c77f5e50..07633ff4 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -7,19 +7,8 @@ import { useState, } from 'react'; -import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; -import { - editEditorImage, - type EditorIconSpritesheetGenerationResult, - type EditorIconSpritesheetIconResult, - type EditorImageGenerationResult, - generateEditorCharacterAnimation, - generateEditorIconSpritesheet, - generateEditorImage, -} from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationDialogState, - CanvasGenerationInputs, CanvasLayer, CanvasTool, CanvasViewport, @@ -50,25 +39,13 @@ import { updateSpecFormDialogValue, } from './ImageCanvasGenerationDialogModel'; import { - createGeneratedResultLayer, - createIconSpritesheetResultLayers, - createQuickEditResultLayer, -} from './ImageCanvasGenerationLayerModel'; -import { - buildEditGenerationInputs, buildQuickEditModelOptions, buildQuickEditSizeOptions, calculateCharacterAnimationPrice, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, - isCanvasGenerationDialog, - resolveImageGenerationErrorMessage, } from './ImageCanvasGenerationModel'; -import { - buildCharacterAnimationSubmissionPlan, - buildIconSpritesheetGenerationSubmissionPlan, - buildImageGenerationSubmissionPlan, -} from './ImageCanvasGenerationSubmissionModel'; +import { useImageCanvasGenerationSubmissionWorkflow } from './useImageCanvasGenerationSubmissionWorkflow'; type CanvasSize = { width: number; height: number }; @@ -303,145 +280,6 @@ export function useImageCanvasGenerationWorkflow({ ], ); - const addGeneratedResultLayer = useCallback( - ( - generated: EditorImageGenerationResult, - options: { - sourceLayer?: CanvasLayer; - frame?: GenerateDialogState['placeholder']; - assetKind?: CanvasLayer['assetKind']; - title?: string; - dialogId?: string; - generationInputs?: CanvasGenerationInputs; - } = {}, - ) => { - layerCounterRef.current += 1; - const generatedIndex = layerCounterRef.current; - const nextLayer = createGeneratedResultLayer({ - generated, - generatedIndex, - canvasSize, - viewport, - sourceLayer: options.sourceLayer, - frame: options.frame, - assetKind: options.assetKind, - title: options.title, - generationInputs: options.generationInputs, - }); - - appendCanvasLayersWithResources([nextLayer]); - selectSingleLayer(nextLayer.id); - setActiveSidebarPanel('layers'); - if (options.sourceLayer) { - setGenerateDialog(null); - setActiveTool('select'); - } else if (options.dialogId) { - updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => - currentDialog.mode === 'character' || currentDialog.mode === 'icon' - ? null - : { - ...currentDialog, - status: 'idle', - composerOpen: true, - generatedLayerId: nextLayer.id, - placeholder: undefined, - errorMessage: undefined, - }, - ); - } - if (options.sourceLayer) { - fitLayers([options.sourceLayer, nextLayer]); - } - }, - [ - appendCanvasLayersWithResources, - canvasSize, - fitLayers, - layerCounterRef, - selectSingleLayer, - setActiveSidebarPanel, - setActiveTool, - setGenerateDialog, - updateCanvasGenerationDialogById, - viewport, - ], - ); - - const addQuickEditResultLayer = useCallback( - ( - generated: EditorImageGenerationResult, - sourceLayer: CanvasLayer, - generationInputs: CanvasGenerationInputs, - ) => { - layerCounterRef.current += 1; - const generatedIndex = layerCounterRef.current; - const nextLayer = createQuickEditResultLayer({ - generated, - generatedIndex, - sourceLayer, - generationInputs, - }); - - appendCanvasLayersWithResources([nextLayer]); - selectSingleLayer(nextLayer.id); - setActiveSidebarPanel('layers'); - setQuickEditPanel(null); - setActiveTool('select'); - fitLayers([sourceLayer, nextLayer]); - }, - [ - appendCanvasLayersWithResources, - fitLayers, - layerCounterRef, - selectSingleLayer, - setActiveSidebarPanel, - setActiveTool, - ], - ); - - const addIconSpritesheetResultLayers = useCallback( - ( - generated: EditorIconSpritesheetGenerationResult, - iconResults: EditorIconSpritesheetIconResult[], - generationInputs: CanvasGenerationInputs, - frame?: GenerateDialogState['placeholder'], - dialogId?: string, - ) => { - const startIndex = layerCounterRef.current + 1; - const nextLayers = createIconSpritesheetResultLayers({ - generated, - iconResults, - startIndex, - canvasSize, - viewport, - generationInputs, - frame, - }); - - if (!nextLayers.length) { - return; - } - layerCounterRef.current += nextLayers.length; - appendCanvasLayersWithResources(nextLayers); - selectSingleLayer(nextLayers[0]?.id ?? null); - setActiveSidebarPanel('layers'); - if (dialogId) { - removeCanvasGenerationDialogById(dialogId); - } - setActiveTool('select'); - }, - [ - appendCanvasLayersWithResources, - canvasSize, - layerCounterRef, - removeCanvasGenerationDialogById, - selectSingleLayer, - setActiveSidebarPanel, - setActiveTool, - viewport, - ], - ); - const pickCharacterSpecFromLayer = useCallback( (layer: CanvasLayer) => { setGenerateDialog((currentDialog) => @@ -493,197 +331,34 @@ export function useImageCanvasGenerationWorkflow({ setGenerateDialog(appendIconDescriptionToDialog); }, [setGenerateDialog]); - const submitIconSpritesheetGeneration = useCallback( - async (dialog: GenerateDialogState) => { - if (dialog.mode !== 'icon') { - return; - } - const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null; - const setSubmittingIconDialog = ( - nextDialog: CanvasGenerationDialogState, - ) => { - updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog); - }; - const submissionPlan = - buildIconSpritesheetGenerationSubmissionPlan(dialog); - if (!submissionPlan.ok) { - if (canvasDialog) { - setSubmittingIconDialog({ - ...canvasDialog, - status: 'failed', - composerOpen: true, - errorMessage: submissionPlan.errorMessage, - }); - } - return; - } - - if (!canvasDialog) { - return; - } - - setSubmittingIconDialog({ - ...canvasDialog, - iconDescriptions: submissionPlan.iconDescriptions, - status: 'generating', - composerOpen: false, - errorMessage: undefined, - }); - - try { - const generated = await generateEditorIconSpritesheet( - submissionPlan.input, - ); - setLastImageModel(submissionPlan.rememberImageModel); - addIconSpritesheetResultLayers( - generated, - generated.iconImageSrcs, - submissionPlan.generationInputs, - getGeneratingDialogPlaceholder(dialog), - canvasDialog.id, - ); - } catch (error) { - setSubmittingIconDialog({ - ...canvasDialog, - iconDescriptions: submissionPlan.iconDescriptions, - status: 'failed', - composerOpen: true, - errorMessage: resolveImageGenerationErrorMessage(error), - }); - } - }, - [ - addIconSpritesheetResultLayers, - getGeneratingDialogPlaceholder, - updateCanvasGenerationDialogById, - ], - ); - - const submitQuickEdit = useCallback(async () => { - if (!quickEditPanel || !quickEditSourceLayer) { - return; - } - - const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片'; - setQuickEditPanel({ - ...quickEditPanel, - prompt: normalizedPrompt, - status: 'generating', - errorMessage: undefined, - }); - - try { - const referenceImageSrc = await resolveEditorImageReferenceDataUrl( - quickEditSourceLayer.src, - ); - const generated = await generateEditorImage({ - prompt: normalizedPrompt, - size: quickEditPanel.size, - kind: 'quick-edit', - model: quickEditPanel.model, - referenceImageSrcs: [referenceImageSrc], - }); - addQuickEditResultLayer( - generated, - quickEditSourceLayer, - buildEditGenerationInputs( - '快速编辑提示词', - normalizedPrompt, - quickEditSourceLayer, - ), - ); - } catch (error) { - setQuickEditPanel({ - ...quickEditPanel, - prompt: normalizedPrompt, - status: 'failed', - errorMessage: resolveImageGenerationErrorMessage(error), - }); - } - }, [addQuickEditResultLayer, quickEditPanel, quickEditSourceLayer]); - - const submitImageGeneration = useCallback( - async (dialog: GenerateDialogState) => { - const normalizedPrompt = - dialog.prompt.trim() || - (dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片'); - const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null; - if (canvasDialog) { - updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({ - ...currentDialog, - prompt: normalizedPrompt, - status: 'generating', - composerOpen: false, - })); - } else { - setGenerateDialog({ - ...dialog, - prompt: normalizedPrompt, - status: 'generating', - composerOpen: dialog.mode === 'edit', - }); - } - - try { - const submissionPlan = buildImageGenerationSubmissionPlan({ - dialog, - layers, - nextGeneratedIndex: layerCounterRef.current + 1, - }); - if (submissionPlan.kind === 'edit') { - const referenceImageSrc = await resolveEditorImageReferenceDataUrl( - submissionPlan.sourceLayer.src, - ); - const generated = await editEditorImage({ - prompt: submissionPlan.normalizedPrompt, - sourceImageSrc: referenceImageSrc, - }); - addGeneratedResultLayer(generated, { - sourceLayer: submissionPlan.sourceLayer, - generationInputs: submissionPlan.generationInputs, - }); - } else { - const generated = await generateEditorImage(submissionPlan.input); - if (submissionPlan.rememberImageModel) { - setLastImageModel(submissionPlan.rememberImageModel); - } - addGeneratedResultLayer(generated, { - frame: getGeneratingDialogPlaceholder(dialog), - assetKind: submissionPlan.result.assetKind, - title: submissionPlan.result.title, - dialogId: canvasDialog?.id, - generationInputs: submissionPlan.result.generationInputs, - }); - } - } catch (error) { - if (canvasDialog) { - updateCanvasGenerationDialogById(canvasDialog.id, () => ({ - ...canvasDialog, - prompt: normalizedPrompt, - status: 'failed', - composerOpen: true, - errorMessage: resolveImageGenerationErrorMessage(error), - })); - } else { - setGenerateDialog({ - ...dialog, - prompt: normalizedPrompt, - status: 'failed', - composerOpen: true, - errorMessage: resolveImageGenerationErrorMessage(error), - }); - } - } - }, - [ - addGeneratedResultLayer, - getGeneratingDialogPlaceholder, - layerCounterRef, - layers, - setGenerateDialog, - updateCanvasGenerationDialogById, - ], - ); + const generationSubmissionWorkflow = useImageCanvasGenerationSubmissionWorkflow({ + layers, + canvasSize, + viewport, + layerCounterRef, + quickEditPanel, + quickEditSourceLayer, + setQuickEditPanel, + characterAnimationPanel, + characterAnimationSourceLayer, + setCharacterAnimationPanel, + setGenerateDialog, + updateCanvasGenerationDialogById, + removeCanvasGenerationDialogById, + getGeneratingDialogPlaceholder, + appendCanvasLayersWithResources, + selectSingleLayer, + fitLayers, + setActiveTool, + setActiveSidebarPanel, + rememberImageModel: setLastImageModel, + }); + const { + submitCharacterAnimation, + submitIconSpritesheetGeneration, + submitImageGeneration, + submitQuickEdit, + } = generationSubmissionWorkflow; const updateSpecFormValue = useCallback( (key: keyof SpecFormValues, value: string) => { @@ -703,52 +378,6 @@ export function useImageCanvasGenerationWorkflow({ [], ); - const submitCharacterAnimation = useCallback(async () => { - if (!characterAnimationPanel || !characterAnimationSourceLayer) { - return; - } - const submissionPlan = buildCharacterAnimationSubmissionPlan({ - panel: characterAnimationPanel, - sourceLayer: characterAnimationSourceLayer, - }); - const nextPanel = { - ...characterAnimationPanel, - promptText: submissionPlan.promptText, - status: 'generating' as const, - errorMessage: undefined, - result: undefined, - }; - setCharacterAnimationPanel(nextPanel); - - try { - const result = await generateEditorCharacterAnimation( - submissionPlan.input, - ); - setCharacterAnimationPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - status: 'completed', - result, - } - : currentPanel, - ); - } catch (error) { - setCharacterAnimationPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - status: 'failed', - errorMessage: - error instanceof Error && error.message.trim() - ? error.message - : '生成角色动画失败', - } - : currentPanel, - ); - } - }, [characterAnimationPanel, characterAnimationSourceLayer]); - const hideGeneratedLayerPanelAfterBlur = useCallback(() => { setGenerateDialog((currentDialog) => hideGeneratedLayerComposerAfterBlur(currentDialog),