Simplify custom world result editing controls
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { type ReactNode, useDeferredValue, useMemo, useState } from 'react';
|
||||
import { type ReactNode, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
getCustomWorldSceneRelativePositionLabel,
|
||||
normalizeCustomWorldLandmarks,
|
||||
} from '../data/customWorldSceneGraph';
|
||||
import {
|
||||
resolveCustomWorldCampSceneImage,
|
||||
@@ -25,11 +24,8 @@ interface CustomWorldEntityCatalogProps {
|
||||
onActiveTabChange: (tab: ResultTab) => void;
|
||||
onEditTarget: (target: CustomWorldEditorTarget) => void;
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
onRegeneratePlayableNpc?: (id: string) => void;
|
||||
onRegenerateStoryNpc?: (id: string) => void;
|
||||
onRegenerateLandmark?: (id: string) => void;
|
||||
onRegenerateStoryExpansion?: () => void;
|
||||
onRegenerateLandmarkNetwork?: () => void;
|
||||
onDeleteStoryNpcs?: (ids: string[]) => void;
|
||||
onDeleteLandmarks?: (ids: string[]) => void;
|
||||
createActionLabel?: string;
|
||||
onCreateAction?: () => void;
|
||||
}
|
||||
@@ -146,6 +142,57 @@ function EmptyState({ title }: { title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CatalogCard({
|
||||
title,
|
||||
description,
|
||||
media,
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
media: ReactNode;
|
||||
isSelectionMode: boolean;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`w-full rounded-[1.4rem] border p-3 text-left transition-colors ${
|
||||
isSelected
|
||||
? 'border-rose-300/35 bg-rose-500/10'
|
||||
: 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-black/28'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-hidden rounded-[1.1rem] border border-white/8 bg-black/25">
|
||||
{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>
|
||||
<div className="text-sm leading-6 text-zinc-300">
|
||||
{description || '暂无描述'}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function matchText(text: string, query: string) {
|
||||
return text.toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
@@ -161,6 +208,8 @@ type CatalogRole =
|
||||
| CustomWorldProfile['playableNpcs'][number]
|
||||
| CustomWorldProfile['storyNpcs'][number];
|
||||
|
||||
type BulkDeleteTab = 'story' | 'landmarks';
|
||||
|
||||
function buildRoleSearchText(role: CatalogRole) {
|
||||
return [
|
||||
role.name,
|
||||
@@ -215,15 +264,14 @@ export function CustomWorldEntityCatalog({
|
||||
onActiveTabChange,
|
||||
onEditTarget,
|
||||
onProfileChange,
|
||||
onRegeneratePlayableNpc,
|
||||
onRegenerateStoryNpc,
|
||||
onRegenerateLandmark,
|
||||
onRegenerateStoryExpansion,
|
||||
onRegenerateLandmarkNetwork,
|
||||
onDeleteStoryNpcs,
|
||||
onDeleteLandmarks,
|
||||
createActionLabel,
|
||||
onCreateAction,
|
||||
}: CustomWorldEntityCatalogProps) {
|
||||
const [searchDraft, setSearchDraft] = useState('');
|
||||
const [bulkDeleteMode, setBulkDeleteMode] = useState<BulkDeleteTab | null>(null);
|
||||
const [selectedBulkIds, setSelectedBulkIds] = useState<string[]>([]);
|
||||
const deferredSearch = useDeferredValue(searchDraft.trim());
|
||||
|
||||
const storyNpcById = useMemo(
|
||||
@@ -289,16 +337,6 @@ export function CustomWorldEntityCatalog({
|
||||
),
|
||||
[profile.creatorIntent],
|
||||
);
|
||||
const lockedLandmarkNames = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
profile.creatorIntent?.keyLandmarks
|
||||
.filter((entry) => entry.locked)
|
||||
.map((entry) => entry.name.trim())
|
||||
.filter(Boolean) ?? [],
|
||||
),
|
||||
[profile.creatorIntent],
|
||||
);
|
||||
|
||||
const counts = {
|
||||
world: 1,
|
||||
@@ -308,6 +346,17 @@ export function CustomWorldEntityCatalog({
|
||||
landmarks: profile.landmarks.length,
|
||||
} satisfies Record<ResultTab, number>;
|
||||
|
||||
const bulkDeleteTab: BulkDeleteTab | null =
|
||||
activeTab === 'story' || activeTab === 'landmarks' ? activeTab : null;
|
||||
const isBulkDeleteMode = bulkDeleteMode === bulkDeleteTab;
|
||||
|
||||
useEffect(() => {
|
||||
if (bulkDeleteMode && bulkDeleteMode !== activeTab) {
|
||||
setBulkDeleteMode(null);
|
||||
setSelectedBulkIds([]);
|
||||
}
|
||||
}, [activeTab, bulkDeleteMode]);
|
||||
|
||||
const removePlayable = (id: string, name: string) => {
|
||||
if (profile.playableNpcs.length <= 1) {
|
||||
window.alert('至少保留一个可扮演角色,才能正常进入自定义世界。');
|
||||
@@ -320,37 +369,43 @@ export function CustomWorldEntityCatalog({
|
||||
});
|
||||
};
|
||||
|
||||
const removeStoryNpc = (id: string, name: string) => {
|
||||
if (!window.confirm(`确认删除场景角色「${name}」吗?`)) return;
|
||||
const nextStoryNpcs = profile.storyNpcs.filter(npc => npc.id !== id);
|
||||
onProfileChange({
|
||||
...profile,
|
||||
storyNpcs: nextStoryNpcs,
|
||||
landmarks: normalizeCustomWorldLandmarks({
|
||||
landmarks: profile.landmarks.map((landmark) => ({
|
||||
...landmark,
|
||||
sceneNpcIds: landmark.sceneNpcIds.filter((npcId) => npcId !== id),
|
||||
})),
|
||||
storyNpcs: nextStoryNpcs,
|
||||
}),
|
||||
});
|
||||
const startBulkDelete = (tab: BulkDeleteTab) => {
|
||||
setBulkDeleteMode(tab);
|
||||
setSelectedBulkIds([]);
|
||||
};
|
||||
|
||||
const removeLandmark = (id: string, name: string) => {
|
||||
if (!window.confirm(`确认删除场景「${name}」吗?`)) return;
|
||||
const nextLandmarks = profile.landmarks.filter(landmark => landmark.id !== id);
|
||||
onProfileChange({
|
||||
...profile,
|
||||
landmarks: normalizeCustomWorldLandmarks({
|
||||
landmarks: nextLandmarks.map((landmark) => ({
|
||||
...landmark,
|
||||
connections: landmark.connections.filter(
|
||||
(connection) => connection.targetLandmarkId !== id,
|
||||
),
|
||||
})),
|
||||
storyNpcs: profile.storyNpcs,
|
||||
}),
|
||||
});
|
||||
const cancelBulkDelete = () => {
|
||||
setBulkDeleteMode(null);
|
||||
setSelectedBulkIds([]);
|
||||
};
|
||||
|
||||
const toggleBulkSelected = (id: string) => {
|
||||
setSelectedBulkIds((current) =>
|
||||
current.includes(id)
|
||||
? current.filter((entry) => entry !== id)
|
||||
: [...current, id],
|
||||
);
|
||||
};
|
||||
|
||||
const confirmBulkDelete = () => {
|
||||
if (!bulkDeleteTab || selectedBulkIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = bulkDeleteTab === 'story' ? '场景角色' : '场景';
|
||||
const confirmed = window.confirm(
|
||||
`确认批量删除 ${selectedBulkIds.length} 个${label}吗?`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkDeleteTab === 'story') {
|
||||
onDeleteStoryNpcs?.(selectedBulkIds);
|
||||
} else {
|
||||
onDeleteLandmarks?.(selectedBulkIds);
|
||||
}
|
||||
cancelBulkDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -378,13 +433,37 @@ export function CustomWorldEntityCatalog({
|
||||
</div>
|
||||
|
||||
{activeTab !== 'world' && activeTab !== 'anchors' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div className="min-w-0 flex-1">
|
||||
<SearchBox value={searchDraft} onChange={setSearchDraft} placeholder={getSearchPlaceholder(activeTab)} />
|
||||
</div>
|
||||
{createActionLabel && onCreateAction ? (
|
||||
<SmallButton onClick={onCreateAction} tone="sky">{createActionLabel}</SmallButton>
|
||||
) : null}
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{isBulkDeleteMode ? (
|
||||
<>
|
||||
<div className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] text-zinc-300">
|
||||
已选 {selectedBulkIds.length}
|
||||
</div>
|
||||
<SmallButton onClick={cancelBulkDelete}>取消</SmallButton>
|
||||
<SmallButton
|
||||
onClick={confirmBulkDelete}
|
||||
tone="rose"
|
||||
>
|
||||
删除选中
|
||||
</SmallButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{createActionLabel && onCreateAction ? (
|
||||
<SmallButton onClick={onCreateAction} tone="sky">{createActionLabel}</SmallButton>
|
||||
) : null}
|
||||
{bulkDeleteTab && ((bulkDeleteTab === 'story' && onDeleteStoryNpcs) || (bulkDeleteTab === 'landmarks' && onDeleteLandmarks)) ? (
|
||||
<SmallButton onClick={() => startBulkDelete(bulkDeleteTab)} tone="rose">
|
||||
批量删除
|
||||
</SmallButton>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -560,14 +639,6 @@ export function CustomWorldEntityCatalog({
|
||||
subtitle={role.title}
|
||||
actions={(
|
||||
<div className="flex items-center gap-2">
|
||||
{onRegeneratePlayableNpc && !lockedCharacterNames.has(role.name.trim()) ? (
|
||||
<SmallButton
|
||||
onClick={() => onRegeneratePlayableNpc(role.id)}
|
||||
tone="sky"
|
||||
>
|
||||
AI重生成
|
||||
</SmallButton>
|
||||
) : null}
|
||||
<SmallButton onClick={() => onEditTarget({ kind: 'playable', mode: 'edit', id: role.id })} tone="sky">编辑</SmallButton>
|
||||
<SmallButton onClick={() => removePlayable(role.id, role.name)} tone="rose">删除</SmallButton>
|
||||
</div>
|
||||
@@ -646,111 +717,32 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'story' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm text-zinc-300">
|
||||
场景角色默认可组合中世纪奇幻角色形象;当角色文本明显指向怪物型 NPC 且初始好感偏敌对时,预览也会自动尝试引用怪物素材。
|
||||
{onRegenerateStoryExpansion ? (
|
||||
<div className="mt-3">
|
||||
<SmallButton onClick={onRegenerateStoryExpansion} tone="sky">
|
||||
重生成长尾场景角色
|
||||
</SmallButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{filteredStory.length === 0 ? (
|
||||
<EmptyState title="当前没有符合搜索条件的场景角色。" />
|
||||
) : (
|
||||
filteredStory.map(npc => (
|
||||
<div key={npc.id}>
|
||||
<Section
|
||||
<CatalogCard
|
||||
title={npc.name}
|
||||
subtitle={npc.role}
|
||||
actions={(
|
||||
<div className="flex items-center gap-2">
|
||||
{onRegenerateStoryNpc && !lockedCharacterNames.has(npc.name.trim()) ? (
|
||||
<SmallButton
|
||||
onClick={() => onRegenerateStoryNpc(npc.id)}
|
||||
tone="sky"
|
||||
>
|
||||
AI重生成
|
||||
</SmallButton>
|
||||
) : null}
|
||||
<SmallButton onClick={() => onEditTarget({ kind: 'story', mode: 'edit', id: npc.id })} tone="sky">编辑</SmallButton>
|
||||
<SmallButton onClick={() => removeStoryNpc(npc.id, npc.name)} tone="rose">删除</SmallButton>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-[7rem_minmax(0,1fr)]">
|
||||
description={npc.description}
|
||||
isSelectionMode={isBulkDeleteMode}
|
||||
isSelected={selectedBulkIds.includes(npc.id)}
|
||||
onClick={() =>
|
||||
isBulkDeleteMode
|
||||
? toggleBulkSelected(npc.id)
|
||||
: onEditTarget({ kind: 'story', mode: 'edit', id: npc.id })
|
||||
}
|
||||
media={(
|
||||
<CustomWorldNpcPortrait
|
||||
npc={npc}
|
||||
profile={profile}
|
||||
visual={npc.visual}
|
||||
className="aspect-square"
|
||||
scale={2.18}
|
||||
preferImageSrc
|
||||
/>
|
||||
<div className="min-w-0 space-y-3">
|
||||
{lockedCharacterNames.has(npc.name.trim()) ? (
|
||||
<div className="inline-flex rounded-full border border-amber-300/18 bg-amber-500/10 px-2.5 py-1 text-[10px] text-amber-100">
|
||||
创作者锁定角色
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm leading-6 text-zinc-300">{npc.description}</div>
|
||||
<div className="rounded-2xl border border-sky-300/12 bg-sky-500/8 px-3 py-3 text-sm leading-6 text-sky-50/95">
|
||||
公开背景:{npc.backstoryReveal.publicSummary || '未填写'}
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">头衔:{npc.title}</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">初始好感:{npc.initialAffinity}</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">性格:{npc.personality || '未填写'}</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">战斗:{npc.combatStyle || '未填写'}</div>
|
||||
</div>
|
||||
{npc.backstory ? (
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">背景:{npc.backstory}</div>
|
||||
) : null}
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">动机:{npc.motivation}</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">好感背景章节</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{npc.backstoryReveal.chapters.map(chapter => (
|
||||
<div key={`${npc.id}-${chapter.id}`} className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
|
||||
{chapter.affinityRequired} 好感 · {chapter.title}:{chapter.teaser}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">技能</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{npc.skills.map(skill => (
|
||||
<div key={`${npc.id}-${skill.id}`} className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
|
||||
{skill.name} · {skill.style}:{skill.summary}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">初始物品</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{npc.initialItems.map(item => (
|
||||
<div key={`${npc.id}-${item.id}`} className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
|
||||
{item.name} x{item.quantity} · {item.category} · {item.rarity}:{item.description}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{npc.relationshipHooks.map(hook => (
|
||||
<span key={`${npc.id}-${hook}`} className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[10px] text-zinc-300">
|
||||
{hook}
|
||||
</span>
|
||||
))}
|
||||
{npc.tags.map(tag => (
|
||||
<span key={`${npc.id}-tag-${tag}`} className="rounded-full border border-sky-300/12 bg-sky-500/8 px-2.5 py-1 text-[10px] text-sky-100">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -759,85 +751,30 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
{activeTab === 'landmarks' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm text-zinc-300">
|
||||
场景图会同步用于结果页和正式世界中的背景展示;这里还能看到每个场景承载的 NPC 和连接关系。
|
||||
{onRegenerateLandmarkNetwork ? (
|
||||
<div className="mt-3">
|
||||
<SmallButton onClick={onRegenerateLandmarkNetwork} tone="sky">
|
||||
重生成场景网络
|
||||
</SmallButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{filteredLandmarks.length === 0 ? (
|
||||
<EmptyState title="当前没有符合搜索条件的场景。" />
|
||||
) : (
|
||||
filteredLandmarks.map(landmark => (
|
||||
<div key={landmark.id}>
|
||||
<Section
|
||||
<CatalogCard
|
||||
title={landmark.name}
|
||||
actions={(
|
||||
<div className="flex items-center gap-2">
|
||||
{onRegenerateLandmark && !lockedLandmarkNames.has(landmark.name.trim()) ? (
|
||||
<SmallButton
|
||||
onClick={() => onRegenerateLandmark(landmark.id)}
|
||||
tone="sky"
|
||||
>
|
||||
AI重生成
|
||||
</SmallButton>
|
||||
) : null}
|
||||
<SmallButton onClick={() => onEditTarget({ kind: 'landmark', mode: 'edit', id: landmark.id })} tone="sky">编辑</SmallButton>
|
||||
<SmallButton onClick={() => removeLandmark(landmark.id, landmark.name)} tone="rose">删除</SmallButton>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{lockedLandmarkNames.has(landmark.name.trim()) ? (
|
||||
<div className="inline-flex rounded-full border border-amber-300/18 bg-amber-500/10 px-2.5 py-1 text-[10px] text-amber-100">
|
||||
创作者锁定场景
|
||||
</div>
|
||||
) : null}
|
||||
description={landmark.description}
|
||||
isSelectionMode={isBulkDeleteMode}
|
||||
isSelected={selectedBulkIds.includes(landmark.id)}
|
||||
onClick={() =>
|
||||
isBulkDeleteMode
|
||||
? toggleBulkSelected(landmark.id)
|
||||
: onEditTarget({ kind: 'landmark', mode: 'edit', id: landmark.id })
|
||||
}
|
||||
media={(
|
||||
<ImageFrame
|
||||
src={landmarkImageById.get(landmark.id) ?? landmark.imageSrc}
|
||||
alt={landmark.name}
|
||||
fallbackLabel={landmark.name.slice(0, 4) || '场景'}
|
||||
tone="landscape"
|
||||
/>
|
||||
<div className="text-sm leading-7 text-zinc-300">{landmark.description}</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 text-sm leading-6 text-zinc-300">
|
||||
危险度:{landmark.dangerLevel || '未填写'}
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">场景内 NPC</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{landmark.sceneNpcIds.length > 0 ? (
|
||||
landmark.sceneNpcIds.map((npcId) => (
|
||||
<span key={`${landmark.id}-npc-${npcId}`} className="rounded-full border border-sky-300/12 bg-sky-500/8 px-2.5 py-1 text-[10px] text-sky-100">
|
||||
{storyNpcById.get(npcId)?.name ?? '未匹配角色'}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-zinc-500">尚未分配场景角色</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">连接关系</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{landmark.connections.length > 0 ? (
|
||||
landmark.connections.map((connection) => (
|
||||
<div key={`${landmark.id}-connection-${connection.targetLandmarkId}-${connection.relativePosition}`} className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
|
||||
{getCustomWorldSceneRelativePositionLabel(connection.relativePosition)} · {landmarkById.get(connection.targetLandmarkId)?.name ?? '未匹配场景'}
|
||||
{connection.summary ? `:${connection.summary}` : ''}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-xs text-zinc-500">尚未配置连接关系</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user