1ad25e30f8
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
324 lines
9.0 KiB
TypeScript
324 lines
9.0 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
|
|
import type {
|
|
CustomWorldCreatorIntent,
|
|
CustomWorldGenerationMode,
|
|
} from '../types';
|
|
import { PlatformActionButton } from './common/PlatformActionButton';
|
|
import { PlatformModalCloseButton } from './common/PlatformModalCloseButton';
|
|
import { PlatformProgressBar } from './common/PlatformProgressBar';
|
|
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
|
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
|
import {
|
|
PlatformSelectField,
|
|
PlatformTextField,
|
|
} from './common/PlatformTextField';
|
|
|
|
type BaseModalProps = {
|
|
isOpen: boolean;
|
|
title: string;
|
|
onClose: () => void;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
};
|
|
|
|
function SelectionModal({
|
|
isOpen,
|
|
title,
|
|
onClose,
|
|
children,
|
|
footer = null,
|
|
}: BaseModalProps) {
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="platform-overlay fixed inset-0 z-[90] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="platform-modal-shell platform-remap-surface flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl shadow-[0_30px_80px_rgba(0,0,0,0.55)]">
|
|
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
|
<div className="text-base font-semibold text-white">{title}</div>
|
|
<PlatformModalCloseButton
|
|
label={`关闭${title}`}
|
|
variant="editorDark"
|
|
onClick={onClose}
|
|
/>
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
|
{children}
|
|
</div>
|
|
{footer ? (
|
|
<div className="flex flex-wrap items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
|
|
{footer}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CharacterDraftModal(props: {
|
|
isOpen: boolean;
|
|
characterLabel: string;
|
|
draftName: string;
|
|
draftBackstory: string;
|
|
onNameChange: (value: string) => void;
|
|
onBackstoryChange: (value: string) => void;
|
|
onClose: () => void;
|
|
onConfirm: () => void;
|
|
error?: string | null;
|
|
}) {
|
|
const {
|
|
isOpen,
|
|
characterLabel,
|
|
draftName,
|
|
draftBackstory,
|
|
onNameChange,
|
|
onBackstoryChange,
|
|
onClose,
|
|
onConfirm,
|
|
error = null,
|
|
} = props;
|
|
|
|
return (
|
|
<SelectionModal
|
|
isOpen={isOpen}
|
|
title="角色自定义"
|
|
onClose={onClose}
|
|
footer={(
|
|
<>
|
|
<PlatformActionButton
|
|
surface="editorDark"
|
|
tone="secondary"
|
|
size="sm"
|
|
onClick={onClose}
|
|
>
|
|
取消
|
|
</PlatformActionButton>
|
|
<PlatformActionButton
|
|
surface="editorDark"
|
|
tone="success"
|
|
size="sm"
|
|
onClick={onConfirm}
|
|
>
|
|
确认进入
|
|
</PlatformActionButton>
|
|
</>
|
|
)}
|
|
>
|
|
<div className="space-y-4">
|
|
<PlatformSubpanel
|
|
as="div"
|
|
surface="dark"
|
|
radius="md"
|
|
padding="row"
|
|
className="text-sm text-zinc-300"
|
|
>
|
|
当前角色:{characterLabel}
|
|
</PlatformSubpanel>
|
|
<label className="block">
|
|
<div className="mb-2 text-sm font-medium text-zinc-200">角色名字</div>
|
|
<PlatformTextField
|
|
value={draftName}
|
|
onChange={(event) => onNameChange(event.target.value)}
|
|
placeholder="输入一个更贴合这次旅程的称呼"
|
|
surface="editorDark"
|
|
tone="emerald"
|
|
density="roomy"
|
|
className="rounded-2xl"
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
<div className="mb-2 text-sm font-medium text-zinc-200">背景补充</div>
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
value={draftBackstory}
|
|
onChange={(event) => onBackstoryChange(event.target.value)}
|
|
rows={6}
|
|
placeholder="可以补充这次开局想强调的身份、经历、执念或禁忌。"
|
|
surface="editorDark"
|
|
tone="emerald"
|
|
size="md"
|
|
density="roomy"
|
|
className="rounded-2xl leading-7"
|
|
/>
|
|
</label>
|
|
{error ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="editorDark"
|
|
size="md"
|
|
className="rounded-2xl"
|
|
>
|
|
{error}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
</div>
|
|
</SelectionModal>
|
|
);
|
|
}
|
|
|
|
type CustomWorldCreatorModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: () => void;
|
|
isGenerating: boolean;
|
|
progress: number;
|
|
progressLabel: string;
|
|
error?: string | null;
|
|
} & (
|
|
| {
|
|
draft: string;
|
|
onDraftChange: (value: string) => void;
|
|
creatorIntent?: never;
|
|
onCreatorIntentChange?: never;
|
|
generationMode?: never;
|
|
onGenerationModeChange?: never;
|
|
}
|
|
| {
|
|
draft?: never;
|
|
onDraftChange?: never;
|
|
creatorIntent: CustomWorldCreatorIntent;
|
|
onCreatorIntentChange: (value: CustomWorldCreatorIntent) => void;
|
|
generationMode: CustomWorldGenerationMode;
|
|
onGenerationModeChange: (value: CustomWorldGenerationMode) => void;
|
|
}
|
|
);
|
|
|
|
function hasCreatorIntentProps(
|
|
props: CustomWorldCreatorModalProps,
|
|
): props is Extract<
|
|
CustomWorldCreatorModalProps,
|
|
{ creatorIntent: CustomWorldCreatorIntent }
|
|
> {
|
|
return 'creatorIntent' in props;
|
|
}
|
|
|
|
export function CustomWorldCreatorModal(props: CustomWorldCreatorModalProps) {
|
|
const {
|
|
isOpen,
|
|
onClose,
|
|
onSubmit,
|
|
isGenerating,
|
|
progress,
|
|
progressLabel,
|
|
error = null,
|
|
} = props;
|
|
|
|
const draftText = hasCreatorIntentProps(props)
|
|
? props.creatorIntent.rawSettingText
|
|
: props.draft;
|
|
|
|
const updateDraftText = (value: string) => {
|
|
if (hasCreatorIntentProps(props)) {
|
|
props.onCreatorIntentChange({
|
|
...props.creatorIntent,
|
|
rawSettingText: value,
|
|
});
|
|
return;
|
|
}
|
|
|
|
props.onDraftChange(value);
|
|
};
|
|
|
|
return (
|
|
<SelectionModal
|
|
isOpen={isOpen}
|
|
title="创建自定义世界"
|
|
onClose={onClose}
|
|
footer={(
|
|
<>
|
|
<PlatformActionButton
|
|
surface="editorDark"
|
|
tone="secondary"
|
|
size="sm"
|
|
onClick={onClose}
|
|
disabled={isGenerating}
|
|
>
|
|
取消
|
|
</PlatformActionButton>
|
|
<PlatformActionButton
|
|
surface="editorDark"
|
|
tone="primary"
|
|
size="sm"
|
|
onClick={onSubmit}
|
|
disabled={isGenerating}
|
|
>
|
|
{isGenerating ? '生成中...' : '开始生成'}
|
|
</PlatformActionButton>
|
|
</>
|
|
)}
|
|
>
|
|
<div className="space-y-4">
|
|
{hasCreatorIntentProps(props) ? (
|
|
<label className="block">
|
|
<div className="mb-2 text-sm font-medium text-zinc-200">生成模式</div>
|
|
<PlatformSelectField
|
|
value={props.generationMode}
|
|
onChange={(event) =>
|
|
props.onGenerationModeChange(
|
|
event.target.value as CustomWorldGenerationMode,
|
|
)
|
|
}
|
|
surface="editorDark"
|
|
tone="sky"
|
|
density="roomy"
|
|
className="rounded-2xl"
|
|
>
|
|
<option value="fast">快速</option>
|
|
<option value="full">完整</option>
|
|
</PlatformSelectField>
|
|
</label>
|
|
) : null}
|
|
|
|
<div className="text-sm leading-7 text-zinc-300">
|
|
用几句话描述世界观、核心矛盾、时代气质和你想体验的叙事方向。系统会据此生成可游玩的自定义世界。
|
|
</div>
|
|
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
value={draftText}
|
|
onChange={(event) => updateDraftText(event.target.value)}
|
|
rows={8}
|
|
placeholder="例:一个被潮雾与失落列岛切碎的边境世界,旧盟约、沉船秘术与灯塔守望者纠缠在一起……"
|
|
surface="editorDark"
|
|
tone="sky"
|
|
size="md"
|
|
density="roomy"
|
|
className="rounded-2xl leading-7"
|
|
/>
|
|
|
|
{isGenerating ? (
|
|
<PlatformStatusMessage
|
|
tone="info"
|
|
surface="editorDark"
|
|
size="md"
|
|
className="rounded-2xl"
|
|
>
|
|
<div className="mb-2 flex items-center justify-between text-xs tracking-[0.16em] text-sky-100/80">
|
|
<span>{progressLabel}</span>
|
|
<span>{Math.max(0, Math.min(100, Math.round(progress)))}%</span>
|
|
</div>
|
|
<PlatformProgressBar
|
|
value={progress}
|
|
minVisibleValue={6}
|
|
ariaLabel="自定义世界生成进度"
|
|
className="bg-white/10"
|
|
fillClassName="bg-gradient-to-r from-sky-300 to-cyan-200"
|
|
/>
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="editorDark"
|
|
size="md"
|
|
className="rounded-2xl"
|
|
>
|
|
{error}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
</div>
|
|
</SelectionModal>
|
|
);
|
|
}
|