1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 11:30:19 +08:00
parent 50759f3c1e
commit 8a7bd90458
85 changed files with 7290 additions and 1903 deletions

View File

@@ -43,3 +43,39 @@ test('draft detail panel renders sections and warnings', () => {
expect(html).toContain('编辑设定');
expect(html).toContain('新增角色');
});
test('draft detail panel renders scene chapter label and background preview', () => {
const html = renderToStaticMarkup(
<CustomWorldAgentDraftDetailPanel
detail={{
id: 'scene-chapter-docks',
kind: 'scene_chapter',
title: '潮汐码头章节',
sections: [
{
id: 'sceneName',
label: '所属场景',
value: '潮汐码头',
},
{
id: 'act:act-docks-1:backgroundImageSrc',
label: '第 1 幕背景图',
value: '/images/scene/docks-act-1.webp',
},
],
linkedIds: ['landmark-docks', 'thread-smuggling'],
locked: false,
editable: true,
editableSectionIds: ['title', 'summary', 'act:act-docks-1:title'],
warningMessages: [],
}}
loading={false}
onClose={() => {}}
onStartEdit={() => {}}
/>,
);
expect(html).toContain('场景章节');
expect(html).toContain('第 1 幕背景图');
expect(html).toContain('img');
});

View File

@@ -28,6 +28,7 @@ function resolveKindLabel(kind: CustomWorldDraftCardDetail['kind']) {
if (kind === 'landmark') return '地点';
if (kind === 'thread') return '线程';
if (kind === 'chapter') return '第一幕';
if (kind === 'scene_chapter') return '场景章节';
return '草稿卡';
}
@@ -72,6 +73,15 @@ export function CustomWorldAgentDraftDetailPanel({
onGenerateLandmark,
onOpenRoleAssetStudio,
}: CustomWorldAgentDraftDetailPanelProps) {
const shouldRenderImagePreview = (
detailKind: CustomWorldDraftCardDetail['kind'],
sectionId: string,
value: string,
) =>
detailKind === 'scene_chapter' &&
sectionId.endsWith(':backgroundImageSrc') &&
value !== '待继续精修';
return (
<section className="platform-remap-surface rounded-[1.5rem] border border-white/10 bg-black/18 px-4 py-4">
<div className="flex items-start justify-between gap-3">
@@ -168,6 +178,13 @@ export function CustomWorldAgentDraftDetailPanel({
<div className="text-[11px] tracking-[0.16em] text-zinc-400">
{section.label}
</div>
{shouldRenderImagePreview(detail.kind, section.id, section.value) ? (
<img
src={section.value}
alt={section.label}
className="mt-3 h-40 w-full rounded-[1rem] border border-white/10 object-cover"
/>
) : null}
<div className="mt-2 whitespace-pre-wrap text-sm leading-7 text-zinc-100">
{section.value}
</div>

View File

@@ -9,6 +9,7 @@ type CustomWorldAgentDraftDrawerProps = {
const DRAWER_KIND_ORDER: CustomWorldDraftCardSummary['kind'][] = [
'world',
'chapter',
'scene_chapter',
'thread',
'faction',
'character',
@@ -19,6 +20,7 @@ const DRAWER_KIND_ORDER: CustomWorldDraftCardSummary['kind'][] = [
function resolveGroupLabel(kind: CustomWorldDraftCardSummary['kind']) {
if (kind === 'world') return '世界总卡';
if (kind === 'chapter') return '第一幕';
if (kind === 'scene_chapter') return '场景章节';
if (kind === 'thread') return '世界线程';
if (kind === 'faction') return '势力';
if (kind === 'character') return '关键角色';

View File

@@ -46,3 +46,57 @@ test('draft detail panel renders editable form in edit mode', () => {
expect(html).toContain('角色名');
expect(html).toContain('textarea');
});
test('draft detail panel uses textarea for scene chapter act narrative fields', () => {
const html = renderToStaticMarkup(
<CustomWorldAgentDraftDetailPanel
detail={{
id: 'scene-chapter-docks',
kind: 'scene_chapter',
title: '潮汐码头章节',
sections: [
{
id: 'title',
label: '场景章节标题',
value: '潮汐码头章节',
},
{
id: 'act:act-docks-1:summary',
label: '第 1 幕摘要',
value: '玩家刚抵达时,林潮先决定要不要放行。',
},
{
id: 'act:act-docks-1:encounterNpcIds',
label: '第 1 幕相遇 NPC',
value: '林潮\n晏九',
},
{
id: 'act:act-docks-1:transitionHook',
label: '第 1 幕过渡钩子',
value: '确认站位后,真正的封锁者会压上来。',
},
],
linkedIds: ['thread-smuggling'],
locked: false,
editable: true,
editableSectionIds: [
'title',
'act:act-docks-1:summary',
'act:act-docks-1:encounterNpcIds',
'act:act-docks-1:transitionHook',
],
warningMessages: [],
}}
loading={false}
editMode
onClose={() => {}}
onCancelEdit={() => {}}
onSave={() => {}}
/>,
);
expect(html).toContain('第 1 幕摘要');
expect(html).toContain('第 1 幕相遇 NPC');
expect(html).toContain('第 1 幕过渡钩子');
expect(html).toContain('textarea');
});

View File

@@ -15,6 +15,7 @@ type CustomWorldDraftEditPanelProps = {
};
function shouldUseTextarea(sectionId: string, value: string) {
const sceneActField = sectionId.match(/^act:[^:]+:(.+)$/u)?.[1] ?? null;
return (
value.length > 28 ||
value.includes('\n') ||
@@ -26,7 +27,11 @@ function shouldUseTextarea(sectionId: string, value: string) {
sectionId === 'stakes' ||
sectionId === 'openingEvent' ||
sectionId === 'understandingShift' ||
sectionId === 'description'
sectionId === 'description' ||
sceneActField === 'summary' ||
sceneActField === 'encounterNpcIds' ||
sceneActField === 'actGoal' ||
sceneActField === 'transitionHook'
);
}