支持规范参考图输入

为角色形象规范、UI素材规范、自定义规范面板新增参考图上传入口。

生成规范时携带参考图并自动追加参考图生成规范语义。

补充生成流程和上传流程回归测试。

更新画板角色形象生成入口设计文档。
This commit is contained in:
2026-06-17 14:20:23 +08:00
parent f8e063a878
commit 05a47816b0
6 changed files with 173 additions and 8 deletions

View File

@@ -621,6 +621,37 @@ export function ImageCanvasGenerationComposerView({
) : null}
</>
)}
{generateDialog.specType !== 'icon' ? (
<div className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
</PlatformFieldLabel>
<button
type="button"
className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
disabled={generateDialog.status === 'generating'}
onClick={() => onRequestUpload('spec-reference')}
>
<span className="image-canvas-editor__reference-tile-visual">
{generateDialog.specReference ? (
<img
src={generateDialog.specReference.src}
alt=""
aria-hidden="true"
/>
) : (
<ImagePlus className="h-4 w-4" aria-hidden="true" />
)}
</span>
<span className="image-canvas-editor__reference-tile-copy">
{generateDialog.specReference?.label ?? '添加参考图'}
</span>
</button>
</div>
) : null}
</div>
{generateDialog.status === 'failed' ? (
<PlatformStatusMessage

View File

@@ -155,6 +155,9 @@ function GenerationWorkflowHarness({
<button type="button" onClick={workflow.openGenerateDialog}>
</button>
<button type="button" onClick={() => workflow.openSpecDialog('ui')}>
UI规范
</button>
<button
type="button"
onClick={() =>
@@ -214,6 +217,25 @@ function GenerationWorkflowHarness({
>
</button>
<button
type="button"
onClick={() =>
dialogs.setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'spec'
? {
...currentDialog,
specReference: {
id: 'spec-ref',
label: '参考.png',
src: 'data:image/png;base64,ref',
},
}
: currentDialog,
)
}
>
</button>
<button
type="button"
onClick={() =>
@@ -307,6 +329,33 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('submits spec generation with reference image and reference prompt semantics', async () => {
generateEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: 'UI规范图' }),
);
render(<GenerationWorkflowHarness />);
fireEvent.click(screen.getByRole('button', { name: '打开UI规范' }));
fireEvent.click(screen.getByRole('button', { name: '添加规范参考图' }));
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
await waitFor(() => {
expect(generateEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
size: '2048x1152',
kind: 'spec',
referenceImageSrcs: ['data:image/png;base64,ref'],
prompt: expect.stringContaining('参考图生成规范'),
}),
);
});
await waitFor(() => {
expect(screen.getByTestId('layers').textContent).toContain(
'layer-generated-1:UI素材规范 1',
);
});
});
it('submits quick edits beside the source and fits source plus result', async () => {
generateEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: '快速修图' }),
@@ -332,7 +381,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
prompt: '快速修图',
size: '1024x768',
kind: 'quick-edit',
model: 'gpt-image-2',
model: 'gemini-3.1-flash-image-preview',
referenceImageSrcs: ['data:image/png;base64,source'],
});
});

View File

@@ -827,19 +827,30 @@ export function useImageCanvasGenerationWorkflow({
const specType = dialog.specType ?? 'custom';
const specValues =
dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
const specPrompt = buildSpecPrompt(specType, specValues);
const specPrompt = buildSpecPrompt(
specType,
specValues,
Boolean(dialog.specReference?.src),
);
const generated = await generateEditorImage({
prompt: specPrompt,
size: SPEC_GENERATION_SIZE,
model: DEFAULT_IMAGE_MODEL,
kind: 'spec',
...(dialog.specReference?.src
? { referenceImageSrcs: [dialog.specReference.src] }
: {}),
});
addGeneratedResultLayer(generated, {
frame: getGeneratingDialogPlaceholder(dialog),
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
dialogId: canvasDialog?.id,
generationInputs: buildSpecGenerationInputs(specType, specValues),
generationInputs: buildSpecGenerationInputs(
specType,
specValues,
dialog.specReference,
),
});
} else if (dialog.mode === 'character') {
const referenceImageSrcs = [

View File

@@ -132,7 +132,7 @@ function UploadWorkflowHarness({
<span data-testid="selected-layer">{selectedLayerId ?? '-'}</span>
<span data-testid="dialog">
{generateDialog
? `${generateDialog.mode}:${generateDialog.status}:${generateDialog.characterSpecReference?.label ?? '-'}:${generateDialog.characterReferences?.length ?? 0}:${generateDialog.iconSpecReference?.label ?? '-'}`
? `${generateDialog.mode}:${generateDialog.status}:${generateDialog.characterSpecReference?.label ?? '-'}:${generateDialog.characterReferences?.length ?? 0}:${generateDialog.iconSpecReference?.label ?? '-'}:${generateDialog.specReference?.label ?? '-'}`
: '-'}
</span>
<button
@@ -168,6 +168,26 @@ function UploadWorkflowHarness({
>
</button>
<button
type="button"
onClick={() =>
setGenerateDialog({
mode: 'spec',
specType: 'ui',
prompt: '',
status: 'failed',
errorMessage: '旧错误',
})
}
>
</button>
<button
type="button"
onClick={() => workflow.setUploadTarget('spec-reference')}
>
</button>
<button
type="button"
onClick={() => workflow.setUploadTarget('character-spec')}
@@ -352,7 +372,7 @@ describe('useImageCanvasUploadWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '选择角色规范' }));
await waitFor(() => {
expect(screen.getByTestId('dialog').textContent).toBe(
'character:failed:-:0:-',
'character:failed:-:0:-:-',
);
});
@@ -364,7 +384,26 @@ describe('useImageCanvasUploadWorkflow', () => {
await waitFor(() => {
expect(screen.getByTestId('dialog').textContent).toContain(
'character:idle:角色规范.png:0:-',
'character:idle:角色规范.png:0:-:-',
);
});
expect(createEditorAssetMock).not.toHaveBeenCalled();
});
it('dispatches file input uploads to spec reference images', async () => {
render(<UploadWorkflowHarness />);
fireEvent.click(screen.getByRole('button', { name: '准备规范生成' }));
fireEvent.click(screen.getByRole('button', { name: '选择规范参考图' }));
fireEvent.change(screen.getByLabelText('上传图片文件'), {
target: {
files: [createTestFile('UI参考.png')],
},
});
await waitFor(() => {
expect(screen.getByTestId('dialog').textContent).toContain(
'spec:idle:-:0:-:UI参考.png',
);
});
expect(createEditorAssetMock).not.toHaveBeenCalled();

View File

@@ -126,6 +126,31 @@ export function useImageCanvasUploadWorkflow({
[setGenerateDialog],
);
const addSpecReferenceFiles = useCallback(
async (files: FileList | File[]) => {
const imageFile = Array.from(files).find(isImageFile);
if (!imageFile) {
window.alert('请选择图片文件');
return;
}
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,
);
},
[setGenerateDialog],
);
const addCharacterReferenceFiles = useCallback(
async (files: FileList | File[]) => {
const imageFiles = Array.from(files).filter(isImageFile);
@@ -485,7 +510,9 @@ export function useImageCanvasUploadWorkflow({
const files = event.currentTarget.files;
const currentUploadTarget = uploadTargetRef.current;
if (files?.length) {
if (currentUploadTarget === 'character-spec') {
if (currentUploadTarget === 'spec-reference') {
void addSpecReferenceFiles(files);
} else if (currentUploadTarget === 'character-spec') {
void addCharacterSpecReferenceFiles(files);
} else if (currentUploadTarget === 'character-reference') {
void addCharacterReferenceFiles(files);
@@ -502,6 +529,7 @@ export function useImageCanvasUploadWorkflow({
activeTool,
addCharacterReferenceFiles,
addCharacterSpecReferenceFiles,
addSpecReferenceFiles,
addIconSpecReferenceFiles,
addUploadedFiles,
setUploadTarget,
@@ -515,6 +543,7 @@ export function useImageCanvasUploadWorkflow({
requestUpload,
handleUploadInputChange,
addUploadedFiles,
addSpecReferenceFiles,
addCharacterSpecReferenceFiles,
addCharacterReferenceFiles,
addIconSpecReferenceFiles,