import { ArrowLeft, CheckCircle2, ImagePlus, Loader2, Play, RefreshCw, Upload, } from 'lucide-react'; import { type ChangeEvent, type ReactNode, useMemo, useRef, useState, } from 'react'; import type { BarkBattleConfigEditorPayload, BarkBattleDraftConfig, } from '../../../packages/shared/src/contracts/barkBattle'; import { type BarkBattleAssetSlot, regenerateBarkBattleImageAsset, uploadBarkBattleAsset, } from '../../services/bark-battle-creation'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { BarkBattlePreviewCard } from './BarkBattlePreviewCard'; type BarkBattleResultViewProps = { draft: BarkBattleDraftConfig; isBusy?: boolean; error?: string | null; onBack: () => void; onDraftChange: (draft: BarkBattleDraftConfig) => void; onStartTestRun: (draft: BarkBattleDraftConfig) => void; onPublish: (draft: BarkBattleDraftConfig) => void; }; const SLOT_LABELS = { 'player-character': '玩家形象', 'opponent-character': '对手形象', 'ui-background': 'UI背景', } satisfies Record; function mapDraftToConfig( draft: BarkBattleDraftConfig, ): BarkBattleConfigEditorPayload { return { title: draft.title, description: draft.description, themeDescription: draft.themeDescription, playerImageDescription: draft.playerImageDescription, opponentImageDescription: draft.opponentImageDescription, onomatopoeia: draft.onomatopoeia, ...(draft.playerCharacterImageSrc ? { playerCharacterImageSrc: draft.playerCharacterImageSrc } : {}), ...(draft.opponentCharacterImageSrc ? { opponentCharacterImageSrc: draft.opponentCharacterImageSrc } : {}), ...(draft.uiBackgroundImageSrc ? { uiBackgroundImageSrc: draft.uiBackgroundImageSrc } : {}), difficultyPreset: draft.difficultyPreset, }; } function applyAssetToDraft( draft: BarkBattleDraftConfig, slot: BarkBattleAssetSlot, assetSrc: string, ): BarkBattleDraftConfig { const updatedAt = new Date().toISOString(); if (slot === 'player-character') { return { ...draft, playerCharacterImageSrc: assetSrc, updatedAt }; } if (slot === 'opponent-character') { return { ...draft, opponentCharacterImageSrc: assetSrc, updatedAt }; } if (slot === 'ui-background') { return { ...draft, uiBackgroundImageSrc: assetSrc, updatedAt }; } return { ...draft, updatedAt }; } function getSlotAssetSrc( draft: BarkBattleDraftConfig, slot: BarkBattleAssetSlot, ) { if (slot === 'player-character') { return draft.playerCharacterImageSrc ?? ''; } if (slot === 'opponent-character') { return draft.opponentCharacterImageSrc ?? ''; } if (slot === 'ui-background') { return draft.uiBackgroundImageSrc ?? ''; } return ''; } function ResultActionButton({ children, disabled, onClick, tone = 'secondary', }: { children: ReactNode; disabled?: boolean; onClick: () => void; tone?: 'primary' | 'secondary'; }) { return ( {children} ); } function BarkBattleAssetSlotControl({ draft, slot, disabled, onChange, onError, }: { draft: BarkBattleDraftConfig; slot: BarkBattleAssetSlot; disabled: boolean; onChange: (draft: BarkBattleDraftConfig) => void; onError: (message: string | null) => void; }) { const fileInputRef = useRef(null); const [isUploading, setIsUploading] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); const assetSrc = getSlotAssetSrc(draft, slot); const assetStatus = assetSrc ? '已替换' : '未替换'; const handleUpload = async (event: ChangeEvent) => { const file = event.currentTarget.files?.[0] ?? null; event.currentTarget.value = ''; if (!file) { return; } setIsUploading(true); onError(null); try { const asset = await uploadBarkBattleAsset({ slot, file, draftId: draft.draftId, }); const nextDraft = applyAssetToDraft(draft, slot, asset.assetSrc); onChange(nextDraft); } catch (error) { onError(error instanceof Error ? error.message : '上传素材失败。'); } finally { setIsUploading(false); } }; const handleRegenerate = async () => { setIsRegenerating(true); onError(null); try { const result = await regenerateBarkBattleImageAsset({ slot, config: mapDraftToConfig(draft), draftId: draft.draftId, }); const nextDraft = applyAssetToDraft(draft, slot, result.imageSrc); onChange(nextDraft); } catch (error) { onError(error instanceof Error ? error.message : '重新生成素材失败。'); } finally { setIsRegenerating(false); } }; const isSlotBusy = isUploading || isRegenerating; return (

{SLOT_LABELS[slot]}

{assetStatus}
{isSlotBusy ? ( ) : ( )}
fileInputRef.current?.click()} tone="secondary" size="xs" shape="pill" className="min-h-8 gap-1.5 px-2.5 py-1 text-[11px] sm:min-h-9 sm:px-3 sm:py-1.5 sm:text-xs" > 上传 重新生成
); } export function BarkBattleResultView({ draft, isBusy = false, error = null, onBack, onDraftChange, onStartTestRun, onPublish, }: BarkBattleResultViewProps) { const [localError, setLocalError] = useState(null); const previewConfig = useMemo(() => mapDraftToConfig(draft), [draft]); const visibleError = localError ?? error; const isActionBusy = isBusy; return (
返回编辑 草稿
草稿编译

{draft.title || '未命名声浪竞技场'}

{( [ 'player-character', 'opponent-character', 'ui-background', ] as const ).map((slot) => ( { setLocalError(null); onDraftChange(nextDraft); }} onError={setLocalError} /> ))}
{visibleError ? ( {visibleError} ) : null}
onStartTestRun(draft)} > 试玩 onPublish(draft)} > {isBusy ? ( ) : ( )} 发布
); } export default BarkBattleResultView;