收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -17,12 +17,22 @@ import { buildCustomWorldScenePresentations } from '../services/customWorldScene
|
||||
import {
|
||||
AnimationState,
|
||||
type Character,
|
||||
type CustomWorldProfile,
|
||||
type CustomWorldOpeningCgProfile,
|
||||
type CustomWorldProfile,
|
||||
type SceneActBlueprint,
|
||||
type SceneChapterBlueprint,
|
||||
} from '../types';
|
||||
import { CharacterAnimator } from './CharacterAnimator';
|
||||
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
||||
import { PlatformMediaFrame } from './common/PlatformMediaFrame';
|
||||
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
||||
import { PlatformProgressBar } from './common/PlatformProgressBar';
|
||||
import { PlatformStatGrid } from './common/PlatformStatGrid';
|
||||
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
||||
import { PlatformTextField } from './common/PlatformTextField';
|
||||
import { UnifiedConfirmDialog } from './common/UnifiedConfirmDialog';
|
||||
import { CustomWorldNpcPortrait } from './CustomWorldNpcVisualEditor';
|
||||
import { ResolvedAssetImage } from './ResolvedAssetImage';
|
||||
import { ResolvedAssetVideo } from './ResolvedAssetVideo';
|
||||
@@ -120,22 +130,17 @@ function SmallButton({
|
||||
disabled?: boolean;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const toneClassName =
|
||||
tone === 'sky'
|
||||
? 'platform-button platform-button--primary'
|
||||
: tone === 'rose'
|
||||
? 'platform-button platform-button--danger'
|
||||
: 'platform-button platform-button--ghost';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${toneClassName} min-h-0 rounded-full px-3 py-1 text-[11px] ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
|
||||
tone={tone === 'sky' ? 'primary' : tone === 'rose' ? 'danger' : 'ghost'}
|
||||
shape="pill"
|
||||
size="xs"
|
||||
className="min-h-0 py-1 text-[11px]"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,52 +154,25 @@ function SearchBox({
|
||||
placeholder: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="platform-subpanel rounded-2xl px-3 py-2">
|
||||
<input
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full bg-transparent text-sm text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageFrame({
|
||||
src,
|
||||
alt,
|
||||
fallbackLabel,
|
||||
tone = 'square',
|
||||
}: {
|
||||
src?: string;
|
||||
alt: string;
|
||||
fallbackLabel: string;
|
||||
tone?: 'square' | 'landscape';
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`overflow-hidden rounded-2xl border border-[var(--platform-subpanel-border)] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.22),transparent_42%),linear-gradient(180deg,rgba(204,117,76,0.9),rgba(223,127,64,0.82))] ${tone === 'landscape' ? 'aspect-[16/9]' : 'aspect-square'}`}
|
||||
>
|
||||
{src ? (
|
||||
<ResolvedAssetImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center px-4 text-center text-sm font-semibold tracking-[0.18em] text-zinc-400">
|
||||
{fallbackLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PlatformTextField
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
density="compact"
|
||||
className="rounded-2xl bg-[var(--platform-subpanel-fill)] px-3 py-2 placeholder:text-[var(--platform-text-soft)]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="platform-subpanel rounded-2xl border-dashed px-5 py-6 text-center">
|
||||
<PlatformEmptyState
|
||||
surface="dashed"
|
||||
size="compact"
|
||||
className="rounded-2xl px-5 py-6 text-center"
|
||||
>
|
||||
<div className="text-sm text-[var(--platform-text-base)]">{title}</div>
|
||||
</div>
|
||||
</PlatformEmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,9 +186,9 @@ function buildFallbackRenderKey(
|
||||
|
||||
function NewBadge() {
|
||||
return (
|
||||
<span className="platform-pill platform-pill--warm px-2.5 py-1 text-[10px] font-semibold">
|
||||
<PlatformPillBadge tone="warning" size="xxs" className="font-semibold">
|
||||
新
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,7 +202,12 @@ function PendingEntityCard({
|
||||
progress: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="platform-banner platform-banner--info rounded-[1.35rem] px-4 py-4">
|
||||
<PlatformStatusMessage
|
||||
tone="info"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="rounded-[1.35rem] py-4"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
@@ -232,17 +215,19 @@ function PendingEntityCard({
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-6">{phaseLabel}</div>
|
||||
</div>
|
||||
<div className="platform-pill platform-pill--cool px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="cool" size="xxs">
|
||||
{Math.round(progress)}%
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
<div className="platform-progress-track mt-3 h-2.5 overflow-hidden rounded-full">
|
||||
<div
|
||||
className="h-full bg-[var(--platform-button-primary-solid)] transition-[width] duration-300"
|
||||
style={{ width: `${Math.max(6, Math.min(100, progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlatformProgressBar
|
||||
value={progress}
|
||||
minVisibleValue={6}
|
||||
size="sm"
|
||||
ariaLabel={`${title} 进度`}
|
||||
className="mt-3"
|
||||
fillClassName="bg-[var(--platform-button-primary-solid)]"
|
||||
/>
|
||||
</PlatformStatusMessage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,16 +273,16 @@ function OpeningCgPreview({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="neutral" size="xxs">
|
||||
80 积分
|
||||
</span>
|
||||
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
|
||||
</PlatformPillBadge>
|
||||
<PlatformPillBadge tone="neutral" size="xxs">
|
||||
预计 10 分钟
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
{hasVideo ? (
|
||||
<span className="platform-pill platform-pill--success px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="success" size="xxs">
|
||||
已生成
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
) : null}
|
||||
{!readOnly && onGenerate ? (
|
||||
<div className="ml-auto">
|
||||
@@ -312,14 +297,22 @@ function OpeningCgPreview({
|
||||
) : 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,#df7f40_0%,#cc754c_52%,#eaccb3_100%)]" />
|
||||
</div>
|
||||
<PlatformProgressBar
|
||||
value={66}
|
||||
ariaLabel={`${buttonLabel} 进度`}
|
||||
indeterminate
|
||||
fillClassName="animate-pulse bg-[linear-gradient(90deg,#df7f40_0%,#cc754c_52%,#eaccb3_100%)]"
|
||||
/>
|
||||
) : null}
|
||||
{openingCg?.status === 'failed' && openingCg.errorMessage ? (
|
||||
<div className="platform-banner platform-banner--danger rounded-2xl px-3 py-2 text-xs leading-5">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="rounded-2xl leading-5"
|
||||
>
|
||||
{openingCg.errorMessage}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -392,17 +385,20 @@ function SceneActPreviewStrip({
|
||||
return (
|
||||
<div className="flex w-full gap-1.5 overflow-x-auto pb-0.5">
|
||||
{acts.map((act) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
key={act.id}
|
||||
className="platform-subpanel h-12 w-[5.25rem] shrink-0 overflow-hidden rounded-xl"
|
||||
title={act.title}
|
||||
as="div"
|
||||
padding="none"
|
||||
radius="sm"
|
||||
className="h-12 w-[5.25rem] shrink-0 overflow-hidden rounded-xl"
|
||||
aria-label={`${sceneName}-${act.title}预览`}
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={act.imageSrc}
|
||||
alt={`${sceneName}-${act.title}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -434,34 +430,37 @@ function CatalogCard({
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const selectionBadge = isSelectionMode ? (
|
||||
<div
|
||||
className={`shrink-0 rounded-full border px-2.5 py-1 text-[10px] ${
|
||||
isSelected
|
||||
? 'border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)] text-[var(--platform-button-danger-text)]'
|
||||
: 'platform-subpanel text-[var(--platform-text-soft)]'
|
||||
}`}
|
||||
<PlatformPillBadge
|
||||
tone={isSelected ? 'danger' : 'muted'}
|
||||
size="xs"
|
||||
className="shrink-0 py-1 text-[10px]"
|
||||
>
|
||||
{isSelected ? '已选' : '选择'}
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
) : null;
|
||||
|
||||
if (layout === 'compact') {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors xl:p-3 ${
|
||||
isSelected ? 'border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)]' : 'platform-subpanel'
|
||||
}`}
|
||||
padding="none"
|
||||
radius="md"
|
||||
surface={isSelected ? 'danger' : 'platform'}
|
||||
className="w-full rounded-[1.3rem] p-2.5 text-left transition-colors xl:p-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]'}`}
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
padding="none"
|
||||
radius="sm"
|
||||
className={`shrink-0 overflow-hidden rounded-[1rem] ${mediaClassName ?? 'h-[4.75rem] w-[4.75rem]'}`}
|
||||
>
|
||||
{media}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
<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 xl:line-clamp-1">
|
||||
@@ -480,26 +479,31 @@ function CatalogCard({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
className={`w-full rounded-[1.4rem] border p-3 text-left transition-colors ${
|
||||
isSelected ? 'border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)]' : 'platform-subpanel'
|
||||
}`}
|
||||
padding="none"
|
||||
radius="md"
|
||||
surface={isSelected ? 'danger' : 'platform'}
|
||||
className="w-full rounded-[1.4rem] p-3 text-left transition-colors"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div
|
||||
className={`platform-subpanel overflow-hidden rounded-[1.1rem] ${mediaClassName ?? ''}`}
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
padding="none"
|
||||
radius="sm"
|
||||
className={`overflow-hidden rounded-[1.1rem] ${mediaClassName ?? ''}`}
|
||||
>
|
||||
{media}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 text-base font-semibold text-white">
|
||||
{title}
|
||||
@@ -514,7 +518,7 @@ function CatalogCard({
|
||||
</div>
|
||||
{actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -584,6 +588,11 @@ type CatalogRole =
|
||||
|
||||
type BulkDeleteTab = 'story' | 'landmarks';
|
||||
|
||||
type EntityCatalogConfirmState =
|
||||
| { kind: 'minimum-playable' }
|
||||
| { kind: 'delete-playable'; id: string; name: string }
|
||||
| { kind: 'bulk-delete'; tab: BulkDeleteTab; ids: string[]; label: string };
|
||||
|
||||
function buildRoleSearchText(role: CatalogRole) {
|
||||
return [
|
||||
role.name,
|
||||
@@ -660,6 +669,8 @@ export function CustomWorldEntityCatalog({
|
||||
null,
|
||||
);
|
||||
const [selectedBulkIds, setSelectedBulkIds] = useState<string[]>([]);
|
||||
const [confirmState, setConfirmState] =
|
||||
useState<EntityCatalogConfirmState | null>(null);
|
||||
const deferredSearch = useDeferredValue(searchDraft.trim());
|
||||
|
||||
const storyNpcById = useMemo(
|
||||
@@ -821,6 +832,11 @@ export function CustomWorldEntityCatalog({
|
||||
1 +
|
||||
(pendingGeneratedEntity?.kind === 'landmark' ? 1 : 0),
|
||||
} satisfies Record<ResultTab, number>;
|
||||
const worldStatItems = [
|
||||
{ label: '可扮演角色', value: profile.playableNpcs.length },
|
||||
{ label: '场景角色', value: profile.storyNpcs.length },
|
||||
{ label: '场景', value: profile.landmarks.length + 1 },
|
||||
];
|
||||
const bulkDeleteTab: BulkDeleteTab | null =
|
||||
activeTab === 'story' || activeTab === 'landmarks' ? activeTab : null;
|
||||
const isBulkDeleteMode =
|
||||
@@ -845,14 +861,10 @@ export function CustomWorldEntityCatalog({
|
||||
|
||||
const removePlayable = (id: string, name: string) => {
|
||||
if (profile.playableNpcs.length <= 1) {
|
||||
window.alert('至少保留一个可扮演角色,才能正常进入自定义世界。');
|
||||
setConfirmState({ kind: 'minimum-playable' });
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确认删除可扮演角色「${name}」吗?`)) return;
|
||||
onProfileChange({
|
||||
...profile,
|
||||
playableNpcs: profile.playableNpcs.filter((role) => role.id !== id),
|
||||
});
|
||||
setConfirmState({ kind: 'delete-playable', id, name });
|
||||
};
|
||||
|
||||
const startBulkDelete = (tab: BulkDeleteTab) => {
|
||||
@@ -879,21 +891,82 @@ export function CustomWorldEntityCatalog({
|
||||
}
|
||||
|
||||
const label = bulkDeleteTab === 'story' ? '场景角色' : '场景';
|
||||
const confirmed = window.confirm(
|
||||
`确认批量删除 ${selectedBulkIds.length} 个${label}吗?`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
setConfirmState({
|
||||
kind: 'bulk-delete',
|
||||
tab: bulkDeleteTab,
|
||||
ids: selectedBulkIds,
|
||||
label,
|
||||
});
|
||||
};
|
||||
|
||||
const closeConfirmDialog = () => {
|
||||
setConfirmState(null);
|
||||
};
|
||||
|
||||
const executeConfirmAction = () => {
|
||||
if (!confirmState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkDeleteTab === 'story') {
|
||||
onDeleteStoryNpcs?.(selectedBulkIds);
|
||||
if (confirmState.kind === 'minimum-playable') {
|
||||
closeConfirmDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmState.kind === 'delete-playable') {
|
||||
onProfileChange({
|
||||
...profile,
|
||||
playableNpcs: profile.playableNpcs.filter(
|
||||
(role) => role.id !== confirmState.id,
|
||||
),
|
||||
});
|
||||
closeConfirmDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmState.tab === 'story') {
|
||||
onDeleteStoryNpcs?.(confirmState.ids);
|
||||
} else {
|
||||
onDeleteLandmarks?.(selectedBulkIds);
|
||||
onDeleteLandmarks?.(confirmState.ids);
|
||||
}
|
||||
cancelBulkDelete();
|
||||
closeConfirmDialog();
|
||||
};
|
||||
|
||||
const confirmDialogConfig = (() => {
|
||||
if (!confirmState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (confirmState.kind === 'minimum-playable') {
|
||||
return {
|
||||
title: '无法删除',
|
||||
confirmLabel: '知道了',
|
||||
confirmTone: 'primary' as const,
|
||||
showCancel: false,
|
||||
body: '至少保留一个可扮演角色,才能正常进入自定义世界。',
|
||||
};
|
||||
}
|
||||
|
||||
if (confirmState.kind === 'delete-playable') {
|
||||
return {
|
||||
title: '删除角色',
|
||||
confirmLabel: '确认删除',
|
||||
confirmTone: 'danger' as const,
|
||||
showCancel: true,
|
||||
body: `确认删除可扮演角色「${confirmState.name}」吗?`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: '批量删除',
|
||||
confirmLabel: '确认删除',
|
||||
confirmTone: 'danger' as const,
|
||||
showCancel: true,
|
||||
body: `确认批量删除 ${confirmState.ids.length} 个${confirmState.label}吗?`,
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
@@ -943,9 +1016,9 @@ export function CustomWorldEntityCatalog({
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{isBulkDeleteMode ? (
|
||||
<>
|
||||
<div className="platform-pill platform-pill--neutral px-3 py-1 text-[11px]">
|
||||
<PlatformPillBadge tone="neutral" size="xs">
|
||||
已选 {selectedBulkIds.length}
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
<SmallButton onClick={cancelBulkDelete}>取消</SmallButton>
|
||||
<SmallButton onClick={confirmBulkDelete} tone="rose">
|
||||
删除选中
|
||||
@@ -983,26 +1056,14 @@ export function CustomWorldEntityCatalog({
|
||||
{activeTab === 'world' ? (
|
||||
<div className="space-y-3 xl:grid xl:grid-cols-[minmax(18rem,0.82fr)_minmax(0,1fr)_minmax(24rem,1.08fr)] xl:items-start xl:gap-3 xl:space-y-0 2xl:gap-4">
|
||||
<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">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.playableNpcs.length}
|
||||
</div>
|
||||
<div>可扮演角色</div>
|
||||
</div>
|
||||
<div className="platform-subpanel rounded-xl px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.storyNpcs.length}
|
||||
</div>
|
||||
<div>场景角色</div>
|
||||
</div>
|
||||
<div className="platform-subpanel rounded-xl px-2 py-3">
|
||||
<div className="text-xl font-black text-white">
|
||||
{profile.landmarks.length + 1}
|
||||
</div>
|
||||
<div>场景</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlatformStatGrid
|
||||
items={worldStatItems}
|
||||
columns="three"
|
||||
density="compact"
|
||||
surface="plain"
|
||||
itemClassName="platform-subpanel rounded-xl py-3"
|
||||
className="text-[11px] text-zinc-300"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="开局 CG">
|
||||
@@ -1038,12 +1099,22 @@ export function CustomWorldEntityCatalog({
|
||||
>
|
||||
<div className="space-y-3 text-sm leading-7 text-zinc-300">
|
||||
<p>{profile.summary}</p>
|
||||
<div className="platform-banner platform-banner--warning rounded-2xl px-3 py-3">
|
||||
<PlatformStatusMessage
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
size="sm"
|
||||
className="rounded-2xl py-3"
|
||||
>
|
||||
主线目标:{profile.playerGoal}
|
||||
</div>
|
||||
<div className="platform-subpanel rounded-2xl px-3 py-3">
|
||||
</PlatformStatusMessage>
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
radius="md"
|
||||
padding="sm"
|
||||
className="rounded-2xl px-3 py-3 text-zinc-300"
|
||||
>
|
||||
世界基调:{profile.tone}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -1069,53 +1140,59 @@ export function CustomWorldEntityCatalog({
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="platform-subpanel rounded-2xl px-4 py-4">
|
||||
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
角色维度
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3 xl:grid-cols-6">
|
||||
{attributeSlots.map((slot) => (
|
||||
<div
|
||||
key={slot.slotId}
|
||||
className="rounded-xl border border-white/10 bg-black/15 px-3 py-3"
|
||||
>
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{slot.name}
|
||||
</div>
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
title="角色维度"
|
||||
radius="md"
|
||||
padding="md"
|
||||
bodyClassName="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3 xl:grid-cols-6"
|
||||
className="rounded-2xl px-4 py-4"
|
||||
>
|
||||
{attributeSlots.map((slot) => (
|
||||
<PlatformSubpanel
|
||||
key={slot.slotId}
|
||||
as="div"
|
||||
surface="dark"
|
||||
radius="xs"
|
||||
padding="sm"
|
||||
className="bg-black/15"
|
||||
>
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{slot.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</PlatformSubpanel>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{structuredFoundationEntries.map((entry) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
key={entry.id}
|
||||
className="platform-subpanel rounded-2xl px-4 py-4"
|
||||
as="div"
|
||||
title={entry.label}
|
||||
radius="md"
|
||||
padding="md"
|
||||
className="rounded-2xl px-4 py-4"
|
||||
bodyClassName={
|
||||
entry.value ? 'mt-3 flex flex-wrap gap-2' : ''
|
||||
}
|
||||
>
|
||||
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
|
||||
{entry.label}
|
||||
</div>
|
||||
{entry.value ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{parseFoundationTagText(entry.value).map(
|
||||
(tag, index) => (
|
||||
<span
|
||||
key={`${entry.id}-${index}-${tag}`}
|
||||
className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs leading-5 text-zinc-100"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
parseFoundationTagText(entry.value).map((tag, index) => (
|
||||
<PlatformPillBadge
|
||||
key={`${entry.id}-${index}-${tag}`}
|
||||
tone="darkSoft"
|
||||
size="sm"
|
||||
className="leading-5"
|
||||
>
|
||||
{tag}
|
||||
</PlatformPillBadge>
|
||||
))
|
||||
) : (
|
||||
<div className="mt-2 text-sm leading-7 text-zinc-100">
|
||||
待补充
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1198,25 +1275,26 @@ export function CustomWorldEntityCatalog({
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2 px-1">
|
||||
{lockedCharacterNames.has(role.name.trim()) ? (
|
||||
<span className="platform-pill platform-pill--warm px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="warning" size="xxs">
|
||||
陶泥儿主锁定
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
) : null}
|
||||
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="neutral" size="xxs">
|
||||
初始好感 {role.initialAffinity}
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
{role.generatedVisualAssetId ? (
|
||||
<span className="platform-pill platform-pill--success px-2.5 py-1 text-[10px]">
|
||||
<PlatformPillBadge tone="success" size="xxs">
|
||||
已生成主图
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
) : null}
|
||||
{role.tags.slice(0, 2).map((tag) => (
|
||||
<span
|
||||
<PlatformPillBadge
|
||||
key={`${role.id}-${tag}`}
|
||||
className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]"
|
||||
tone="neutral"
|
||||
size="xxs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
))}
|
||||
{!readOnly ? (
|
||||
<div className="ml-auto">
|
||||
@@ -1344,11 +1422,11 @@ export function CustomWorldEntityCatalog({
|
||||
})
|
||||
}
|
||||
media={
|
||||
<ImageFrame
|
||||
<PlatformMediaFrame
|
||||
src={scene.imageSrc}
|
||||
alt={scene.name}
|
||||
fallbackLabel={scene.name.slice(0, 4) || '场景'}
|
||||
tone="landscape"
|
||||
aspect="landscape"
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
@@ -1363,6 +1441,20 @@ export function CustomWorldEntityCatalog({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{confirmDialogConfig ? (
|
||||
<UnifiedConfirmDialog
|
||||
open
|
||||
title={confirmDialogConfig.title}
|
||||
onClose={closeConfirmDialog}
|
||||
onConfirm={executeConfirmAction}
|
||||
confirmLabel={confirmDialogConfig.confirmLabel}
|
||||
confirmTone={confirmDialogConfig.confirmTone}
|
||||
showCancel={confirmDialogConfig.showCancel}
|
||||
closeOnBackdrop={confirmState?.kind !== 'minimum-playable'}
|
||||
>
|
||||
{confirmDialogConfig.body}
|
||||
</UnifiedConfirmDialog>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user