完善快速编辑参考图与生成占位

- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交

- 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示

- 快速编辑提交前同步改写原图引用为实际图序号

- 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象

- 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
This commit is contained in:
2026-06-18 22:39:03 +08:00
parent b04b6cee2b
commit ea29887d9c
65 changed files with 2819 additions and 531 deletions
@@ -1,22 +1,47 @@
import { X } from 'lucide-react';
import { type CSSProperties, type Dispatch, type SetStateAction } from 'react';
import { Cpu, ImageIcon, ImagePlus, X } from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
} from 'react';
import { PlatformActionButton } from '../common/PlatformActionButton';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSelectField, PlatformTextField } from '../common/PlatformTextField';
import { PlatformTextField } from '../common/PlatformTextField';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type {
CanvasLayer,
CharacterReferenceImage,
QuickEditPanelState,
} from './ImageCanvasEditorTypes';
import {
EDITOR_GENERATION_MUD_POINT_CONFIG,
QUICK_EDIT_REFERENCE_LIMIT,
} from './ImageCanvasGenerationModel';
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
type ImageCanvasQuickEditPanelViewProps = {
panel: QuickEditPanelState;
sourceLayer: CanvasLayer;
style: CSSProperties;
sizeOptions: string[];
modelOptions: Array<{ label: string; value: string }>;
referenceButtonRef?: RefObject<HTMLButtonElement | null>;
isReferenceMenuOpen?: boolean;
setIsReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
setIsPickingQuickEditReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
renderEditorPortal?: (node: ReactNode) => ReactNode;
buildPortalMenuStyle?: (
anchor: HTMLElement | null,
placement: 'above' | 'below',
) => CSSProperties;
onRequestUpload?: (target: 'quick-edit-reference') => void;
onSubmit: () => void;
};
@@ -30,118 +55,226 @@ function resetFailedPanelStatus<T extends { status: string; errorMessage?: strin
};
}
function QuickEditReferenceChip({
reference,
index,
onRemove,
}: {
reference: CharacterReferenceImage;
index: number;
onRemove: () => void;
}) {
const label = `图${index + 1}`;
return (
<ImageCanvasReferenceSlot
tone="quick-edit"
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
imageSrc={reference.src}
label={label}
ariaLabel={label}
title={reference.label}
onRemove={onRemove}
removeLabel={`删除${label}`}
/>
);
}
export function ImageCanvasQuickEditPanelView({
panel,
sourceLayer,
style,
sizeOptions,
modelOptions,
referenceButtonRef,
isReferenceMenuOpen = false,
setIsReferenceMenuOpen,
setIsPickingQuickEditReferenceFromCanvas,
setQuickEditPanel,
renderEditorPortal = (node) => node,
buildPortalMenuStyle = () => ({}),
onRequestUpload = () => {},
onSubmit,
}: ImageCanvasQuickEditPanelViewProps) {
const isRedraw = panel.mode === 'redraw';
const isGenerating = panel.status === 'generating';
const panelLabel = isRedraw ? '重绘图片' : '快速编辑图片';
const promptLabel = isRedraw ? '重绘提示词' : '快速编辑提示词';
const references = panel.quickEditReferences ?? [];
const sourceReferenceIndex = references.length + 1;
const canAddReferences =
!isRedraw && !isGenerating && references.length < QUICK_EDIT_REFERENCE_LIMIT;
return (
<form
className="image-canvas-editor__quick-edit-panel"
style={style}
role="dialog"
aria-label={panelLabel}
onPointerDown={(event) => event.stopPropagation()}
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
>
<div className="image-canvas-editor__quick-edit-head">
<div className="image-canvas-editor__quick-edit-reference">
<img src={sourceLayer.src} alt={`${sourceLayer.title}参考图`} />
<span>{sourceLayer.title}</span>
<>
<form
className="image-canvas-editor__quick-edit-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--quick-edit"
style={style}
role="dialog"
aria-label={panelLabel}
onPointerDown={(event) => event.stopPropagation()}
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
>
<div className="image-canvas-editor__reference-strip">
{!isRedraw
? references.map((reference, index) => (
<QuickEditReferenceChip
key={reference.id}
reference={reference}
index={index}
onRemove={() =>
setQuickEditPanel((currentPanel) =>
currentPanel
? {
...resetFailedPanelStatus(currentPanel),
quickEditReferences: (
currentPanel.quickEditReferences ?? []
).filter((item) => item.id !== reference.id),
}
: currentPanel,
)
}
/>
))
: null}
{!isRedraw ? (
<ImageCanvasReferenceSlot
buttonRef={referenceButtonRef}
tone="quick-edit"
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
label="参考图"
ariaLabel="添加快速编辑参考图"
disabled={!canAddReferences}
isAdd
onClick={() => setIsReferenceMenuOpen?.((open) => !open)}
/>
) : null}
<ImageCanvasReferenceSlot
tone="quick-edit"
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
imageSrc={sourceLayer.src}
label={`图${sourceReferenceIndex}`}
ariaLabel={`原图 图${sourceReferenceIndex}`}
title={sourceLayer.title}
/>
</div>
<PlatformTextField
variant="textarea"
aria-label={promptLabel}
value={panel.prompt}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__generation-prompt image-canvas-editor__quick-edit-prompt"
onChange={(event) =>
setQuickEditPanel((currentPanel) =>
currentPanel
? {
...resetFailedPanelStatus(currentPanel),
prompt: event.target.value,
}
: currentPanel,
)
}
/>
{panel.status === 'failed' ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="xs"
className="image-canvas-editor__generate-status"
role="alert"
>
{panel.errorMessage}
</PlatformStatusMessage>
) : null}
<div className="image-canvas-editor__generation-composer-footer image-canvas-editor__quick-edit-footer">
<div className="image-canvas-editor__readonly-generation-option">
<PlatformInlineOptionButton
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
aria-label={`${isRedraw ? '重绘' : '快速编辑'}尺寸 ${panel.size}`}
disabled
>
{panel.size}
</PlatformInlineOptionButton>
</div>
<div className="image-canvas-editor__readonly-generation-option">
<PlatformInlineOptionButton
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
aria-label={`${isRedraw ? '重绘' : '快速编辑'}模型 ${panel.model}`}
disabled
>
<span className="image-canvas-editor__model-trigger-label">
<span
className="image-canvas-editor__model-icon"
aria-hidden="true"
>
<Cpu />
</span>
<span>{panel.model}</span>
</span>
</PlatformInlineOptionButton>
</div>
<PlatformActionButton
type="submit"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__generation-submit image-canvas-editor__quick-edit-submit"
disabled={isGenerating}
aria-label={isGenerating ? '生成中' : '生成'}
>
{isGenerating ? (
'生成中'
) : isRedraw ? (
'生成'
) : (
<>
<span>生成</span>
<span className="image-canvas-editor__mud-point-inline">
{EDITOR_GENERATION_MUD_POINT_CONFIG.image}泥点
</span>
</>
)}
</PlatformActionButton>
</div>
<EditorIconButton
className="image-canvas-editor__generation-close"
label={`关闭${panelLabel}`}
title="关闭"
icon={X}
variant="surfaceFloating"
disabled={isGenerating}
onClick={() => setQuickEditPanel(null)}
/>
</div>
<PlatformTextField
variant="textarea"
aria-label={promptLabel}
value={panel.prompt}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__quick-edit-prompt"
onChange={(event) =>
setQuickEditPanel((currentPanel) =>
currentPanel
? {
...resetFailedPanelStatus(currentPanel),
prompt: event.target.value,
}
: currentPanel,
</form>
{!isRedraw && isReferenceMenuOpen && referenceButtonRef
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
label="快速编辑参考图来源"
placement="top-start"
style={buildPortalMenuStyle(referenceButtonRef.current, 'above')}
>
<PlatformFloatingMenuItem
onClick={() => {
setIsReferenceMenuOpen?.(false);
setIsPickingQuickEditReferenceFromCanvas?.(true);
}}
>
从画布中选择
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
onClick={() => {
setIsReferenceMenuOpen?.(false);
setIsPickingQuickEditReferenceFromCanvas?.(false);
onRequestUpload('quick-edit-reference');
}}
>
上传图片
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
}
/>
<div className="image-canvas-editor__quick-edit-controls">
<PlatformSelectField
aria-label={isRedraw ? '重绘尺寸' : '快速编辑尺寸'}
value={panel.size}
disabled={isGenerating}
size="xs"
density="compact"
onChange={(event) =>
setQuickEditPanel((currentPanel) =>
currentPanel
? { ...currentPanel, size: event.target.value }
: currentPanel,
)
}
>
{sizeOptions.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</PlatformSelectField>
<PlatformSelectField
aria-label={isRedraw ? '重绘模型' : '快速编辑模型'}
value={panel.model}
disabled={isGenerating}
size="xs"
density="compact"
onChange={(event) =>
setQuickEditPanel((currentPanel) =>
currentPanel
? { ...currentPanel, model: event.target.value }
: currentPanel,
)
}
>
{modelOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</PlatformSelectField>
</div>
{panel.status === 'failed' ? (
<PlatformStatusMessage tone="error" surface="platform" size="xs" role="alert">
{panel.errorMessage}
</PlatformStatusMessage>
) : null}
<PlatformActionButton
type="submit"
tone="secondary"
size="sm"
className="image-canvas-editor__quick-edit-submit"
disabled={isGenerating}
>
{isGenerating ? '生成中' : '生成'}
</PlatformActionButton>
</form>
: null}
</>
);
}