Files
Genarrative/src/components/image-editor/ImageCanvasSpecGenerationPanelView.tsx
T
k88936 d1562ab65f
Project CI / Repository checks (pull_request) Successful in 1m35s
Project CI / Frontend tests (pull_request) Successful in 3m18s
Project CI / Backend tests (pull_request) Successful in 4m14s
Project CI / Native shell tests (pull_request) Successful in 16m18s
合并主分支并适配图标精灵图编辑
合入统一 Lexical 文本编辑器与滚动条样式调整
保留图标规范字段优化、字符限制与提交门禁
补齐图标生成及画布集成测试的 Lexical 交互适配
2026-08-08 15:23:29 +08:00

815 lines
32 KiB
TypeScript

import {
ClipboardList,
Cpu,
ImagePlus,
Loader2,
Sparkles,
Undo2,
} from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import {
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
refineEditorIconSpecArtStyle,
refineEditorIconSpecPlaySetting,
} from '../../services/image-editor/editorProjectClient';
import { AutoGrowTextArea } from '../common/AutoGrowTextArea';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import {
PlatformSelectField,
PlatformTextField,
} from '../common/PlatformTextField';
import type {
GenerateDialogState,
SpecFormValues,
SpecGenerationType,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
import {
calculateEditorSpecGenerationPrice,
calculateEditorUiDesignPrice,
CHARACTER_SPEC_VIEW_OPTIONS,
getEditorImageModelDisplayName,
IMAGE_MODEL_GPT_IMAGE_2,
resolveEditorImageSizeLabel,
SPEC_GENERATION_ASPECT_RATIO,
SPEC_GENERATION_IMAGE_SIZE,
SPEC_GENERATION_MODEL,
} from './ImageCanvasGenerationModel';
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
type ImageCanvasSpecGenerationPanelViewProps = {
dialog: GenerateDialogState;
style: CSSProperties;
isGenerationReferenceMenuOpen?: boolean;
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
setGenerateDialog?: Dispatch<SetStateAction<GenerateDialogState | null>>;
renderEditorPortal?: (node: ReactNode) => ReactNode;
buildPortalMenuStyle?: (
anchor: HTMLElement | null,
placement: 'above' | 'below',
) => CSSProperties;
onOpenSpecDialog?: (specType: SpecGenerationType) => void;
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
onRequestUpload: (target: UploadTarget) => void;
onRememberImageModel?: (model: string) => void;
hasPendingImageReferenceUploads?: boolean;
onSubmit: (dialog: GenerateDialogState) => void;
};
function shouldShowSpecInputPlaceholders(dialog: GenerateDialogState) {
return dialog.specType === 'character' || dialog.specType === 'icon';
}
type IconSpecOptimizationState = {
optimizing: boolean;
undoValue: string | null;
};
const INITIAL_ICON_SPEC_OPTIMIZATION_STATE: IconSpecOptimizationState = {
optimizing: false,
undoValue: null,
};
function getIconSpecPromptLength(value: string) {
return Array.from(value).length;
}
function limitIconSpecPrompt(value: string) {
return Array.from(value)
.slice(0, EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH)
.join('');
}
function isValidIconSpecPrompt(value: string | undefined) {
const normalized = value?.trim() ?? '';
return (
normalized.length > 0 &&
getIconSpecPromptLength(normalized) <= EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH
);
}
export function ImageCanvasSpecGenerationPanelView({
dialog,
style,
isGenerationReferenceMenuOpen = false,
generationReferenceButtonRef,
setIsGenerationReferenceMenuOpen,
setIsPickingGenerationReferenceFromCanvas,
setGenerateDialog,
renderEditorPortal = (node) => node,
buildPortalMenuStyle = () => ({}),
onOpenSpecDialog,
onUpdateSpecFormValue,
onRequestUpload,
onRememberImageModel = () => {},
hasPendingImageReferenceUploads = false,
onSubmit,
}: ImageCanvasSpecGenerationPanelViewProps) {
const isUiDesignDialog = dialog.mode === 'ui-design';
const showSpecInputPlaceholders = shouldShowSpecInputPlaceholders(dialog);
const specSizeLabel = resolveEditorImageSizeLabel({
aspectRatio: SPEC_GENERATION_ASPECT_RATIO,
imageSize: SPEC_GENERATION_IMAGE_SIZE,
});
const specModelLabel = getEditorImageModelDisplayName(SPEC_GENERATION_MODEL);
const referenceLabel = isUiDesignDialog ? 'UI设计图标规范' : '参考图';
const referenceSlotLabel = isUiDesignDialog
? '图标规范'
: dialog.specType === 'character'
? '角色规范'
: '参考图';
const uploadTarget: UploadTarget = isUiDesignDialog
? 'ui-design-icon-spec'
: 'spec-reference';
const reference = isUiDesignDialog
? dialog.uiDesignSpecReference
: dialog.specReference;
const isGenerating = dialog.status === 'generating';
const isIconSpec = dialog.mode === 'spec' && dialog.specType === 'icon';
const [playSettingOptimization, setPlaySettingOptimization] =
useState<IconSpecOptimizationState>(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
const [artStyleOptimization, setArtStyleOptimization] =
useState<IconSpecOptimizationState>(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
const [optimizationError, setOptimizationError] = useState<string | null>(
null,
);
const optimizationDialogKey = `${dialog.id ?? 'active'}:${dialog.mode}:${dialog.specType ?? ''}`;
const activeOptimizationDialogKeyRef = useRef<string | null>(
optimizationDialogKey,
);
const optimizationRequestVersionsRef = useRef({
playSetting: 0,
artStyle: 0,
});
useEffect(() => {
activeOptimizationDialogKeyRef.current = optimizationDialogKey;
optimizationRequestVersionsRef.current.playSetting += 1;
optimizationRequestVersionsRef.current.artStyle += 1;
setPlaySettingOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
setArtStyleOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
setOptimizationError(null);
return () => {
if (activeOptimizationDialogKeyRef.current === optimizationDialogKey) {
activeOptimizationDialogKeyRef.current = null;
}
};
}, [optimizationDialogKey]);
const isOptimizingIconSpec =
playSettingOptimization.optimizing || artStyleOptimization.optimizing;
const hasRequiredIconSpecValues =
isValidIconSpecPrompt(dialog.specValues?.playSetting) &&
isValidIconSpecPrompt(dialog.specValues?.artStyle);
const canSubmit =
!isGenerating &&
!hasPendingImageReferenceUploads &&
(!isIconSpec || (hasRequiredIconSpecValues && !isOptimizingIconSpec));
const specGenerationCost = calculateEditorSpecGenerationPrice(
SPEC_GENERATION_MODEL,
);
useImageCanvasFloatingOptionDismiss({
isOpen: isGenerationReferenceMenuOpen,
boundaryRefs: [generationReferenceButtonRef],
onDismiss: () => setIsGenerationReferenceMenuOpen?.(false),
});
const openReferenceMenu = () => {
if (setIsGenerationReferenceMenuOpen) {
setIsGenerationReferenceMenuOpen((open) => !open);
return;
}
onRequestUpload(uploadTarget);
};
const optimizeIconSpecField = async (field: 'playSetting' | 'artStyle') => {
const isPlaySetting = field === 'playSetting';
const key: keyof SpecFormValues = isPlaySetting
? 'playSetting'
: 'artStyle';
const undoValue = dialog.specValues?.[key] ?? '';
const value = undoValue.trim();
if (!value) {
return;
}
const setOptimization = isPlaySetting
? setPlaySettingOptimization
: setArtStyleOptimization;
const requestVersion = optimizationRequestVersionsRef.current[field] + 1;
optimizationRequestVersionsRef.current[field] = requestVersion;
const requestDialogKey = optimizationDialogKey;
setOptimization((current) => ({ ...current, optimizing: true }));
setOptimizationError(null);
try {
const refined = isPlaySetting
? await refineEditorIconSpecPlaySetting(value)
: await refineEditorIconSpecArtStyle(value);
if (
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
optimizationRequestVersionsRef.current[field] !== requestVersion
) {
return;
}
onUpdateSpecFormValue(key, refined);
setOptimization({ optimizing: false, undoValue });
} catch (error) {
if (
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
optimizationRequestVersionsRef.current[field] !== requestVersion
) {
return;
}
setOptimization((current) => ({ ...current, optimizing: false }));
const fallbackMessage = isPlaySetting
? '优化玩法设定失败'
: '优化美术风格失败';
const errorMessage =
error instanceof Error && error.message.trim()
? error.message
: fallbackMessage;
setOptimizationError(errorMessage);
}
};
const undoIconSpecOptimization = (field: 'playSetting' | 'artStyle') => {
const isPlaySetting = field === 'playSetting';
const key: keyof SpecFormValues = isPlaySetting
? 'playSetting'
: 'artStyle';
const state = isPlaySetting
? playSettingOptimization
: artStyleOptimization;
if (state.undoValue === null) {
return;
}
onUpdateSpecFormValue(key, state.undoValue);
(isPlaySetting ? setPlaySettingOptimization : setArtStyleOptimization)(
INITIAL_ICON_SPEC_OPTIMIZATION_STATE,
);
setOptimizationError(null);
};
return (
<>
<div
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__spec-composer"
style={style}
role="dialog"
aria-label={isUiDesignDialog ? '生成UI设计图' : '生成规范'}
onPointerDown={(event) => event.stopPropagation()}
>
<div className="image-canvas-editor__reference-strip">
{isUiDesignDialog || dialog.specType ? (
<ImageCanvasReferenceSlot
buttonRef={generationReferenceButtonRef}
tone={isUiDesignDialog ? 'ui' : 'spec'}
icon={
isUiDesignDialog ? (
<ImagePlus className="h-4 w-4" aria-hidden="true" />
) : (
<ClipboardList className="h-4 w-4" aria-hidden="true" />
)
}
imageSrc={reference?.src}
objectKey={reference?.objectKey}
label={referenceSlotLabel}
ariaLabel={referenceLabel}
title={reference?.label ?? referenceLabel}
disabled={isGenerating}
isAdd={!reference}
onClick={openReferenceMenu}
onRemove={
reference && setGenerateDialog
? () =>
setGenerateDialog((currentDialog) => {
if (isUiDesignDialog) {
return currentDialog?.mode === 'ui-design'
? {
...currentDialog,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
uiDesignSpecReference: null,
}
: currentDialog;
}
return currentDialog?.mode === 'spec'
? {
...currentDialog,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
specReference: null,
}
: currentDialog;
})
: undefined
}
removeLabel={`删除${referenceLabel}`}
/>
) : null}
{isUiDesignDialog
? (dialog.generationReferences ?? []).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 === 'ui-design'
? {
...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}`}`}
/>
))
: null}
{isUiDesignDialog ? (
<ImageCanvasReferenceSlot
tone="default"
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
label="参考图"
ariaLabel="上传UI设计参考图"
disabled={isGenerating}
isAdd
onClick={() => onRequestUpload('generation-reference')}
/>
) : null}
</div>
<div className="image-canvas-editor__spec-fields">
{isUiDesignDialog ? (
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
<AutoGrowTextArea
aria-label="UI设计要求"
value={dialog.prompt}
disabled={isGenerating}
placeholder="你希望这个 UI 长什么样?"
className="image-canvas-editor__generation-prompt"
onValueChange={(nextPrompt) => {
if (setGenerateDialog) {
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'ui-design'
? {
...currentDialog,
prompt: nextPrompt,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
}
: currentDialog,
);
return;
}
onUpdateSpecFormValue('customPrompt', nextPrompt);
}}
/>
</div>
) : null}
{!isUiDesignDialog && dialog.specType === 'custom' ? (
<div className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
自定义规范提示词
</PlatformFieldLabel>
<AutoGrowTextArea
aria-label="自定义规范提示词"
value={dialog.specValues?.customPrompt ?? ''}
placeholder="写下你想要的任何内容,陶泥儿帮你形成新的约束"
disabled={isGenerating}
className="image-canvas-editor__generation-prompt image-canvas-editor__spec-textarea"
onValueChange={(value) =>
onUpdateSpecFormValue('customPrompt', value)
}
/>
</div>
) : null}
{!isUiDesignDialog && isIconSpec ? (
<>
{(
[
{
field: 'playSetting' as const,
key: 'playSetting' as const,
title: '玩法设定',
placeholder: '游戏的核心玩法是什么?',
optimization: playSettingOptimization,
},
{
field: 'artStyle' as const,
key: 'artStyle' as const,
title: '美术风格',
placeholder: '游戏的画风是怎样的?',
optimization: artStyleOptimization,
},
] as const
).map((item) => {
const value = dialog.specValues?.[item.key] ?? '';
return (
<div
key={item.field}
className="image-canvas-editor__field-block"
>
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
{item.title}
</PlatformFieldLabel>
<AutoGrowTextArea
aria-label={item.title}
value={value}
placeholder={item.placeholder}
disabled={isGenerating || item.optimization.optimizing}
className="image-canvas-editor__generation-prompt image-canvas-editor__spec-textarea"
onValueChange={(nextValue) => {
onUpdateSpecFormValue(
item.key,
limitIconSpecPrompt(nextValue),
);
(item.field === 'playSetting'
? setPlaySettingOptimization
: setArtStyleOptimization)(
INITIAL_ICON_SPEC_OPTIMIZATION_STATE,
);
setOptimizationError(null);
}}
/>
<div className="flex items-center gap-1">
<PlatformActionButton
type="button"
tone="ghost"
size="xxs"
shape="pill"
className="!gap-1 !px-2 !py-0.5 !text-[12px] !leading-3"
aria-label={`一键优化${item.title}`}
disabled={
isGenerating ||
item.optimization.optimizing ||
!isValidIconSpecPrompt(value)
}
onClick={() => optimizeIconSpecField(item.field)}
>
{item.optimization.optimizing ? (
<>
<Loader2
className="h-3 w-3 animate-spin"
aria-hidden="true"
/>
正在优化
</>
) : (
<>
<Sparkles className="h-3 w-3" aria-hidden="true" />
一键优化
</>
)}
</PlatformActionButton>
{item.optimization.undoValue !== null &&
!item.optimization.optimizing ? (
<PlatformActionButton
type="button"
tone="ghost"
size="xxs"
shape="pill"
className="!gap-1 !px-2 !py-0.5 !text-[12px] !leading-3"
aria-label={`撤销${item.title}优化`}
disabled={isGenerating}
onClick={() => undoIconSpecOptimization(item.field)}
>
<Undo2 className="h-3 w-3" aria-hidden="true" />
撤销
</PlatformActionButton>
) : null}
<span
className="ml-auto text-[10px] leading-4 tabular-nums text-slate-400"
aria-label={`${item.title}字符计数`}
>
{getIconSpecPromptLength(value)}/
{EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH}
</span>
</div>
</div>
);
})}
</>
) : null}
{!isUiDesignDialog && dialog.specType !== 'custom' && !isIconSpec ? (
<>
<label className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
玩法设定
</PlatformFieldLabel>
<PlatformTextField
aria-label="玩法设定"
value={dialog.specValues?.playSetting ?? ''}
placeholder={
showSpecInputPlaceholders
? '这是什么类型的游戏?'
: undefined
}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__spec-input"
onChange={(event) =>
onUpdateSpecFormValue('playSetting', event.target.value)
}
/>
</label>
<label className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
美术风格
</PlatformFieldLabel>
<PlatformTextField
aria-label="美术风格"
value={dialog.specValues?.artStyle ?? ''}
placeholder={
showSpecInputPlaceholders
? '游戏的画风是怎样的?'
: undefined
}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__spec-input"
onChange={(event) =>
onUpdateSpecFormValue('artStyle', event.target.value)
}
/>
</label>
{dialog.specType === 'character' ? (
<>
<label className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
头身比
</PlatformFieldLabel>
<PlatformSelectField
aria-label="头身比"
value={dialog.specValues?.bodyRatio ?? '3'}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__spec-input"
onChange={(event) =>
onUpdateSpecFormValue('bodyRatio', event.target.value)
}
>
{['2', '3', '4', '5', '6'].map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</PlatformSelectField>
</label>
<label className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="form"
className="image-canvas-editor__field-title"
>
角色视角
</PlatformFieldLabel>
<PlatformSelectField
aria-label="角色视角"
value={dialog.specValues?.characterView ?? ''}
disabled={isGenerating}
size="sm"
density="compact"
className="image-canvas-editor__spec-input"
onChange={(event) =>
onUpdateSpecFormValue(
'characterView',
event.target.value,
)
}
>
{CHARACTER_SPEC_VIEW_OPTIONS.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</PlatformSelectField>
</label>
</>
) : null}
</>
) : null}
</div>
{dialog.status === 'failed' ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="xs"
className="image-canvas-editor__generate-status"
role="alert"
>
{dialog.errorMessage}
</PlatformStatusMessage>
) : null}
{optimizationError ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="xs"
className="image-canvas-editor__generate-status"
role="alert"
>
{optimizationError}
</PlatformStatusMessage>
) : null}
<div className="image-canvas-editor__generation-composer-footer image-canvas-editor__spec-footer">
{isUiDesignDialog ? (
<ImageCanvasGenerationImageOptionsView
dialog={dialog}
setGenerateDialog={setGenerateDialog ?? (() => undefined)}
includeDimensions
onRememberImageModel={onRememberImageModel}
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
lockedModel={IMAGE_MODEL_GPT_IMAGE_2}
cost={calculateEditorUiDesignPrice(
IMAGE_MODEL_GPT_IMAGE_2,
dialog.imageSize,
)}
submitLabel="生成"
submitAriaLabel="生成UI设计图"
onSubmitRequest={() => {
if (dialog.status !== 'generating') {
onSubmit(dialog);
}
}}
optionLabelPrefix="生成图片"
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
) : (
<>
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions image-canvas-editor__readonly-generation-option">
<PlatformInlineOptionButton
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
aria-label={`生成图片尺寸 ${specSizeLabel}`}
disabled
>
{specSizeLabel}
</PlatformInlineOptionButton>
</div>
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__readonly-generation-option">
<PlatformInlineOptionButton
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
aria-label={`生成图片模型 ${specModelLabel}`}
disabled
>
<span className="image-canvas-editor__model-trigger-label">
<span
className="image-canvas-editor__model-icon"
aria-hidden="true"
>
<Cpu />
</span>
<span>{specModelLabel}</span>
</span>
</PlatformInlineOptionButton>
</div>
<PlatformActionButton
type="button"
tone="secondary"
size="xs"
shape="pill"
className="image-canvas-editor__generation-submit image-canvas-editor__spec-submit"
disabled={!canSubmit}
aria-label="提交生成规范"
onClick={() => {
if (dialog.status !== 'generating') {
onSubmit(dialog);
}
}}
>
{isGenerating ? (
'生成中'
) : (
<>
<span>生成</span>
<span className="image-canvas-editor__mud-point-inline">
{specGenerationCost}泥点
</span>
</>
)}
</PlatformActionButton>
</>
)}
</div>
</div>
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
label="参考图来源"
placement="top-start"
style={buildPortalMenuStyle(
generationReferenceButtonRef.current,
'above',
)}
>
<PlatformFloatingMenuItem
onClick={() => {
setIsGenerationReferenceMenuOpen?.(false);
setIsPickingGenerationReferenceFromCanvas?.(true);
}}
>
从画布中选择
</PlatformFloatingMenuItem>
{isUiDesignDialog ? (
<PlatformFloatingMenuItem
onClick={() => {
setIsGenerationReferenceMenuOpen?.(false);
onOpenSpecDialog?.('icon');
}}
>
新建图标规范
</PlatformFloatingMenuItem>
) : null}
<PlatformFloatingMenuItem
onClick={() => {
setIsGenerationReferenceMenuOpen?.(false);
setIsPickingGenerationReferenceFromCanvas?.(false);
onRequestUpload(uploadTarget);
}}
>
上传图片
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
: null}
</>
);
}