Files
Genarrative/src/components/image-editor/ImageCanvasGenerationComposerView.tsx
T
Codex 0ebf6242bb 完成图片编辑器宣发素材生成
新增宣发素材入口、工作流模型、画布生成卡片和参考图选择能力。

接入宣发素材真实生图请求,隐藏 Prompt 摘要和约束预览,并将约束作为内部 Prompt 使用。

支持游戏首图、详情五图、运营海报尺寸,其中详情五图一次生成五张 720x1280 竖图。

后端仅对 publication-material 生图扣 5 泥点,并沿用现有失败退款逻辑。

补充前后端测试和宣发素材设计文档。
2026-06-18 00:05:25 +08:00

394 lines
14 KiB
TypeScript

import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
} from 'react';
import { createPortal } from 'react-dom';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView';
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
import { ImageCanvasPublicationMaterialsDemoPanelView } from './ImageCanvasPublicationMaterialsDemoPanelView';
import {
getPublicationMaterialsWorkflow,
PUBLICATION_MATERIALS_WORKFLOWS,
} from './ImageCanvasPublicationMaterialsModel';
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView';
import { SPEC_TYPE_LABEL } from './ImageCanvasGenerationModel';
import type {
CharacterAnimationPanelState,
CanvasLayer,
GenerateDialogState,
QuickEditPanelState,
SpecFormValues,
SpecGenerationType,
UploadTarget,
} from './ImageCanvasEditorTypes';
type ImageCanvasGenerationComposerViewProps = {
specToolWrapRef: RefObject<HTMLSpanElement | null>;
publicationToolWrapRef: RefObject<HTMLSpanElement | null>;
publicationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
isSpecMenuOpen: boolean;
isCharacterSpecMenuOpen: boolean;
isCharacterReferenceMenuOpen: boolean;
isIconSpecMenuOpen: boolean;
isPublicationMenuOpen: boolean;
isPublicationReferenceMenuOpen: boolean;
isPickingCharacterSpecFromCanvas: boolean;
isPickingCharacterReferenceFromCanvas: boolean;
isPickingIconSpecFromCanvas: boolean;
isPickingPublicationReferenceFromCanvas: boolean;
generateDialog: GenerateDialogState | null;
generationComposerStyle: CSSProperties | null;
iconComposerStyle: CSSProperties | null;
quickEditPanel: QuickEditPanelState | null;
quickEditSourceLayer: CanvasLayer | null;
quickEditPanelStyle: CSSProperties | null;
quickEditSizeOptions: string[];
quickEditModelOptions: Array<{ label: string; value: string }>;
characterAnimationPanel: CharacterAnimationPanelState | null;
characterAnimationSourceLayer: CanvasLayer | null;
characterAnimationPanelStyle: CSSProperties | null;
characterAnimationPrice: number;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
setCharacterAnimationPanel: Dispatch<
SetStateAction<CharacterAnimationPanelState | null>
>;
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsPublicationMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsPublicationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
setIsPickingPublicationReferenceFromCanvas: Dispatch<
SetStateAction<boolean>
>;
onOpenSpecDialog: (specType: SpecGenerationType) => void;
onOpenPublicationWorkflow: (
workflowId: NonNullable<GenerateDialogState['publicationWorkflowId']>,
) => void;
onRequestUpload: (target: UploadTarget) => void;
onSubmitImageGeneration: (dialog: GenerateDialogState) => void;
onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void;
onSubmitQuickEdit: () => void;
onSubmitCharacterAnimation: () => void;
onCloseGenerateComposer: () => void;
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
onUpdateIconDescription: (index: number, value: string) => void;
onAddIconDescription: () => void;
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
onRememberImageModel: (model: string) => void;
};
function buildPortalMenuStyle(
anchor: HTMLElement | null,
placement: 'above' | 'below',
): CSSProperties {
const rect = anchor?.getBoundingClientRect();
if (!rect) {
return {
position: 'fixed',
left: 0,
top: 0,
right: 'auto',
bottom: 'auto',
zIndex: 70,
};
}
return {
position: 'fixed',
left: Math.round(rect.left),
top:
placement === 'above'
? Math.round(rect.top)
: Math.round(rect.bottom + 8),
right: 'auto',
bottom: 'auto',
zIndex: 70,
transform:
placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined,
};
}
function renderEditorPortal(node: ReactNode) {
if (typeof document === 'undefined') {
return node;
}
return createPortal(node, document.body);
}
export function ImageCanvasGenerationComposerView({
specToolWrapRef,
publicationToolWrapRef,
publicationReferenceButtonRef,
characterSpecButtonRef,
characterReferenceButtonRef,
iconSpecButtonRef,
isSpecMenuOpen,
isCharacterSpecMenuOpen,
isCharacterReferenceMenuOpen,
isIconSpecMenuOpen,
isPublicationMenuOpen,
isPublicationReferenceMenuOpen,
isPickingCharacterSpecFromCanvas,
isPickingCharacterReferenceFromCanvas,
isPickingIconSpecFromCanvas,
isPickingPublicationReferenceFromCanvas,
generateDialog,
generationComposerStyle,
iconComposerStyle,
quickEditPanel,
quickEditSourceLayer,
quickEditPanelStyle,
quickEditSizeOptions,
quickEditModelOptions,
characterAnimationPanel,
characterAnimationSourceLayer,
characterAnimationPanelStyle,
characterAnimationPrice,
setGenerateDialog,
setQuickEditPanel,
setCharacterAnimationPanel,
setIsCharacterSpecMenuOpen,
setIsCharacterReferenceMenuOpen,
setIsIconSpecMenuOpen,
setIsPublicationMenuOpen,
setIsPublicationReferenceMenuOpen,
setIsPickingCharacterSpecFromCanvas,
setIsPickingCharacterReferenceFromCanvas,
setIsPickingIconSpecFromCanvas,
setIsPickingPublicationReferenceFromCanvas,
onOpenSpecDialog,
onOpenPublicationWorkflow,
onRequestUpload,
onSubmitImageGeneration,
onSubmitIconSpritesheetGeneration,
onSubmitQuickEdit,
onSubmitCharacterAnimation,
onCloseGenerateComposer,
onUpdateSpecFormValue,
onUpdateIconDescription,
onAddIconDescription,
onUpdateCharacterAnimationDuration,
onRememberImageModel,
}: ImageCanvasGenerationComposerViewProps) {
return (
<>
{isSpecMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
label="生成规范类型"
placement="top-start"
style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
>
{(['character', 'ui', 'custom'] as const).map((specType) => (
<PlatformFloatingMenuItem
key={specType}
className="image-canvas-editor__spec-menu-item"
onClick={() => onOpenSpecDialog(specType)}
>
{SPEC_TYPE_LABEL[specType]}
</PlatformFloatingMenuItem>
))}
</PlatformFloatingMenu>,
)
: null}
{isPublicationMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__publication-menu image-canvas-editor__portal-menu"
label="宣发素材类型"
placement="top-start"
style={buildPortalMenuStyle(
publicationToolWrapRef.current,
'above',
)}
>
{PUBLICATION_MATERIALS_WORKFLOWS.map((workflow) => (
<PlatformFloatingMenuItem
key={workflow.id}
className="image-canvas-editor__publication-menu-item"
onClick={() => onOpenPublicationWorkflow(workflow.id)}
>
{workflow.label}
</PlatformFloatingMenuItem>
))}
</PlatformFloatingMenu>,
)
: null}
{generateDialog?.mode === 'generate' &&
generateDialog.composerOpen !== false &&
generationComposerStyle ? (
<ImageCanvasBasicGenerationComposerView
dialog={generateDialog}
style={generationComposerStyle}
setGenerateDialog={setGenerateDialog}
onRequestUpload={onRequestUpload}
onSubmit={onSubmitImageGeneration}
onClose={onCloseGenerateComposer}
/>
) : null}
{generateDialog?.mode === 'publication' &&
generateDialog.composerOpen !== false &&
generationComposerStyle ? (
<ImageCanvasPublicationMaterialsDemoPanelView
dialog={generateDialog}
workflow={getPublicationMaterialsWorkflow(
generateDialog.publicationWorkflowId ?? 'publication-cover-image',
)}
style={generationComposerStyle}
publicationReferenceButtonRef={publicationReferenceButtonRef}
isPublicationReferenceMenuOpen={isPublicationReferenceMenuOpen}
setGenerateDialog={setGenerateDialog}
setIsPublicationReferenceMenuOpen={
setIsPublicationReferenceMenuOpen
}
setIsPickingPublicationReferenceFromCanvas={
setIsPickingPublicationReferenceFromCanvas
}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
onRequestUpload={onRequestUpload}
onSubmit={onSubmitImageGeneration}
/>
) : null}
{generateDialog?.mode === 'spec' &&
generateDialog.composerOpen !== false &&
generationComposerStyle ? (
<ImageCanvasSpecGenerationPanelView
dialog={generateDialog}
style={generationComposerStyle}
onUpdateSpecFormValue={onUpdateSpecFormValue}
onRequestUpload={onRequestUpload}
onSubmit={onSubmitImageGeneration}
/>
) : null}
{generateDialog?.mode === 'character' && generationComposerStyle ? (
<ImageCanvasCharacterGenerationComposerView
dialog={generateDialog}
style={generationComposerStyle}
characterSpecButtonRef={characterSpecButtonRef}
characterReferenceButtonRef={characterReferenceButtonRef}
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
isCharacterReferenceMenuOpen={isCharacterReferenceMenuOpen}
setGenerateDialog={setGenerateDialog}
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
setIsCharacterReferenceMenuOpen={setIsCharacterReferenceMenuOpen}
setIsPickingCharacterSpecFromCanvas={
setIsPickingCharacterSpecFromCanvas
}
setIsPickingCharacterReferenceFromCanvas={
setIsPickingCharacterReferenceFromCanvas
}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
onOpenSpecDialog={onOpenSpecDialog}
onRequestUpload={onRequestUpload}
onRememberImageModel={onRememberImageModel}
onSubmit={onSubmitImageGeneration}
/>
) : null}
{generateDialog?.mode === 'icon' &&
generateDialog.composerOpen !== false &&
iconComposerStyle ? (
<ImageCanvasIconSpritesheetComposerView
dialog={generateDialog}
style={iconComposerStyle}
iconSpecButtonRef={iconSpecButtonRef}
isIconSpecMenuOpen={isIconSpecMenuOpen}
setGenerateDialog={setGenerateDialog}
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
onOpenSpecDialog={onOpenSpecDialog}
onRequestUpload={onRequestUpload}
onUpdateIconDescription={onUpdateIconDescription}
onAddIconDescription={onAddIconDescription}
onRememberImageModel={onRememberImageModel}
onSubmit={onSubmitIconSpritesheetGeneration}
/>
) : null}
{isPickingCharacterSpecFromCanvas ? (
<div className="image-canvas-editor__canvas-pick-hint">
请选择画布中的图片作为角色形象规范,按 Esc 退出
</div>
) : null}
{isPickingCharacterReferenceFromCanvas ? (
<div className="image-canvas-editor__canvas-pick-hint">
请选择画布中的图片作为常规参考图,按 Esc 退出
</div>
) : null}
{isPickingIconSpecFromCanvas ? (
<div className="image-canvas-editor__canvas-pick-hint">
请选择画布中的图标素材规范,按 Esc 退出
</div>
) : null}
{isPickingPublicationReferenceFromCanvas ? (
<div className="image-canvas-editor__canvas-pick-hint">
请选择画布中的图片作为宣发素材参考图,按 Esc 退出
</div>
) : null}
{quickEditPanel &&
quickEditPanel.status !== 'generating' &&
quickEditSourceLayer &&
quickEditPanelStyle ? (
<ImageCanvasQuickEditPanelView
panel={quickEditPanel}
sourceLayer={quickEditSourceLayer}
style={quickEditPanelStyle}
sizeOptions={quickEditSizeOptions}
modelOptions={quickEditModelOptions}
setQuickEditPanel={setQuickEditPanel}
onSubmit={onSubmitQuickEdit}
/>
) : null}
{characterAnimationPanel &&
characterAnimationSourceLayer &&
characterAnimationPanelStyle ? (
<ImageCanvasCharacterAnimationPanelView
panel={characterAnimationPanel}
style={characterAnimationPanelStyle}
price={characterAnimationPrice}
setCharacterAnimationPanel={setCharacterAnimationPanel}
onUpdateDuration={onUpdateCharacterAnimationDuration}
onSubmit={onSubmitCharacterAnimation}
/>
) : null}
<ImageCanvasEditGenerationModalView
dialog={generateDialog}
setGenerateDialog={setGenerateDialog}
onSubmit={onSubmitImageGeneration}
/>
</>
);
}