收口创作流程统一总计划并修复等待页窄屏裁切
This commit is contained in:
@@ -142,12 +142,17 @@ describe('CustomWorldGenerationView', () => {
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
.className,
|
||||
).toContain('w-[400px]');
|
||||
).toContain('w-[min(400px,calc(100%_-_0.75rem))]');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
.className,
|
||||
).toContain('h-[400px]');
|
||||
).toContain('max-w-full');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
.className,
|
||||
).toContain('aspect-square');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
|
||||
@@ -133,44 +133,14 @@ export function GenerationProgressHero({
|
||||
const ringFillDasharray = `${ringMetrics.progressLength.toFixed(2)} ${ringMetrics.circumference.toFixed(2)}`;
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex w-full max-w-[60rem] flex-col items-center px-1 pb-1 pt-1 sm:pt-4">
|
||||
<div className="relative mx-auto flex w-full min-w-0 max-w-[60rem] flex-col items-center px-1 pb-1 pt-1 sm:pt-4">
|
||||
<div className="sr-only">
|
||||
{title}
|
||||
{phaseLabel ? ` ${phaseLabel}` : ''}
|
||||
</div>
|
||||
<div className="relative w-full max-w-[56rem] sm:max-w-[60rem]">
|
||||
<div className="relative w-full min-w-0 max-w-[56rem] sm:max-w-[60rem]">
|
||||
<div
|
||||
className="absolute left-0 top-1/2 z-20 w-[min(6.8rem,28vw)] -translate-y-1/2 rounded-[1.1rem] border border-white/58 bg-white/58 px-2.5 py-2 text-center shadow-[0_14px_30px_rgba(112,57,30,0.10)] backdrop-blur-md sm:w-[8rem] sm:px-3 sm:py-2.5"
|
||||
data-testid="generation-hero-wait-card"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1.5 text-[#2a1c14]">
|
||||
<Hourglass className="h-3.5 w-3.5 shrink-0" strokeWidth={2.2} />
|
||||
<div className="text-[9px] font-black tracking-[0.1em] text-[#7e421f] sm:text-[10px]">
|
||||
预计等待
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1.5 break-keep text-[12px] font-black leading-tight text-[#161211] sm:text-[13px]">
|
||||
{estimatedWaitText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute right-0 top-1/2 z-20 w-[min(6.8rem,28vw)] -translate-y-1/2 rounded-[1.1rem] border border-white/58 bg-white/58 px-2.5 py-2 text-center shadow-[0_14px_30px_rgba(112,57,30,0.10)] backdrop-blur-md sm:w-[8rem] sm:px-3 sm:py-2.5"
|
||||
data-testid="generation-hero-elapsed-card"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1.5 text-[#2a1c14]">
|
||||
<div className="text-[9px] font-black tracking-[0.1em] text-[#7e421f] sm:text-[10px]">
|
||||
已耗时
|
||||
</div>
|
||||
<Clock3 className="h-3.5 w-3.5 shrink-0" strokeWidth={2.2} />
|
||||
</div>
|
||||
<div className="mt-1.5 break-keep text-[12px] font-black leading-tight text-[#161211] sm:text-[13px]">
|
||||
{elapsedText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative mx-auto h-[400px] w-[400px] shrink-0 overflow-visible rounded-full"
|
||||
className="relative mx-auto aspect-square w-[min(400px,calc(100%_-_0.75rem))] max-w-full shrink-0 overflow-visible rounded-full"
|
||||
role="progressbar"
|
||||
aria-label={title}
|
||||
aria-valuemin={0}
|
||||
@@ -244,6 +214,38 @@ export function GenerationProgressHero({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-20 mt-[-0.3rem] grid w-full grid-cols-2 gap-2 px-0.5 sm:absolute sm:inset-0 sm:mt-0 sm:block sm:px-0">
|
||||
<div
|
||||
className="w-full rounded-[1.1rem] border border-white/58 bg-white/58 px-2.5 py-2 text-center shadow-[0_14px_30px_rgba(112,57,30,0.10)] backdrop-blur-md sm:absolute sm:left-0 sm:top-1/2 sm:w-[8rem] sm:-translate-y-1/2 sm:px-3 sm:py-2.5"
|
||||
data-testid="generation-hero-wait-card"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1.5 text-[#2a1c14]">
|
||||
<Hourglass className="h-3.5 w-3.5 shrink-0" strokeWidth={2.2} />
|
||||
<div className="text-[9px] font-black tracking-[0.1em] text-[#7e421f] sm:text-[10px]">
|
||||
预计等待
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1.5 break-keep text-[12px] font-black leading-tight text-[#161211] sm:text-[13px]">
|
||||
{estimatedWaitText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-full rounded-[1.1rem] border border-white/58 bg-white/58 px-2.5 py-2 text-center shadow-[0_14px_30px_rgba(112,57,30,0.10)] backdrop-blur-md sm:absolute sm:right-0 sm:top-1/2 sm:w-[8rem] sm:-translate-y-1/2 sm:px-3 sm:py-2.5"
|
||||
data-testid="generation-hero-elapsed-card"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1.5 text-[#2a1c14]">
|
||||
<div className="text-[9px] font-black tracking-[0.1em] text-[#7e421f] sm:text-[10px]">
|
||||
已耗时
|
||||
</div>
|
||||
<Clock3 className="h-3.5 w-3.5 shrink-0" strokeWidth={2.2} />
|
||||
</div>
|
||||
<div className="mt-1.5 break-keep text-[12px] font-black leading-tight text-[#161211] sm:text-[13px]">
|
||||
{elapsedText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -130,12 +130,17 @@ describe('BarkBattleGeneratingView', () => {
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('w-[400px]');
|
||||
).toContain('w-[min(400px,calc(100%_-_0.75rem))]');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('h-[400px]');
|
||||
).toContain('max-w-full');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.className,
|
||||
).toContain('aspect-square');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
|
||||
92
src/components/jump-hop-creation/JumpHopWorkspace.test.tsx
Normal file
92
src/components/jump-hop-creation/JumpHopWorkspace.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { JumpHopSessionResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import { JumpHopWorkspace } from './JumpHopWorkspace';
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
createSession: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockCreateSession = vi.mocked(jumpHopClient.createSession);
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreateSession.mockReset();
|
||||
});
|
||||
|
||||
function createSessionResponse(): JumpHopSessionResponse {
|
||||
return {
|
||||
session: {
|
||||
sessionId: 'jump-session-1',
|
||||
ownerUserId: 'user-1',
|
||||
status: 'draft',
|
||||
draft: null,
|
||||
createdAt: '2026-05-30T10:00:00.000Z',
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('jump hop workspace submits structured payload after required fields are filled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitted = vi.fn();
|
||||
const sessionResponse = createSessionResponse();
|
||||
mockCreateSession.mockResolvedValue(sessionResponse);
|
||||
|
||||
render(
|
||||
<JumpHopWorkspace onBack={() => {}} onSubmitted={onSubmitted} />,
|
||||
);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: '生成' });
|
||||
expect(submitButton).toHaveProperty('disabled', true);
|
||||
|
||||
await user.type(screen.getByLabelText('作品标题'), '云朵跳台');
|
||||
await user.type(screen.getByLabelText('作品简介'), '在云端一路跳到星星。');
|
||||
await user.type(screen.getByLabelText('主题标签'), '云朵 星星');
|
||||
await user.selectOptions(screen.getByLabelText('难度'), 'standard');
|
||||
await user.selectOptions(screen.getByLabelText('风格'), 'paper-toy');
|
||||
await user.type(screen.getByLabelText('角色提示词'), '一只纸片小兔');
|
||||
await user.type(screen.getByLabelText('地块提示词'), '柔软云朵平台');
|
||||
await user.type(screen.getByLabelText('终点氛围'), '星光门');
|
||||
|
||||
expect(submitButton).toHaveProperty('disabled', false);
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
templateId: 'jump-hop',
|
||||
workTitle: '云朵跳台',
|
||||
workDescription: '在云端一路跳到星星。',
|
||||
themeTags: ['云朵', '星星'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
characterPrompt: '一只纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
});
|
||||
});
|
||||
expect(onSubmitted).toHaveBeenCalledWith(
|
||||
sessionResponse,
|
||||
expect.objectContaining({
|
||||
templateId: 'jump-hop',
|
||||
workTitle: '云朵跳台',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('jump hop workspace calls back when return button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
|
||||
render(<JumpHopWorkspace onBack={onBack} onSubmitted={() => {}} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
144
src/components/jump-hop-result/JumpHopResultView.test.tsx
Normal file
144
src/components/jump-hop-result/JumpHopResultView.test.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { JumpHopDraftResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { JumpHopResultView } from './JumpHopResultView';
|
||||
|
||||
const draft: JumpHopDraftResponse = {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'profile-1',
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵', '星空'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
characterPrompt: '纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
characterAsset: {
|
||||
assetId: 'character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '角色图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '地块图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [
|
||||
{
|
||||
tileType: 'start',
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
{
|
||||
tileType: 'finish',
|
||||
imageSrc: 'data:image/png;base64,tile-finish',
|
||||
imageObjectKey: 'jump-hop/tile-finish.png',
|
||||
assetObjectId: 'asset-tile-finish',
|
||||
sourceAtlasCell: 'A2',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
],
|
||||
path: {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-2',
|
||||
tileType: 'finish',
|
||||
x: 16,
|
||||
y: 18,
|
||||
width: 60,
|
||||
height: 42,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 2,
|
||||
},
|
||||
],
|
||||
finishIndex: 1,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
},
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
generationStatus: 'ready',
|
||||
};
|
||||
|
||||
test('jump hop result view exposes test run and publish actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
const onEdit = vi.fn();
|
||||
const onStartTestRun = vi.fn();
|
||||
const onPublish = vi.fn();
|
||||
const onRegenerateCharacter = vi.fn();
|
||||
const onRegenerateTiles = vi.fn();
|
||||
|
||||
render(
|
||||
<JumpHopResultView
|
||||
profile={draft}
|
||||
onBack={onBack}
|
||||
onEdit={onEdit}
|
||||
onStartTestRun={onStartTestRun}
|
||||
onPublish={onPublish}
|
||||
onRegenerateCharacter={onRegenerateCharacter}
|
||||
onRegenerateTiles={onRegenerateTiles}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('云端跳台')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '试玩' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
await user.click(screen.getByRole('button', { name: '发布' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回编辑' }));
|
||||
await user.click(screen.getByRole('button', { name: '角色' }));
|
||||
await user.click(screen.getByRole('button', { name: '地块' }));
|
||||
|
||||
expect(onStartTestRun).toHaveBeenCalledTimes(1);
|
||||
expect(onPublish).toHaveBeenCalledTimes(1);
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onRegenerateCharacter).toHaveBeenCalledTimes(1);
|
||||
expect(onRegenerateTiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
212
src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx
Normal file
212
src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { JumpHopRuntimeShell } from './JumpHopRuntimeShell';
|
||||
|
||||
const profile: JumpHopWorkProfileResponse = {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'work-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'session-1',
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: 'data:image/png;base64,cover',
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
draft: {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'profile-1',
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
characterPrompt: '纸片小兔',
|
||||
tilePrompt: '云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
characterAsset: {
|
||||
assetId: 'character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '角色图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '地块图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [
|
||||
{
|
||||
tileType: 'start',
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
],
|
||||
path: {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
],
|
||||
finishIndex: 0,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
},
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
path: {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
],
|
||||
finishIndex: 0,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
},
|
||||
characterAsset: {
|
||||
assetId: 'character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '角色图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '地块图',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [
|
||||
{
|
||||
tileType: 'start',
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const run: JumpHopRuntimeRunSnapshotResponse = {
|
||||
runId: 'run-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
status: 'playing',
|
||||
currentPlatformIndex: 0,
|
||||
score: 0,
|
||||
combo: 0,
|
||||
path: profile.path,
|
||||
lastJump: null,
|
||||
startedAtMs: 1000,
|
||||
finishedAtMs: null,
|
||||
};
|
||||
|
||||
test('jump hop runtime shell supports jump, restart and exit actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onJump = vi.fn().mockResolvedValue(undefined);
|
||||
const onRestart = vi.fn();
|
||||
const onExit = vi.fn();
|
||||
|
||||
render(
|
||||
<JumpHopRuntimeShell
|
||||
profile={profile}
|
||||
run={run}
|
||||
onJump={onJump}
|
||||
onRestart={onRestart}
|
||||
onExit={onExit}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.pointer([
|
||||
{ target: screen.getByRole('button', { name: '起跳' }), keys: '[MouseLeft>]' },
|
||||
]);
|
||||
await user.pointer([
|
||||
{ target: screen.getByRole('button', { name: '起跳' }), keys: '[/MouseLeft]' },
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onJump).toHaveBeenCalledWith({ chargeMs: expect.any(Number) });
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '重开' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(onRestart).toHaveBeenCalledTimes(1);
|
||||
expect(onExit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -112,6 +112,28 @@ test('match3d workspace submits derived entry form payload instead of agent chat
|
||||
expect(onExecuteAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('match3d workspace can defer visible chrome to the unified creation page', () => {
|
||||
const { container } = render(
|
||||
<Match3DAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={() => {}}
|
||||
title={null}
|
||||
unifiedChrome
|
||||
/>,
|
||||
);
|
||||
|
||||
const workspace = container.querySelector('.match3d-agent-workspace');
|
||||
expect(workspace?.getAttribute('data-unified-chrome')).toBe('true');
|
||||
expect(workspace?.className).toContain('max-w-none');
|
||||
expect(workspace?.className).not.toContain('h-full');
|
||||
expect(workspace?.className).not.toContain('overflow-hidden');
|
||||
expect(workspace?.className).not.toContain('platform-remap-surface');
|
||||
expect(screen.queryByRole('heading', { name: '想做个什么玩法?' })).toBeNull();
|
||||
expect(screen.getByLabelText('想做一个什么题材的抓大鹅?')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('match3d workspace omits legacy asset style fields from entry payload', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type Match3DAgentWorkspaceProps = {
|
||||
initialFormPayload?: CreateMatch3DSessionRequest | null;
|
||||
showBackButton?: boolean;
|
||||
title?: string | null;
|
||||
unifiedChrome?: boolean;
|
||||
};
|
||||
|
||||
type Match3DFormState = {
|
||||
@@ -116,6 +117,7 @@ export function Match3DAgentWorkspace({
|
||||
initialFormPayload = null,
|
||||
showBackButton = true,
|
||||
title = '想做个什么玩法?',
|
||||
unifiedChrome = false,
|
||||
}: Match3DAgentWorkspaceProps) {
|
||||
const [formState, setFormState] = useState<Match3DFormState>(() =>
|
||||
resolveInitialFormState(session, initialFormPayload),
|
||||
@@ -183,7 +185,14 @@ export function Match3DAgentWorkspace({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
unifiedChrome
|
||||
? 'match3d-agent-workspace mx-auto flex min-h-0 w-full max-w-none flex-col overflow-visible'
|
||||
: 'match3d-agent-workspace platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden'
|
||||
}
|
||||
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
|
||||
>
|
||||
{showBackButton ? (
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 sm:mb-4">
|
||||
<button
|
||||
@@ -197,8 +206,14 @@ export function Match3DAgentWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden pr-0">
|
||||
{title ? (
|
||||
<div
|
||||
className={
|
||||
unifiedChrome
|
||||
? 'flex flex-col pr-0'
|
||||
: 'flex min-h-0 flex-1 flex-col overflow-hidden pr-0'
|
||||
}
|
||||
>
|
||||
{title && !unifiedChrome ? (
|
||||
<div className="mb-3 shrink-0 sm:mb-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="m-0 text-3xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
|
||||
@@ -211,9 +226,19 @@ export function Match3DAgentWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<section
|
||||
className={
|
||||
unifiedChrome
|
||||
? 'flex flex-col'
|
||||
: 'flex min-h-0 flex-1 flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
||||
className={`grid gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(16rem,0.9fr)] ${
|
||||
unifiedChrome
|
||||
? ''
|
||||
: 'min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] lg:grid-rows-1'
|
||||
} ${isBusy ? 'opacity-55' : ''}`}
|
||||
>
|
||||
<label className="block min-h-0">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
|
||||
@@ -3138,6 +3138,38 @@ function LazyPanelFallback({ label }: { label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CreationResultRecoveryPanel({
|
||||
title,
|
||||
message,
|
||||
actionLabel,
|
||||
onAction,
|
||||
}: {
|
||||
title: string;
|
||||
message: string;
|
||||
actionLabel: string;
|
||||
onAction: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 items-center justify-center px-3 py-6">
|
||||
<div className="platform-subpanel w-full max-w-sm rounded-[1.5rem] p-5 text-center">
|
||||
<div className="text-base font-black text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-base)]">
|
||||
{message}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAction}
|
||||
className="platform-button platform-button--primary mt-4 min-h-11 justify-center px-4 py-3 text-sm"
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function mergePuzzleServiceRuntimeState(
|
||||
currentRun: PuzzleRunSnapshot,
|
||||
serviceRun: PuzzleRunSnapshot,
|
||||
@@ -12930,15 +12962,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
if (path.startsWith('/creation/jump-hop')) {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
let session: JumpHopSessionSnapshotResponse | null = null;
|
||||
let work: JumpHopWorkProfileResponse | null = null;
|
||||
try {
|
||||
const { session } = await jumpHopClient.getSession(sessionId);
|
||||
let work: JumpHopWorkProfileResponse | null = null;
|
||||
if (profileId) {
|
||||
work = (await jumpHopClient.getWorkDetail(profileId)).item;
|
||||
}
|
||||
} catch {
|
||||
work = null;
|
||||
}
|
||||
try {
|
||||
if (sessionId) {
|
||||
session = (await jumpHopClient.getSession(sessionId)).session;
|
||||
}
|
||||
} catch {
|
||||
session = null;
|
||||
}
|
||||
if (!session && !work) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setJumpHopSession(session);
|
||||
setJumpHopWork(work);
|
||||
writeCreationUrlState(
|
||||
@@ -12948,7 +12991,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSelectionStage(
|
||||
path.includes('/generating')
|
||||
? 'jump-hop-generating'
|
||||
: session.draft
|
||||
: session?.draft || work
|
||||
? 'jump-hop-result'
|
||||
: 'jump-hop-workspace',
|
||||
);
|
||||
@@ -15689,7 +15732,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -15711,6 +15754,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
void executeMatch3DAction(payload);
|
||||
}}
|
||||
initialFormPayload={match3dFormDraftPayload}
|
||||
title={null}
|
||||
unifiedChrome
|
||||
onCreateFromForm={(payload) => {
|
||||
runProtectedAction(() => {
|
||||
void createMatch3DDraftFromForm(payload);
|
||||
@@ -16411,36 +16456,55 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'jump-hop-result' && jumpHopSession?.draft && (
|
||||
<motion.div
|
||||
key="jump-hop-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载跳一跳结果..." />}
|
||||
>
|
||||
<JumpHopResultView
|
||||
profile={jumpHopWork ?? jumpHopSession.draft}
|
||||
error={jumpHopError}
|
||||
onBack={leaveJumpHopFlow}
|
||||
onEdit={() => {
|
||||
setSelectionStage('jump-hop-workspace');
|
||||
}}
|
||||
onStartTestRun={startJumpHopTestRunFromProfile}
|
||||
onPublish={publishJumpHopDraft}
|
||||
onRegenerateCharacter={() => {
|
||||
void regenerateJumpHopAsset('regenerate-character');
|
||||
}}
|
||||
onRegenerateTiles={() => {
|
||||
void regenerateJumpHopAsset('regenerate-tiles');
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
{selectionStage === 'jump-hop-result' &&
|
||||
(() => {
|
||||
const activeJumpHopResultProfile =
|
||||
jumpHopWork ?? jumpHopSession?.draft ?? null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="jump-hop-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
{activeJumpHopResultProfile ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<LazyPanelFallback label="正在加载跳一跳结果..." />
|
||||
}
|
||||
>
|
||||
<JumpHopResultView
|
||||
profile={activeJumpHopResultProfile}
|
||||
error={jumpHopError}
|
||||
onBack={leaveJumpHopFlow}
|
||||
onEdit={() => {
|
||||
setSelectionStage('jump-hop-workspace');
|
||||
}}
|
||||
onStartTestRun={startJumpHopTestRunFromProfile}
|
||||
onPublish={publishJumpHopDraft}
|
||||
onRegenerateCharacter={() => {
|
||||
void regenerateJumpHopAsset('regenerate-character');
|
||||
}}
|
||||
onRegenerateTiles={() => {
|
||||
void regenerateJumpHopAsset('regenerate-tiles');
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<CreationResultRecoveryPanel
|
||||
title="跳一跳草稿未恢复"
|
||||
message="当前链接缺少可恢复的跳一跳草稿信息。"
|
||||
actionLabel="返回创作"
|
||||
onAction={() => {
|
||||
setSelectionStage('jump-hop-workspace');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{selectionStage === 'jump-hop-runtime' && (
|
||||
<motion.div
|
||||
@@ -16476,7 +16540,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载敲木鱼创作..." />}
|
||||
@@ -16631,7 +16695,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
|
||||
@@ -16654,6 +16718,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
executePuzzleWorkspaceAction(payload);
|
||||
}}
|
||||
initialFormPayload={puzzleFormDraftPayload}
|
||||
title={null}
|
||||
unifiedChrome
|
||||
onCreateFromForm={(payload) => {
|
||||
void createPuzzleDraftFromForm(payload);
|
||||
}}
|
||||
|
||||
@@ -215,6 +215,29 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
expect(screen.queryByText('旧会话消息不再渲染为聊天入口。')).toBeNull();
|
||||
});
|
||||
|
||||
test('puzzle workspace can defer visible chrome to the unified creation page', () => {
|
||||
const { container } = render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={() => {}}
|
||||
unifiedChrome
|
||||
title={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const workspace = container.querySelector('.puzzle-agent-workspace');
|
||||
expect(workspace?.getAttribute('data-unified-chrome')).toBe('true');
|
||||
expect(workspace?.className).toContain('max-w-none');
|
||||
expect(workspace?.className).not.toContain('h-full');
|
||||
expect(workspace?.className).not.toContain('overflow-hidden');
|
||||
expect(workspace?.className).not.toContain('platform-remap-surface');
|
||||
expect(screen.queryByRole('heading', { name: '想做个什么玩法?' })).toBeNull();
|
||||
expect(screen.getByLabelText('画面描述')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('puzzle workspace keeps the reference image upload as a primary panel', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
const { container } = render(
|
||||
|
||||
@@ -49,6 +49,7 @@ type PuzzleAgentWorkspaceProps = {
|
||||
initialFormPayload?: CreatePuzzleAgentSessionRequest | null;
|
||||
showBackButton?: boolean;
|
||||
title?: string | null;
|
||||
unifiedChrome?: boolean;
|
||||
};
|
||||
|
||||
type PuzzleFormState = {
|
||||
@@ -246,6 +247,7 @@ export function PuzzleAgentWorkspace({
|
||||
initialFormPayload = null,
|
||||
showBackButton = true,
|
||||
title = '想做个什么玩法?',
|
||||
unifiedChrome = false,
|
||||
}: PuzzleAgentWorkspaceProps) {
|
||||
const [formState, setFormState] = useState<PuzzleFormState>(() =>
|
||||
resolveInitialFormState(session, initialFormPayload),
|
||||
@@ -592,7 +594,14 @@ export function PuzzleAgentWorkspace({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="platform-remap-surface puzzle-agent-workspace mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
unifiedChrome
|
||||
? 'puzzle-agent-workspace mx-auto flex min-h-0 w-full max-w-none flex-col overflow-visible'
|
||||
: 'puzzle-agent-workspace platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden'
|
||||
}
|
||||
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
|
||||
>
|
||||
{showBackButton ? (
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 sm:mb-4">
|
||||
<button
|
||||
@@ -609,7 +618,7 @@ export function PuzzleAgentWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{title ? (
|
||||
{title && !unifiedChrome ? (
|
||||
<div className="mb-3 shrink-0 sm:mb-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="m-0 text-3xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
|
||||
@@ -623,6 +632,7 @@ export function PuzzleAgentWorkspace({
|
||||
) : null}
|
||||
|
||||
<CreativeImageInputPanel
|
||||
className={unifiedChrome ? 'min-h-0 flex-none' : ''}
|
||||
disabled={isBusy}
|
||||
isSubmitting={isBusy}
|
||||
uploadedImageSrc={formState.referenceImageSrc}
|
||||
|
||||
@@ -17,6 +17,10 @@ import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
@@ -82,6 +86,7 @@ import {
|
||||
regenerateBabyObjectMatchDraftAssets,
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import {
|
||||
createServerMatch3DRuntimeAdapter,
|
||||
@@ -625,6 +630,22 @@ vi.mock('../../services/edutainment-baby-object', () => ({
|
||||
saveBabyObjectMatchDraft: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
createSession: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
submitJump: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3d-creation', () => ({
|
||||
match3dCreationClient: {
|
||||
createSession: vi.fn(),
|
||||
@@ -1465,6 +1486,139 @@ function buildMockBabyObjectMatchDraft(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockJumpHopWork(
|
||||
overrides: Partial<JumpHopWorkProfileResponse> = {},
|
||||
): JumpHopWorkProfileResponse {
|
||||
const profileId = overrides.summary?.profileId ?? 'jump-hop-profile-1';
|
||||
const path = overrides.path ?? {
|
||||
seed: 'jump-hop-seed',
|
||||
difficulty: 'standard' as const,
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-start',
|
||||
tileType: 'start' as const,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 48,
|
||||
height: 36,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-finish',
|
||||
tileType: 'finish' as const,
|
||||
x: 16,
|
||||
y: 18,
|
||||
width: 60,
|
||||
height: 42,
|
||||
landingRadius: 22,
|
||||
perfectRadius: 12,
|
||||
scoreValue: 2,
|
||||
},
|
||||
],
|
||||
finishIndex: 1,
|
||||
cameraPreset: 'default',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1.2,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 20,
|
||||
perfectBonus: 50,
|
||||
},
|
||||
};
|
||||
const characterAsset = overrides.characterAsset ?? {
|
||||
assetId: 'jump-hop-character-1',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
imageObjectKey: 'jump-hop/character.png',
|
||||
assetObjectId: 'asset-jump-hop-character',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '纸片小兔',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const tileAtlasAsset = overrides.tileAtlasAsset ?? {
|
||||
assetId: 'jump-hop-tiles-1',
|
||||
imageSrc: 'data:image/png;base64,tiles',
|
||||
imageObjectKey: 'jump-hop/tiles.png',
|
||||
assetObjectId: 'asset-jump-hop-tiles',
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: '柔软云朵平台',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const tileAssets = overrides.tileAssets ?? [
|
||||
{
|
||||
tileType: 'start' as const,
|
||||
imageSrc: 'data:image/png;base64,tile-start',
|
||||
imageObjectKey: 'jump-hop/tile-start.png',
|
||||
assetObjectId: 'asset-jump-hop-tile-start',
|
||||
sourceAtlasCell: 'A1',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
{
|
||||
tileType: 'finish' as const,
|
||||
imageSrc: 'data:image/png;base64,tile-finish',
|
||||
imageObjectKey: 'jump-hop/tile-finish.png',
|
||||
assetObjectId: 'asset-jump-hop-tile-finish',
|
||||
sourceAtlasCell: 'A2',
|
||||
visualWidth: 128,
|
||||
visualHeight: 96,
|
||||
topSurfaceRadius: 24,
|
||||
landingRadius: 28,
|
||||
},
|
||||
];
|
||||
const draft = overrides.draft ?? {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId,
|
||||
workTitle: '云端跳台',
|
||||
workDescription: '一路跳到星星。',
|
||||
themeTags: ['云朵', '星空'],
|
||||
difficulty: 'standard' as const,
|
||||
stylePreset: 'paper-toy' as const,
|
||||
characterPrompt: '纸片小兔',
|
||||
tilePrompt: '柔软云朵平台',
|
||||
endMoodPrompt: '星光门',
|
||||
characterAsset,
|
||||
tileAtlasAsset,
|
||||
tileAssets,
|
||||
path,
|
||||
coverComposite: 'data:image/png;base64,cover',
|
||||
generationStatus: 'ready' as const,
|
||||
};
|
||||
|
||||
return {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-1',
|
||||
profileId,
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-1',
|
||||
workTitle: draft.workTitle,
|
||||
workDescription: draft.workDescription,
|
||||
themeTags: draft.themeTags,
|
||||
difficulty: draft.difficulty,
|
||||
stylePreset: draft.stylePreset,
|
||||
coverImageSrc: draft.coverComposite,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
...overrides.summary,
|
||||
},
|
||||
draft,
|
||||
path,
|
||||
characterAsset,
|
||||
tileAtlasAsset,
|
||||
tileAssets,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockBarkBattleWork(
|
||||
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||
): BarkBattleWorkSummary {
|
||||
@@ -2520,6 +2674,18 @@ beforeEach(() => {
|
||||
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
|
||||
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
|
||||
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
|
||||
items: [],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({ items: [] });
|
||||
vi.mocked(jumpHopClient.getSession).mockRejectedValue(
|
||||
new Error('未找到跳一跳会话'),
|
||||
);
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockRejectedValue(
|
||||
new Error('未找到跳一跳作品'),
|
||||
);
|
||||
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
|
||||
draft: payload.draft,
|
||||
}));
|
||||
@@ -7215,6 +7381,58 @@ test('refreshing RPG agent path restores stored agent workspace pointer', async
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('direct jump hop result route shows recovery panel when no draft pointer exists', async () => {
|
||||
window.history.replaceState(null, '', '/creation/jump-hop/result');
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText('跳一跳草稿未恢复')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '返回创作' })).toBeTruthy();
|
||||
expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
|
||||
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('direct jump hop result route restores work detail by profile id', async () => {
|
||||
const work = buildMockJumpHopWork({
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-restore-1',
|
||||
profileId: 'jump-hop-profile-restore-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: null,
|
||||
workTitle: '恢复后的云端跳台',
|
||||
workDescription: '从 profileId 回读完整跳一跳结果。',
|
||||
themeTags: ['云朵'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
});
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
|
||||
item: work,
|
||||
} satisfies JumpHopWorkDetailResponse);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/creation/jump-hop/result?profileId=jump-hop-profile-restore-1',
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText('恢复后的云端跳台')).toBeTruthy();
|
||||
expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
|
||||
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
|
||||
'jump-hop-profile-restore-1',
|
||||
);
|
||||
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -6038,6 +6038,11 @@ export function RpgEntryHomeView({
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<section>
|
||||
<SectionHeader title="我的创作" detail="草稿与已发布" />
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取你的作品..." />
|
||||
) : myEntries.length > 0 ? (
|
||||
@@ -6074,7 +6079,16 @@ export function RpgEntryHomeView({
|
||||
const createContent: ReactNode =
|
||||
createTabContent ?? fallbackCreateStartContent;
|
||||
|
||||
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
|
||||
const savesContent: ReactNode = (
|
||||
<>
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
{draftTabContent ?? fallbackDraftContent}
|
||||
</>
|
||||
);
|
||||
|
||||
const profileContent: ReactNode = (
|
||||
<div className={`${MOBILE_PROFILE_PAGE_STAGE_CLASS} platform-profile-page`}>
|
||||
|
||||
@@ -870,6 +870,10 @@ export function resolvePlatformWorkAuthorDisplayName(
|
||||
const displayName = authorSummary?.displayName?.trim();
|
||||
const publicUserCode = authorSummary?.publicUserCode?.trim();
|
||||
|
||||
if (displayName && publicUserCode) {
|
||||
return `${displayName} · ${publicUserCode}`;
|
||||
}
|
||||
|
||||
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
|
||||
}
|
||||
|
||||
@@ -1079,4 +1083,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
}
|
||||
}
|
||||
|
||||
102
src/components/square-hole-result/SquareHoleResultView.test.tsx
Normal file
102
src/components/square-hole-result/SquareHoleResultView.test.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { SquareHoleWorkProfile } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import { SquareHoleResultView } from './SquareHoleResultView';
|
||||
|
||||
vi.mock('../../services/square-hole-works', () => ({
|
||||
publishSquareHoleWork: vi.fn(),
|
||||
regenerateSquareHoleWorkImage: vi.fn(),
|
||||
squareHoleAssetClient: {
|
||||
listHistoryAssets: vi.fn(),
|
||||
},
|
||||
updateSquareHoleWork: vi.fn(),
|
||||
}));
|
||||
|
||||
function createProfile(): SquareHoleWorkProfile {
|
||||
return {
|
||||
profileId: 'profile-1',
|
||||
workId: 'work-1',
|
||||
ownerUserId: 'user-1',
|
||||
gameName: '方洞挑战',
|
||||
themeText: '几何反差',
|
||||
twistRule: '形状要投进对应洞口',
|
||||
summary: '把所有形状投入正确洞口。',
|
||||
tags: ['方洞', '反差'],
|
||||
coverImageSrc: 'data:image/png;base64,cover',
|
||||
backgroundPrompt: '几何场景',
|
||||
backgroundImageSrc: 'data:image/png;base64,background',
|
||||
shapeOptions: [
|
||||
{
|
||||
optionId: 'shape-1',
|
||||
shapeKind: 'square',
|
||||
label: '方块',
|
||||
targetHoleId: 'hole-1',
|
||||
imagePrompt: '方块贴图',
|
||||
imageSrc: 'data:image/png;base64,shape-1',
|
||||
},
|
||||
],
|
||||
holeOptions: [
|
||||
{
|
||||
holeId: 'hole-1',
|
||||
holeKind: 'hole-1',
|
||||
label: '洞口 1',
|
||||
imagePrompt: '洞口 1 贴图',
|
||||
imageSrc: 'data:image/png;base64,hole-1',
|
||||
},
|
||||
],
|
||||
shapeCount: 6,
|
||||
difficulty: 2,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
};
|
||||
}
|
||||
|
||||
test('square hole result view exposes test run and publish actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBack = vi.fn();
|
||||
const onStartTestRun = vi.fn();
|
||||
const onPublished = vi.fn();
|
||||
const { publishSquareHoleWork, updateSquareHoleWork } = await import(
|
||||
'../../services/square-hole-works'
|
||||
);
|
||||
const mockUpdateSquareHoleWork = vi.mocked(updateSquareHoleWork);
|
||||
const mockPublishSquareHoleWork = vi.mocked(publishSquareHoleWork);
|
||||
const nextProfile = createProfile();
|
||||
mockUpdateSquareHoleWork.mockResolvedValue({
|
||||
item: nextProfile,
|
||||
} as Awaited<ReturnType<typeof updateSquareHoleWork>>);
|
||||
mockPublishSquareHoleWork.mockResolvedValue({
|
||||
item: nextProfile,
|
||||
} as Awaited<ReturnType<typeof publishSquareHoleWork>>);
|
||||
|
||||
render(
|
||||
<SquareHoleResultView
|
||||
profile={createProfile()}
|
||||
onBack={onBack}
|
||||
onStartTestRun={onStartTestRun}
|
||||
onPublished={onPublished}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '试玩' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '发布' })).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
await user.click(screen.getByRole('button', { name: '发布' }));
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onStartTestRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(onPublished).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -38,5 +38,17 @@ describe('UnifiedCreationPage', () => {
|
||||
]);
|
||||
expect(fields[2]?.getAttribute('data-field-kind')).toBe('audio');
|
||||
expect(fields[3]?.getAttribute('data-required')).toBe('true');
|
||||
expect(screen.getByTestId('unified-creation-play-badge').textContent).toBe(
|
||||
'wooden-fish',
|
||||
);
|
||||
expect(screen.queryByLabelText('创作字段')).toBeNull();
|
||||
expect(screen.queryByTestId('unified-creation-visible-field')).toBeNull();
|
||||
expect(
|
||||
screen
|
||||
.getByText('敲木鱼工作台')
|
||||
.closest('.unified-creation-page__content')
|
||||
?.className,
|
||||
).not.toContain('overflow-y-auto');
|
||||
expect(root?.className).not.toContain('overflow-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,26 @@ export function UnifiedCreationPage({
|
||||
}: UnifiedCreationPageProps) {
|
||||
return (
|
||||
<div
|
||||
className="unified-creation-page flex h-full min-h-0 flex-col"
|
||||
className="unified-creation-page platform-remap-surface mx-auto flex w-full max-w-5xl flex-col px-3 pt-2 sm:px-4 sm:pt-3"
|
||||
data-play-id={spec.playId}
|
||||
data-field-kinds={spec.fields.map((field) => field.kind).join(',')}
|
||||
data-workspace-stage={spec.workspaceStage}
|
||||
data-generation-stage={spec.generationStage}
|
||||
data-result-stage={spec.resultStage}
|
||||
>
|
||||
<header className="unified-creation-page__header shrink-0 pb-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h1 className="m-0 min-w-0 truncate text-[1.35rem] font-black leading-tight tracking-normal text-[var(--platform-text-strong)] sm:text-[1.65rem]">
|
||||
{spec.title}
|
||||
</h1>
|
||||
<span
|
||||
className="unified-creation-page__play-badge shrink-0 rounded-full border border-[var(--platform-subpanel-border)] bg-white/80 px-3 py-1 text-[11px] font-black text-[var(--platform-text-soft)]"
|
||||
data-testid="unified-creation-play-badge"
|
||||
>
|
||||
{spec.playId}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<div className="sr-only" data-testid="unified-creation-spec">
|
||||
<h1>{spec.title}</h1>
|
||||
<ul>
|
||||
@@ -36,7 +49,9 @@ export function UnifiedCreationPage({
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{children}
|
||||
<div className="unified-creation-page__content pb-3 sm:pb-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CustomWorldGenerationProgress } from '../../../packages/shared/src
|
||||
import type { CustomWorldStructuredAnchorEntry } from '../../services/customWorldAgentGenerationProgress';
|
||||
import { CustomWorldGenerationView } from '../CustomWorldGenerationView';
|
||||
import type { UnifiedCreationPlayId } from './unifiedCreationSpecs';
|
||||
import { getUnifiedGenerationCopy } from './unifiedGenerationCopy';
|
||||
|
||||
type UnifiedGenerationPageProps = {
|
||||
playId: UnifiedCreationPlayId;
|
||||
@@ -16,39 +17,6 @@ type UnifiedGenerationPageProps = {
|
||||
hideBatchModule?: boolean;
|
||||
};
|
||||
|
||||
const UNIFIED_GENERATION_COPY = {
|
||||
puzzle: {
|
||||
retryLabel: '重新生成图片',
|
||||
settingTitle: '当前拼图信息',
|
||||
progressTitle: '拼图图片生成进度',
|
||||
activeBadgeLabel: '图片生成中',
|
||||
},
|
||||
match3d: {
|
||||
retryLabel: '重新生成草稿',
|
||||
settingTitle: '当前抓大鹅信息',
|
||||
progressTitle: '抓大鹅草稿生成进度',
|
||||
activeBadgeLabel: '素材生成中',
|
||||
},
|
||||
'wooden-fish': {
|
||||
retryLabel: '重新生成草稿',
|
||||
settingTitle: '当前敲木鱼信息',
|
||||
progressTitle: '敲木鱼草稿生成进度',
|
||||
activeBadgeLabel: '素材生成中',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
UnifiedCreationPlayId,
|
||||
{
|
||||
retryLabel: string;
|
||||
settingTitle: string;
|
||||
progressTitle: string;
|
||||
activeBadgeLabel: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export function getUnifiedGenerationCopy(playId: UnifiedCreationPlayId) {
|
||||
return UNIFIED_GENERATION_COPY[playId];
|
||||
}
|
||||
|
||||
export function UnifiedGenerationPage({
|
||||
playId,
|
||||
settingText,
|
||||
|
||||
34
src/components/unified-creation/unifiedGenerationCopy.ts
Normal file
34
src/components/unified-creation/unifiedGenerationCopy.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { UnifiedCreationPlayId } from './unifiedCreationSpecs';
|
||||
|
||||
const UNIFIED_GENERATION_COPY = {
|
||||
puzzle: {
|
||||
retryLabel: '重新生成图片',
|
||||
settingTitle: '当前拼图信息',
|
||||
progressTitle: '拼图图片生成进度',
|
||||
activeBadgeLabel: '图片生成中',
|
||||
},
|
||||
match3d: {
|
||||
retryLabel: '重新生成草稿',
|
||||
settingTitle: '当前抓大鹅信息',
|
||||
progressTitle: '抓大鹅草稿生成进度',
|
||||
activeBadgeLabel: '素材生成中',
|
||||
},
|
||||
'wooden-fish': {
|
||||
retryLabel: '重新生成草稿',
|
||||
settingTitle: '当前敲木鱼信息',
|
||||
progressTitle: '敲木鱼草稿生成进度',
|
||||
activeBadgeLabel: '素材生成中',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
UnifiedCreationPlayId,
|
||||
{
|
||||
retryLabel: string;
|
||||
settingTitle: string;
|
||||
progressTitle: string;
|
||||
activeBadgeLabel: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export function getUnifiedGenerationCopy(playId: UnifiedCreationPlayId) {
|
||||
return UNIFIED_GENERATION_COPY[playId];
|
||||
}
|
||||
@@ -118,6 +118,11 @@ test('visual novel generation helpers build process page data', () => {
|
||||
expect(buildVisualNovelEntryGenerationAnchorEntries(payload)).toEqual([
|
||||
{ id: 'visual-novel-idea', label: '一句话', value: '雨夜书店' },
|
||||
{ id: 'visual-novel-style', label: '视觉画风', value: '水彩绘本' },
|
||||
{
|
||||
id: 'visual-novel-target',
|
||||
label: '生成目标',
|
||||
value: '可编辑并可试玩的视觉小说草稿',
|
||||
},
|
||||
]);
|
||||
|
||||
const progress = buildVisualNovelEntryGenerationProgress(
|
||||
@@ -126,7 +131,8 @@ test('visual novel generation helpers build process page data', () => {
|
||||
8_000,
|
||||
);
|
||||
|
||||
expect(progress.phaseId).toBe('generating');
|
||||
expect(progress.phaseId).toBe('visual-novel-world');
|
||||
expect(progress.overallProgress).toBeGreaterThan(0);
|
||||
expect(progress.phaseLabel).toBe('扩展世界观');
|
||||
expect(progress.steps.some((step) => step.status === 'active')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -107,6 +107,27 @@ test('敲击音效临时关闭提示词生成入口,仅保留上传和录音',
|
||||
expect(within(section as HTMLElement).getByText('录音')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('敲击音效和功德词条不放进独立滚动窗', () => {
|
||||
const { container } = render(
|
||||
<WoodenFishWorkspace
|
||||
onBack={() => {}}
|
||||
onSubmitted={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const audioSection = screen.getByText('敲击音效').closest('section');
|
||||
const floatingWordsSection = screen.getByText('功德有什么').closest('section');
|
||||
const sidePanel = audioSection?.parentElement;
|
||||
|
||||
expect(audioSection).not.toBeNull();
|
||||
expect(floatingWordsSection).not.toBeNull();
|
||||
expect(sidePanel).toBe(floatingWordsSection?.parentElement);
|
||||
expect(sidePanel?.className).not.toContain('overflow-y-auto');
|
||||
expect(sidePanel?.className).not.toContain('min-h-0');
|
||||
expect(container.querySelector('.overflow-y-auto')).toBeNull();
|
||||
expect(container.firstElementChild?.className).not.toContain('h-full');
|
||||
});
|
||||
|
||||
test('工作台只保留一个生成按钮', () => {
|
||||
render(
|
||||
<WoodenFishWorkspace
|
||||
|
||||
@@ -155,7 +155,7 @@ export function WoodenFishWorkspace({
|
||||
};
|
||||
|
||||
return (
|
||||
<div 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="platform-remap-surface mx-auto flex 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"
|
||||
@@ -167,7 +167,7 @@ export function WoodenFishWorkspace({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(19rem,0.88fr)]">
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(19rem,0.88fr)]">
|
||||
<div className="flex min-h-[26rem] min-w-0 flex-col">
|
||||
<CreativeImageInputPanel
|
||||
disabled={isBusy || isSubmitting}
|
||||
@@ -231,7 +231,7 @@ export function WoodenFishWorkspace({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col gap-3 overflow-y-auto pr-0 lg:pr-1">
|
||||
<div className="flex flex-col gap-3 pr-0 lg:pr-1">
|
||||
<CreativeAudioInputPanel<WoodenFishAudioAsset>
|
||||
disabled={isBusy || isSubmitting}
|
||||
title="敲击音效"
|
||||
@@ -251,7 +251,7 @@ export function WoodenFishWorkspace({
|
||||
<div className="mb-3 text-sm font-black text-[var(--platform-text-strong)]">
|
||||
功德有什么
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{formState.floatingWords.map((word, index) => (
|
||||
<div key={index} className="relative">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user