@@ -3,6 +3,7 @@ import {
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
} from '../data/customWorldVisuals';
|
||||
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
|
||||
import {
|
||||
buildCustomWorldCreatorIntentDisplayText,
|
||||
buildCustomWorldCreatorIntentFoundationText,
|
||||
normalizeCustomWorldCreatorIntent,
|
||||
} from '../services/customWorldCreatorIntent';
|
||||
import { AnimationState, Character, CustomWorldProfile } from '../types';
|
||||
@@ -24,6 +25,16 @@ import { CustomWorldNpcPortrait } from './CustomWorldNpcVisualEditor';
|
||||
|
||||
export type ResultTab = 'world' | 'playable' | 'story' | 'landmarks';
|
||||
|
||||
type PendingGeneratedEntity = {
|
||||
id: string;
|
||||
kind: 'playable' | 'story' | 'landmark';
|
||||
title: string;
|
||||
progress: number;
|
||||
phaseLabel: string;
|
||||
};
|
||||
|
||||
type RecentGeneratedIds = Record<'playable' | 'story' | 'landmark', string[]>;
|
||||
|
||||
interface CustomWorldEntityCatalogProps {
|
||||
profile: CustomWorldProfile;
|
||||
previewCharacters: Character[];
|
||||
@@ -35,6 +46,9 @@ interface CustomWorldEntityCatalogProps {
|
||||
onDeleteLandmarks?: (ids: string[]) => void;
|
||||
createActionLabel?: string;
|
||||
onCreateAction?: () => void;
|
||||
createActionDisabled?: boolean;
|
||||
pendingGeneratedEntity?: PendingGeneratedEntity | null;
|
||||
recentGeneratedIds?: RecentGeneratedIds;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
@@ -48,11 +62,13 @@ const RESULT_TABS: Array<{ id: ResultTab; label: string }> = [
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
badge,
|
||||
actions,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
badge?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
@@ -72,7 +88,10 @@ function Section({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{actions}
|
||||
<div className="flex items-center gap-2">
|
||||
{badge}
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">{children}</div>
|
||||
</div>
|
||||
@@ -83,10 +102,12 @@ function SmallButton({
|
||||
onClick,
|
||||
children,
|
||||
tone = 'default',
|
||||
disabled = false,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
tone?: 'default' | 'sky' | 'rose';
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const toneClassName =
|
||||
tone === 'sky'
|
||||
@@ -99,7 +120,8 @@ function SmallButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded-full border px-3 py-1 text-[11px] transition-colors ${toneClassName}`}
|
||||
disabled={disabled}
|
||||
className={`rounded-full border px-3 py-1 text-[11px] transition-colors ${toneClassName} ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
@@ -161,23 +183,120 @@ function EmptyState({ title }: { title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function NewBadge() {
|
||||
return (
|
||||
<span className="rounded-full border border-amber-300/24 bg-amber-500/12 px-2.5 py-1 text-[10px] font-semibold text-amber-100">
|
||||
新
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function PendingEntityCard({
|
||||
title,
|
||||
phaseLabel,
|
||||
progress,
|
||||
}: {
|
||||
title: string;
|
||||
phaseLabel: string;
|
||||
progress: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-[1.35rem] border border-sky-300/18 bg-sky-500/10 px-4 py-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{title}</div>
|
||||
<div className="mt-1 text-xs leading-6 text-sky-50/90">
|
||||
{phaseLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-sky-300/20 bg-black/20 px-2.5 py-1 text-[10px] text-sky-100">
|
||||
{Math.round(progress)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/30">
|
||||
<div
|
||||
className="h-full bg-[linear-gradient(90deg,#38bdf8_0%,#67e8f9_100%)] transition-[width] duration-300"
|
||||
style={{ width: `${Math.max(6, Math.min(100, progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CatalogCard({
|
||||
title,
|
||||
description,
|
||||
media,
|
||||
badge,
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
onClick,
|
||||
layout = 'stacked',
|
||||
mediaClassName,
|
||||
disabled = false,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
media: ReactNode;
|
||||
badge?: ReactNode;
|
||||
isSelectionMode: boolean;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
layout?: 'stacked' | 'compact';
|
||||
mediaClassName?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const selectionBadge = isSelectionMode ? (
|
||||
<div
|
||||
className={`shrink-0 rounded-full border px-2.5 py-1 text-[10px] ${
|
||||
isSelected
|
||||
? 'border-rose-300/25 bg-rose-500/14 text-rose-50'
|
||||
: 'border-white/10 bg-black/20 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{isSelected ? '已选' : '选择'}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (layout === 'compact') {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors ${
|
||||
isSelected
|
||||
? 'border-rose-300/35 bg-rose-500/10'
|
||||
: disabled
|
||||
? 'border-white/10 bg-black/20'
|
||||
: 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-black/28'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`shrink-0 overflow-hidden rounded-[1rem] border border-white/8 bg-black/25 ${mediaClassName ?? 'h-[4.75rem] w-[4.75rem]'}`}
|
||||
>
|
||||
{media}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 text-[15px] font-semibold leading-5 text-white">
|
||||
{title}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{badge}
|
||||
{selectionBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1.5 text-sm leading-5 text-zinc-300">
|
||||
{description || '暂无描述'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -192,24 +311,19 @@ function CatalogCard({
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-hidden rounded-[1.1rem] border border-white/8 bg-black/25">
|
||||
<div
|
||||
className={`overflow-hidden rounded-[1.1rem] border border-white/8 bg-black/25 ${mediaClassName ?? ''}`}
|
||||
>
|
||||
{media}
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 text-base font-semibold text-white">
|
||||
{title}
|
||||
</div>
|
||||
{isSelectionMode ? (
|
||||
<div
|
||||
className={`shrink-0 rounded-full border px-2.5 py-1 text-[10px] ${
|
||||
isSelected
|
||||
? 'border-rose-300/25 bg-rose-500/14 text-rose-50'
|
||||
: 'border-white/10 bg-black/20 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{isSelected ? '已选' : '选择'}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-2">
|
||||
{badge}
|
||||
{selectionBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-zinc-300">
|
||||
{description || '暂无描述'}
|
||||
@@ -276,7 +390,7 @@ function buildStructuredFoundationEntries(profile: CustomWorldProfile) {
|
||||
return [
|
||||
{
|
||||
id: 'world-hook',
|
||||
label: '世界核心',
|
||||
label: '世界一句话',
|
||||
value:
|
||||
creatorIntent?.worldHook ||
|
||||
profile.anchorPack?.worldSummary ||
|
||||
@@ -384,8 +498,16 @@ export function CustomWorldEntityCatalog({
|
||||
onDeleteLandmarks,
|
||||
createActionLabel,
|
||||
onCreateAction,
|
||||
createActionDisabled = false,
|
||||
pendingGeneratedEntity = null,
|
||||
recentGeneratedIds = {
|
||||
playable: [],
|
||||
story: [],
|
||||
landmark: [],
|
||||
},
|
||||
readOnly = false,
|
||||
}: CustomWorldEntityCatalogProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [searchDraft, setSearchDraft] = useState('');
|
||||
const [bulkDeleteMode, setBulkDeleteMode] = useState<BulkDeleteTab | null>(
|
||||
null,
|
||||
@@ -423,6 +545,18 @@ export function CustomWorldEntityCatalog({
|
||||
),
|
||||
[previewCharacters, profile.playableNpcs],
|
||||
);
|
||||
const recentPlayableIdSet = useMemo(
|
||||
() => new Set(recentGeneratedIds.playable),
|
||||
[recentGeneratedIds.playable],
|
||||
);
|
||||
const recentStoryIdSet = useMemo(
|
||||
() => new Set(recentGeneratedIds.story),
|
||||
[recentGeneratedIds.story],
|
||||
);
|
||||
const recentLandmarkIdSet = useMemo(
|
||||
() => new Set(recentGeneratedIds.landmark),
|
||||
[recentGeneratedIds.landmark],
|
||||
);
|
||||
|
||||
const filteredPlayable = useMemo(
|
||||
() =>
|
||||
@@ -460,6 +594,16 @@ export function CustomWorldEntityCatalog({
|
||||
() => buildStructuredFoundationEntries(profile),
|
||||
[profile],
|
||||
);
|
||||
const structuredFoundationSourceText = useMemo(
|
||||
() =>
|
||||
buildCustomWorldCreatorIntentFoundationText(profile.creatorIntent).trim() ||
|
||||
profile.settingText.trim(),
|
||||
[profile.creatorIntent, profile.settingText],
|
||||
);
|
||||
const normalizedCreatorIntent = useMemo(
|
||||
() => normalizeCustomWorldCreatorIntent(profile.creatorIntent),
|
||||
[profile.creatorIntent],
|
||||
);
|
||||
const filteredSceneEntries = useMemo(() => {
|
||||
const openingSceneEntry = {
|
||||
id: 'custom-world-opening-scene',
|
||||
@@ -477,7 +621,13 @@ export function CustomWorldEntityCatalog({
|
||||
imageSrc: landmarkImageById.get(landmark.id) ?? landmark.imageSrc,
|
||||
searchText: buildLandmarkSearchText(landmark, storyNpcById, landmarkById),
|
||||
}));
|
||||
const allEntries = [openingSceneEntry, ...landmarkEntries];
|
||||
const recentEntries = landmarkEntries.filter((entry) =>
|
||||
recentLandmarkIdSet.has(entry.id),
|
||||
);
|
||||
const restEntries = landmarkEntries.filter(
|
||||
(entry) => !recentLandmarkIdSet.has(entry.id),
|
||||
);
|
||||
const allEntries = [...recentEntries, openingSceneEntry, ...restEntries];
|
||||
|
||||
if (!deferredSearch) {
|
||||
return allEntries;
|
||||
@@ -492,32 +642,35 @@ export function CustomWorldEntityCatalog({
|
||||
landmarkById,
|
||||
landmarkImageById,
|
||||
profile,
|
||||
recentLandmarkIdSet,
|
||||
resolvedCampImageSrc,
|
||||
resolvedCampScene,
|
||||
storyNpcById,
|
||||
]);
|
||||
|
||||
const creatorIntentSummary = useMemo(
|
||||
() =>
|
||||
buildCustomWorldCreatorIntentDisplayText(profile.creatorIntent).trim(),
|
||||
[profile.creatorIntent],
|
||||
);
|
||||
const lockedCharacterNames = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
profile.creatorIntent?.keyCharacters
|
||||
normalizedCreatorIntent?.keyCharacters
|
||||
.filter((entry) => entry.locked)
|
||||
.map((entry) => entry.name.trim())
|
||||
.filter(Boolean) ?? [],
|
||||
),
|
||||
[profile.creatorIntent],
|
||||
[normalizedCreatorIntent],
|
||||
);
|
||||
|
||||
const counts = {
|
||||
world: 1,
|
||||
playable: profile.playableNpcs.length,
|
||||
story: profile.storyNpcs.length,
|
||||
landmarks: profile.landmarks.length + 1,
|
||||
playable:
|
||||
profile.playableNpcs.length +
|
||||
(pendingGeneratedEntity?.kind === 'playable' ? 1 : 0),
|
||||
story:
|
||||
profile.storyNpcs.length +
|
||||
(pendingGeneratedEntity?.kind === 'story' ? 1 : 0),
|
||||
landmarks:
|
||||
profile.landmarks.length +
|
||||
1 +
|
||||
(pendingGeneratedEntity?.kind === 'landmark' ? 1 : 0),
|
||||
} satisfies Record<ResultTab, number>;
|
||||
|
||||
const bulkDeleteTab: BulkDeleteTab | null =
|
||||
@@ -532,6 +685,16 @@ export function CustomWorldEntityCatalog({
|
||||
}
|
||||
}, [activeTab, bulkDeleteMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
if (typeof container.scrollTo === 'function') {
|
||||
container.scrollTo({ top: 0, behavior: 'auto' });
|
||||
return;
|
||||
}
|
||||
container.scrollTop = 0;
|
||||
}, [activeTab]);
|
||||
|
||||
const removePlayable = (id: string, name: string) => {
|
||||
if (profile.playableNpcs.length <= 1) {
|
||||
window.alert('至少保留一个可扮演角色,才能正常进入自定义世界。');
|
||||
@@ -584,7 +747,10 @@ export function CustomWorldEntityCatalog({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide"
|
||||
>
|
||||
<div className="px-1 pb-1 text-center">
|
||||
<div className="text-[11px] font-bold tracking-[0.28em] text-zinc-500">
|
||||
世界档案
|
||||
@@ -638,7 +804,11 @@ export function CustomWorldEntityCatalog({
|
||||
) : (
|
||||
<>
|
||||
{!readOnly && createActionLabel && onCreateAction ? (
|
||||
<SmallButton onClick={onCreateAction} tone="sky">
|
||||
<SmallButton
|
||||
onClick={onCreateAction}
|
||||
tone="sky"
|
||||
disabled={createActionDisabled}
|
||||
>
|
||||
{createActionLabel}
|
||||
</SmallButton>
|
||||
) : null}
|
||||
@@ -662,6 +832,29 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'world' ? (
|
||||
<>
|
||||
<Section title="档案规模">
|
||||
<div className="grid grid-cols-3 gap-2 text-center text-[11px] text-zinc-300">
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.playableNpcs.length}
|
||||
</div>
|
||||
<div>可扮演角色</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.storyNpcs.length}
|
||||
</div>
|
||||
<div>场景角色</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.landmarks.length + 1}
|
||||
</div>
|
||||
<div>场景</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="世界概述"
|
||||
actions={
|
||||
@@ -694,10 +887,29 @@ export function CustomWorldEntityCatalog({
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="原始设定"
|
||||
subtitle="把开局最关键的 6 个原始锚点拆开看,后续精修会更顺。"
|
||||
title="基本设定"
|
||||
actions={
|
||||
readOnly ? (
|
||||
<SmallButton
|
||||
onClick={() => onEditTarget({ kind: 'world' })}
|
||||
tone="sky"
|
||||
>
|
||||
查看详情
|
||||
</SmallButton>
|
||||
) : (
|
||||
<SmallButton
|
||||
onClick={() => onEditTarget({ kind: 'world' })}
|
||||
tone="sky"
|
||||
>
|
||||
编辑
|
||||
</SmallButton>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
解析字段
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{structuredFoundationEntries.map((entry) => (
|
||||
<div
|
||||
@@ -713,45 +925,16 @@ export function CustomWorldEntityCatalog({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{profile.settingText ? (
|
||||
<div className="whitespace-pre-line rounded-2xl border border-white/8 bg-black/20 px-4 py-4 text-sm leading-7 text-zinc-200">
|
||||
{profile.settingText}
|
||||
{structuredFoundationSourceText ? (
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
锚点原文
|
||||
</div>
|
||||
<div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-200">
|
||||
{structuredFoundationSourceText}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{creatorIntentSummary && creatorIntentSummary !== profile.settingText ? (
|
||||
<div className="whitespace-pre-line rounded-2xl border border-sky-300/14 bg-sky-500/8 px-4 py-4 text-sm leading-7 text-sky-50/95">
|
||||
{creatorIntentSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="档案规模"
|
||||
subtitle="结果页现在会同时维护场景角色归属和场景之间的相对位置连接关系。"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-2 text-center text-[11px] text-zinc-300 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.playableNpcs.length}
|
||||
</div>
|
||||
<div>可扮演角色</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.storyNpcs.length}
|
||||
</div>
|
||||
<div>场景角色</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.landmarks.length + 1}
|
||||
</div>
|
||||
<div>场景</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 rounded-2xl border border-sky-300/14 bg-sky-500/8 px-4 py-3 text-sm leading-6 text-sky-50/90">
|
||||
自定义世界不再预生成物品档案。进入世界后的交易、掉落和初始装备会按当前世界主题即时生成。
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
@@ -759,6 +942,13 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'playable' ? (
|
||||
<div className="space-y-3">
|
||||
{pendingGeneratedEntity?.kind === 'playable' ? (
|
||||
<PendingEntityCard
|
||||
title={pendingGeneratedEntity.title}
|
||||
phaseLabel={pendingGeneratedEntity.phaseLabel}
|
||||
progress={pendingGeneratedEntity.progress}
|
||||
/>
|
||||
) : null}
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm text-zinc-300">
|
||||
{readOnly
|
||||
? '当前是草稿结果预览,可先浏览角色结构,再回到工作区继续精修。'
|
||||
@@ -776,6 +966,7 @@ export function CustomWorldEntityCatalog({
|
||||
<Section
|
||||
title={role.name}
|
||||
subtitle={role.title}
|
||||
badge={recentPlayableIdSet.has(role.id) ? <NewBadge /> : null}
|
||||
actions={
|
||||
readOnly ? (
|
||||
<SmallButton
|
||||
@@ -927,6 +1118,13 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'story' ? (
|
||||
<div className="space-y-3">
|
||||
{pendingGeneratedEntity?.kind === 'story' ? (
|
||||
<PendingEntityCard
|
||||
title={pendingGeneratedEntity.title}
|
||||
phaseLabel={pendingGeneratedEntity.phaseLabel}
|
||||
progress={pendingGeneratedEntity.progress}
|
||||
/>
|
||||
) : null}
|
||||
{filteredStory.length === 0 ? (
|
||||
<EmptyState title="当前没有符合搜索条件的场景角色。" />
|
||||
) : (
|
||||
@@ -935,8 +1133,11 @@ export function CustomWorldEntityCatalog({
|
||||
<CatalogCard
|
||||
title={npc.name}
|
||||
description={npc.description}
|
||||
badge={recentStoryIdSet.has(npc.id) ? <NewBadge /> : null}
|
||||
isSelectionMode={isBulkDeleteMode}
|
||||
isSelected={selectedBulkIds.includes(npc.id)}
|
||||
layout="compact"
|
||||
mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem]"
|
||||
onClick={() =>
|
||||
isBulkDeleteMode
|
||||
? toggleBulkSelected(npc.id)
|
||||
@@ -957,8 +1158,9 @@ export function CustomWorldEntityCatalog({
|
||||
npc={npc}
|
||||
profile={profile}
|
||||
visual={npc.visual}
|
||||
className="aspect-square"
|
||||
scale={2.18}
|
||||
className="h-full w-full"
|
||||
contentClassName="min-h-0 p-2"
|
||||
scale={1.82}
|
||||
preferImageSrc
|
||||
/>
|
||||
}
|
||||
@@ -971,6 +1173,13 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'landmarks' ? (
|
||||
<div className="space-y-3">
|
||||
{pendingGeneratedEntity?.kind === 'landmark' ? (
|
||||
<PendingEntityCard
|
||||
title={pendingGeneratedEntity.title}
|
||||
phaseLabel={pendingGeneratedEntity.phaseLabel}
|
||||
progress={pendingGeneratedEntity.progress}
|
||||
/>
|
||||
) : null}
|
||||
{filteredSceneEntries.length === 0 ? (
|
||||
<EmptyState title="当前没有符合搜索条件的场景。" />
|
||||
) : (
|
||||
@@ -983,6 +1192,11 @@ export function CustomWorldEntityCatalog({
|
||||
? `开局场景 · ${scene.description}`
|
||||
: scene.description
|
||||
}
|
||||
badge={
|
||||
scene.kind === 'landmark' && recentLandmarkIdSet.has(scene.id) ? (
|
||||
<NewBadge />
|
||||
) : null
|
||||
}
|
||||
isSelectionMode={scene.kind === 'landmark' && isBulkDeleteMode}
|
||||
isSelected={
|
||||
scene.kind === 'landmark' &&
|
||||
@@ -990,7 +1204,7 @@ export function CustomWorldEntityCatalog({
|
||||
}
|
||||
onClick={() =>
|
||||
scene.kind === 'camp'
|
||||
? onEditTarget({ kind: 'world' })
|
||||
? onEditTarget({ kind: 'camp' })
|
||||
: isBulkDeleteMode
|
||||
? toggleBulkSelected(scene.id)
|
||||
: onEditTarget({
|
||||
|
||||
Reference in New Issue
Block a user