1
This commit is contained in:
471
src/components/big-fish-result/BigFishResultView.tsx
Normal file
471
src/components/big-fish-result/BigFishResultView.tsx
Normal file
@@ -0,0 +1,471 @@
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
ImagePlus,
|
||||
Loader2,
|
||||
Play,
|
||||
Sparkles,
|
||||
Waves,
|
||||
} from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
BigFishAssetSlotResponse,
|
||||
BigFishGameDraftResponse,
|
||||
BigFishLevelBlueprintResponse,
|
||||
BigFishSessionSnapshotResponse,
|
||||
ExecuteBigFishActionRequest,
|
||||
} from '../../../packages/shared/src/contracts/bigFish';
|
||||
|
||||
type BigFishAssetStudioTarget =
|
||||
| {
|
||||
kind: 'level_main_image';
|
||||
level: BigFishLevelBlueprintResponse;
|
||||
}
|
||||
| {
|
||||
kind: 'level_motion';
|
||||
level: BigFishLevelBlueprintResponse;
|
||||
motionKey: 'idle_float' | 'move_swim';
|
||||
}
|
||||
| {
|
||||
kind: 'stage_background';
|
||||
};
|
||||
|
||||
type BigFishResultViewProps = {
|
||||
session: BigFishSessionSnapshotResponse;
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onBack: () => void;
|
||||
onExecuteAction: (payload: ExecuteBigFishActionRequest) => void;
|
||||
onStartTestRun: () => void;
|
||||
};
|
||||
|
||||
function findAssetSlot(
|
||||
slots: BigFishAssetSlotResponse[],
|
||||
assetKind: string,
|
||||
level?: number,
|
||||
motionKey?: string,
|
||||
) {
|
||||
return slots.find((slot) => {
|
||||
if (slot.assetKind !== assetKind) {
|
||||
return false;
|
||||
}
|
||||
if (level !== undefined && slot.level !== level) {
|
||||
return false;
|
||||
}
|
||||
if (motionKey !== undefined && slot.motionKey !== motionKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function assetReadyLabel(slot: BigFishAssetSlotResponse | undefined) {
|
||||
return slot?.status === 'ready' ? '已生成' : '待生成';
|
||||
}
|
||||
|
||||
function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) {
|
||||
if (slot?.assetUrl) {
|
||||
return slot.assetUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function BigFishAssetStudioModal({
|
||||
draft,
|
||||
target,
|
||||
isBusy,
|
||||
onClose,
|
||||
onExecuteAction,
|
||||
}: {
|
||||
draft: BigFishGameDraftResponse;
|
||||
target: BigFishAssetStudioTarget;
|
||||
isBusy: boolean;
|
||||
onClose: () => void;
|
||||
onExecuteAction: (payload: ExecuteBigFishActionRequest) => void;
|
||||
}) {
|
||||
const title =
|
||||
target.kind === 'stage_background'
|
||||
? '场地背景工坊'
|
||||
: target.kind === 'level_main_image'
|
||||
? `Lv.${target.level.level} 主图工坊`
|
||||
: `Lv.${target.level.level} 动作工坊`;
|
||||
const prompt =
|
||||
target.kind === 'stage_background'
|
||||
? draft.background.backgroundPromptSeed
|
||||
: target.kind === 'level_main_image'
|
||||
? target.level.visualPromptSeed
|
||||
: `${target.level.motionPromptSeed} / ${target.motionKey}`;
|
||||
|
||||
const execute = () => {
|
||||
if (target.kind === 'stage_background') {
|
||||
onExecuteAction({ action: 'big_fish_generate_stage_background' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.kind === 'level_main_image') {
|
||||
onExecuteAction({
|
||||
action: 'big_fish_generate_level_main_image',
|
||||
level: target.level.level,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onExecuteAction({
|
||||
action: 'big_fish_generate_level_motion',
|
||||
level: target.level.level,
|
||||
motionKey: target.motionKey,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="platform-overlay fixed inset-0 z-[95] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div className="platform-modal-shell w-full max-w-xl overflow-hidden rounded-[1.8rem]">
|
||||
<div className="border-b border-[var(--platform-subpanel-border)] px-4 py-4">
|
||||
<div className="text-lg font-black text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-[var(--platform-text-base)]">
|
||||
{target.kind === 'stage_background'
|
||||
? draft.background.theme
|
||||
: target.level.oneLineFantasy}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
<div className="rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/72 p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
PROMPT
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-strong)]">
|
||||
{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>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 border-t border-[var(--platform-subpanel-border)] px-4 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isBusy}
|
||||
className="rounded-full border border-[var(--platform-subpanel-border)] px-4 py-2 text-sm font-semibold text-[var(--platform-text-base)] disabled:opacity-45"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={execute}
|
||||
disabled={isBusy}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BigFishLevelCard({
|
||||
level,
|
||||
slots,
|
||||
isBusy,
|
||||
onOpenStudio,
|
||||
}: {
|
||||
level: BigFishLevelBlueprintResponse;
|
||||
slots: BigFishAssetSlotResponse[];
|
||||
isBusy: boolean;
|
||||
onOpenStudio: (target: BigFishAssetStudioTarget) => void;
|
||||
}) {
|
||||
const mainImageSlot = findAssetSlot(
|
||||
slots,
|
||||
'level_main_image',
|
||||
level.level,
|
||||
);
|
||||
const idleSlot = findAssetSlot(
|
||||
slots,
|
||||
'level_motion',
|
||||
level.level,
|
||||
'idle_float',
|
||||
);
|
||||
const moveSlot = findAssetSlot(
|
||||
slots,
|
||||
'level_motion',
|
||||
level.level,
|
||||
'move_swim',
|
||||
);
|
||||
const previewUrl = buildLevelAssetPreview(mainImageSlot);
|
||||
|
||||
return (
|
||||
<article className="overflow-hidden rounded-[1.45rem] border border-[var(--platform-subpanel-border)] bg-white/78">
|
||||
<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
|
||||
src={previewUrl}
|
||||
alt={level.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Waves className="h-8 w-8 text-cyan-100/72" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-xs font-black tracking-[0.18em] text-cyan-700">
|
||||
LV.{level.level}
|
||||
</div>
|
||||
<div className="mt-1 text-lg font-black text-[var(--platform-text-strong)]">
|
||||
{level.name}
|
||||
</div>
|
||||
</div>
|
||||
{level.isFinalLevel ? (
|
||||
<span className="rounded-full bg-amber-100 px-2 py-1 text-xs font-bold text-amber-700">
|
||||
终局
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-2 text-sm leading-5 text-[var(--platform-text-base)]">
|
||||
{level.oneLineFantasy}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-[var(--platform-text-soft)]">
|
||||
<span>猎物 {level.preyWindow.join('/') || '-'}</span>
|
||||
<span>威胁 {level.threatWindow.join('/') || '-'}</span>
|
||||
<span>主图 {assetReadyLabel(mainImageSlot)}</span>
|
||||
<span>
|
||||
动作 {[assetReadyLabel(idleSlot), assetReadyLabel(moveSlot)].join('/')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 border-t border-[var(--platform-subpanel-border)] p-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onOpenStudio({ kind: 'level_main_image', level });
|
||||
}}
|
||||
className="rounded-full bg-cyan-600 px-3 py-2 text-xs font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
主图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onOpenStudio({
|
||||
kind: 'level_motion',
|
||||
level,
|
||||
motionKey: 'idle_float',
|
||||
});
|
||||
}}
|
||||
className="rounded-full border border-[var(--platform-subpanel-border)] px-3 py-2 text-xs font-bold text-[var(--platform-text-base)] disabled:opacity-45"
|
||||
>
|
||||
待机
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onOpenStudio({
|
||||
kind: 'level_motion',
|
||||
level,
|
||||
motionKey: 'move_swim',
|
||||
});
|
||||
}}
|
||||
className="rounded-full border border-[var(--platform-subpanel-border)] px-3 py-2 text-xs font-bold text-[var(--platform-text-base)] disabled:opacity-45"
|
||||
>
|
||||
移动
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function BigFishResultView({
|
||||
session,
|
||||
isBusy = false,
|
||||
error = null,
|
||||
onBack,
|
||||
onExecuteAction,
|
||||
onStartTestRun,
|
||||
}: BigFishResultViewProps) {
|
||||
const [studioTarget, setStudioTarget] =
|
||||
useState<BigFishAssetStudioTarget | null>(null);
|
||||
const draft = session.draft;
|
||||
const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background');
|
||||
const blockers = useMemo(
|
||||
() => session.assetCoverage.blockers.filter(Boolean),
|
||||
[session.assetCoverage.blockers],
|
||||
);
|
||||
|
||||
if (!draft) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
还没有可编辑的玩法草稿
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
|
||||
<div className="relative overflow-hidden rounded-[1.8rem] border border-cyan-100/16 bg-[radial-gradient(circle_at_top_left,rgba(45,212,191,0.2),transparent_32%),linear-gradient(135deg,rgba(8,47,73,0.98),rgba(15,23,42,0.98))] px-4 py-4 text-white sm:px-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84 disabled:opacity-45"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onStartTestRun();
|
||||
}}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-white/12 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
测试
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onExecuteAction({ action: 'big_fish_publish_game' });
|
||||
}}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-cyan-200 px-4 py-2 text-sm font-bold text-slate-950 disabled:opacity-45"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{draft.title}
|
||||
</div>
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-cyan-50/76">
|
||||
{draft.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2 text-xs text-cyan-50/78">
|
||||
<span className="rounded-full bg-white/10 px-3 py-1">
|
||||
{draft.coreFun}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/10 px-3 py-1">
|
||||
{draft.ecologyTheme}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/10 px-3 py-1">
|
||||
{draft.runtimeParams.levelCount} 级
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-[1.15rem] border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid min-h-0 flex-1 gap-3 overflow-hidden lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div className="min-h-0 overflow-y-auto pr-1">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{draft.levels.map((level) => (
|
||||
<BigFishLevelCard
|
||||
key={level.level}
|
||||
level={level}
|
||||
slots={session.assetSlots}
|
||||
isBusy={isBusy}
|
||||
onOpenStudio={setStudioTarget}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="min-h-0 space-y-3 overflow-y-auto">
|
||||
<div className="rounded-[1.45rem] border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
场地背景
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[var(--platform-text-soft)]">
|
||||
{assetReadyLabel(backgroundSlot)}
|
||||
</div>
|
||||
</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))]" />
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
setStudioTarget({ kind: 'stage_background' });
|
||||
}}
|
||||
className="mt-3 inline-flex w-full items-center justify-center gap-2 rounded-full bg-cyan-600 px-4 py-2 text-sm font-bold text-white disabled:opacity-45"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
生成背景
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[1.45rem] border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] p-4">
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
发布校验
|
||||
</div>
|
||||
<div className="mt-3 space-y-2 text-sm text-[var(--platform-text-base)]">
|
||||
<div>
|
||||
主图 {session.assetCoverage.levelMainImageReadyCount}/
|
||||
{session.assetCoverage.requiredLevelCount}
|
||||
</div>
|
||||
<div>
|
||||
动作 {session.assetCoverage.levelMotionReadyCount}/
|
||||
{session.assetCoverage.requiredLevelCount * 2}
|
||||
</div>
|
||||
<div>
|
||||
背景 {session.assetCoverage.backgroundReady ? '已完成' : '待生成'}
|
||||
</div>
|
||||
</div>
|
||||
{blockers.length > 0 ? (
|
||||
<div className="mt-3 space-y-1 text-xs leading-5 text-amber-700">
|
||||
{blockers.slice(0, 4).map((blocker) => (
|
||||
<div key={blocker}>{blocker}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 text-sm font-semibold text-emerald-600">
|
||||
已达到发布条件
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{studioTarget ? (
|
||||
<BigFishAssetStudioModal
|
||||
draft={draft}
|
||||
target={studioTarget}
|
||||
isBusy={isBusy}
|
||||
onClose={() => {
|
||||
setStudioTarget(null);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
onExecuteAction(payload);
|
||||
setStudioTarget(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BigFishResultView;
|
||||
Reference in New Issue
Block a user