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