收口首页与详情动作按钮

为 CopyFeedbackButton 增加 actionShape 共享能力
将拼图广场详情 hero 动作迁移到共享按钮组件
将智能创作首页与抽屉入口迁移到共享按钮组件
将绑定手机号身份提示块迁移到 PlatformSubpanel
同步更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 15:57:58 +08:00
parent 7411b9a435
commit eb73ffb34d
10 changed files with 163 additions and 53 deletions

View File

@@ -47,6 +47,9 @@ test('绑定手机号表单复用平台输入和字段标题', async () => {
expect(screen.getByText('手机号').className).toContain(
'text-[var(--platform-text-strong)]',
);
expect(screen.getByText('当前登录身份:微信旅人').className).toContain(
'platform-subpanel',
);
await user.type(phoneInput, '13800000000');
await user.type(codeInput, '123456');

View File

@@ -5,6 +5,7 @@ import type { AuthCaptchaChallenge, AuthUser } from '../../services/authService'
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 { CaptchaChallengeField } from './CaptchaChallengeField';
@@ -78,9 +79,14 @@ export function BindPhoneScreen({
<p className="mt-4 max-w-md text-sm leading-7 text-[var(--platform-text-base)]">
</p>
<div className="platform-subpanel mt-8 rounded-2xl px-4 py-4 text-sm text-[var(--platform-text-base)]">
<PlatformSubpanel
as="div"
radius="sm"
padding="md"
className="mt-8 text-sm text-[var(--platform-text-base)]"
>
{user.displayName}
</div>
</PlatformSubpanel>
</div>
<form

View File

@@ -103,6 +103,7 @@ test('can opt into platform action button chrome', () => {
state="idle"
idleLabel="复制报错"
actionSurface="platform"
actionShape="pill"
actionFullWidth
/>,
);
@@ -111,6 +112,7 @@ test('can opt into platform action button chrome', () => {
expect(button.className).toContain('platform-button--primary');
expect(button.className).toContain('w-full');
expect(button.className).toContain('rounded-full');
expect(button.className).toContain('disabled:cursor-not-allowed');
});

View File

@@ -4,6 +4,7 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react';
import {
getPlatformActionButtonClassName,
type PlatformActionButtonSize,
type PlatformActionButtonShape,
type PlatformActionButtonSurface,
type PlatformActionButtonTone,
} from './platformActionButtonModel';
@@ -34,6 +35,7 @@ type CopyFeedbackButtonProps = Omit<
actionSurface?: PlatformActionButtonSurface;
actionTone?: PlatformActionButtonTone;
actionSize?: PlatformActionButtonSize;
actionShape?: PlatformActionButtonShape;
actionFullWidth?: boolean;
actionAppearance?: CopyFeedbackButtonActionAppearance;
actionPillTone?: PlatformPillBadgeTone;
@@ -74,6 +76,7 @@ export function CopyFeedbackButton({
actionSurface,
actionTone = 'primary',
actionSize = 'sm',
actionShape = 'default',
actionFullWidth = false,
actionAppearance = 'plain',
actionPillTone = 'neutral',
@@ -112,6 +115,7 @@ export function CopyFeedbackButton({
surface: actionSurface,
tone: actionTone,
size: actionSize,
shape: actionShape,
fullWidth: actionFullWidth,
})
: actionAppearance === 'pill'

View File

@@ -1,6 +1,7 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test, vi } from 'vitest';
import { CreativeAgentHome } from './CreativeAgentHome';
@@ -45,3 +46,29 @@ test('CreativeAgentHome uses shared status message for home error', () => {
'border-[var(--platform-button-danger-border)]',
);
});
test('CreativeAgentHome reuses shared button chrome for drawer and reward actions', async () => {
const user = userEvent.setup();
renderHome();
const menuButton = screen.getByRole('button', { name: '打开侧边栏' });
const rewardButton = screen.getByRole('button', {
name: '搓闪应用 分1亿激励',
});
expect(menuButton.className).toContain('platform-icon-button');
expect(rewardButton.className).toContain('platform-button');
await user.click(menuButton);
expect(
screen.getByRole('button', { name: '开启新对话' }).className,
).toContain('platform-button');
expect(
screen.getByRole('button', { name: '我的创作' }).className,
).toContain('platform-button');
expect(screen.getByRole('button', { name: '账号' }).className).toContain(
'platform-icon-button',
);
});

View File

@@ -14,6 +14,7 @@ import {
import { useMemo, useState } from 'react';
import type { CreativeAgentInputPart } from '../../../packages/shared/src/contracts/creativeAgent';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -203,30 +204,31 @@ function CreativeAgentDrawer({
</header>
<div className="shrink-0 space-y-3 px-5">
<button
type="button"
<PlatformActionButton
onClick={() => {
onStartNewChat();
onClose();
}}
className="creative-agent-drawer__new-chat"
fullWidth
>
<MessageCircle className="h-5 w-5" />
<span></span>
</button>
</PlatformActionButton>
<button
type="button"
<PlatformActionButton
onClick={() => {
onOpenDrafts();
onClose();
}}
className="creative-agent-drawer__nav-row"
align="start"
fullWidth
>
<Bookmark className="h-5 w-5" />
<span></span>
<ChevronRight className="ml-auto h-4 w-4 opacity-55" />
</button>
</PlatformActionButton>
</div>
<div className="mt-5 min-h-0 flex-1 overflow-y-auto px-5 pb-5">
@@ -261,17 +263,16 @@ function CreativeAgentDrawer({
</div>
<footer className="flex shrink-0 items-center justify-between gap-3 px-5 py-5 pb-[max(1.15rem,env(safe-area-inset-bottom))]">
<button
type="button"
<PlatformIconButton
onClick={() => {
onOpenAccount();
onClose();
}}
className="creative-agent-drawer__avatar"
aria-label="账号"
>
<UserRound className="h-5 w-5" />
</button>
label="账号"
title="账号"
icon={<UserRound className="h-5 w-5" />}
/>
<div className="flex items-center gap-3">
<PlatformIconButton
onClick={() => {
@@ -331,15 +332,13 @@ export function CreativeAgentHome({
<div className="creative-agent-home platform-remap-surface">
<div className="creative-agent-home__backdrop" />
<header className="creative-agent-home__topbar">
<button
type="button"
<PlatformIconButton
onClick={() => setDrawerOpen(true)}
className="creative-agent-home__topbar-button"
aria-label="打开侧边栏"
label="打开侧边栏"
title="菜单"
>
<Menu className="h-6 w-6" />
</button>
icon={<Menu className="h-6 w-6" />}
/>
<RpgEntryBrandLogo className="creative-agent-home__brand" decorative />
</header>
@@ -358,15 +357,15 @@ export function CreativeAgentHome({
onClick={() => submitText(item.prompt)}
/>
))}
<button
type="button"
<PlatformActionButton
className="creative-agent-home__reward"
disabled={isBusy}
onClick={() => submitText('帮我做一个能马上分享的创意拼图。')}
shape="pill"
>
<Sparkles className="h-6 w-6" />
<span> 1亿</span>
</button>
</PlatformActionButton>
</div>
{error ? (

View File

@@ -180,6 +180,61 @@ test('reuses platform subpanel and pill badge chrome for puzzle detail metadata'
);
});
test('reuses shared action button chrome for puzzle detail hero actions', () => {
render(
<PuzzleGalleryDetailView
item={{
...detailItem,
coverImageSrc: '/hero-cover.png',
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '第一关画面',
selectedCandidateId: null,
coverImageSrc: '/level-1.png',
coverAssetId: null,
generationStatus: 'ready',
candidates: [],
},
{
levelId: 'level-2',
levelName: '第二关',
pictureDescription: '第二关画面',
selectedCandidateId: null,
coverImageSrc: '/level-2.png',
coverAssetId: null,
generationStatus: 'ready',
candidates: [],
},
],
}}
onBack={vi.fn()}
onEdit={vi.fn()}
onStartGame={vi.fn()}
/>,
);
expect(screen.getByRole('button', { name: '修改作品' }).className).toContain(
'platform-action-button--editor-dark',
);
expect(screen.getByRole('button', { name: '分享作品' }).className).toContain(
'platform-action-button--editor-dark',
);
expect(
screen.getByRole('button', { name: '进入第 1 关' }).className,
).toContain('platform-action-button--accent');
expect(screen.getByRole('button', { name: '返回' }).className).toContain(
'rounded-full',
);
expect(
screen.getByRole('button', { name: '上一张关卡图' }).className,
).toContain('rounded-full');
expect(
screen.getByRole('button', { name: '下一张关卡图' }).className,
).toContain('rounded-full');
});
test('falls back to legacy selection copy when clipboard api rejects', async () => {
const user = userEvent.setup();
const writeText = vi.fn(async () => {

View File

@@ -14,6 +14,8 @@ import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import { buildPuzzlePublicWorkCode } from '../../services/publicWorkCode';
import { CopyCodeButton } from '../common/CopyCodeButton';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -136,25 +138,27 @@ export function PuzzleGalleryDetailView({
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
<div className="relative overflow-hidden rounded-[1.8rem] border border-amber-100/16 bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.18),transparent_32%),linear-gradient(135deg,rgba(76,29,19,0.98),rgba(15,23,42,0.98))] px-4 py-4 text-white sm:px-5">
<div className="flex items-start justify-between gap-3">
<button
type="button"
<PlatformIconButton
onClick={onBack}
aria-label="返回"
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84"
>
<ArrowLeft className="h-4 w-4" />
</button>
label="返回"
title="返回"
variant="darkMini"
className="h-10 w-10 !border-white/16 !bg-white/10 !text-white/84 backdrop-blur hover:!bg-white/16 hover:!text-white"
icon={<ArrowLeft className="h-4 w-4" />}
/>
<div className="flex flex-wrap justify-end gap-2">
{onEdit ? (
<button
type="button"
<PlatformActionButton
disabled={isBusy}
onClick={onEdit}
className="inline-flex items-center gap-2 rounded-full bg-white/12 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
surface="editorDark"
tone="secondary"
shape="pill"
className="!border-white/16 !bg-white/12 !text-white hover:!bg-white/18"
>
<Pencil className="h-4 w-4" />
</button>
</PlatformActionButton>
) : null}
<CopyFeedbackButton
state={shareState}
@@ -162,17 +166,20 @@ export function PuzzleGalleryDetailView({
onClick={sharePublicWork}
idleLabel="分享作品"
idleIcon={<Share2 className="h-4 w-4" />}
className="inline-flex items-center gap-2 rounded-full bg-white/12 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
actionSurface="editorDark"
actionTone="secondary"
actionShape="pill"
className="!border-white/16 !bg-white/12 !text-white hover:!bg-white/18"
/>
<button
type="button"
<PlatformActionButton
disabled={isBusy}
onClick={onStartGame}
className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-4 py-2 text-sm font-bold text-slate-950 disabled:opacity-45"
tone="accent"
shape="pill"
>
<Play className="h-4 w-4" />
1
</button>
</PlatformActionButton>
</div>
</div>
@@ -231,24 +238,22 @@ export function PuzzleGalleryDetailView({
>
{coverImageSrc && hasCoverCarousel ? (
<>
<button
type="button"
<PlatformIconButton
onClick={showPreviousCover}
className="absolute left-3 top-1/2 z-10 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/30 bg-slate-950/36 text-white backdrop-blur"
aria-label="上一张关卡图"
label="上一张关卡图"
title="上一张关卡图"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
type="button"
variant="darkMini"
className="absolute left-3 top-1/2 z-10 h-10 w-10 -translate-y-1/2 !border-white/30 !bg-slate-950/36 !text-white backdrop-blur hover:!bg-slate-950/52"
icon={<ChevronLeft className="h-5 w-5" />}
/>
<PlatformIconButton
onClick={showNextCover}
className="absolute right-3 top-1/2 z-10 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/30 bg-slate-950/36 text-white backdrop-blur"
aria-label="下一张关卡图"
label="下一张关卡图"
title="下一张关卡图"
>
<ChevronRight className="h-5 w-5" />
</button>
variant="darkMini"
className="absolute right-3 top-1/2 z-10 h-10 w-10 -translate-y-1/2 !border-white/30 !bg-slate-950/36 !text-white backdrop-blur hover:!bg-slate-950/52"
icon={<ChevronRight className="h-5 w-5" />}
/>
<div className="absolute inset-x-4 bottom-3 z-10 flex justify-center gap-1.5">
{coverSlides.map((slide, index) => (
<button