收口前端平台组件库能力

新增 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

@@ -191,6 +191,14 @@ test('creation hub draft card renders compiled work summary fields', () => {
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
getWorkState={(item) =>
item.kind === 'rpg'
? {
hasGenerationFailure: true,
generationFailureSummary: '生成失败',
}
: null
}
/>,
);
@@ -960,9 +968,128 @@ test('creation hub published work uses unified list card layout', () => {
expect(html).toContain('creation-work-list');
expect(html).toContain('platform-category-game-item');
expect(html).toContain('creation-work-card__side-cover');
expect(html).toContain('creation-work-card__badge');
expect(html).toContain('border-[var(--platform-subpanel-border)]');
expect(html).not.toContain('platform-pill');
expect(html).not.toContain('col-span-2 sm:col-span-1');
});
test('creation hub failed draft badge reuses PlatformPillBadge danger chrome', () => {
const html = renderToStaticMarkup(
<CustomWorldCreationHub
mode="works-only"
shelfItems={[
{
id: 'failed-card',
kind: 'bark-battle',
status: 'draft',
hasGenerationFailure: true,
generationFailureSummary: '生成失败',
title: '失败但仍可恢复的草稿',
summary: '失败草稿也来自真实作品架摘要。',
authorDisplayName: '测试作者',
updatedAt: buildUpdatedAtDaysAgo(1),
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode: null,
sharePath: null,
openActionLabel: '继续创作',
canDelete: false,
canShare: false,
badges: [
{ id: 'status', label: '草稿', tone: 'neutral' },
{ id: 'type', label: '汪汪', tone: 'neutral' },
],
metrics: [],
actions: { open: () => {} },
source: {
kind: 'bark-battle',
item: {
workId: 'failed-card',
draftId: 'failed-draft',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
title: '失败但仍可恢复的草稿',
summary: '失败草稿也来自真实作品架摘要。',
themeDescription: '公园舞台',
playerImageDescription: '柯基选手',
opponentImageDescription: '哈士奇对手',
playerCharacterImageSrc: null,
opponentCharacterImageSrc: null,
uiBackgroundImageSrc: null,
difficultyPreset: 'normal',
status: 'draft',
generationStatus: 'failed',
publishReady: false,
playCount: 0,
updatedAt: buildUpdatedAtDaysAgo(1),
publishedAt: null,
},
},
},
]}
items={[]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(html).toContain('creation-work-card__failure-status');
expect(html).toContain('border-[var(--platform-button-danger-border)]');
expect(html).toContain('失败草稿也来自真实作品架摘要。');
});
test('creation hub empty shelf reuses PlatformEmptyState chrome', () => {
const html = renderToStaticMarkup(
<CustomWorldCreationHub
mode="works-only"
items={[]}
loading={false}
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(html).toContain('还没有作品');
expect(html).toContain('platform-surface platform-surface--soft');
expect(html).toContain('min-h-[14rem]');
expect(html).not.toContain('platform-subpanel flex min-h-[14rem]');
});
test('creation hub loading shelf reuses PlatformSubpanel skeleton cards', () => {
const html = renderToStaticMarkup(
<CustomWorldCreationHub
mode="works-only"
items={[]}
loading
error={null}
onRetry={() => {}}
onCreateType={noopCreateType}
onOpenDraft={() => {}}
onEnterPublished={() => {}}
entryConfig={testEntryConfig}
creationTypes={testCreationTypes}
/>,
);
expect(html.match(/platform-subpanel/g)?.length).toBe(3);
expect(html).toContain('min-h-[10.5rem]');
expect(html).toContain('rounded-[1.25rem]');
expect(html).not.toContain('rounded-[1.2rem]');
});
test('creation hub draft cards use cover background and hide updated time', () => {
const html = renderToStaticMarkup(
<CustomWorldCreationHub

View File

@@ -2,6 +2,9 @@ import { useEffect, useMemo, useState } from 'react';
import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
import type {
PlatformCreationTypeCard,
@@ -12,9 +15,7 @@ import {
type CreationWorkShelfMetricId,
getCreationWorkShelfItemTime,
} from './creationWorkShelf';
import {
CustomWorldCreationStartCard,
} from './CustomWorldCreationStartCard';
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
import {
type CustomWorldWorkFilter,
@@ -53,11 +54,14 @@ type CustomWorldCreationHubProps = {
function EmptyState({ title }: { title: string }) {
return (
<div className="platform-subpanel flex min-h-[14rem] flex-col items-center justify-center rounded-[1.6rem] px-6 py-8 text-center">
<div className="text-lg font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</div>
<PlatformEmptyState
surface="soft"
size="panel"
tone="base"
className="font-semibold text-[var(--platform-text-strong)]"
>
{title}
</PlatformEmptyState>
);
}
@@ -260,13 +264,14 @@ export function CustomWorldCreationHub({
{showWorkShelf && error ? (
<div className="flex justify-end">
<button
type="button"
<PlatformActionButton
onClick={onRetry}
className="platform-button platform-button--ghost min-h-0 rounded-full px-4 py-2 text-sm"
tone="ghost"
shape="pill"
className="min-h-0 py-2"
>
</button>
</PlatformActionButton>
</div>
) : null}
@@ -274,9 +279,12 @@ export function CustomWorldCreationHub({
loading ? (
<div className={WORK_GRID_CLASS}>
{Array.from({ length: 3 }).map((_, index) => (
<div
<PlatformSubpanel
as="div"
key={`skeleton-${index}`}
className="platform-subpanel min-h-[10.5rem] rounded-[1.2rem] p-3 sm:min-h-[12rem] sm:rounded-[1.6rem] sm:p-5"
padding="sm"
radius="md"
className="min-h-[10.5rem] sm:min-h-[12rem] sm:p-5"
>
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
<div className="mt-5 h-6 w-24 rounded-full bg-[var(--platform-track-fill)] sm:mt-6 sm:h-8 sm:w-36" />
@@ -286,7 +294,7 @@ export function CustomWorldCreationHub({
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
</div>
</div>
</PlatformSubpanel>
))}
</div>
) : filteredItems.length > 0 ? (

View File

@@ -7,8 +7,8 @@ import {
Trash2,
} from 'lucide-react';
import {
default as React,
type CSSProperties,
default as React,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
type TouchEvent as ReactTouchEvent,
@@ -18,6 +18,7 @@ import {
useState,
} from 'react';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
import {
formatPlatformWorkDisplayName,
@@ -44,10 +45,17 @@ type CustomWorldWorkCardProps = {
pointIncentiveBusy?: boolean;
};
const BADGE_TONE_CLASS: Record<CreationWorkShelfBadgeTone, string> = {
warm: 'platform-pill--warm',
success: 'platform-pill--success',
neutral: 'platform-pill--neutral',
type WorkCardPillBadgeTone = React.ComponentProps<
typeof PlatformPillBadge
>['tone'];
const BADGE_TONE_CLASS: Record<
CreationWorkShelfBadgeTone,
NonNullable<WorkCardPillBadgeTone>
> = {
warm: 'warning',
success: 'success',
neutral: 'neutral',
};
const METRIC_ANIMATION_DURATION_MS = 820;
@@ -671,12 +679,14 @@ export function CustomWorldWorkCard({
<div className="creation-work-card__meta platform-category-game-item__meta">
{item.badges.slice(1).map((badge) => (
<span
<PlatformPillBadge
key={`${item.id}-${badge.id}`}
className={`creation-work-card__badge platform-pill ${BADGE_TONE_CLASS[badge.tone]}`}
tone={BADGE_TONE_CLASS[badge.tone]}
size="xs"
className="creation-work-card__badge"
>
{formatPlatformWorkDisplayTag(badge.label)}
</span>
</PlatformPillBadge>
))}
</div>
@@ -685,13 +695,15 @@ export function CustomWorldWorkCard({
</div>
{item.hasGenerationFailure ? (
<div
<PlatformPillBadge
tone="danger"
size="xs"
aria-label={item.generationFailureSummary ?? '生成失败'}
className="creation-work-card__failure-status"
icon={<CircleAlert aria-hidden="true" className="h-3.5 w-3.5" />}
>
<CircleAlert aria-hidden="true" className="h-3.5 w-3.5" />
<span>{item.generationFailureSummary ?? '生成失败'}</span>
</div>
</PlatformPillBadge>
) : null}
{isPublished ? (