166
src/components/CustomWorldResultView.tsx
Normal file
166
src/components/CustomWorldResultView.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { type ReactNode,useMemo, useState } from 'react';
|
||||
|
||||
import { Character, CustomWorldProfile } from '../types';
|
||||
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { CustomWorldEntityCatalog, type ResultTab } from './CustomWorldEntityCatalog';
|
||||
import { type CustomWorldEditorTarget,CustomWorldEntityEditorModal } from './CustomWorldEntityEditorModal';
|
||||
|
||||
interface CustomWorldResultViewProps {
|
||||
profile: CustomWorldProfile;
|
||||
previewCharacters: Character[];
|
||||
isGenerating: boolean;
|
||||
progress: number;
|
||||
progressLabel: string;
|
||||
error: string | null;
|
||||
onBack: () => void;
|
||||
onEditSetting: () => void;
|
||||
onRegenerate: () => void;
|
||||
onSave: () => void;
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
}
|
||||
|
||||
function SmallButton({
|
||||
onClick,
|
||||
children,
|
||||
tone = 'default',
|
||||
disabled = false,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
tone?: 'default' | 'sky';
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`rounded-full border px-3 py-2 text-sm transition-colors ${
|
||||
tone === 'sky'
|
||||
? 'border-sky-300/20 bg-sky-500/10 text-sky-100 hover:text-white'
|
||||
: 'border-white/10 bg-black/20 text-zinc-300 hover:text-white'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function getCreateTargetByTab(activeTab: ResultTab): CustomWorldEditorTarget | null {
|
||||
if (activeTab === 'playable') return { kind: 'playable', mode: 'create' };
|
||||
if (activeTab === 'story') return { kind: 'story', mode: 'create' };
|
||||
if (activeTab === 'landmarks') return { kind: 'landmark', mode: 'create' };
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCreateLabelByTab(activeTab: ResultTab) {
|
||||
if (activeTab === 'playable') return '新增可扮演角色';
|
||||
if (activeTab === 'story') return '新增场景角色';
|
||||
if (activeTab === 'landmarks') return '新增场景';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function CustomWorldResultView({
|
||||
profile,
|
||||
previewCharacters,
|
||||
isGenerating,
|
||||
progress,
|
||||
progressLabel,
|
||||
error,
|
||||
onBack,
|
||||
onEditSetting,
|
||||
onRegenerate: triggerRegenerate,
|
||||
onSave,
|
||||
onProfileChange,
|
||||
}: CustomWorldResultViewProps) {
|
||||
const [editorTarget, setEditorTarget] = useState<CustomWorldEditorTarget | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<ResultTab>('world');
|
||||
|
||||
const createTarget = useMemo(() => getCreateTargetByTab(activeTab), [activeTab]);
|
||||
const createLabel = useMemo(() => getCreateLabelByTab(activeTab), [activeTab]);
|
||||
const onRegenerate = () => {
|
||||
if (isGenerating) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`确认重新生成“${profile.name}”吗?\n\n重新生成会重新生成当前世界中的所有信息,包括你修改和新增的所有内容。`,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
triggerRegenerate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="mb-4 flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
disabled={isGenerating}
|
||||
className={`self-start rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white ${isGenerating ? 'opacity-45' : ''}`}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<CustomWorldEntityCatalog
|
||||
profile={profile}
|
||||
previewCharacters={previewCharacters}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
onEditTarget={setEditorTarget}
|
||||
onProfileChange={onProfileChange}
|
||||
createActionLabel={createLabel}
|
||||
onCreateAction={createTarget ? () => setEditorTarget(createTarget) : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isGenerating && (
|
||||
<div className="mt-3 rounded-2xl border border-sky-400/18 bg-sky-500/10 px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-semibold text-white">{progressLabel}</div>
|
||||
<div className="text-xs text-sky-100">{Math.round(progress)}%</div>
|
||||
</div>
|
||||
<div className="mt-3 h-3 overflow-hidden rounded-full border border-white/10 bg-black/35">
|
||||
<div
|
||||
className="h-full bg-[linear-gradient(90deg,#38bdf8_0%,#67e8f9_48%,#fef08a_100%)] transition-[width] duration-300"
|
||||
style={{ width: `${Math.max(0, Math.min(100, progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<div className="mt-3 rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<SmallButton onClick={onEditSetting}>修改设定</SmallButton>
|
||||
<SmallButton onClick={onRegenerate} tone="sky">重新生成</SmallButton>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
disabled={isGenerating}
|
||||
className={`pixel-nine-slice pixel-pressable text-left ${isGenerating ? 'opacity-55' : ''}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 16, paddingY: 10 })}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-sm font-semibold text-white">保存并进入世界</span>
|
||||
<span className="text-white/60">→</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomWorldEntityEditorModal
|
||||
profile={profile}
|
||||
target={editorTarget}
|
||||
onClose={() => setEditorTarget(null)}
|
||||
onProfileChange={onProfileChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user