diff --git a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md index aeffa21ca..fca149a6e 100644 --- a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md @@ -85,9 +85,9 @@ ## 生成规范参考图 -- `生成规范 -> 角色规范`、`图标规范`、`图标规范`、`自定义规范` 的设定面板都支持上传 1 张参考图,并统一放在面板首行参考图区域。 +- `生成规范 -> 角色规范`、`自定义规范` 的设定面板支持上传多张参考图,最多 5 张;`图标规范` 保持单张规范参考图。所有参考图统一放在面板首行参考图区域,并可逐张删除。 - 参考图入口只展示字段标题、缩略图或上传图标、文件名,不把参考规则说明铺在 UI 上。 -- 提交生成规范时,若存在参考图,前端必须把参考图作为 `referenceImageSrcs[0]` 提交到 `/api/editor/images/generations`,并在生图提示词开头自动追加“参考图生成规范”语义:要求模型参考图 1 的构图、风格、材质、色彩、形状语言和视觉层级生成规范图,但不要复制参考图中的文字、水印或无关背景。 +- 提交生成规范时,若存在参考图,前端必须按用户当前顺序把全部参考图提交到 `/api/editor/images/generations` 的 `referenceImageSrcs`,并在生图提示词开头自动追加“参考图生成规范”语义:要求模型参考图的构图、风格、材质、色彩、形状语言和视觉层级生成规范图,但不要复制参考图中的文字、水印或无关背景。 - 生成结果的信息快照必须记录该参考图,标题为 `参考图`,便于后续在图片信息面板回看生成输入。 ## 新建生成图落点避让 diff --git a/src/components/image-editor/ImageCanvasGenerationModel.test.ts b/src/components/image-editor/ImageCanvasGenerationModel.test.ts index d0fd5bb6a..cc74bf77f 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.test.ts @@ -52,6 +52,7 @@ import { isNormalizedCanvasGenerationInputs, isQuickEditSupportedLayer, resolveCharacterAnimationSourceImageSrc, + resolveDialogExtraImageReferenceLimit, resolveEditorImageGenerationPixelSize, resolveImageGenerationErrorMessage, VIDEO_MODEL_KLING_3, @@ -68,6 +69,26 @@ describe('ImageCanvasGenerationModel', () => { }); }); + it('uses the fixed spec model provider limit for multi-reference spec dialogs', () => { + expect( + resolveDialogExtraImageReferenceLimit({ + mode: 'spec', + specType: 'character', + prompt: '', + status: 'idle', + }), + ).toBe(5); + expect( + resolveDialogExtraImageReferenceLimit({ + mode: 'spec', + specType: 'custom', + prompt: '', + status: 'idle', + imageModel: IMAGE_MODEL_NANOBANANA2, + }), + ).toBe(5); + }); + it('展示时长时收敛到两位小数且不改动其他字段', () => { const format = (title: string, value: string) => formatEditorGenerationInputFieldValue({ title, value }); diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts index 724ff72b1..bc63f4ddc 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.ts @@ -464,7 +464,8 @@ export function resolveExtraImageReferenceLimit( export function resolveDialogExtraImageReferenceLimit( dialog: GenerateDialogState, - model: string | null | undefined = dialog.imageModel, + model: string | null | undefined = dialog.imageModel ?? + (dialog.mode === 'spec' ? SPEC_GENERATION_MODEL : undefined), ) { if (dialog.mode === 'icon') { return resolveExtraImageReferenceLimit( @@ -494,7 +495,10 @@ export function resolveDialogExtraImageReferenceLimit( 1, ); } - return IMAGE_GENERATION_REFERENCE_LIMIT; + return resolveExtraImageReferenceLimit( + model, + IMAGE_GENERATION_REFERENCE_LIMIT, + ); } export function countDialogExtraImageReferences(dialog: GenerateDialogState) { diff --git a/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx b/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx index b4c730ac9..0a67e9b54 100644 --- a/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx +++ b/src/components/image-editor/ImageCanvasReferenceSlot.test.tsx @@ -1,6 +1,6 @@ /* @vitest-environment jsdom */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot'; @@ -14,7 +14,10 @@ vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({ describe('ImageCanvasReferenceSlot', () => { beforeEach(() => { useResolvedAssetReadUrlMock.mockImplementation( - (source: string | null | undefined, options?: { objectKey?: string | null }) => ({ + ( + source: string | null | undefined, + options?: { objectKey?: string | null }, + ) => ({ resolvedUrl: options?.objectKey ? `https://oss.example.com/${options.objectKey}?signature=1` : (source ?? ''), @@ -49,4 +52,21 @@ describe('ImageCanvasReferenceSlot', () => { 'https://oss.example.com/generated-character-drafts/editor/legacy-resources/editor-resource-1.png?signature=1', ); }); + + it('keeps an explicitly marked reference remove action visible', () => { + render( + } + imageSrc="/reference.png" + label="参考图1" + ariaLabel="参考图1" + onRemove={vi.fn()} + alwaysShowRemove + />, + ); + + expect( + screen.getByRole('button', { name: '删除参考图1' }).className, + ).toContain('image-canvas-editor__reference-chip-remove--always-visible'); + }); }); diff --git a/src/components/image-editor/ImageCanvasReferenceSlot.tsx b/src/components/image-editor/ImageCanvasReferenceSlot.tsx index f0304f6e2..8a515570f 100644 --- a/src/components/image-editor/ImageCanvasReferenceSlot.tsx +++ b/src/components/image-editor/ImageCanvasReferenceSlot.tsx @@ -28,6 +28,7 @@ type ImageCanvasReferenceSlotProps = { onClick?: () => void; onRemove?: () => void; removeLabel?: string; + alwaysShowRemove?: boolean; }; export function ImageCanvasReferenceSlot({ @@ -45,6 +46,7 @@ export function ImageCanvasReferenceSlot({ onClick, onRemove, removeLabel, + alwaysShowRemove = false, }: ImageCanvasReferenceSlotProps) { const hasPreviewImage = Boolean(imageSrc?.trim() || objectKey?.trim()); const slotClassName = [ @@ -106,7 +108,11 @@ export function ImageCanvasReferenceSlot({ {onRemove ? (