Refine creation tab UX, generation flow, and bindings

Large changes across frontend, backend and docs to align creation-tab and generation-page behavior with new product UI/UX and Spacetime bindings. Updated hermes decision-log and pitfalls with concrete rules (banner carousel, font sizing, unread-dot tokens, template-card layout, direct card->entry routing, separation of account balance vs prize pools, removal of global page card shell, generation progress milestones and unified circular progress, and background video handling). Added GenerationProgressHero component and media assets, plus generation-related UI/tests updates (CustomWorldGenerationView, BarkBattleGeneratingView, creation hub/cards, platform entry routing, index tests). Backend and contract updates include new category fields in admin API types and admin UI form/list, spacetime-client/module/migration changes and generated bindings script. Misc: many tests adjusted, new docs and plan files added, and several server-rs crate changes to support the updated creation/ generation workflows.
This commit is contained in:
2026-05-25 00:41:30 +08:00
parent 2ba4691bc0
commit 50a0d6f982
75 changed files with 5533 additions and 1101 deletions

View File

@@ -52,9 +52,9 @@ function createProgress(
describe('CustomWorldGenerationView', () => {
test.each(['拼图草稿生成进度', '抓大鹅草稿生成进度'])(
'hides batch module and keeps wait/timer in one row for %s',
'renders the circular hero and only the current step summary for %s',
(progressTitle) => {
render(
const { container } = render(
<CustomWorldGenerationView
settingText="竖屏生成题材"
progress={createProgress()}
@@ -63,67 +63,189 @@ describe('CustomWorldGenerationView', () => {
onBack={() => {}}
onEditSetting={() => {}}
onRetry={() => {}}
backLabel="返回创作中心"
settingDescription={null}
settingActionLabel={null}
progressTitle={progressTitle}
/>,
);
expect(container.firstChild).toBeTruthy();
expect((container.firstChild as HTMLElement).className).toContain(
'z-[1]',
);
const pageVideo = screen.getByTestId(
'generation-page-background-video',
) as HTMLVideoElement;
expect(pageVideo.parentElement?.className).toContain('z-0');
expect(pageVideo.parentElement?.className).toContain('bg-transparent');
expect(pageVideo.parentElement?.className).not.toContain('bg-[#fff4ea]');
expect((container.firstChild as HTMLElement).contains(pageVideo)).toBe(
true,
);
expect(pageVideo.autoplay).toBe(true);
expect(pageVideo.loop).toBe(true);
expect(pageVideo.muted).toBe(true);
expect(pageVideo.playsInline).toBe(true);
expect(pageVideo.getAttribute('preload')).toBe('auto');
expect(
document.querySelector(
'video[data-testid="generation-page-background-video"] source[type="video/mp4"]',
),
).toBeTruthy();
expect(
screen.getByRole('button', { name: '返回创作中心' }),
).toBeTruthy();
expect(
screen.getByRole('button', { name: '返回创作中心' }).className,
).toContain('text-xs');
expect(screen.getByText('世界建设中')).toBeTruthy();
expect(screen.getByText('世界建设中').className).toContain('text-xs');
expect(screen.getByTestId('generation-hero-wait-card').className).toContain(
'text-center',
);
expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain(
'text-center',
);
expect(screen.getByTestId('generation-hero-wait-card').className).toContain(
'bg-white/58',
);
expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain(
'bg-white/58',
);
expect(screen.getByText('预计等待').className).toContain('text-[9px]');
expect(screen.getByText('已耗时').className).toContain('text-[9px]');
expect(screen.getByText('预计等待').parentElement?.className).toContain(
'justify-center',
);
expect(screen.getByText('已耗时').parentElement?.className).toContain(
'justify-center',
);
expect(screen.getByText('1 分 15 秒')).toBeTruthy();
expect(screen.getByText('2 分 5 秒')).toBeTruthy();
expect(screen.queryByText('预计还需 1 分 15 秒')).toBeNull();
expect(screen.queryByText('已耗时 2 分 5 秒')).toBeNull();
expect(screen.queryByText('计时')).toBeNull();
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
'justify-start',
);
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
'pt-[4%]',
);
expect(screen.getByText('总进度').className).toContain('text-[9px]');
expect(screen.getByText('42%').className).toContain('text-[1.15rem]');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.className,
).toContain('w-[min(35rem,94vw)]');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.className,
).toContain('sm:w-[52rem]');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.getAttribute('data-ring-start-degrees'),
).toBe('225');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.getAttribute('data-ring-sweep-degrees'),
).toBe('270');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.getAttribute('data-ring-gap-degrees'),
).toBe('90');
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.getAttribute('data-ring-fill-degrees'),
).toBe('113');
expect(screen.getByTestId('generation-hero-progress-ring').tagName).toBe(
'svg',
);
expect(
screen
.getByTestId('generation-hero-progress-ring')
.getAttribute('viewBox'),
).toBe('0 0 400 400');
expect(
screen
.getByTestId('generation-hero-progress-ring-track')
.getAttribute('r'),
).toBe('166');
expect(
screen
.getByTestId('generation-hero-progress-ring-track')
.getAttribute('stroke-width'),
).toBe('18');
expect(
screen
.getByTestId('generation-hero-progress-ring-fill')
.getAttribute('stroke-dasharray'),
).toMatch(/^328\.\d{2} 1043\.\d{2}$/u);
expect(
screen.getByRole('progressbar', { name: progressTitle }),
).toBeTruthy();
expect(
screen
.getByRole('progressbar', { name: progressTitle })
.getAttribute('aria-valuenow'),
).toBe('42');
expect(screen.getByText('当前步骤')).toBeTruthy();
expect(screen.getByText('当前步骤').className).toContain('text-[10px]');
expect(screen.getByText('编译草稿')).toBeTruthy();
expect(screen.getByText('编译草稿').className).toContain('text-[14px]');
expect(screen.getByText('进行中 50%')).toBeTruthy();
expect(screen.getByText('进行中 50%').className).toContain('text-[11px]');
expect(
screen.getByTestId('generation-current-step-card').className,
).toContain('bg-white/58');
expect(
screen.getByRole('progressbar', { name: '编译草稿 进度' }),
).toBeTruthy();
expect(screen.queryByText('收集设定')).toBeNull();
expect(screen.queryByText('写回结果')).toBeNull();
expect(screen.queryByText('当前批次')).toBeNull();
expect(screen.getByText('预计等待')).toBeTruthy();
expect(screen.getByText('计时')).toBeTruthy();
const statsNode = screen
.getByText('预计等待')
.closest('.custom-world-generation-stats');
expect(statsNode?.className).toContain(
'custom-world-generation-stats--two-column',
);
expect(statsNode?.getAttribute('style')).toContain(
'grid-template-columns: repeat(2, minmax(0, 1fr))',
);
const stepNodes = [
screen.getByText('收集设定'),
screen.getByText('编译草稿'),
screen.getByText('写回结果'),
].map((node) => node.closest('.custom-world-generation-step'));
expect(stepNodes.every(Boolean)).toBe(true);
expect(stepNodes[0]?.getAttribute('style')).toContain(
'--generation-step-delay: 0ms',
);
expect(stepNodes[1]?.getAttribute('style')).toContain(
'--generation-step-delay: 90ms',
);
expect(stepNodes[2]?.getAttribute('style')).toContain(
'--generation-step-delay: 180ms',
);
expect(screen.queryByText('正在整理当前设定步骤')).toBeNull();
},
);
test('keeps batch module for other generation pages', () => {
test('keeps the setting information panel as compact information cards', () => {
render(
<CustomWorldGenerationView
settingText="大鱼吃小鱼题材"
anchorEntries={[
{ id: 'topic', label: '题材', value: '火锅' },
{ id: 'count', label: '素材数量', value: '20 种素材' },
]}
progress={createProgress()}
isGenerating
error={null}
onBack={() => {}}
onEditSetting={() => {}}
onRetry={() => {}}
backLabel="返回创作中心"
settingDescription={null}
settingActionLabel={null}
settingTitle="当前大鱼吃小鱼信息"
progressTitle="大鱼吃小鱼草稿生成进度"
/>,
);
expect(screen.getByText('当前批次')).toBeTruthy();
expect(
screen
.getByText('预计等待')
.closest('.custom-world-generation-stats')
?.className,
).not.toContain('custom-world-generation-stats--two-column');
expect(screen.getByText('当前大鱼吃小鱼信息')).toBeTruthy();
expect(screen.getByText('当前大鱼吃小鱼信息').className).toContain('text-[13px]');
expect(screen.getByText('题材')).toBeTruthy();
expect(screen.getByText('题材').className).toContain('text-[9px]');
expect(screen.getByText('火锅')).toBeTruthy();
expect(screen.getByText('火锅').className).toContain('text-[13px]');
expect(screen.getByText('素材数量')).toBeTruthy();
expect(screen.getByText('20 种素材')).toBeTruthy();
expect(screen.queryByText('大鱼吃小鱼题材')).toBeNull();
expect(screen.getByTestId('generation-page-background-video')).toBeTruthy();
});
});