修复画布参考图上传删除链路
Project CI / Repository checks (pull_request) Successful in 1m36s
Project CI / Frontend tests (pull_request) Failing after 2m28s
Project CI / Backend tests (pull_request) Successful in 4m20s
Project CI / Native shell tests (pull_request) Successful in 14m43s

主站规范生成支持多张参考图

修复普通生成参考图删除与提交状态不同步

优化触屏设备删除按钮可达性并补充回归测试
This commit is contained in:
2026-08-12 16:20:49 +08:00
parent 8abeb333bb
commit bb14cf80b4
10 changed files with 198 additions and 34 deletions
@@ -156,6 +156,49 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
expect(screen.queryByText('参考图A')).toBeNull();
});
it('removes only the selected reference when multiple references are present', () => {
const submitGeneration = vi.fn();
render(
<BasicGenerationHarness
initialDialog={createDialog({
generationReferences: [
{
id: 'ref-a',
label: '参考图A',
src: 'data:image/png;base64,cmVmQQ==',
},
{
id: 'ref-b',
label: '参考图B',
src: 'data:image/png;base64,cmVmQg==',
},
{
id: 'ref-c',
label: '参考图C',
src: 'data:image/png;base64,cmVmQw==',
},
],
})}
onSubmit={submitGeneration}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '删除参考图B' }));
expect(screen.queryByText('参考图B')).toBeNull();
expect(screen.getByText('参考图A')).toBeTruthy();
expect(screen.getByText('参考图C')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect(submitGeneration).toHaveBeenCalledWith(
expect.objectContaining({
generationReferences: [
expect.objectContaining({ id: 'ref-a' }),
expect.objectContaining({ id: 'ref-c' }),
],
}),
);
});
it('shows only current image options and opens upward option panels', () => {
render(<BasicGenerationHarness />);
@@ -1666,8 +1666,14 @@ export function ImageCanvasEditorView({
return generateDialog?.mode === 'spec'
? {
label: '规范参考图',
currentCount: 0,
limit: 1,
currentCount:
generateDialog.specType === 'icon'
? 0
: imageReferenceCount(generateDialog.generationReferences),
limit:
generateDialog.specType === 'icon'
? 1
: resolveDialogExtraImageReferenceLimit(generateDialog),
contextId: generateDialogContextId,
}
: null;
@@ -1017,12 +1017,22 @@ function createNormalizedGenerationDialogDraft({
characterView: getNormalizedString(sourceLayer, 'characterView'),
customPrompt: getNormalizedString(sourceLayer, 'customPrompt'),
},
specReference:
resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
)[0] ?? null,
...(specType === 'icon'
? {
specReference:
resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
)[0] ?? null,
}
: {
generationReferences: resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
),
}),
},
sourceLayer,
);
@@ -2104,7 +2114,15 @@ export function appendGenerationReference(
}
return {
...resetFailedGenerationDialog(dialog),
specReference: createCanvasLayerReference(layer),
...(dialog.specType === 'icon' || dialog.specType === undefined
? { specReference: createCanvasLayerReference(layer) }
: {
generationReferences: appendLimitedImageReferences(
dialog.generationReferences,
[createCanvasLayerReference(layer)],
resolveDialogExtraImageReferenceLimit(dialog),
),
}),
composerOpen: true,
};
}
@@ -1875,12 +1875,18 @@ export function buildSpecGenerationInputs(
specType: SpecGenerationType,
values: SpecFormValues,
reference?: CharacterReferenceImage | null,
extraReferences: CharacterReferenceImage[] = [],
): CanvasGenerationInputs {
const references = reference
? createGenerationInputReference('参考图', reference, {
id: 'reference',
})
: [];
const references = [
...(reference ? [reference] : []),
...extraReferences,
].flatMap((item, index) =>
createGenerationInputReference(
index === 0 ? '参考图' : `参考图${index + 1}`,
item,
{ id: index === 0 ? 'reference' : `reference-${index + 1}` },
),
);
if (specType === 'custom') {
return {
version: 2,
@@ -756,6 +756,11 @@ export function buildImageGenerationSubmissionPlan({
},
};
}
const specReferences = dialog.generationReferences?.length
? dialog.generationReferences
: dialog.specReference
? [dialog.specReference]
: [];
return {
kind: 'image',
normalizedPrompt,
@@ -763,18 +768,18 @@ export function buildImageGenerationSubmissionPlan({
prompt: buildSpecPrompt(
specType,
specValues,
Boolean(dialog.specReference?.src),
specReferences.some((reference) => Boolean(reference.src)),
),
size: SPEC_GENERATION_SIZE,
model: SPEC_GENERATION_MODEL,
aspectRatio: SPEC_GENERATION_ASPECT_RATIO,
imageSize: SPEC_GENERATION_IMAGE_SIZE,
kind: 'spec',
...(dialog.specReference?.src
...(specReferences.length
? {
referenceImageSrcs: [
resolveImageReferenceSubmissionSource(dialog.specReference),
],
referenceImageSrcs: specReferences.map(
resolveImageReferenceSubmissionSource,
),
}
: {}),
},
@@ -787,7 +792,8 @@ export function buildImageGenerationSubmissionPlan({
generationInputs: buildSpecGenerationInputs(
specType,
specValues,
dialog.specReference,
specReferences[0],
specReferences.slice(1),
),
},
};
@@ -144,7 +144,13 @@ export function ImageCanvasSpecGenerationPanelView({
: 'spec-reference';
const reference = isUiDesignDialog
? dialog.uiDesignSpecReference
: dialog.specReference;
: dialog.specType === 'icon'
? dialog.specReference
: null;
const extraSpecReferences =
!isUiDesignDialog && dialog.specType !== 'icon'
? (dialog.generationReferences ?? [])
: [];
const isGenerating = dialog.status === 'generating';
const isIconSpec = dialog.mode === 'spec' && dialog.specType === 'icon';
const [playSettingOptimization, setPlaySettingOptimization] =
@@ -278,7 +284,7 @@ export function ImageCanvasSpecGenerationPanelView({
onPointerDown={(event) => event.stopPropagation()}
>
<div className="image-canvas-editor__reference-strip">
{isUiDesignDialog || dialog.specType ? (
{isUiDesignDialog || dialog.specType === 'icon' ? (
<ImageCanvasReferenceSlot
buttonRef={generationReferenceButtonRef}
tone={isUiDesignDialog ? 'ui' : 'spec'}
@@ -337,6 +343,57 @@ export function ImageCanvasSpecGenerationPanelView({
removeLabel={`删除${referenceLabel}`}
/>
) : null}
{extraSpecReferences.map((item, index) => (
<ImageCanvasReferenceSlot
key={item.id}
tone="default"
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
imageSrc={item.src}
objectKey={item.objectKey}
label={`参考图${index + 1}`}
ariaLabel={item.label || `参考图${index + 1}`}
title={item.label}
disabled={isGenerating}
onRemove={
setGenerateDialog
? () =>
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'spec'
? {
...currentDialog,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
generationReferences: (
currentDialog.generationReferences ?? []
).filter(
(referenceItem) =>
referenceItem.id !== item.id,
),
}
: currentDialog,
)
: undefined
}
removeLabel={`删除${item.label || `参考图${index + 1}`}`}
/>
))}
{!isUiDesignDialog && dialog.specType !== 'icon' ? (
<ImageCanvasReferenceSlot
tone="default"
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
label="参考图"
ariaLabel={referenceLabel}
disabled={isGenerating}
isAdd
onClick={openReferenceMenu}
/>
) : null}
{isUiDesignDialog
? (dialog.generationReferences ?? []).map((item, index) => (
<ImageCanvasReferenceSlot
@@ -171,7 +171,15 @@ export function applyGenerationReferenceUpload({
return dialog?.mode === 'spec'
? {
...setFailedGenerationIdle(dialog),
specReference: firstReference,
...(dialog.specType === 'character' || dialog.specType === 'custom'
? {
generationReferences: appendLimitedImageReferences(
dialog.generationReferences,
references,
resolveDialogExtraImageReferenceLimit(dialog),
),
}
: { specReference: firstReference }),
}
: dialog;
}
@@ -420,7 +420,10 @@ function resolveImageGenerationDialogReferences(
dialog: GenerateDialogState,
): CharacterReferenceImage[] {
if (dialog.mode === 'spec') {
return dialog.specReference ? [dialog.specReference] : [];
return [
...(dialog.specReference ? [dialog.specReference] : []),
...(dialog.generationReferences ?? []),
];
}
if (dialog.mode === 'character') {
return [
@@ -528,24 +528,33 @@ export function useImageCanvasUploadWorkflow({
return;
}
const reservation = reserveImageReferenceUpload('spec-reference', 1);
const reservation = reserveImageReferenceUpload(
'spec-reference',
Array.from(files).filter(isImageFile).length,
);
if (!reservation) {
return;
}
try {
const imageSrc = await readImageFileAsDataUrl(imageFile);
const imageFiles = Array.from(files).filter(isImageFile);
const fileReferences = await Promise.all(
imageFiles.map(async (file, index) => ({
file,
index,
imageSrc: await readImageFileAsDataUrl(file),
})),
);
if (!validateImageReferenceUploadReservation(reservation)) {
return;
}
const references = await persistReferenceUploads(
[
{
idPrefix: 'upload-spec-reference',
file: imageFile,
fallbackLabel: '参考图',
imageSrc,
},
],
fileReferences.map(({ file, imageSrc, index }) => ({
idPrefix: 'upload-spec-reference',
index,
file,
fallbackLabel: `参考图${index + 1}`,
imageSrc,
})),
reservation,
);
if (
+8
View File
@@ -7033,6 +7033,14 @@ button.image-canvas-editor__reference-chip:disabled {
transform: scale(1);
}
@media (hover: none) {
.image-canvas-editor__reference-chip-remove {
opacity: 1;
pointer-events: auto;
transform: scale(1);
}
}
.image-canvas-editor__reference-chip-remove:disabled {
cursor: not-allowed;
opacity: 0;