收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user