收口前端平台组件库能力

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

@@ -59,7 +59,8 @@ function createSessionResponse(): PuzzleClearSessionResponse {
boardBackgroundPrompt: '星港中央棋盘底图',
generateBoardBackground: false,
boardBackgroundAsset: null,
cardBackImageSrc: '/creation-type-references/puzzle-clear-card-back.webp',
cardBackImageSrc:
'/creation-type-references/puzzle-clear-card-back.webp',
atlasAsset: null,
patternGroups: [],
cardAssets: [],
@@ -84,12 +85,7 @@ test('工作台提交结构化表单与底图槽位 payload', async () => {
'data:image/png;base64,board-background',
);
render(
<PuzzleClearWorkspace
onBack={vi.fn()}
onSubmitted={onSubmitted}
/>,
);
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={onSubmitted} />);
fireEvent.change(screen.getByLabelText('作品标题'), {
target: { value: ' 星港拼消消 ' },
@@ -145,23 +141,35 @@ test('工作台提交结构化表单与底图槽位 payload', async () => {
});
test('工作台不渲染聊天式 Agent 输入', () => {
render(
<PuzzleClearWorkspace
onBack={vi.fn()}
onSubmitted={vi.fn()}
/>,
);
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={vi.fn()} />);
expect(screen.queryByText(/|||/u)).toBeNull();
});
test('关闭 AI 生成底图且未上传底图时不允许提交', async () => {
render(
<PuzzleClearWorkspace
onBack={vi.fn()}
onSubmitted={vi.fn()}
/>,
test('工作台字段标题复用平台字段标签样式', () => {
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={vi.fn()} />);
expect(screen.getByText('作品标题').className).toContain('tracking-[0.18em]');
expect(screen.getByText('简介').className).toContain('tracking-[0.18em]');
expect(screen.getByText('主题词').className).toContain('tracking-[0.18em]');
});
test('工作台表单控件复用平台输入框与整行开关', () => {
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={vi.fn()} />);
const formPanel = screen.getByText('作品标题').closest('section');
expect(formPanel?.className).toContain('platform-subpanel');
expect(formPanel?.className).toContain('rounded-[1.25rem]');
expect(screen.getByLabelText('作品标题').className).toContain('bg-white/90');
expect(screen.getByLabelText('简介').className).toContain('resize-none');
expect(screen.getByLabelText('主题词').className).toContain('bg-white/90');
expect(screen.getByText('AI 生成底图').closest('label')?.className).toContain(
'bg-white/74',
);
});
test('关闭 AI 生成底图且未上传底图时不允许提交', async () => {
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={vi.fn()} />);
fireEvent.change(screen.getByLabelText('作品标题'), {
target: { value: '星港拼消消' },
@@ -172,7 +180,8 @@ test('关闭 AI 生成底图且未上传底图时不允许提交', async () => {
fireEvent.click(screen.getByRole('checkbox', { name: 'AI 生成底图' }));
expect(
(screen.getByRole('button', { name: '生成' }) as HTMLButtonElement).disabled,
(screen.getByRole('button', { name: '生成' }) as HTMLButtonElement)
.disabled,
).toBe(true);
fireEvent.click(screen.getByRole('button', { name: '生成' }));
@@ -187,12 +196,7 @@ test('工作台支持原生表单提交生成', async () => {
const onSubmitted = vi.fn();
vi.mocked(puzzleClearClient.createSession).mockResolvedValue(response);
render(
<PuzzleClearWorkspace
onBack={vi.fn()}
onSubmitted={onSubmitted}
/>,
);
render(<PuzzleClearWorkspace onBack={vi.fn()} onSubmitted={onSubmitted} />);
fireEvent.change(screen.getByLabelText('作品标题'), {
target: { value: '星港拼消消' },

View File

@@ -9,6 +9,12 @@ import type {
import { puzzleClearClient } from '../../services/puzzle-clear/puzzleClearClient';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
import { PlatformToggleRow } from '../common/PlatformToggleRow';
type PuzzleClearWorkspaceProps = {
isBusy?: boolean;
@@ -68,7 +74,9 @@ export function PuzzleClearWorkspace({
const hasBoardBackgroundInput = useMemo(
() =>
formState.generateBoardBackground ||
Boolean(formState.boardBackgroundAsset || formState.boardBackgroundImageSrc),
Boolean(
formState.boardBackgroundAsset || formState.boardBackgroundImageSrc,
),
[
formState.boardBackgroundAsset,
formState.boardBackgroundImageSrc,
@@ -134,23 +142,22 @@ export function PuzzleClearWorkspace({
className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4"
>
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
<PlatformActionButton
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
tone="ghost"
size="xs"
className="min-h-0 gap-2 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</PlatformActionButton>
</div>
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,0.88fr)_minmax(0,1.12fr)]">
<section className="platform-subpanel flex min-h-0 flex-col gap-3 overflow-y-auto rounded-[1.25rem] p-4">
<PlatformSubpanel className="flex min-h-0 flex-col gap-3 overflow-y-auto">
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<input
<PlatformFieldLabel variant="section"></PlatformFieldLabel>
<PlatformTextField
value={formState.workTitle}
maxLength={32}
disabled={isBusy || isSubmitting}
@@ -160,15 +167,15 @@ export function PuzzleClearWorkspace({
workTitle: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
density="roomy"
className="mt-2 px-3"
/>
</label>
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<textarea
<PlatformFieldLabel variant="section"></PlatformFieldLabel>
<PlatformTextField
variant="textarea"
value={formState.workDescription}
maxLength={120}
disabled={isBusy || isSubmitting}
@@ -179,15 +186,15 @@ export function PuzzleClearWorkspace({
workDescription: event.target.value,
}))
}
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
size="md"
density="roomy"
className="mt-2 px-3"
/>
</label>
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<input
<PlatformFieldLabel variant="section"></PlatformFieldLabel>
<PlatformTextField
value={formState.themePrompt}
maxLength={80}
disabled={isBusy || isSubmitting}
@@ -197,34 +204,35 @@ export function PuzzleClearWorkspace({
themePrompt: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
density="roomy"
className="mt-2 px-3"
/>
</label>
<label className="flex items-center justify-between gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
<span className="text-sm font-bold text-[var(--platform-text-strong)]">
AI
</span>
<input
type="checkbox"
checked={formState.generateBoardBackground}
disabled={isBusy || isSubmitting}
onChange={(event) =>
setFormState((current) => ({
...current,
generateBoardBackground: event.target.checked,
}))
}
className="h-5 w-5 accent-[var(--platform-accent)]"
/>
</label>
<PlatformToggleRow
label="AI 生成底图"
checked={formState.generateBoardBackground}
disabled={isBusy || isSubmitting}
onChange={(checked) =>
setFormState((current) => ({
...current,
generateBoardBackground: checked,
}))
}
labelClassName="font-bold"
/>
{localError || error ? (
<div className="platform-banner platform-banner--danger rounded-2xl text-sm leading-6">
<PlatformStatusMessage
tone="error"
surface="platform"
size="md"
className="rounded-2xl"
>
{localError ?? error}
</div>
</PlatformStatusMessage>
) : null}
</section>
</PlatformSubpanel>
<div className="flex min-h-[28rem] min-w-0 flex-col">
<CreativeImageInputPanel
@@ -302,14 +310,11 @@ export function PuzzleClearWorkspace({
</div>
<div className="mt-auto flex justify-end gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] pt-3">
<button
<PlatformActionButton
type="submit"
disabled={!canSubmit || isSubmitting || isBusy}
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-5 py-3 ${
!canSubmit || isSubmitting || isBusy
? 'cursor-not-allowed opacity-55'
: ''
}`}
size="md"
className="min-h-11 gap-2 px-5"
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -317,7 +322,7 @@ export function PuzzleClearWorkspace({
<Send className="h-4 w-4" />
)}
</button>
</PlatformActionButton>
</div>
</form>
);