1
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import type { ChangeEvent, ReactNode } from 'react';
|
||||
import { ImagePlus, RefreshCcw } from 'lucide-react';
|
||||
|
||||
type ActionButtonProps = {
|
||||
icon?: ReactNode;
|
||||
label: string;
|
||||
subLabel?: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
tone?: 'default' | 'sky' | 'green';
|
||||
};
|
||||
|
||||
type FieldProps = {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type TextAreaProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
type SectionProps = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function RpgCreationRoleVisualSection(props: {
|
||||
ActionButton: (props: ActionButtonProps) => ReactNode;
|
||||
Field: (props: FieldProps) => ReactNode;
|
||||
Section: (props: SectionProps) => ReactNode;
|
||||
TextArea: (props: TextAreaProps) => ReactNode;
|
||||
handleReferenceImageUpload: (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => Promise<void>;
|
||||
hasGeneratedVisualPreview: boolean;
|
||||
isApplyingVisual: boolean;
|
||||
isGeneratingVisuals: boolean;
|
||||
previewImageSrc: string;
|
||||
referenceImageDataUrls: string[];
|
||||
selectedTemplatePortrait?: string | null;
|
||||
selectedTemplateName?: string | null;
|
||||
syncBusy: boolean;
|
||||
visualPointCost: number;
|
||||
visualPromptText: string;
|
||||
visualStatus: string | null;
|
||||
workingRoleName: string;
|
||||
onClearReferenceImages: () => void;
|
||||
onGenerateVisuals: () => void;
|
||||
onVisualPromptChange: (value: string) => void;
|
||||
}) {
|
||||
const {
|
||||
ActionButton,
|
||||
Field,
|
||||
Section,
|
||||
TextArea,
|
||||
handleReferenceImageUpload,
|
||||
hasGeneratedVisualPreview,
|
||||
isApplyingVisual,
|
||||
isGeneratingVisuals,
|
||||
previewImageSrc,
|
||||
referenceImageDataUrls,
|
||||
selectedTemplateName,
|
||||
selectedTemplatePortrait,
|
||||
syncBusy,
|
||||
visualPointCost,
|
||||
visualPromptText,
|
||||
visualStatus,
|
||||
workingRoleName,
|
||||
onClearReferenceImages,
|
||||
onGenerateVisuals,
|
||||
onVisualPromptChange,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Section title="角色形象">
|
||||
<div className="space-y-4">
|
||||
<div className="platform-role-studio__preview overflow-hidden rounded-3xl">
|
||||
<div className="flex min-h-[18rem] items-center justify-center p-4 sm:min-h-[22rem]">
|
||||
{previewImageSrc ? (
|
||||
<img
|
||||
src={previewImageSrc}
|
||||
alt={workingRoleName}
|
||||
className="max-h-[28rem] w-full object-contain"
|
||||
/>
|
||||
) : selectedTemplatePortrait ? (
|
||||
<img
|
||||
src={selectedTemplatePortrait}
|
||||
alt={selectedTemplateName ?? workingRoleName}
|
||||
className="max-h-[20rem] w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="px-6 text-center text-sm text-zinc-500">
|
||||
暂无角色形象预览
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field label="形象描述">
|
||||
<TextArea
|
||||
value={visualPromptText}
|
||||
onChange={onVisualPromptChange}
|
||||
rows={6}
|
||||
placeholder="这里默认展示角色形象描述,也可以继续手动细化。"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="参考图">
|
||||
<label className="block rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
void handleReferenceImageUpload(event);
|
||||
}}
|
||||
className="w-full text-xs text-zinc-300 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-500 file:px-3 file:py-1.5 file:text-xs file:font-medium file:text-black"
|
||||
/>
|
||||
</label>
|
||||
{referenceImageDataUrls.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{referenceImageDataUrls.map((imageSrc, index) => (
|
||||
<div
|
||||
key={`${imageSrc}-${index}`}
|
||||
className="h-16 w-16 overflow-hidden rounded-xl border border-white/10 bg-black/25"
|
||||
>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={`reference-${index + 1}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
label="清空参考图"
|
||||
onClick={onClearReferenceImages}
|
||||
disabled={isGeneratingVisuals || isApplyingVisual || syncBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Field>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<ActionButton
|
||||
icon={
|
||||
hasGeneratedVisualPreview ? (
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
) : (
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
label={
|
||||
isGeneratingVisuals
|
||||
? '生成中...'
|
||||
: hasGeneratedVisualPreview
|
||||
? '重新生成角色形象'
|
||||
: '生成角色形象'
|
||||
}
|
||||
subLabel={`消耗${visualPointCost}叙世币`}
|
||||
onClick={onGenerateVisuals}
|
||||
disabled={isGeneratingVisuals || isApplyingVisual || syncBusy}
|
||||
tone="sky"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{visualStatus ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-zinc-300">
|
||||
{visualStatus}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export default RpgCreationRoleVisualSection;
|
||||
Reference in New Issue
Block a user