0ebf6242bb
新增宣发素材入口、工作流模型、画布生成卡片和参考图选择能力。 接入宣发素材真实生图请求,隐藏 Prompt 摘要和约束预览,并将约束作为内部 Prompt 使用。 支持游戏首图、详情五图、运营海报尺寸,其中详情五图一次生成五张 720x1280 竖图。 后端仅对 publication-material 生图扣 5 泥点,并沿用现有失败退款逻辑。 补充前后端测试和宣发素材设计文档。
394 lines
14 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|