收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -11,10 +11,19 @@ describe('BarkBattleConfigEditor', () => {
|
||||
const onPreview = vi.fn();
|
||||
render(<BarkBattleConfigEditor isBusy={false} onPreview={onPreview} />);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '汪汪声浪大作战' })).toBeTruthy();
|
||||
expect(screen.getByText('轻配置')).toBeTruthy();
|
||||
expect((screen.getByLabelText('作品标题') as HTMLInputElement).value).toBe('我的声浪竞技场');
|
||||
expect((screen.getByLabelText('难度预设') as HTMLSelectElement).value).toBe('normal');
|
||||
expect(
|
||||
screen.getByRole('heading', { name: '汪汪声浪大作战' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('轻配置').className).toContain('rounded-full');
|
||||
expect(screen.getByText('轻配置').className).toContain(
|
||||
'border-emerald-200',
|
||||
);
|
||||
expect((screen.getByLabelText('作品标题') as HTMLInputElement).value).toBe(
|
||||
'我的声浪竞技场',
|
||||
);
|
||||
expect((screen.getByLabelText('难度预设') as HTMLSelectElement).value).toBe(
|
||||
'normal',
|
||||
);
|
||||
expect(screen.queryByLabelText('资源 URL')).toBeNull();
|
||||
expect(screen.queryByLabelText('玩家图片 URL')).toBeNull();
|
||||
expect(screen.queryByLabelText('对手图片 URL')).toBeNull();
|
||||
@@ -27,7 +36,10 @@ describe('BarkBattleConfigEditor', () => {
|
||||
await userEvent.clear(screen.getByLabelText('作品标题'));
|
||||
await userEvent.type(screen.getByLabelText('作品标题'), '狗狗冠军杯');
|
||||
await userEvent.clear(screen.getByLabelText('主题/场景描述'));
|
||||
await userEvent.type(screen.getByLabelText('主题/场景描述'), '霓虹公园声浪擂台');
|
||||
await userEvent.type(
|
||||
screen.getByLabelText('主题/场景描述'),
|
||||
'霓虹公园声浪擂台',
|
||||
);
|
||||
await userEvent.clear(screen.getByLabelText('玩家形象描述'));
|
||||
await userEvent.type(screen.getByLabelText('玩家形象描述'), '红围巾柴犬');
|
||||
await userEvent.clear(screen.getByLabelText('对手形象描述'));
|
||||
@@ -55,8 +67,10 @@ describe('BarkBattleConfigEditor', () => {
|
||||
const onPreview = vi.fn();
|
||||
render(<BarkBattleConfigEditor isBusy={false} onPreview={onPreview} />);
|
||||
|
||||
const defaultWords = (screen.getByLabelText('拟声词') as HTMLTextAreaElement)
|
||||
.value.split(/\n+/u)
|
||||
const defaultWords = (
|
||||
screen.getByLabelText('拟声词') as HTMLTextAreaElement
|
||||
).value
|
||||
.split(/\n+/u)
|
||||
.map((word) => word.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -77,8 +91,10 @@ describe('BarkBattleConfigEditor', () => {
|
||||
await userEvent.clear(screen.getByLabelText('对手形象描述'));
|
||||
await userEvent.type(screen.getByLabelText('对手形象描述'), '机器人拳手');
|
||||
|
||||
const updatedWords = (screen.getByLabelText('拟声词') as HTMLTextAreaElement)
|
||||
.value.split(/\n+/u)
|
||||
const updatedWords = (
|
||||
screen.getByLabelText('拟声词') as HTMLTextAreaElement
|
||||
).value
|
||||
.split(/\n+/u)
|
||||
.map((word) => word.trim())
|
||||
.filter(Boolean);
|
||||
expect(updatedWords).toEqual(
|
||||
@@ -94,7 +110,10 @@ describe('BarkBattleConfigEditor', () => {
|
||||
await userEvent.clear(screen.getByLabelText('拟声词'));
|
||||
await userEvent.type(screen.getByLabelText('拟声词'), '轰!\n破阵!');
|
||||
await userEvent.clear(screen.getByLabelText('主题/场景描述'));
|
||||
await userEvent.type(screen.getByLabelText('主题/场景描述'), '星舰机甲擂台');
|
||||
await userEvent.type(
|
||||
screen.getByLabelText('主题/场景描述'),
|
||||
'星舰机甲擂台',
|
||||
);
|
||||
|
||||
expect((screen.getByLabelText('拟声词') as HTMLTextAreaElement).value).toBe(
|
||||
'轰!\n破阵!',
|
||||
@@ -124,7 +143,9 @@ describe('BarkBattleConfigEditor', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('heading', { name: '汪汪声浪大作战' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: '汪汪声浪大作战' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '返回' })).toBeNull();
|
||||
expect(screen.getByLabelText('汪汪声浪轻配置编辑器')).toBeTruthy();
|
||||
expect(screen.getByText('外部错误')).toBeTruthy();
|
||||
@@ -144,7 +165,9 @@ describe('BarkBattleConfigEditor', () => {
|
||||
const editor = screen.getByLabelText('汪汪声浪轻配置编辑器');
|
||||
expect(editor.className).toContain('overflow-visible');
|
||||
expect(editor.className).toContain('lg:overflow-y-auto');
|
||||
expect(editor.className).not.toContain('overflow-y-auto overscroll-y-contain pr-0.5');
|
||||
expect(editor.className).not.toContain(
|
||||
'overflow-y-auto overscroll-y-contain pr-0.5',
|
||||
);
|
||||
|
||||
const themeLabel = screen.getByText('主题/场景描述');
|
||||
expect(themeLabel.className).toContain('bg-rose-50');
|
||||
|
||||
@@ -4,6 +4,14 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BarkBattleDifficultyPreset } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import { buildBarkBattleDefaultOnomatopoeia } from '../../games/bark-battle/application/BarkBattleConfig';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import {
|
||||
PlatformSelectField,
|
||||
PlatformTextField,
|
||||
} from '../common/PlatformTextField';
|
||||
import { BarkBattlePreviewCard } from './BarkBattlePreviewCard';
|
||||
|
||||
export type BarkBattleConfigEditorProps = {
|
||||
@@ -15,15 +23,14 @@ export type BarkBattleConfigEditorProps = {
|
||||
title?: string | null;
|
||||
};
|
||||
|
||||
const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: string }> = [
|
||||
const DIFFICULTY_OPTIONS: Array<{
|
||||
value: BarkBattleDifficultyPreset;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: 'easy', label: '轻松' },
|
||||
{ value: 'normal', label: '标准' },
|
||||
{ value: 'hard', label: '硬核' },
|
||||
];
|
||||
const FIELD_LABEL_CLASS =
|
||||
'mb-2 inline-flex rounded-full px-2 py-0.5 text-sm font-black text-[var(--platform-text-strong)]';
|
||||
const ACCENT_FIELD_LABEL_CLASS =
|
||||
'mb-2 inline-flex rounded-full border border-rose-200/70 bg-rose-50/88 px-2.5 py-1 text-sm font-black text-rose-700 shadow-sm';
|
||||
const DEFAULT_THEME_DESCRIPTION = '阳光草坪上的圆形声浪擂台';
|
||||
const DEFAULT_PLAYER_IMAGE_DESCRIPTION = '戴红色围巾的勇敢小狗';
|
||||
const DEFAULT_OPPONENT_IMAGE_DESCRIPTION = '戴蓝色头带的活力小狗';
|
||||
@@ -64,7 +71,8 @@ export function BarkBattleConfigEditor({
|
||||
opponentImageDescription: DEFAULT_OPPONENT_IMAGE_DESCRIPTION,
|
||||
}),
|
||||
);
|
||||
const [difficultyPreset, setDifficultyPreset] = useState<BarkBattleDifficultyPreset>('normal');
|
||||
const [difficultyPreset, setDifficultyPreset] =
|
||||
useState<BarkBattleDifficultyPreset>('normal');
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -143,17 +151,16 @@ export function BarkBattleConfigEditor({
|
||||
>
|
||||
{showBackButton && onBack ? (
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 sm:mb-4">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 self-start gap-1.5 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</span>
|
||||
</button>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -164,9 +171,9 @@ export function BarkBattleConfigEditor({
|
||||
<h1 className="m-0 text-3xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
|
||||
{headingTitle}
|
||||
</h1>
|
||||
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-black text-emerald-700">
|
||||
<PlatformPillBadge tone="success" size="xs">
|
||||
轻配置
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -176,24 +183,29 @@ export function BarkBattleConfigEditor({
|
||||
>
|
||||
<div className="flex flex-col gap-3 pr-0 lg:pr-1">
|
||||
<label className="block shrink-0">
|
||||
<span className={FIELD_LABEL_CLASS}>作品标题</span>
|
||||
<input
|
||||
<PlatformFieldLabel variant="pill">作品标题</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
value={title}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-base font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]"
|
||||
size="lg"
|
||||
density="roomy"
|
||||
className="h-11 rounded-[1.05rem] py-0"
|
||||
maxLength={40}
|
||||
aria-label="作品标题"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block shrink-0">
|
||||
<span className={FIELD_LABEL_CLASS}>简介</span>
|
||||
<textarea
|
||||
<PlatformFieldLabel variant="pill">简介</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={description}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]"
|
||||
size="lg"
|
||||
density="roomy"
|
||||
className="h-[5.5rem] min-h-[5.5rem] rounded-[1.05rem] font-normal leading-6"
|
||||
maxLength={160}
|
||||
placeholder=""
|
||||
aria-label="简介"
|
||||
@@ -202,8 +214,8 @@ export function BarkBattleConfigEditor({
|
||||
|
||||
<div className="grid shrink-0 gap-2.5 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className={FIELD_LABEL_CLASS}>难度预设</span>
|
||||
<select
|
||||
<PlatformFieldLabel variant="pill">难度预设</PlatformFieldLabel>
|
||||
<PlatformSelectField
|
||||
value={difficultyPreset}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
@@ -211,7 +223,9 @@ export function BarkBattleConfigEditor({
|
||||
event.target.value as BarkBattleDifficultyPreset,
|
||||
)
|
||||
}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)]"
|
||||
size="sm"
|
||||
density="roomy"
|
||||
className="h-11 rounded-[1.05rem] py-0 font-black"
|
||||
aria-label="难度预设"
|
||||
>
|
||||
{DIFFICULTY_OPTIONS.map((option) => (
|
||||
@@ -219,19 +233,23 @@ export function BarkBattleConfigEditor({
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</PlatformSelectField>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="block shrink-0">
|
||||
<span className={ACCENT_FIELD_LABEL_CLASS}>
|
||||
<PlatformFieldLabel variant="accentPill">
|
||||
主题/场景描述
|
||||
</span>
|
||||
<textarea
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={themeDescription}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setThemeDescription(event.target.value)}
|
||||
className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
size="lg"
|
||||
density="roomy"
|
||||
tone="rose"
|
||||
className="h-[5.5rem] min-h-[5.5rem] rounded-[1.05rem] font-normal leading-6"
|
||||
maxLength={240}
|
||||
placeholder=""
|
||||
aria-label="主题/场景描述"
|
||||
@@ -240,24 +258,40 @@ export function BarkBattleConfigEditor({
|
||||
|
||||
<div className="grid shrink-0 gap-2.5 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className={FIELD_LABEL_CLASS}>玩家形象描述</span>
|
||||
<textarea
|
||||
<PlatformFieldLabel variant="pill">
|
||||
玩家形象描述
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={playerImageDescription}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setPlayerImageDescription(event.target.value)}
|
||||
className="h-[5rem] min-h-[5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm font-semibold leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
onChange={(event) =>
|
||||
setPlayerImageDescription(event.target.value)
|
||||
}
|
||||
size="sm"
|
||||
density="roomy"
|
||||
tone="rose"
|
||||
className="h-[5rem] min-h-[5rem] rounded-[1.05rem] leading-6"
|
||||
maxLength={220}
|
||||
aria-label="玩家形象描述"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className={FIELD_LABEL_CLASS}>对手形象描述</span>
|
||||
<textarea
|
||||
<PlatformFieldLabel variant="pill">
|
||||
对手形象描述
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={opponentImageDescription}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setOpponentImageDescription(event.target.value)}
|
||||
className="h-[5rem] min-h-[5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm font-semibold leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
onChange={(event) =>
|
||||
setOpponentImageDescription(event.target.value)
|
||||
}
|
||||
size="sm"
|
||||
density="roomy"
|
||||
tone="rose"
|
||||
className="h-[5rem] min-h-[5rem] rounded-[1.05rem] leading-6"
|
||||
maxLength={220}
|
||||
aria-label="对手形象描述"
|
||||
/>
|
||||
@@ -265,24 +299,35 @@ export function BarkBattleConfigEditor({
|
||||
</div>
|
||||
|
||||
<label className="block shrink-0">
|
||||
<span className={ACCENT_FIELD_LABEL_CLASS}>拟声词</span>
|
||||
<textarea
|
||||
<PlatformFieldLabel variant="accentPill">
|
||||
拟声词
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
value={onomatopoeiaText}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => {
|
||||
setIsOnomatopoeiaCustomized(true);
|
||||
setOnomatopoeiaText(event.target.value);
|
||||
}}
|
||||
className="h-[6.5rem] min-h-[6.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm font-black leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
size="sm"
|
||||
density="roomy"
|
||||
tone="rose"
|
||||
className="h-[6.5rem] min-h-[6.5rem] rounded-[1.05rem] font-black leading-6"
|
||||
maxLength={260}
|
||||
aria-label="拟声词"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{visibleError ? (
|
||||
<div className="platform-banner platform-banner--danger shrink-0 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="shrink-0 rounded-2xl"
|
||||
>
|
||||
{visibleError}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -291,21 +336,18 @@ export function BarkBattleConfigEditor({
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex shrink-0 flex-wrap justify-center gap-2 pb-[calc(env(safe-area-inset-bottom,0px)+0.75rem)] sm:mt-4 lg:pb-[max(0.25rem,env(safe-area-inset-bottom))]">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
disabled={isBusy}
|
||||
onClick={() => runValidatedAction(onPreview)}
|
||||
className={`platform-button platform-button--primary min-h-10 px-4 py-2 text-sm sm:min-h-11 sm:px-5 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
className="min-h-10 gap-1.5 px-4 py-2 text-sm sm:min-h-11 sm:gap-2 sm:px-5"
|
||||
>
|
||||
<span className="inline-flex flex-wrap items-center justify-center gap-1.5 sm:gap-2">
|
||||
{isBusy ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
<span>{isBusy ? '处理中' : '生成草稿'}</span>
|
||||
</span>
|
||||
</button>
|
||||
{isBusy ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
<span>{isBusy ? '处理中' : '生成草稿'}</span>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -84,31 +84,35 @@ describe('BarkBattleGeneratingView', () => {
|
||||
'video[data-testid="generation-page-background-video"] source[type="video/mp4"]',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '返回编辑' }).className).toContain(
|
||||
'text-xs',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '返回编辑' }).className,
|
||||
).toContain('text-xs');
|
||||
expect(screen.getByText('生成中').className).toContain('text-[11px]');
|
||||
expect(screen.getByText('生成中').className).toContain(
|
||||
'border-[var(--platform-warm-border)]',
|
||||
);
|
||||
expect(screen.getByText('生成中').className).toContain(
|
||||
'bg-[var(--platform-warm-bg)]',
|
||||
);
|
||||
expect(screen.getByText('当前步骤')).toBeTruthy();
|
||||
expect(screen.getByText('当前步骤').className).toContain('text-[10px]');
|
||||
expect(screen.getByTestId('generation-hero-wait-card').className).toContain(
|
||||
'text-center',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain(
|
||||
'text-center',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-elapsed-card').className,
|
||||
).toContain('text-center');
|
||||
expect(screen.getByTestId('generation-hero-wait-card').className).toContain(
|
||||
'bg-white/58',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-elapsed-card').className).toContain(
|
||||
'bg-white/58',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-wait-card').parentElement
|
||||
?.className,
|
||||
screen.getByTestId('generation-hero-elapsed-card').className,
|
||||
).toContain('bg-white/58');
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-wait-card').parentElement?.className,
|
||||
).toContain('mt-3');
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-wait-card').parentElement
|
||||
?.className,
|
||||
screen.getByTestId('generation-hero-wait-card').parentElement?.className,
|
||||
).toContain('px-0');
|
||||
expect(screen.getByText('预计等待').className).toContain('text-[9px]');
|
||||
expect(screen.getByText('已耗时').className).toContain('text-[9px]');
|
||||
@@ -122,33 +126,30 @@ describe('BarkBattleGeneratingView', () => {
|
||||
expect(screen.getByText('1 秒')).toBeTruthy();
|
||||
expect(screen.queryByText('预计还需 3 分钟')).toBeNull();
|
||||
expect(screen.queryByText('已耗时 1 秒')).toBeNull();
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'justify-start',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'z-30',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'pt-[2%]',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-progress-content').className,
|
||||
).toContain('justify-start');
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-progress-content').className,
|
||||
).toContain('z-30');
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-progress-content').className,
|
||||
).toContain('pt-[2%]');
|
||||
expect(screen.getByText('玩家形象')).toBeTruthy();
|
||||
expect(screen.getByText('进行中 36%')).toBeTruthy();
|
||||
expect(screen.getByText('进行中 36%').className).toContain('text-[11px]');
|
||||
expect(screen.getByText('总进度').className).toContain('text-[9px]');
|
||||
expect(screen.getByText('0%').className).toContain('text-[1.15rem]');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
screen.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('w-[min(400px,calc(100%_-_0.75rem))]');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
screen.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('max-w-full');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
screen.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('aspect-square');
|
||||
expect(
|
||||
@@ -184,9 +185,9 @@ describe('BarkBattleGeneratingView', () => {
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').tagName).toBe(
|
||||
'svg',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').getAttribute('class')).toContain(
|
||||
'z-0',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('generation-hero-progress-ring').getAttribute('class'),
|
||||
).toContain('z-0');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring')
|
||||
@@ -218,8 +219,8 @@ describe('BarkBattleGeneratingView', () => {
|
||||
.getAttribute('stroke-dasharray'),
|
||||
).toMatch(/^0\.00 1043\.\d{2}$/u);
|
||||
expect(
|
||||
screen.getByRole('progressbar', { name: '玩家形象 进度' }),
|
||||
).toBeTruthy();
|
||||
screen.getByRole('progressbar', { name: '玩家形象 进度' }).className,
|
||||
).toContain('platform-progress-track');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '玩家形象 进度' })
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
generateAllBarkBattleImageAssets,
|
||||
updateBarkBattleDraftConfig,
|
||||
} from '../../services/bark-battle-creation';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import {
|
||||
GenerationCurrentStepCard,
|
||||
GenerationPageBackdrop,
|
||||
@@ -191,7 +192,11 @@ export function BarkBattleGeneratingView({
|
||||
(hasSlotAsset(previewDraft, currentStep.slot) ? 'ready' : 'generating'))
|
||||
: 'generating';
|
||||
const currentStepProgress =
|
||||
currentStepStatus === 'ready' ? 100 : currentStepStatus === 'failed' ? 100 : 36;
|
||||
currentStepStatus === 'ready'
|
||||
? 100
|
||||
: currentStepStatus === 'failed'
|
||||
? 100
|
||||
: 36;
|
||||
const currentStepLabel = currentStep?.label ?? '竞技素材';
|
||||
const currentStepStatusLabel = getSlotStatusLabel(currentStepStatus);
|
||||
|
||||
@@ -336,7 +341,10 @@ export function BarkBattleGeneratingView({
|
||||
onComplete(draft, true);
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeBarkBattleGenerationTasks.get(startedDraftKey) === generationTask) {
|
||||
if (
|
||||
activeBarkBattleGenerationTasks.get(startedDraftKey) ===
|
||||
generationTask
|
||||
) {
|
||||
activeBarkBattleGenerationTasks.delete(startedDraftKey);
|
||||
}
|
||||
});
|
||||
@@ -344,7 +352,9 @@ export function BarkBattleGeneratingView({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
// 中文注释:离开生成页后不再全局复用同一 Promise,避免悬挂生成任务导致再次进入时一直转圈。
|
||||
if (activeBarkBattleGenerationTasks.get(startedDraftKey) === generationTask) {
|
||||
if (
|
||||
activeBarkBattleGenerationTasks.get(startedDraftKey) === generationTask
|
||||
) {
|
||||
activeBarkBattleGenerationTasks.delete(startedDraftKey);
|
||||
}
|
||||
if (startedDraftIdRef.current === startedDraftKey) {
|
||||
@@ -366,9 +376,13 @@ export function BarkBattleGeneratingView({
|
||||
<ArrowLeft className="h-5 w-5" strokeWidth={2.6} />
|
||||
<span className="break-keep">返回编辑</span>
|
||||
</button>
|
||||
<span className="rounded-full border border-[#f05816] bg-white/72 px-3 py-1.5 text-[11px] font-black tracking-[0.08em] text-[#df6118] shadow-[0_12px_30px_rgba(214,77,31,0.08)] backdrop-blur-md sm:px-4 sm:text-xs">
|
||||
<PlatformPillBadge
|
||||
tone="warning"
|
||||
size="xs"
|
||||
className="px-3 py-1.5 tracking-[0.08em] shadow-[0_12px_30px_rgba(214,77,31,0.08)] backdrop-blur-md sm:px-4 sm:text-xs"
|
||||
>
|
||||
生成中
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import { PlatformInfoBlock } from '../common/PlatformInfoBlock';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type BarkBattlePreviewCardProps = {
|
||||
@@ -13,11 +16,19 @@ const DIFFICULTY_LABELS = {
|
||||
|
||||
export function BarkBattlePreviewCard({ config }: BarkBattlePreviewCardProps) {
|
||||
return (
|
||||
<aside
|
||||
className="platform-subpanel flex min-h-0 flex-col overflow-hidden rounded-[1.2rem] p-3 max-lg:p-2 sm:p-4"
|
||||
<PlatformSubpanel
|
||||
as="aside"
|
||||
padding="none"
|
||||
className="flex min-h-0 flex-col overflow-hidden rounded-[1.2rem] p-3 max-lg:p-2 sm:p-4"
|
||||
aria-label="作品预览卡片"
|
||||
>
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/76 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)] sm:p-4">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
className="flex min-h-0 flex-1 flex-col bg-white/76 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)] sm:p-4"
|
||||
>
|
||||
<div
|
||||
className="relative mb-2.5 grid min-h-[5.75rem] grid-cols-[1fr_auto_1fr] items-center gap-2 overflow-hidden rounded-[1rem] bg-[linear-gradient(135deg,rgba(255,255,255,0.96),rgba(255,236,241,0.9)_46%,rgba(224,247,250,0.82))] px-3 text-center text-2xl shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] sm:mb-4 sm:min-h-[10rem] sm:gap-3 sm:px-4 sm:text-3xl"
|
||||
data-testid="bark-battle-preview-stage"
|
||||
@@ -41,9 +52,13 @@ export function BarkBattlePreviewCard({ config }: BarkBattlePreviewCardProps) {
|
||||
<span className="text-4xl sm:text-6xl">🐕</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="relative rounded-full bg-white/70 px-2.5 py-0.5 text-xs font-black text-[var(--platform-text-strong)] sm:px-3 sm:py-1 sm:text-base">
|
||||
<PlatformPillBadge
|
||||
tone="neutral"
|
||||
size="xs"
|
||||
className="relative border-transparent bg-white/70 px-2.5 py-0.5 text-xs text-[var(--platform-text-strong)] sm:px-3 sm:py-1 sm:text-base"
|
||||
>
|
||||
VS
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
<span className="relative grid place-items-center">
|
||||
{config.opponentCharacterImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
@@ -62,35 +77,23 @@ export function BarkBattlePreviewCard({ config }: BarkBattlePreviewCardProps) {
|
||||
<p className="mt-1.5 min-h-0 text-xs font-semibold leading-5 text-[var(--platform-text-muted)] sm:mt-2 sm:min-h-[2.625rem] sm:text-sm sm:leading-6">
|
||||
{config.description || '30 秒声浪拔河,喊出你的能量优势。'}
|
||||
</p>
|
||||
<dl className="mt-2.5 grid gap-1.5 text-xs sm:mt-4 sm:gap-2 sm:text-sm">
|
||||
<div className="flex justify-between gap-2 rounded-[0.85rem] bg-white/74 px-2.5 py-1.5 sm:gap-3 sm:px-3 sm:py-2">
|
||||
<dt className="text-[var(--platform-text-muted)]">场景</dt>
|
||||
<dd className="font-black text-[var(--platform-text-strong)]">
|
||||
{config.themeDescription || '声浪擂台'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 rounded-[0.85rem] bg-white/74 px-2.5 py-1.5 sm:gap-3 sm:px-3 sm:py-2">
|
||||
<dt className="text-[var(--platform-text-muted)]">形象</dt>
|
||||
<dd className="font-black text-[var(--platform-text-strong)]">
|
||||
{config.playerImageDescription || '玩家'}
|
||||
{' vs '}
|
||||
{config.opponentImageDescription || '对手'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 rounded-[0.85rem] bg-white/74 px-2.5 py-1.5 sm:gap-3 sm:px-3 sm:py-2">
|
||||
<dt className="text-[var(--platform-text-muted)]">难度</dt>
|
||||
<dd className="font-black text-[var(--platform-text-strong)]">
|
||||
{DIFFICULTY_LABELS[config.difficultyPreset]}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 rounded-[0.85rem] bg-white/74 px-2.5 py-1.5 sm:gap-3 sm:px-3 sm:py-2">
|
||||
<dt className="text-[var(--platform-text-muted)]">声浪</dt>
|
||||
<dd className="font-black text-[var(--platform-text-strong)]">
|
||||
{config.onomatopoeia?.slice(0, 3).join(' / ') || '炸场!'}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="mt-2.5 grid gap-1.5 sm:mt-4 sm:gap-2">
|
||||
<PlatformInfoBlock label="场景" variant="compactRow">
|
||||
{config.themeDescription || '声浪擂台'}
|
||||
</PlatformInfoBlock>
|
||||
<PlatformInfoBlock label="形象" variant="compactRow">
|
||||
{config.playerImageDescription || '玩家'}
|
||||
{' vs '}
|
||||
{config.opponentImageDescription || '对手'}
|
||||
</PlatformInfoBlock>
|
||||
<PlatformInfoBlock label="难度" variant="compactRow">
|
||||
{DIFFICULTY_LABELS[config.difficultyPreset]}
|
||||
</PlatformInfoBlock>
|
||||
<PlatformInfoBlock label="声浪" variant="compactRow">
|
||||
{config.onomatopoeia?.slice(0, 3).join(' / ') || '炸场!'}
|
||||
</PlatformInfoBlock>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ describe('BarkBattleResultView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('草稿').className).toContain('rounded-full');
|
||||
expect(screen.getByText('草稿').className).toContain('border-emerald-200');
|
||||
expect(screen.getByText('霓虹公园擂台')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(draft);
|
||||
@@ -66,7 +68,7 @@ describe('BarkBattleResultView', () => {
|
||||
});
|
||||
|
||||
it('uses compact mobile-first result layout classes', () => {
|
||||
render(
|
||||
const { container } = render(
|
||||
<BarkBattleResultView
|
||||
draft={draft}
|
||||
onBack={() => {}}
|
||||
@@ -76,13 +78,47 @@ describe('BarkBattleResultView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '汪汪冠军杯', level: 1 }).className).toContain(
|
||||
'text-2xl',
|
||||
expect(
|
||||
screen.getByRole('heading', { name: '汪汪冠军杯', level: 1 }).className,
|
||||
).toContain('text-2xl');
|
||||
expect(screen.getByLabelText('作品预览卡片').className).toContain(
|
||||
'platform-subpanel',
|
||||
);
|
||||
expect(screen.getByLabelText('作品预览卡片').className).toContain(
|
||||
'max-lg:p-2',
|
||||
);
|
||||
expect(screen.getByLabelText('作品预览卡片').className).toContain('max-lg:p-2');
|
||||
expect(screen.getByTestId('bark-battle-preview-stage').className).toContain(
|
||||
'min-h-[5.75rem]',
|
||||
);
|
||||
const previewVersusBadge = screen.getByText('VS');
|
||||
expect(previewVersusBadge.className).toContain('inline-flex');
|
||||
expect(previewVersusBadge.className).toContain('rounded-full');
|
||||
expect(previewVersusBadge.className).toContain('border-transparent');
|
||||
expect(previewVersusBadge.className).toContain('bg-white/70');
|
||||
expect(previewVersusBadge.className).toContain(
|
||||
'text-[var(--platform-text-strong)]',
|
||||
);
|
||||
const previewSceneBlock = screen.getByText('场景').parentElement;
|
||||
expect(previewSceneBlock?.className).toContain('bg-white/74');
|
||||
expect(previewSceneBlock?.className).toContain('rounded-[0.85rem]');
|
||||
expect(previewSceneBlock?.className).toContain('sm:px-3');
|
||||
expect(screen.getByText('场景').className).toContain(
|
||||
'text-[var(--platform-text-muted)]',
|
||||
);
|
||||
expect(
|
||||
within(previewSceneBlock as HTMLElement).getByText('霓虹公园擂台')
|
||||
.className,
|
||||
).toContain('font-black');
|
||||
expect(container.querySelectorAll('article.bg-white\\/72')).toHaveLength(3);
|
||||
const draftSummaryPanel = screen.getByTestId(
|
||||
'bark-battle-draft-summary-panel',
|
||||
);
|
||||
expect(draftSummaryPanel.className).toContain('bg-white/72');
|
||||
expect(draftSummaryPanel.className).toContain('rounded-[1.25rem]');
|
||||
expect(draftSummaryPanel.className).toContain('p-3');
|
||||
expect(draftSummaryPanel.className).toContain(
|
||||
'border-[var(--platform-subpanel-border)]',
|
||||
);
|
||||
});
|
||||
|
||||
it('uploads replacement image assets into the selected slot', async () => {
|
||||
@@ -137,7 +173,8 @@ describe('BarkBattleResultView', () => {
|
||||
<BarkBattleResultView
|
||||
draft={{
|
||||
...draft,
|
||||
playerCharacterImageSrc: 'generated-bark-battle-assets/player-character/very-long-object-key.png',
|
||||
playerCharacterImageSrc:
|
||||
'generated-bark-battle-assets/player-character/very-long-object-key.png',
|
||||
}}
|
||||
onBack={() => {}}
|
||||
onDraftChange={() => {}}
|
||||
@@ -146,7 +183,9 @@ describe('BarkBattleResultView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const playerSlot = screen.getByRole('heading', { name: '玩家形象' }).closest('article');
|
||||
const playerSlot = screen
|
||||
.getByRole('heading', { name: '玩家形象' })
|
||||
.closest('article');
|
||||
expect(playerSlot).toBeTruthy();
|
||||
expect(within(playerSlot as HTMLElement).getByText('已替换')).toBeTruthy();
|
||||
expect(
|
||||
@@ -154,7 +193,9 @@ describe('BarkBattleResultView', () => {
|
||||
'generated-bark-battle-assets/player-character/very-long-object-key.png',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(within(playerSlot as HTMLElement).queryByText(/objectKey|object key/i)).toBeNull();
|
||||
expect(
|
||||
within(playerSlot as HTMLElement).queryByText(/objectKey|object key/i),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps result assets to three image slots with per-slot regeneration only', async () => {
|
||||
@@ -190,7 +231,9 @@ describe('BarkBattleResultView', () => {
|
||||
.closest('article');
|
||||
expect(playerSlot).toBeTruthy();
|
||||
await user.click(
|
||||
within(playerSlot as HTMLElement).getByRole('button', { name: '重新生成' }),
|
||||
within(playerSlot as HTMLElement).getByRole('button', {
|
||||
name: '重新生成',
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
RefreshCw,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { type ChangeEvent, type ReactNode, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
BarkBattleConfigEditorPayload,
|
||||
@@ -18,6 +24,10 @@ import {
|
||||
regenerateBarkBattleImageAsset,
|
||||
uploadBarkBattleAsset,
|
||||
} from '../../services/bark-battle-creation';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { BarkBattlePreviewCard } from './BarkBattlePreviewCard';
|
||||
|
||||
type BarkBattleResultViewProps = {
|
||||
@@ -36,7 +46,9 @@ const SLOT_LABELS = {
|
||||
'ui-background': 'UI背景',
|
||||
} satisfies Record<BarkBattleAssetSlot, string>;
|
||||
|
||||
function mapDraftToConfig(draft: BarkBattleDraftConfig): BarkBattleConfigEditorPayload {
|
||||
function mapDraftToConfig(
|
||||
draft: BarkBattleDraftConfig,
|
||||
): BarkBattleConfigEditorPayload {
|
||||
return {
|
||||
title: draft.title,
|
||||
description: draft.description,
|
||||
@@ -75,7 +87,10 @@ function applyAssetToDraft(
|
||||
return { ...draft, updatedAt };
|
||||
}
|
||||
|
||||
function getSlotAssetSrc(draft: BarkBattleDraftConfig, slot: BarkBattleAssetSlot) {
|
||||
function getSlotAssetSrc(
|
||||
draft: BarkBattleDraftConfig,
|
||||
slot: BarkBattleAssetSlot,
|
||||
) {
|
||||
if (slot === 'player-character') {
|
||||
return draft.playerCharacterImageSrc ?? '';
|
||||
}
|
||||
@@ -100,16 +115,14 @@ function ResultActionButton({
|
||||
tone?: 'primary' | 'secondary';
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={`platform-button ${
|
||||
tone === 'primary' ? 'platform-button--primary' : 'platform-button--secondary'
|
||||
} min-h-10 justify-center text-sm disabled:cursor-not-allowed disabled:opacity-55 sm:min-h-11`}
|
||||
tone={tone}
|
||||
className="min-h-10 gap-2 text-sm sm:min-h-11"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,7 +190,13 @@ function BarkBattleAssetSlotControl({
|
||||
const isSlotBusy = isUploading || isRegenerating;
|
||||
|
||||
return (
|
||||
<article className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)] sm:p-3">
|
||||
<PlatformSubpanel
|
||||
as="article"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
className="p-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)] sm:p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 sm:gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="m-0 text-xs font-black text-[var(--platform-text-strong)] sm:text-sm">
|
||||
@@ -202,26 +221,30 @@ function BarkBattleAssetSlotControl({
|
||||
aria-label={`上传${SLOT_LABELS[slot]}文件`}
|
||||
onChange={handleUpload}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
disabled={disabled || isSlotBusy}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="platform-button platform-button--secondary min-h-8 justify-center rounded-full px-2.5 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-55 sm:min-h-9 sm:px-3 sm:py-1.5 sm:text-xs"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="min-h-8 gap-1.5 px-2.5 py-1 text-[11px] sm:min-h-9 sm:px-3 sm:py-1.5 sm:text-xs"
|
||||
>
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
上传
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
disabled={disabled || isSlotBusy}
|
||||
onClick={handleRegenerate}
|
||||
className="platform-button platform-button--secondary min-h-8 justify-center rounded-full px-2.5 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-55 sm:min-h-9 sm:px-3 sm:py-1.5 sm:text-xs"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="min-h-8 gap-1.5 px-2.5 py-1 text-[11px] sm:min-h-9 sm:px-3 sm:py-1.5 sm:text-xs"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
重新生成
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</article>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,31 +266,43 @@ export function BarkBattleResultView({
|
||||
<div className="platform-page-stage platform-remap-surface flex h-full min-h-0 flex-col overflow-hidden px-2 pb-2 pt-2 sm:px-4 sm:pt-4 xl:px-5 xl:pb-4 xl:pt-4">
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-4xl flex-col">
|
||||
<div className="mb-2 flex shrink-0 items-center justify-between gap-2 sm:mb-3 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onBack}
|
||||
disabled={isActionBusy}
|
||||
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isActionBusy ? 'opacity-45' : ''}`}
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 gap-1.5 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回编辑
|
||||
</button>
|
||||
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-[11px] font-black text-emerald-700 sm:px-3 sm:py-1">
|
||||
</PlatformActionButton>
|
||||
<PlatformPillBadge
|
||||
tone="success"
|
||||
size="xs"
|
||||
className="sm:px-3 sm:py-1"
|
||||
>
|
||||
草稿
|
||||
</span>
|
||||
</PlatformPillBadge>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-0.5">
|
||||
<section className="grid gap-2.5 lg:grid-cols-[minmax(0,0.94fr)_minmax(18rem,0.86fr)] lg:gap-3">
|
||||
<div className="grid gap-2.5 lg:gap-3">
|
||||
<div className="rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)] sm:p-4">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="md"
|
||||
padding="sm"
|
||||
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.74)]"
|
||||
data-testid="bark-battle-draft-summary-panel"
|
||||
>
|
||||
<div className="text-xs font-black text-[var(--platform-text-soft)] sm:text-sm">
|
||||
草稿编译
|
||||
</div>
|
||||
<h1 className="m-0 mt-1 text-2xl font-black leading-tight tracking-normal text-[var(--platform-text-strong)] sm:mt-2 sm:text-4xl lg:text-5xl">
|
||||
{draft.title || '未命名声浪竞技场'}
|
||||
</h1>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{(
|
||||
[
|
||||
@@ -294,9 +329,14 @@ export function BarkBattleResultView({
|
||||
</section>
|
||||
|
||||
{visibleError ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="mt-3 rounded-2xl"
|
||||
>
|
||||
{visibleError}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user