Files
Genarrative/src/components/image-editor/ImageCanvasIconSpritesheetComposerView.tsx
T
k88936 99d239c8bf
Project CI / Repository checks (push) Successful in 50s
Project CI / Backend tests (push) Successful in 3m30s
Project CI / Frontend tests (push) Successful in 3m42s
Project CI / Native shell tests (push) Successful in 11m34s
style: 画布编辑器多行输入框样式调整 (#130)
复用了画布agent的输入框:

* 高度自适应
* 滑动条样式变为浅色和位置不再出界

before
![shotmd-1785745933-compressed.webp](/attachments/95727f6d-63e1-48f7-b806-8203cd73d1eb)
after
![shotmd-1785745957-compressed.webp](/attachments/b5af077d-d3b9-4572-9135-39e288291e24)

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/130
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-04 17:07:30 +08:00

253 lines
9.0 KiB
TypeScript

import { ImageIcon, ImagePlus } from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
} from 'react';
import { AutoGrowTextArea } from '../common/AutoGrowTextArea';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import type {
GenerateDialogState,
SpecGenerationType,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
import {
calculateEditorIconSpritesheetPrice,
} from './ImageCanvasGenerationModel';
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
type ImageCanvasIconSpritesheetComposerViewProps = {
dialog: GenerateDialogState;
style: CSSProperties;
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
isIconSpecMenuOpen: boolean;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
renderEditorPortal: (node: ReactNode) => ReactNode;
buildPortalMenuStyle: (
anchor: HTMLElement | null,
placement: 'above' | 'below',
) => CSSProperties;
onOpenSpecDialog: (specType: SpecGenerationType) => void;
onRequestUpload: (target: UploadTarget) => void;
onUpdateIconDescriptionText: (value: string) => void;
onRememberImageModel: (model: string) => void;
onSubmit: (dialog: GenerateDialogState) => void;
};
export function ImageCanvasIconSpritesheetComposerView({
dialog,
style,
iconSpecButtonRef,
isIconSpecMenuOpen,
setGenerateDialog,
setIsIconSpecMenuOpen,
setIsPickingIconSpecFromCanvas,
renderEditorPortal,
buildPortalMenuStyle,
onOpenSpecDialog,
onRequestUpload,
onUpdateIconDescriptionText,
onRememberImageModel,
onSubmit,
}: ImageCanvasIconSpritesheetComposerViewProps) {
const descriptionText =
dialog.prompt || (dialog.iconDescriptions ?? []).join('\n');
useImageCanvasFloatingOptionDismiss({
isOpen: isIconSpecMenuOpen,
boundaryRefs: [iconSpecButtonRef],
onDismiss: () => setIsIconSpecMenuOpen(false),
});
const pickIconSpecFromCanvas = () => {
setIsPickingIconSpecFromCanvas(true);
setIsIconSpecMenuOpen(false);
};
const openIconSpecDialog = () => {
setIsIconSpecMenuOpen(false);
onOpenSpecDialog('icon');
};
const requestIconSpecUpload = () => {
setIsIconSpecMenuOpen(false);
onRequestUpload('icon-spec');
};
const references = dialog.generationReferences ?? [];
return (
<form
className="image-canvas-editor__icon-composer"
style={style}
role="dialog"
aria-label="生成图标素材"
onPointerDown={(event) => event.stopPropagation()}
onSubmit={(event) => {
event.preventDefault();
if (dialog.status !== 'generating') {
onSubmit({ ...dialog, prompt: descriptionText });
}
}}
>
<div className="image-canvas-editor__reference-strip">
<ImageCanvasReferenceSlot
buttonRef={iconSpecButtonRef}
tone="icon"
className="image-canvas-editor__icon-spec-card"
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
imageSrc={dialog.iconSpecReference?.src}
objectKey={dialog.iconSpecReference?.objectKey}
label="图标规范"
ariaLabel={dialog.iconSpecReference?.label ?? '图标规范'}
title={dialog.iconSpecReference?.label ?? '图标规范'}
disabled={dialog.status === 'generating'}
isAdd={!dialog.iconSpecReference}
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
onRemove={
dialog.iconSpecReference
? () =>
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'icon'
? {
...currentDialog,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
iconSpecReference: null,
}
: currentDialog,
)
: undefined
}
removeLabel="删除图标规范"
/>
{isIconSpecMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
label="图标规范来源"
placement="top-start"
style={buildPortalMenuStyle(iconSpecButtonRef.current, 'above')}
>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={pickIconSpecFromCanvas}
>
从画布中选择
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={openIconSpecDialog}
>
新建图标规范
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={requestIconSpecUpload}
>
上传图片
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
: null}
{references.map((reference, index) => (
<ImageCanvasReferenceSlot
key={reference.id}
tone="default"
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
imageSrc={reference.src}
objectKey={reference.objectKey}
label={`参考图${index + 1}`}
ariaLabel={reference.label || `参考图${index + 1}`}
title={reference.label}
disabled={dialog.status === 'generating'}
onRemove={() =>
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'icon'
? {
...currentDialog,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
generationReferences: (
currentDialog.generationReferences ?? []
).filter((item) => item.id !== reference.id),
}
: currentDialog,
)
}
removeLabel={`删除${reference.label || `参考图${index + 1}`}`}
/>
))}
<ImageCanvasReferenceSlot
tone="default"
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
label="参考图"
ariaLabel="上传图标素材参考图"
disabled={dialog.status === 'generating'}
isAdd
onClick={() => onRequestUpload('generation-reference')}
/>
</div>
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
<AutoGrowTextArea
aria-label="素材描述"
value={descriptionText}
disabled={dialog.status === 'generating'}
placeholder="你需要哪些图标?"
className="image-canvas-editor__generation-prompt"
onChange={(event) => onUpdateIconDescriptionText(event.target.value)}
/>
</label>
{dialog.status === 'failed' ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="xs"
className="image-canvas-editor__generate-status"
role="alert"
>
{dialog.errorMessage}
</PlatformStatusMessage>
) : null}
<div className="image-canvas-editor__generation-composer-footer">
<ImageCanvasGenerationImageOptionsView
dialog={dialog}
setGenerateDialog={setGenerateDialog}
includeDimensions
onRememberImageModel={onRememberImageModel}
optionLabelPrefix="生成图片"
cost={calculateEditorIconSpritesheetPrice(
dialog.imageModel,
dialog.imageSize,
)}
submitLabel="生成"
submitAriaLabel="生成"
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
</div>
</form>
);
}