继续收口生成页顶部返回按钮

抽出生成页共用返回按钮壳并复用共享图标按钮能力
将自定义世界生成页接入共享返回按钮壳
将汪汪声浪生成页接入共享返回按钮壳并保留禁用态
补充两个生成页返回按钮的样式与交互回归测试
This commit is contained in:
2026-06-11 04:23:55 +08:00
parent 7f8400fd3a
commit a8012109ae
5 changed files with 123 additions and 20 deletions

View File

@@ -1,7 +1,8 @@
/* @vitest-environment jsdom */
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime';
import { CustomWorldGenerationView } from './CustomWorldGenerationView';
@@ -104,6 +105,12 @@ describe('CustomWorldGenerationView', () => {
expect(
screen.getByRole('button', { name: '返回创作中心' }).className,
).toContain('text-xs');
expect(
screen.getByRole('button', { name: '返回创作中心' }).className,
).toContain('bg-transparent');
expect(
screen.getByRole('button', { name: '返回创作中心' }).className,
).toContain('gap-2');
expect(screen.getByText('世界建设中')).toBeTruthy();
expect(screen.getByText('世界建设中').className).toContain('text-xs');
expect(screen.getByText('世界建设中').className).toContain(
@@ -289,4 +296,29 @@ describe('CustomWorldGenerationView', () => {
expect(screen.queryByText('大鱼吃小鱼题材')).toBeNull();
expect(screen.getByTestId('generation-page-background-video')).toBeTruthy();
});
test('keeps the shared generation back button click behavior', async () => {
const user = userEvent.setup();
const onBack = vi.fn();
render(
<CustomWorldGenerationView
settingText="大鱼吃小鱼题材"
progress={createProgress()}
isGenerating
error={null}
onBack={onBack}
onEditSetting={() => {}}
onRetry={() => {}}
backLabel="返回创作中心"
settingDescription={null}
settingActionLabel={null}
progressTitle="大鱼吃小鱼草稿生成进度"
/>,
);
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
expect(onBack).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,11 +1,10 @@
import { ArrowLeft } from 'lucide-react';
import type { CustomWorldGenerationProgress } from '../../packages/shared/src/contracts/runtime';
import type { CustomWorldStructuredAnchorEntry } from '../services/customWorldAgentGenerationProgress';
import { PlatformActionButton } from './common/PlatformActionButton';
import { PlatformPillBadge } from './common/PlatformPillBadge';
import {
GenerationCurrentStepCard,
GenerationHeaderBackButton,
GenerationPageBackdrop,
GenerationProgressHero,
} from './GenerationProgressHero';
@@ -137,14 +136,7 @@ export function CustomWorldGenerationView({
<div className="relative isolate z-[1] -mx-3 -my-3 flex h-[calc(100%+1.5rem)] min-h-0 flex-col overflow-hidden bg-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-4 text-[#3d1f10] sm:mx-0 sm:my-0 sm:h-full sm:rounded-[2rem] sm:px-5 sm:pt-5">
<GenerationPageBackdrop />
<div className="relative z-30 mb-4 flex shrink-0 items-center justify-between gap-3 py-2 sm:mb-5">
<button
type="button"
onClick={onBack}
className="inline-flex items-center gap-2 rounded-full bg-transparent px-0 py-2 text-xs font-black text-[#171411] sm:text-sm"
>
<ArrowLeft className="h-5 w-5 shrink-0" strokeWidth={2.6} />
<span className="break-keep">{backLabel}</span>
</button>
<GenerationHeaderBackButton label={backLabel} onClick={onBack} />
<PlatformPillBadge
tone="warning"
size="xs"

View File

@@ -1,7 +1,8 @@
import { Clock3, Hourglass } from 'lucide-react';
import { ArrowLeft, Clock3, Hourglass } from 'lucide-react';
import { useEffect, useId, useRef } from 'react';
import generationHeroVideo from '../../media/create_bg_video.mp4';
import { PlatformIconButton } from './common/PlatformIconButton';
import { PlatformProgressBar } from './common/PlatformProgressBar';
const GENERATION_PROGRESS_RING_GAP_DEGREES = 90;
@@ -35,6 +36,14 @@ type GenerationCurrentStepCardProps = {
progressValue: number;
};
type GenerationHeaderBackButtonProps = {
label: string;
onClick: () => void;
disabled?: boolean;
disabledOpacity?: number;
className?: string;
};
function clampGenerationProgress(value: number) {
return Math.max(0, Math.min(100, Math.round(value)));
}
@@ -51,6 +60,34 @@ function buildGenerationRingMetrics(progressValue: number) {
};
}
export function GenerationHeaderBackButton({
label,
onClick,
disabled = false,
disabledOpacity,
className,
}: GenerationHeaderBackButtonProps) {
return (
<PlatformIconButton
label={label}
title={label}
variant="darkMini"
onClick={onClick}
disabled={disabled}
className={[
'gap-2 rounded-full !border-transparent !bg-transparent px-0 py-2 text-xs font-black !text-[#171411] shadow-none hover:!bg-transparent hover:!text-[#171411] sm:text-sm',
className,
]
.filter(Boolean)
.join(' ')}
style={disabled && disabledOpacity != null ? { opacity: disabledOpacity } : undefined}
icon={<ArrowLeft className="h-5 w-5 shrink-0" strokeWidth={2.6} />}
>
<span className="break-keep">{label}</span>
</PlatformIconButton>
);
}
export function GenerationPageBackdrop() {
const videoRef = useRef<HTMLVideoElement | null>(null);

View File

@@ -1,5 +1,6 @@
/* @vitest-environment jsdom */
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
@@ -87,6 +88,12 @@ describe('BarkBattleGeneratingView', () => {
expect(
screen.getByRole('button', { name: '返回编辑' }).className,
).toContain('text-xs');
expect(
screen.getByRole('button', { name: '返回编辑' }).className,
).toContain('bg-transparent');
expect(
screen.getByRole('button', { name: '返回编辑' }).className,
).toContain('gap-2');
expect(screen.getByText('生成中').className).toContain('text-[11px]');
expect(screen.getByText('生成中').className).toContain(
'border-[var(--platform-warm-border)]',
@@ -479,4 +486,42 @@ describe('BarkBattleGeneratingView', () => {
);
});
});
it('keeps the shared generation back button disabled state and click behavior', async () => {
const user = userEvent.setup();
const onBack = vi.fn();
vi.mocked(generateAllBarkBattleImageAssets).mockReturnValue(
new Promise<BarkBattleImageGenerationBatchResult>(() => {}),
);
const { rerender } = render(
<BarkBattleGeneratingView
draft={draft}
isBusy
onBack={onBack}
onComplete={() => {}}
onError={() => {}}
/>,
);
const busyBackButton = screen.getByRole('button', { name: '返回编辑' });
expect(busyBackButton.getAttribute('disabled')).toBe('');
expect(busyBackButton.style.opacity).toBe('0.45');
await user.click(busyBackButton);
expect(onBack).not.toHaveBeenCalled();
rerender(
<BarkBattleGeneratingView
draft={draft}
isBusy={false}
onBack={onBack}
onComplete={() => {}}
onError={() => {}}
/>,
);
await user.click(screen.getByRole('button', { name: '返回编辑' }));
expect(onBack).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,4 +1,3 @@
import { ArrowLeft } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { BarkBattleDraftConfig } from '../../../packages/shared/src/contracts/barkBattle';
@@ -15,6 +14,7 @@ import {
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import {
GenerationCurrentStepCard,
GenerationHeaderBackButton,
GenerationPageBackdrop,
GenerationProgressHero,
} from '../GenerationProgressHero';
@@ -367,15 +367,12 @@ export function BarkBattleGeneratingView({
<div className="relative isolate z-[1] -mx-3 -my-3 flex h-[calc(100%+1.5rem)] min-h-0 flex-col overflow-hidden bg-transparent px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-4 text-[#3d1f10] sm:mx-0 sm:my-0 sm:h-full sm:rounded-[2rem] sm:px-5 sm:pt-5 xl:px-6 xl:pb-5 xl:pt-5">
<GenerationPageBackdrop />
<div className="relative z-30 mx-auto mb-4 flex w-full max-w-[48rem] shrink-0 items-center justify-between gap-3 sm:mb-5">
<button
type="button"
<GenerationHeaderBackButton
label="返回编辑"
onClick={onBack}
disabled={isBusy}
className={`inline-flex items-center gap-2 rounded-full bg-transparent px-0 py-2 text-xs font-black text-[#171411] sm:text-sm ${isBusy ? 'opacity-45' : ''}`}
>
<ArrowLeft className="h-5 w-5" strokeWidth={2.6} />
<span className="break-keep"></span>
</button>
disabledOpacity={0.45}
/>
<PlatformPillBadge
tone="warning"
size="xs"