This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -18,12 +18,14 @@ import {
AnimationState,
type Character,
type CustomWorldProfile,
type CustomWorldOpeningCgProfile,
type SceneActBlueprint,
type SceneChapterBlueprint,
} from '../types';
import { CharacterAnimator } from './CharacterAnimator';
import { CustomWorldNpcPortrait } from './CustomWorldNpcVisualEditor';
import { ResolvedAssetImage } from './ResolvedAssetImage';
import { ResolvedAssetVideo } from './ResolvedAssetVideo';
import type { RpgCreationEditorTarget } from './rpg-creation-editor/RpgCreationEntityEditorModal';
export type ResultTab = 'world' | 'playable' | 'story' | 'landmarks';
@@ -50,6 +52,10 @@ interface CustomWorldEntityCatalogProps {
createActionLabel?: string;
onCreateAction?: () => void;
createActionDisabled?: boolean;
openingCgGenerating?: boolean;
openingCgPhaseLabel?: string | null;
openingCgGenerateDisabled?: boolean;
onGenerateOpeningCg?: () => void;
pendingGeneratedEntity?: PendingGeneratedEntity | null;
recentGeneratedIds?: RecentGeneratedIds;
readOnly?: boolean;
@@ -240,6 +246,85 @@ function PendingEntityCard({
);
}
function OpeningCgPreview({
openingCg,
isGenerating,
phaseLabel,
generateDisabled,
readOnly,
onGenerate,
}: {
openingCg?: CustomWorldOpeningCgProfile | null;
isGenerating: boolean;
phaseLabel?: string | null;
generateDisabled?: boolean;
readOnly: boolean;
onGenerate?: () => void;
}) {
const hasVideo = Boolean(openingCg?.videoSrc?.trim());
const buttonLabel = hasVideo ? '重新生成' : '生成';
return (
<div className="space-y-3">
<div className="overflow-hidden rounded-2xl border border-[var(--platform-subpanel-border)] bg-black/35 aspect-video">
{hasVideo ? (
<ResolvedAssetVideo
src={openingCg?.videoSrc}
className="h-full w-full object-cover"
controls
playsInline
preload="metadata"
/>
) : openingCg?.storyboardImageSrc ? (
<ResolvedAssetImage
src={openingCg.storyboardImageSrc}
alt="开局 CG 故事板"
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-sm font-semibold tracking-[0.18em] text-zinc-500">
CG
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
80
</span>
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
10
</span>
{hasVideo ? (
<span className="platform-pill platform-pill--success px-2.5 py-1 text-[10px]">
</span>
) : null}
{!readOnly && onGenerate ? (
<div className="ml-auto">
<SmallButton
onClick={onGenerate}
tone="sky"
disabled={isGenerating || generateDisabled}
>
{isGenerating ? (phaseLabel ?? '生成中') : buttonLabel}
</SmallButton>
</div>
) : null}
</div>
{isGenerating ? (
<div className="platform-progress-track h-2 overflow-hidden rounded-full">
<div className="h-full w-2/3 animate-pulse bg-[linear-gradient(90deg,#ff4f8b_0%,#ff8a73_52%,#ffd2a6_100%)]" />
</div>
) : null}
{openingCg?.status === 'failed' && openingCg.errorMessage ? (
<div className="platform-banner platform-banner--danger rounded-2xl px-3 py-2 text-xs leading-5">
{openingCg.errorMessage}
</div>
) : null}
</div>
);
}
function buildSceneActParticipantText(
act: SceneActBlueprint,
roleById: Map<
@@ -557,6 +642,10 @@ export function CustomWorldEntityCatalog({
createActionLabel,
onCreateAction,
createActionDisabled = false,
openingCgGenerating = false,
openingCgPhaseLabel = null,
openingCgGenerateDisabled = false,
onGenerateOpeningCg,
pendingGeneratedEntity = null,
recentGeneratedIds = {
playable: [],
@@ -916,6 +1005,17 @@ export function CustomWorldEntityCatalog({
</div>
</Section>
<Section title="开局 CG">
<OpeningCgPreview
openingCg={profile.openingCg}
isGenerating={openingCgGenerating}
phaseLabel={openingCgPhaseLabel}
generateDisabled={openingCgGenerateDisabled}
readOnly={readOnly}
onGenerate={onGenerateOpeningCg}
/>
</Section>
<Section
title="世界概述"
actions={