收口首页与详情动作按钮
为 CopyFeedbackButton 增加 actionShape 共享能力 将拼图广场详情 hero 动作迁移到共享按钮组件 将智能创作首页与抽屉入口迁移到共享按钮组件 将绑定手机号身份提示块迁移到 PlatformSubpanel 同步更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user