This commit is contained in:
2026-04-23 06:01:00 +08:00
parent 9dc56f0fc0
commit f6046ef658
123 changed files with 7752 additions and 436 deletions

View File

@@ -0,0 +1,149 @@
// @vitest-environment jsdom
import { render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
import { BigFishResultView } from './BigFishResultView';
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: ({
src,
alt,
className,
}: {
src?: string | null;
alt?: string;
className?: string;
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
}));
function createSession(): BigFishSessionSnapshotResponse {
return {
sessionId: 'big-fish-session-1',
currentTurn: 2,
progressPercent: 88,
stage: 'asset_refining',
anchorPack: {
gameplayPromise: {
key: 'gameplayPromise',
label: '玩法承诺',
value: '弱小逆袭',
status: 'confirmed',
},
ecologyVisualTheme: {
key: 'ecologyVisualTheme',
label: '生态与视觉母题',
value: '深海谜境',
status: 'confirmed',
},
growthLadder: {
key: 'growthLadder',
label: '成长阶梯',
value: '8 级进化',
status: 'confirmed',
},
riskTempo: {
key: 'riskTempo',
label: '风险节奏',
value: '平衡',
status: 'confirmed',
},
},
draft: {
title: '深海谜境',
subtitle: '逐级吞噬成长',
coreFun: '弱小逆袭',
ecologyTheme: '深海谜境',
levels: [
{
level: 1,
name: '荧潮幼体',
oneLineFantasy: '在深海荧光裂谷中寻找第一个同伴。',
silhouetteDirection: '圆润鱼苗',
sizeRatio: 1,
visualPromptSeed: '深海荧光幼体',
motionPromptSeed: '轻微摆尾',
mergeSourceLevel: null,
preyWindow: [1],
threatWindow: [2],
isFinalLevel: false,
},
],
background: {
theme: '深海谜境',
colorMood: '深蓝与青绿',
foregroundHints: '漂浮微粒',
midgroundComposition: '中央留白',
backgroundDepth: '深海纵深',
safePlayAreaHint: '中央 70%',
spawnEdgeHint: '四周边缘',
backgroundPromptSeed: '深海谜境背景',
},
runtimeParams: {
levelCount: 1,
mergeCountPerUpgrade: 3,
spawnTargetCount: 12,
leaderMoveSpeed: 160,
followerCatchUpSpeed: 120,
offscreenCullSeconds: 3,
preySpawnDeltaLevels: [1],
threatSpawnDeltaLevels: [1],
winLevel: 1,
},
},
assetSlots: [
{
slotId: 'big-fish-asset-level-main',
assetKind: 'level_main_image',
level: 1,
motionKey: null,
status: 'ready',
assetUrl:
'/generated-big-fish-assets/big-fish-session-1/level-main-image/level-1/image.png',
promptSnapshot: '深海荧光幼体',
updatedAt: '2026-04-23T10:00:00.000Z',
},
{
slotId: 'big-fish-asset-background',
assetKind: 'stage_background',
level: null,
motionKey: null,
status: 'ready',
assetUrl:
'/generated-big-fish-assets/big-fish-session-1/stage-background/image.png',
promptSnapshot: '深海谜境背景',
updatedAt: '2026-04-23T10:00:00.000Z',
},
],
assetCoverage: {
levelMainImageReadyCount: 1,
levelMotionReadyCount: 0,
backgroundReady: true,
requiredLevelCount: 1,
publishReady: false,
blockers: ['还缺少 2 个基础动作'],
},
messages: [],
lastAssistantReply: '主图占位图已生成。',
publishReady: false,
updatedAt: '2026-04-23T10:00:00.000Z',
};
}
describe('BigFishResultView', () => {
test('renders generated formal previews with accurate status copy', () => {
render(
<BigFishResultView
session={createSession()}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
expect(screen.getByText('主图 已生成')).toBeTruthy();
expect(screen.getByAltText('荧潮幼体')).toBeTruthy();
expect(screen.getByAltText('深海谜境 场地背景')).toBeTruthy();
});
});

View File

@@ -16,6 +16,7 @@ import type {
BigFishSessionSnapshotResponse,
ExecuteBigFishActionRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
type BigFishAssetStudioTarget =
| {
@@ -61,7 +62,10 @@ function findAssetSlot(
}
function assetReadyLabel(slot: BigFishAssetSlotResponse | undefined) {
return slot?.status === 'ready' ? '已生成' : '待生成';
if (slot?.status !== 'ready') {
return '待生成';
}
return isBigFishPlaceholderAsset(slot) ? '占位已生成' : '已生成';
}
function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) {
@@ -71,15 +75,43 @@ function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) {
return null;
}
function isBigFishPlaceholderAsset(slot: BigFishAssetSlotResponse | undefined) {
return Boolean(slot?.assetUrl?.includes('/generated-big-fish/'));
}
function buildStudioAssetPreview(
slots: BigFishAssetSlotResponse[],
target: BigFishAssetStudioTarget,
) {
if (target.kind === 'stage_background') {
return buildLevelAssetPreview(findAssetSlot(slots, 'stage_background'));
}
if (target.kind === 'level_main_image') {
return buildLevelAssetPreview(
findAssetSlot(slots, 'level_main_image', target.level.level),
);
}
return buildLevelAssetPreview(
findAssetSlot(
slots,
'level_motion',
target.level.level,
target.motionKey,
),
);
}
function BigFishAssetStudioModal({
draft,
target,
previewUrl,
isBusy,
onClose,
onExecuteAction,
}: {
draft: BigFishGameDraftResponse;
target: BigFishAssetStudioTarget;
previewUrl?: string | null;
isBusy: boolean;
onClose: () => void;
onExecuteAction: (payload: ExecuteBigFishActionRequest) => void;
@@ -140,8 +172,16 @@ function BigFishAssetStudioModal({
{prompt}
</div>
</div>
<div className="flex aspect-[9/5] items-center justify-center rounded-[1.4rem] border border-dashed border-cyan-300/50 bg-cyan-50/40 text-sm text-[var(--platform-text-base)]">
AI
<div className="flex aspect-[9/5] items-center justify-center overflow-hidden rounded-[1.4rem] border border-dashed border-cyan-300/50 bg-cyan-50/40 text-sm text-[var(--platform-text-base)]">
{previewUrl ? (
<ResolvedAssetImage
src={previewUrl}
alt={title}
className="h-full w-full object-cover"
/>
) : (
'AI 资产候选预览'
)}
</div>
</div>
<div className="flex justify-end gap-2 border-t border-[var(--platform-subpanel-border)] px-4 py-4">
@@ -160,7 +200,7 @@ function BigFishAssetStudioModal({
className="inline-flex items-center gap-2 rounded-full bg-cyan-600 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
>
{isBusy ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
</button>
</div>
</div>
@@ -203,7 +243,7 @@ function BigFishLevelCard({
<div className="flex gap-3 p-3">
<div className="flex h-24 w-24 shrink-0 items-center justify-center overflow-hidden rounded-[1.15rem] bg-[radial-gradient(circle_at_center,rgba(34,211,238,0.28),transparent_68%),linear-gradient(145deg,rgba(8,47,73,0.88),rgba(15,23,42,0.94))] text-white">
{previewUrl ? (
<img
<ResolvedAssetImage
src={previewUrl}
alt={level.name}
className="h-full w-full object-cover"
@@ -297,10 +337,17 @@ export function BigFishResultView({
useState<BigFishAssetStudioTarget | null>(null);
const draft = session.draft;
const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background');
const backgroundPreviewUrl = buildLevelAssetPreview(backgroundSlot);
const blockers = useMemo(
() => session.assetCoverage.blockers.filter(Boolean),
[session.assetCoverage.blockers],
);
const studioPreviewUrl = useMemo(() => {
if (!studioTarget) {
return null;
}
return buildStudioAssetPreview(session.assetSlots, studioTarget);
}, [session.assetSlots, studioTarget]);
if (!draft) {
return (
@@ -404,7 +451,15 @@ export function BigFishResultView({
</div>
<ImagePlus className="h-5 w-5 text-cyan-600" />
</div>
<div className="mt-3 aspect-[9/16] rounded-[1.2rem] bg-[radial-gradient(circle_at_center,rgba(34,211,238,0.2),transparent_62%),linear-gradient(180deg,rgba(8,47,73,0.88),rgba(15,23,42,0.94))]" />
<div className="mt-3 aspect-[9/16] overflow-hidden rounded-[1.2rem] bg-[radial-gradient(circle_at_center,rgba(34,211,238,0.2),transparent_62%),linear-gradient(180deg,rgba(8,47,73,0.88),rgba(15,23,42,0.94))]">
{backgroundPreviewUrl ? (
<ResolvedAssetImage
src={backgroundPreviewUrl}
alt={`${draft.background.theme} 场地背景`}
className="h-full w-full object-cover"
/>
) : null}
</div>
<button
type="button"
disabled={isBusy}
@@ -454,6 +509,7 @@ export function BigFishResultView({
<BigFishAssetStudioModal
draft={draft}
target={studioTarget}
previewUrl={studioPreviewUrl}
isBusy={isBusy}
onClose={() => {
setStudioTarget(null);