收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -21,8 +21,118 @@ vi.mock('../../services/visual-novel-creation', () => ({
vi.mock('../../services/assetReadUrlService', () => ({
resolveAssetReadUrl: vi.fn().mockResolvedValue(''),
shouldResolveAssetReadUrl: vi.fn(() => false),
}));
test('visual novel profile tab uses PlatformSubpanel shells', () => {
const { container } = render(
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
);
const profilePanels = Array.from(
container.querySelectorAll('section.platform-subpanel'),
).filter((panel) => panel.className.includes('rounded-[1.35rem]'));
const workTitlePanel = screen.getByText('作品名称').closest('section');
expect(profilePanels).toHaveLength(2);
for (const panel of profilePanels) {
expect(panel.className).toContain('p-4');
}
expect(workTitlePanel?.className).toContain('platform-subpanel');
expect(workTitlePanel?.className).toContain('rounded-[1.35rem]');
});
test('visual novel profile media previews use PlatformMediaFrame aspects', () => {
const draftWithCover = {
...mockVisualNovelDraft,
coverImageSrc: '/visual-novel/cover.png',
};
const { container } = render(
<VisualNovelResultView draft={draftWithCover} onBack={() => {}} />,
);
const coverImages = Array.from(
container.querySelectorAll('img[src="/visual-novel/cover.png"]'),
);
const mediaFrameClassNames = coverImages.map(
(image) => image.closest('div.relative')?.className ?? '',
);
expect(coverImages).toHaveLength(2);
expect(mediaFrameClassNames).toContainEqual(
expect.stringContaining('aspect-[4/3]'),
);
expect(mediaFrameClassNames).toContainEqual(
expect.stringContaining('aspect-[16/9]'),
);
const assetPreviewFrameClassName = mediaFrameClassNames.find((className) =>
className.includes('aspect-[16/9]'),
);
expect(assetPreviewFrameClassName).toBeTruthy();
expect(assetPreviewFrameClassName).not.toContain(
'bg-[var(--platform-subpanel-fill)]',
);
});
test('visual novel upload-only asset picker uses PlatformEmptyState chrome', async () => {
const user = userEvent.setup();
render(
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
);
await user.click(screen.getByRole('button', { name: '封面' }));
const pickerDialog = screen.getByRole('dialog', { name: '封面' });
const uploadOnlyEmptyState =
within(pickerDialog).getByText('选择本地文件上传');
expect(uploadOnlyEmptyState.className).toContain('border-dashed');
expect(uploadOnlyEmptyState.className).toContain('rounded-[1.35rem]');
expect(uploadOnlyEmptyState.className).toContain('min-h-24');
expect(uploadOnlyEmptyState.className).toContain(
'text-[var(--platform-text-base)]',
);
});
test('visual novel entity list items use interactive PlatformSubpanel shells', async () => {
const user = userEvent.setup();
render(
<VisualNovelResultView draft={mockVisualNovelDraft} onBack={() => {}} />,
);
await user.click(screen.getByRole('button', { name: '角色' }));
const characterCard = screen.getByRole('button', { name: //u });
expect(characterCard.className).toContain('platform-subpanel');
expect(characterCard.className).toContain('hover:bg-white');
expect(characterCard.className).toContain('disabled:cursor-not-allowed');
expect(characterCard.className).toContain('rounded-[1.25rem]');
});
test('visual novel empty entity list uses PlatformSubpanel shell', async () => {
const user = userEvent.setup();
const emptyCharacterDraft = {
...mockVisualNovelDraft,
characters: [],
};
render(
<VisualNovelResultView draft={emptyCharacterDraft} onBack={() => {}} />,
);
await user.click(screen.getByRole('button', { name: '角色' }));
const emptyPanel = screen.getByText('暂无角色').closest('.platform-subpanel');
expect(emptyPanel?.className).toContain('platform-subpanel');
expect(emptyPanel?.className).toContain('rounded-[1.25rem]');
expect(emptyPanel?.className).toContain('min-h-32');
});
test('visual novel result opens complex editors as a dialog', async () => {
const user = userEvent.setup();
@@ -140,7 +250,9 @@ test('visual novel result uploads scene and character assets into platform refer
test('visual novel result generates scene background from asset picker', async () => {
const user = userEvent.setup();
const onSaveDraft = vi.fn();
const visualNovelCreation = await import('../../services/visual-novel-creation');
const visualNovelCreation = await import(
'../../services/visual-novel-creation'
);
const generateImageMock = vi.mocked(
visualNovelCreation.generateVisualNovelImageAsset,
);