diff --git a/TRACKING.md b/TRACKING.md index b671815b..b3e47ea1 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -169,3 +169,5 @@ - 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。 - 2026-06-17 前端拆分第四十四执行批次:继续收口 `useImageCanvasAssetLibrary`,扩展 `ImageCanvasAssetLibraryModel`,把素材文件夹坐标命中、素材拖拽目标文件夹置顶判断、素材框选起点 / 移动 / 反向拖拽矩形归一化、框选只命中 uploaded 素材和框选启动规则从 hook 中抽成纯几何模型;asset library hook 保留 DOM 查询、dataset 读取、pointer capture / release、React 状态写入、后端 CRUD 和登录弹窗副作用,避免把 DOM 与服务调用塞进模型。新增模型单测覆盖文件夹边界命中、列表外返回空、文件夹 header 视野外置顶、反向框选归一化、边缘相交命中、忽略 built-in / unknown 素材;只读子代理复核同意优先抽 Rect/Point + folder hit + pinned + marquee selection,不继续拆新 hook。验证命令:`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/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`。 - 2026-06-17 前端拆分第四十四执行批次验证补充:统一编辑器回归命令 `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/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx` 通过 33 个文件 / 219 个测试;`npm run typecheck`、`npm run check:encoding`、`git diff --check` 均通过。浏览器回归:`http://127.0.0.1:10007/editor/canvas` 使用 Playwright CLI headless 打开成功,未登录显示 `账号入口`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;默认素材侧栏显示 `素材` / `项目素材` / `上传到项目素材`,切换 `打开图层` 后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见;网络请求仅有预期未登录 `/api/auth/refresh` 401,其余 editor smoke 相关请求正常。 +- 2026-06-17 前端拆分第四十五执行批次:继续收口 `useImageCanvasUploadWorkflow`,扩展 `ImageCanvasUploadModel`,把生成参考图上传后的 dialog 状态转换从 hook 中抽成纯模型:统一创建上传参考图对象、将 failed 生成面板恢复为 idle、按 `spec-reference` / `character-spec` / `character-reference` / `icon-spec` 写入对应引用字段,并保持不匹配 dialog 不变;upload workflow hook 继续保留文件筛选、Data URL 读取、文件 input、未登录素材上传续传、真实素材创建 API、上传进度和画布落图副作用。新增模型单测覆盖参考图 fallback label、failed 状态清理、规范 / 角色 / 图标参考图写入和角色参考图追加;局部验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`。 +- 2026-06-17 前端拆分第四十五执行批次验证补充:统一编辑器回归命令 `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/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx` 通过 33 个文件 / 223 个测试;`npm run typecheck`、`npm run check:encoding`、`git diff --check` 均通过。浏览器回归:`http://127.0.0.1:10007/editor/canvas` 使用 Playwright CLI headless 打开成功,未登录显示 `账号入口`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;默认素材侧栏显示 `素材` / `项目素材` / `上传到项目素材`,切换 `打开图层` 后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator` 占位、`生成图片` 对话框和 `AI画布工具栏` 均可见;网络请求仅有预期未登录 `/api/auth/refresh` 401,其余 editor smoke 相关请求正常。只读子代理复核生成提交链路后建议暂停继续硬拆,除非后续新增生成模式再考虑按“生成成功后的落图提交事务”抽内部 hook。 diff --git a/src/components/image-editor/ImageCanvasUploadModel.test.ts b/src/components/image-editor/ImageCanvasUploadModel.test.ts index 2bdef324..663d289c 100644 --- a/src/components/image-editor/ImageCanvasUploadModel.test.ts +++ b/src/components/image-editor/ImageCanvasUploadModel.test.ts @@ -1,14 +1,18 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import type { CanvasLayer, EditorAssetFolder, + GenerateDialogState, } from './ImageCanvasEditorTypes'; import { + applyGenerationReferenceUpload, createUploadCanvasLayer, + createUploadedGenerationReference, createUploadingAssetPlaceholder, resizeUploadCanvasLayerToImage, resolveUploadFolderId, + setFailedGenerationIdle, } from './ImageCanvasUploadModel'; function createFolder( @@ -43,7 +47,23 @@ function createLayer(overrides: Partial = {}): CanvasLayer { }; } +function createDialog( + overrides: Partial = {}, +): GenerateDialogState { + return { + mode: 'character', + prompt: '', + status: 'failed', + errorMessage: '旧错误', + ...overrides, + }; +} + describe('ImageCanvasUploadModel', () => { + afterEach(() => { + vi.useRealTimers(); + }); + it('resolves upload folder ids with project fallback', () => { const folders = [ createFolder(), @@ -89,6 +109,115 @@ describe('ImageCanvasUploadModel', () => { }); }); + it('creates uploaded generation references with fallback labels', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-17T12:00:00.000Z')); + + expect( + createUploadedGenerationReference({ + idPrefix: 'upload-character-reference', + index: 2, + fileName: '', + fallbackLabel: '参考图3', + imageSrc: 'data:image/png;base64,ref', + }), + ).toEqual({ + id: 'upload-character-reference-1781697600000-2', + label: '参考图3', + src: 'data:image/png;base64,ref', + }); + }); + + it('resets failed generation dialogs when reference uploads are applied', () => { + expect( + setFailedGenerationIdle( + createDialog({ status: 'failed', errorMessage: '旧错误' }), + ), + ).toMatchObject({ status: 'idle', errorMessage: undefined }); + expect( + setFailedGenerationIdle(createDialog({ status: 'generating' })), + ).toMatchObject({ status: 'generating', errorMessage: '旧错误' }); + }); + + it('applies uploaded references to matching generation dialogs', () => { + const reference = { + id: 'ref-a', + label: '参考图', + src: 'data:image/png;base64,ref', + }; + + expect( + applyGenerationReferenceUpload({ + dialog: createDialog({ mode: 'spec' }), + target: 'spec-reference', + references: [reference], + }), + ).toMatchObject({ + mode: 'spec', + status: 'idle', + specReference: reference, + errorMessage: undefined, + }); + expect( + applyGenerationReferenceUpload({ + dialog: createDialog({ mode: 'character' }), + target: 'character-spec', + references: [reference], + }), + ).toMatchObject({ + mode: 'character', + status: 'idle', + characterSpecReference: reference, + }); + expect( + applyGenerationReferenceUpload({ + dialog: createDialog({ mode: 'icon' }), + target: 'icon-spec', + references: [reference], + }), + ).toMatchObject({ + mode: 'icon', + status: 'idle', + iconSpecReference: reference, + }); + }); + + it('appends character reference uploads without changing unmatched dialogs', () => { + const previousReference = { + id: 'ref-old', + label: '旧参考图', + src: 'data:image/png;base64,old', + }; + const nextReference = { + id: 'ref-new', + label: '新参考图', + src: 'data:image/png;base64,new', + }; + + expect( + applyGenerationReferenceUpload({ + dialog: createDialog({ + mode: 'character', + characterReferences: [previousReference], + }), + target: 'character-reference', + references: [nextReference], + }), + ).toMatchObject({ + mode: 'character', + status: 'idle', + characterReferences: [previousReference, nextReference], + }); + const iconDialog = createDialog({ mode: 'icon' }); + expect( + applyGenerationReferenceUpload({ + dialog: iconDialog, + target: 'character-reference', + references: [nextReference], + }), + ).toBe(iconDialog); + }); + it('creates upload canvas layers centered on the target canvas point', () => { const layer = createUploadCanvasLayer({ uploadIndex: 1, diff --git a/src/components/image-editor/ImageCanvasUploadModel.ts b/src/components/image-editor/ImageCanvasUploadModel.ts index 84033df9..5337c0b6 100644 --- a/src/components/image-editor/ImageCanvasUploadModel.ts +++ b/src/components/image-editor/ImageCanvasUploadModel.ts @@ -1,13 +1,20 @@ import { resolveLayerResolutionSize } from './ImageCanvasEditorModel'; import type { + CharacterReferenceImage, CanvasLayer, CanvasViewport, EditorAsset, EditorAssetFolder, + GenerateDialogState, } from './ImageCanvasEditorTypes'; type CanvasSize = { width: number; height: number }; type CanvasPoint = { x: number; y: number }; +type GenerationReferenceUploadTarget = + | 'character-reference' + | 'character-spec' + | 'icon-spec' + | 'spec-reference'; export const UPLOAD_LAYER_FALLBACK_SIZE = { width: 420, @@ -29,6 +36,85 @@ export function resolveUploadFolderId({ : 'project'; } +export function createUploadedGenerationReference({ + idPrefix, + index, + fileName, + fallbackLabel, + imageSrc, +}: { + idPrefix: string; + index?: number; + fileName?: string; + fallbackLabel: string; + imageSrc: string; +}): CharacterReferenceImage { + return { + id: + typeof index === 'number' + ? `${idPrefix}-${Date.now()}-${index}` + : `${idPrefix}-${Date.now()}`, + label: fileName || fallbackLabel, + src: imageSrc, + }; +} + +export function setFailedGenerationIdle(dialog: GenerateDialogState) { + return { + ...dialog, + status: dialog.status === 'failed' ? 'idle' : dialog.status, + errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, + }; +} + +export function applyGenerationReferenceUpload({ + dialog, + target, + references, +}: { + dialog: GenerateDialogState | null; + target: GenerationReferenceUploadTarget; + references: CharacterReferenceImage[]; +}): GenerateDialogState | null { + const firstReference = references[0]; + if (!firstReference) { + return dialog; + } + if (target === 'spec-reference') { + return dialog?.mode === 'spec' + ? { + ...setFailedGenerationIdle(dialog), + specReference: firstReference, + } + : dialog; + } + if (target === 'character-spec') { + return dialog?.mode === 'character' + ? { + ...setFailedGenerationIdle(dialog), + characterSpecReference: firstReference, + } + : dialog; + } + if (target === 'icon-spec') { + return dialog?.mode === 'icon' + ? { + ...setFailedGenerationIdle(dialog), + iconSpecReference: firstReference, + } + : dialog; + } + return dialog?.mode === 'character' + ? { + ...setFailedGenerationIdle(dialog), + characterReferences: [ + ...(dialog.characterReferences ?? []), + ...references, + ], + } + : dialog; +} + export function createUploadingAssetPlaceholder({ uploadIndex, fileName, diff --git a/src/components/image-editor/useImageCanvasUploadWorkflow.ts b/src/components/image-editor/useImageCanvasUploadWorkflow.ts index 220fa2fa..2dc5fefb 100644 --- a/src/components/image-editor/useImageCanvasUploadWorkflow.ts +++ b/src/components/image-editor/useImageCanvasUploadWorkflow.ts @@ -20,7 +20,9 @@ import type { } from './ImageCanvasEditorTypes'; import { isImageFile, readImageFileAsDataUrl } from './ImageCanvasFileModel'; import { + applyGenerationReferenceUpload, createUploadCanvasLayer, + createUploadedGenerationReference, createUploadingAssetPlaceholder, resizeUploadCanvasLayerToImage, resolveUploadFolderId, @@ -60,14 +62,6 @@ function isEditorAuthError(error: unknown) { ); } -function setFailedGenerationIdle(dialog: GenerateDialogState) { - return { - ...dialog, - status: dialog.status === 'failed' ? 'idle' : dialog.status, - errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, - }; -} - export function useImageCanvasUploadWorkflow({ canAccessProtectedData, openEditorLoginModal, @@ -113,16 +107,18 @@ export function useImageCanvasUploadWorkflow({ const imageSrc = await readImageFileAsDataUrl(imageFile); setGenerateDialog((currentDialog) => - currentDialog?.mode === 'character' - ? { - ...setFailedGenerationIdle(currentDialog), - characterSpecReference: { - id: `upload-character-spec-${Date.now()}`, - label: imageFile.name || '角色形象规范', - src: imageSrc, - }, - } - : currentDialog, + applyGenerationReferenceUpload({ + dialog: currentDialog, + target: 'character-spec', + references: [ + createUploadedGenerationReference({ + idPrefix: 'upload-character-spec', + fileName: imageFile.name, + fallbackLabel: '角色形象规范', + imageSrc, + }), + ], + }), ); }, [setGenerateDialog], @@ -138,16 +134,18 @@ export function useImageCanvasUploadWorkflow({ const imageSrc = await readImageFileAsDataUrl(imageFile); setGenerateDialog((currentDialog) => - currentDialog?.mode === 'spec' - ? { - ...setFailedGenerationIdle(currentDialog), - specReference: { - id: `upload-spec-reference-${Date.now()}`, - label: imageFile.name || '参考图', - src: imageSrc, - }, - } - : currentDialog, + applyGenerationReferenceUpload({ + dialog: currentDialog, + target: 'spec-reference', + references: [ + createUploadedGenerationReference({ + idPrefix: 'upload-spec-reference', + fileName: imageFile.name, + fallbackLabel: '参考图', + imageSrc, + }), + ], + }), ); }, [setGenerateDialog], @@ -163,21 +161,25 @@ export function useImageCanvasUploadWorkflow({ const references = await Promise.all( imageFiles.map(async (file, index) => ({ - id: `upload-character-reference-${Date.now()}-${index}`, - label: file.name || `参考图${index + 1}`, - src: await readImageFileAsDataUrl(file), + file, + index, + imageSrc: await readImageFileAsDataUrl(file), })), ); setGenerateDialog((currentDialog) => - currentDialog?.mode === 'character' - ? { - ...setFailedGenerationIdle(currentDialog), - characterReferences: [ - ...(currentDialog.characterReferences ?? []), - ...references, - ], - } - : currentDialog, + applyGenerationReferenceUpload({ + dialog: currentDialog, + target: 'character-reference', + references: references.map(({ file, imageSrc, index }) => + createUploadedGenerationReference({ + idPrefix: 'upload-character-reference', + index, + fileName: file.name, + fallbackLabel: `参考图${index + 1}`, + imageSrc, + }), + ), + }), ); }, [setGenerateDialog], @@ -193,16 +195,18 @@ export function useImageCanvasUploadWorkflow({ const imageSrc = await readImageFileAsDataUrl(imageFile); setGenerateDialog((currentDialog) => - currentDialog?.mode === 'icon' - ? { - ...setFailedGenerationIdle(currentDialog), - iconSpecReference: { - id: `upload-icon-spec-${Date.now()}`, - label: imageFile.name || '图标素材规范', - src: imageSrc, - }, - } - : currentDialog, + applyGenerationReferenceUpload({ + dialog: currentDialog, + target: 'icon-spec', + references: [ + createUploadedGenerationReference({ + idPrefix: 'upload-icon-spec', + fileName: imageFile.name, + fallbackLabel: '图标素材规范', + imageSrc, + }), + ], + }), ); }, [setGenerateDialog],