fix: polish bark battle creation flow

This commit is contained in:
kdletters
2026-05-22 05:00:07 +08:00
parent 01da85a577
commit bf82f04b64
73 changed files with 9362 additions and 2663 deletions

View File

@@ -6,7 +6,6 @@ import {
Play,
RefreshCw,
Upload,
Volume2,
} from 'lucide-react';
import { type ChangeEvent, type ReactNode, useMemo, useRef, useState } from 'react';
@@ -31,22 +30,20 @@ type BarkBattleResultViewProps = {
onPublish: (draft: BarkBattleDraftConfig) => void;
};
type BarkBattleImageSlot = Exclude<BarkBattleAssetSlot, 'bark-sound'>;
const SLOT_LABELS = {
'player-character': '玩家形象',
'opponent-character': '对手形象',
'ui-background': 'UI背景',
'bark-sound': '狗叫音效',
} satisfies Record<BarkBattleAssetSlot, string>;
function mapDraftToConfig(draft: BarkBattleDraftConfig): BarkBattleConfigEditorPayload {
return {
title: draft.title,
description: draft.description,
themePreset: draft.themePreset,
playerDogSkinPreset: draft.playerDogSkinPreset,
opponentDogSkinPreset: draft.opponentDogSkinPreset,
themeDescription: draft.themeDescription,
playerImageDescription: draft.playerImageDescription,
opponentImageDescription: draft.opponentImageDescription,
onomatopoeia: draft.onomatopoeia,
...(draft.playerCharacterImageSrc
? { playerCharacterImageSrc: draft.playerCharacterImageSrc }
: {}),
@@ -56,9 +53,7 @@ function mapDraftToConfig(draft: BarkBattleDraftConfig): BarkBattleConfigEditorP
...(draft.uiBackgroundImageSrc
? { uiBackgroundImageSrc: draft.uiBackgroundImageSrc }
: {}),
...(draft.barkSoundSrc ? { barkSoundSrc: draft.barkSoundSrc } : {}),
difficultyPreset: draft.difficultyPreset,
leaderboardEnabled: draft.leaderboardEnabled,
};
}
@@ -77,7 +72,7 @@ function applyAssetToDraft(
if (slot === 'ui-background') {
return { ...draft, uiBackgroundImageSrc: assetSrc, updatedAt };
}
return { ...draft, barkSoundSrc: assetSrc, updatedAt };
return { ...draft, updatedAt };
}
function getSlotAssetSrc(draft: BarkBattleDraftConfig, slot: BarkBattleAssetSlot) {
@@ -90,7 +85,7 @@ function getSlotAssetSrc(draft: BarkBattleDraftConfig, slot: BarkBattleAssetSlot
if (slot === 'ui-background') {
return draft.uiBackgroundImageSrc ?? '';
}
return draft.barkSoundSrc ?? '';
return '';
}
function ResultActionButton({
@@ -111,7 +106,7 @@ function ResultActionButton({
onClick={onClick}
className={`platform-button ${
tone === 'primary' ? 'platform-button--primary' : 'platform-button--secondary'
} min-h-11 justify-center disabled:cursor-not-allowed disabled:opacity-55`}
} min-h-10 justify-center text-sm disabled:cursor-not-allowed disabled:opacity-55 sm:min-h-11`}
>
{children}
</button>
@@ -135,7 +130,7 @@ function BarkBattleAssetSlotControl({
const [isUploading, setIsUploading] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false);
const assetSrc = getSlotAssetSrc(draft, slot);
const isImageSlot = slot !== 'bark-sound';
const assetStatus = assetSrc ? '已替换' : '未替换';
const handleUpload = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.currentTarget.files?.[0] ?? null;
@@ -152,7 +147,8 @@ function BarkBattleAssetSlotControl({
file,
draftId: draft.draftId,
});
onChange(applyAssetToDraft(draft, slot, asset.assetSrc));
const nextDraft = applyAssetToDraft(draft, slot, asset.assetSrc);
onChange(nextDraft);
} catch (error) {
onError(error instanceof Error ? error.message : '上传素材失败。');
} finally {
@@ -161,20 +157,16 @@ function BarkBattleAssetSlotControl({
};
const handleRegenerate = async () => {
if (!isImageSlot) {
onError('狗叫音效暂未接入自动生成,请先手动上传音频。');
return;
}
setIsRegenerating(true);
onError(null);
try {
const result = await regenerateBarkBattleImageAsset({
slot: slot as BarkBattleImageSlot,
slot,
config: mapDraftToConfig(draft),
draftId: draft.draftId,
});
onChange(applyAssetToDraft(draft, slot, result.imageSrc));
const nextDraft = applyAssetToDraft(draft, slot, result.imageSrc);
onChange(nextDraft);
} catch (error) {
onError(error instanceof Error ? error.message : '重新生成素材失败。');
} finally {
@@ -185,29 +177,27 @@ function BarkBattleAssetSlotControl({
const isSlotBusy = isUploading || isRegenerating;
return (
<article className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)]">
<div className="flex items-center justify-between gap-3">
<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">
<div className="flex items-center justify-between gap-2 sm:gap-3">
<div className="min-w-0">
<h3 className="m-0 text-sm font-black text-[var(--platform-text-strong)]">
<h3 className="m-0 text-xs font-black text-[var(--platform-text-strong)] sm:text-sm">
{SLOT_LABELS[slot]}
</h3>
<div className="mt-1 truncate text-xs font-semibold text-[var(--platform-text-soft)]">
{assetSrc || '未替换'}
<div className="mt-0.5 truncate text-[11px] font-semibold text-[var(--platform-text-soft)] sm:mt-1 sm:text-xs">
{assetStatus}
</div>
</div>
{isSlotBusy ? (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-[var(--platform-text-soft)]" />
) : isImageSlot ? (
<ImagePlus className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
) : (
<Volume2 className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
<ImagePlus className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
)}
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="mt-2 grid grid-cols-2 gap-1.5 sm:mt-3 sm:gap-2">
<input
ref={fileInputRef}
type="file"
accept={isImageSlot ? 'image/png,image/jpeg,image/webp' : 'audio/mpeg,audio/wav,audio/ogg,audio/webm'}
accept="image/png,image/jpeg,image/webp"
className="hidden"
aria-label={`上传${SLOT_LABELS[slot]}文件`}
onChange={handleUpload}
@@ -216,7 +206,7 @@ function BarkBattleAssetSlotControl({
type="button"
disabled={disabled || isSlotBusy}
onClick={() => fileInputRef.current?.click()}
className="platform-button platform-button--secondary min-h-9 justify-center rounded-full px-3 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-55"
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"
>
<Upload className="h-3.5 w-3.5" />
@@ -225,7 +215,7 @@ function BarkBattleAssetSlotControl({
type="button"
disabled={disabled || isSlotBusy}
onClick={handleRegenerate}
className="platform-button platform-button--secondary min-h-9 justify-center rounded-full px-3 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-55"
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"
>
<RefreshCw className="h-3.5 w-3.5" />
@@ -247,33 +237,34 @@ export function BarkBattleResultView({
const [localError, setLocalError] = useState<string | null>(null);
const previewConfig = useMemo(() => mapDraftToConfig(draft), [draft]);
const visibleError = localError ?? error;
const isActionBusy = isBusy;
return (
<div className="platform-page-stage platform-remap-surface flex h-full min-h-0 flex-col overflow-hidden px-3 pb-3 pt-3 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-5xl flex-col">
<div className="mb-3 flex shrink-0 items-center justify-between gap-3">
<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"
onClick={onBack}
disabled={isBusy}
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
disabled={isActionBusy}
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isActionBusy ? 'opacity-45' : ''}`}
>
<ArrowLeft className="h-3.5 w-3.5" />
</button>
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-black text-emerald-700">
<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">
稿
</span>
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-0.5">
<section className="grid gap-3 lg:grid-cols-[minmax(0,0.94fr)_minmax(18rem,0.86fr)]">
<div className="grid gap-3">
<div className="rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.74)]">
<div className="text-sm font-black text-[var(--platform-text-soft)]">
<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">
<div className="text-xs font-black text-[var(--platform-text-soft)] sm:text-sm">
稿
</div>
<h1 className="m-0 mt-2 text-3xl font-black leading-tight tracking-normal text-[var(--platform-text-strong)] sm:text-5xl">
<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>
@@ -283,14 +274,13 @@ export function BarkBattleResultView({
'player-character',
'opponent-character',
'ui-background',
'bark-sound',
] as const
).map((slot) => (
<BarkBattleAssetSlotControl
key={slot}
draft={draft}
slot={slot}
disabled={isBusy}
disabled={isActionBusy}
onChange={(nextDraft) => {
setLocalError(null);
onDraftChange(nextDraft);
@@ -312,7 +302,7 @@ export function BarkBattleResultView({
<div className="mt-3 grid shrink-0 gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:grid-cols-2">
<ResultActionButton
disabled={isBusy}
disabled={isActionBusy}
onClick={() => onStartTestRun(draft)}
>
<Play className="h-4 w-4" />
@@ -320,7 +310,7 @@ export function BarkBattleResultView({
</ResultActionButton>
<ResultActionButton
tone="primary"
disabled={isBusy}
disabled={isActionBusy}
onClick={() => onPublish(draft)}
>
{isBusy ? (