This commit is contained in:
2026-04-24 17:59:48 +08:00
parent 929febb4fe
commit 6cb3efae61
55 changed files with 2373 additions and 435 deletions

View File

@@ -18,7 +18,6 @@ import {
resolveCustomWorldLandmarkImageMap,
} from '../data/customWorldVisuals';
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import { resolveCustomWorldCoverPresentation } from '../services/customWorldCover';
import { normalizeCustomWorldCreatorIntent } from '../services/customWorldCreatorIntent';
import {
AnimationState,
@@ -28,7 +27,6 @@ import {
type SceneChapterBlueprint,
} from '../types';
import { CharacterAnimator } from './CharacterAnimator';
import { CustomWorldCoverArtwork } from './CustomWorldCoverArtwork';
import type { RpgCreationEditorTarget } from './rpg-creation-editor/RpgCreationEntityEditorModal';
import { CustomWorldNpcPortrait } from './CustomWorldNpcVisualEditor';
import { ResolvedAssetImage } from './ResolvedAssetImage';
@@ -54,8 +52,6 @@ interface CustomWorldEntityCatalogProps {
onProfileChange: (profile: CustomWorldProfile) => void;
onDeleteStoryNpcs?: (ids: string[]) => void;
onDeleteLandmarks?: (ids: string[]) => void;
onGenerateRoleAssets?: (roleId: string) => void;
onGenerateSceneAssets?: (sceneId: string, sceneKind: 'camp' | 'landmark') => void;
createActionLabel?: string;
onCreateAction?: () => void;
createActionDisabled?: boolean;
@@ -389,21 +385,21 @@ function CatalogCard({
tabIndex={disabled ? -1 : 0}
onClick={disabled ? undefined : onClick}
aria-disabled={disabled}
className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors ${
className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors xl:p-3 ${
isSelected
? 'border-rose-300/35 bg-rose-500/10'
: 'platform-subpanel'
}`}
>
<div className="flex items-start gap-3">
<div className="flex items-start gap-3 xl:gap-3.5">
<div
className={`platform-subpanel shrink-0 overflow-hidden rounded-[1rem] ${mediaClassName ?? 'h-[4.75rem] w-[4.75rem]'}`}
>
{media}
</div>
<div className="min-w-0 flex-1">
<div className="min-w-0 flex-1 xl:min-h-[5.6rem]">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 text-[15px] font-semibold leading-5 text-white">
<div className="min-w-0 text-[15px] font-semibold leading-5 text-white xl:line-clamp-1">
{title}
</div>
<div className="flex items-center gap-2">
@@ -411,7 +407,7 @@ function CatalogCard({
{selectionBadge}
</div>
</div>
<div className="mt-1.5 text-sm leading-5 text-zinc-300">
<div className="mt-1.5 text-sm leading-5 text-zinc-300 xl:line-clamp-2">
{description || '暂无描述'}
</div>
{actions ? <div className="mt-2 flex flex-wrap gap-2">{actions}</div> : null}
@@ -891,8 +887,6 @@ export function CustomWorldEntityCatalog({
onProfileChange,
onDeleteStoryNpcs,
onDeleteLandmarks,
onGenerateRoleAssets,
onGenerateSceneAssets,
createActionLabel,
onCreateAction,
createActionDisabled = false,
@@ -1104,11 +1098,6 @@ export function CustomWorldEntityCatalog({
1 +
(pendingGeneratedEntity?.kind === 'landmark' ? 1 : 0),
} satisfies Record<ResultTab, number>;
const coverPresentation = useMemo(
() => resolveCustomWorldCoverPresentation(profile),
[profile],
);
const bulkDeleteTab: BulkDeleteTab | null =
activeTab === 'story' || activeTab === 'landmarks' ? activeTab : null;
const isBulkDeleteMode =
@@ -1185,28 +1174,28 @@ export function CustomWorldEntityCatalog({
return (
<div
ref={scrollContainerRef}
className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide"
className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide xl:space-y-4 xl:pr-2"
>
<div className="px-1 pb-1 text-center">
<div className="px-1 pb-1 text-center xl:rounded-[2rem] xl:border xl:border-[var(--platform-subpanel-border)] xl:bg-white/55 xl:px-6 xl:py-4 xl:text-left xl:shadow-[0_18px_70px_rgba(255,79,139,0.08)] xl:backdrop-blur-sm">
<div className="text-[11px] font-bold tracking-[0.28em] text-zinc-500">
</div>
<div className="mt-2 text-3xl font-black text-[var(--platform-text-strong)] sm:text-[2.2rem]">
<div className="mt-2 text-3xl font-black text-[var(--platform-text-strong)] sm:text-[2.2rem] xl:mt-1 xl:text-[2rem]">
{profile.name}
</div>
<div className="mt-2 text-sm tracking-[0.18em] text-zinc-400">
<div className="mt-2 text-sm tracking-[0.18em] text-zinc-400 xl:mt-1 xl:text-xs">
{profile.subtitle}
</div>
</div>
<div className="platform-sticky-fade sticky top-0 z-10 -mx-1 space-y-3 px-1 pb-3 pt-1 backdrop-blur-sm">
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
<div className="platform-sticky-fade sticky top-0 z-10 -mx-1 space-y-3 px-1 pb-3 pt-1 backdrop-blur-sm xl:rounded-[1.75rem] xl:border xl:border-[var(--platform-subpanel-border)] xl:bg-white/70 xl:px-4 xl:py-3 xl:shadow-[0_16px_48px_rgba(255,79,139,0.08)]">
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide xl:pb-0">
{RESULT_TABS.map((tab) => (
<div key={tab.id}>
<button
type="button"
onClick={() => onActiveTabChange(tab.id)}
className={`platform-tab px-3 py-2 text-left text-sm ${activeTab === tab.id ? 'platform-tab--active' : ''}`}
className={`platform-tab px-3 py-2 text-left text-sm xl:min-w-[5.25rem] xl:px-4 xl:py-2 ${activeTab === tab.id ? 'platform-tab--active' : ''}`}
>
<div className="font-semibold">{tab.label}</div>
<div className="mt-1 text-[10px] tracking-[0.16em] text-[var(--platform-text-soft)]">
@@ -1218,7 +1207,7 @@ export function CustomWorldEntityCatalog({
</div>
{activeTab !== 'world' ? (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center xl:gap-3">
<div className="min-w-0 flex-1">
<SearchBox
value={searchDraft}
@@ -1267,7 +1256,7 @@ export function CustomWorldEntityCatalog({
</div>
{activeTab === 'world' ? (
<>
<div className="space-y-3 xl:grid xl:grid-cols-[0.8fr_1.2fr] xl:items-start xl:gap-3 xl:space-y-0">
<Section title="档案规模">
<div className="grid grid-cols-3 gap-2 text-center text-[11px] text-zinc-300">
<div className="platform-subpanel rounded-xl px-2 py-3">
@@ -1291,40 +1280,6 @@ export function CustomWorldEntityCatalog({
</div>
</Section>
<Section
title="作品封面"
badge={
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
{coverPresentation.sourceType === 'uploaded'
? '上传封面'
: coverPresentation.sourceType === 'generated'
? 'AI封面'
: '默认封面'}
</span>
}
actions={
!readOnly ? (
<SmallButton
onClick={() => onEditTarget({ kind: 'cover' })}
tone="sky"
>
</SmallButton>
) : null
}
>
<div className="space-y-3">
<CustomWorldCoverArtwork
imageSrc={coverPresentation.imageSrc}
title={profile.name}
fallbackLabel={profile.name.slice(0, 4) || '封面'}
renderMode={coverPresentation.renderMode}
characterImageSrcs={coverPresentation.characterImageSrcs}
className="aspect-[16/9] rounded-[1.4rem] border border-[var(--platform-subpanel-border)]"
/>
</div>
</Section>
<Section
title="世界概述"
actions={
@@ -1394,11 +1349,11 @@ export function CustomWorldEntityCatalog({
</div>
</div>
</Section>
</>
</div>
) : null}
{activeTab === 'playable' ? (
<div className="space-y-3">
<div className="space-y-3 xl:grid xl:grid-cols-2 xl:gap-3 xl:space-y-0 2xl:grid-cols-3">
{pendingGeneratedEntity?.kind === 'playable' ? (
<PendingEntityCard
title={pendingGeneratedEntity.title}
@@ -1433,7 +1388,7 @@ export function CustomWorldEntityCatalog({
isSelectionMode={false}
isSelected={false}
layout="compact"
mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem]"
mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem] xl:h-[5.75rem] xl:w-[5.75rem]"
onClick={() =>
onEditTarget({
kind: 'playable',
@@ -1441,19 +1396,6 @@ export function CustomWorldEntityCatalog({
id: role.id,
})
}
actions={
!readOnly && onGenerateRoleAssets ? (
<SmallButton
onClick={(event) => {
event?.stopPropagation();
onGenerateRoleAssets(role.id);
}}
tone="sky"
>
</SmallButton>
) : null
}
media={
role.imageSrc?.trim() ? (
<ResolvedAssetImage
@@ -1522,7 +1464,7 @@ export function CustomWorldEntityCatalog({
) : null}
{activeTab === 'story' ? (
<div className="space-y-3">
<div className="space-y-3 xl:grid xl:grid-cols-2 xl:gap-3 xl:space-y-0 2xl:grid-cols-3">
{pendingGeneratedEntity?.kind === 'story' ? (
<PendingEntityCard
title={pendingGeneratedEntity.title}
@@ -1547,7 +1489,7 @@ export function CustomWorldEntityCatalog({
isSelectionMode={isBulkDeleteMode}
isSelected={selectedBulkIds.includes(npc.id)}
layout="compact"
mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem]"
mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem] xl:h-[5.75rem] xl:w-[5.75rem]"
onClick={() =>
isBulkDeleteMode
? toggleBulkSelected(npc.id)
@@ -1563,19 +1505,6 @@ export function CustomWorldEntityCatalog({
id: npc.id,
})
}
actions={
!readOnly && !isBulkDeleteMode && onGenerateRoleAssets ? (
<SmallButton
onClick={(event) => {
event?.stopPropagation();
onGenerateRoleAssets(npc.id);
}}
tone="sky"
>
</SmallButton>
) : null
}
media={
<CustomWorldNpcPortrait
npc={npc}
@@ -1595,7 +1524,7 @@ export function CustomWorldEntityCatalog({
) : null}
{activeTab === 'landmarks' ? (
<div className="space-y-3">
<div className="space-y-3 xl:grid xl:grid-cols-2 xl:gap-3 xl:space-y-0 2xl:grid-cols-3">
{pendingGeneratedEntity?.kind === 'landmark' ? (
<PendingEntityCard
title={pendingGeneratedEntity.title}
@@ -1639,20 +1568,6 @@ export function CustomWorldEntityCatalog({
id: scene.id,
})
}
actions={
!readOnly && !isBulkDeleteMode && onGenerateSceneAssets ? (
<SmallButton
onClick={(event) => {
event?.stopPropagation();
onGenerateSceneAssets(scene.id, scene.kind);
}}
tone="sky"
disabled={scene.kind === 'camp' && isBulkDeleteMode}
>
</SmallButton>
) : null
}
media={
<ImageFrame
src={scene.imageSrc}