feat: complete bark battle draft publish flow
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Loader2, Trophy, WandSparkles } from 'lucide-react';
|
||||
import { ArrowLeft, Loader2, Play } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
@@ -8,7 +8,7 @@ import { BarkBattlePreviewCard } from './BarkBattlePreviewCard';
|
||||
export type BarkBattleConfigEditorProps = {
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>;
|
||||
onPreview: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
showBackButton?: boolean;
|
||||
title?: string | null;
|
||||
@@ -20,12 +20,6 @@ const THEME_OPTIONS = [
|
||||
{ value: 'moonlight-rooftop', label: '月光天台' },
|
||||
];
|
||||
|
||||
const DOG_SKIN_OPTIONS = [
|
||||
{ value: 'corgi', label: '柯基' },
|
||||
{ value: 'shiba', label: '柴犬' },
|
||||
{ value: 'husky', label: '哈士奇' },
|
||||
];
|
||||
|
||||
const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: string }> = [
|
||||
{ value: 'easy', label: '轻松' },
|
||||
{ value: 'normal', label: '标准' },
|
||||
@@ -35,7 +29,7 @@ const DIFFICULTY_OPTIONS: Array<{ value: BarkBattleDifficultyPreset; label: stri
|
||||
export function BarkBattleConfigEditor({
|
||||
isBusy = false,
|
||||
error: externalError = null,
|
||||
onPublish,
|
||||
onPreview,
|
||||
onBack,
|
||||
showBackButton = true,
|
||||
title: headingTitle = '汪汪声浪大作战',
|
||||
@@ -43,10 +37,13 @@ export function BarkBattleConfigEditor({
|
||||
const [title, setTitle] = useState('我的声浪竞技场');
|
||||
const [description, setDescription] = useState('');
|
||||
const [themePreset, setThemePreset] = useState('sunny-yard');
|
||||
const [playerDogSkinPreset, setPlayerDogSkinPreset] = useState('corgi');
|
||||
const [opponentDogSkinPreset, setOpponentDogSkinPreset] = useState('husky');
|
||||
const [playerDogSkinPreset, setPlayerDogSkinPreset] = useState('主角');
|
||||
const [opponentDogSkinPreset, setOpponentDogSkinPreset] = useState('对手');
|
||||
const [playerCharacterImageSrc, setPlayerCharacterImageSrc] = useState('');
|
||||
const [opponentCharacterImageSrc, setOpponentCharacterImageSrc] = useState('');
|
||||
const [uiBackgroundImageSrc, setUiBackgroundImageSrc] = useState('');
|
||||
const [barkSoundSrc, setBarkSoundSrc] = useState('');
|
||||
const [difficultyPreset, setDifficultyPreset] = useState<BarkBattleDifficultyPreset>('normal');
|
||||
const [leaderboardEnabled, setLeaderboardEnabled] = useState(true);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
const payload = useMemo<BarkBattleConfigEditorPayload>(
|
||||
@@ -56,8 +53,18 @@ export function BarkBattleConfigEditor({
|
||||
themePreset,
|
||||
playerDogSkinPreset,
|
||||
opponentDogSkinPreset,
|
||||
...(playerCharacterImageSrc.trim()
|
||||
? { playerCharacterImageSrc: playerCharacterImageSrc.trim() }
|
||||
: {}),
|
||||
...(opponentCharacterImageSrc.trim()
|
||||
? { opponentCharacterImageSrc: opponentCharacterImageSrc.trim() }
|
||||
: {}),
|
||||
...(uiBackgroundImageSrc.trim()
|
||||
? { uiBackgroundImageSrc: uiBackgroundImageSrc.trim() }
|
||||
: {}),
|
||||
...(barkSoundSrc.trim() ? { barkSoundSrc: barkSoundSrc.trim() } : {}),
|
||||
difficultyPreset,
|
||||
leaderboardEnabled,
|
||||
leaderboardEnabled: true,
|
||||
}),
|
||||
[
|
||||
title,
|
||||
@@ -65,24 +72,29 @@ export function BarkBattleConfigEditor({
|
||||
themePreset,
|
||||
playerDogSkinPreset,
|
||||
opponentDogSkinPreset,
|
||||
playerCharacterImageSrc,
|
||||
opponentCharacterImageSrc,
|
||||
uiBackgroundImageSrc,
|
||||
barkSoundSrc,
|
||||
difficultyPreset,
|
||||
leaderboardEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePublish = () => {
|
||||
const runValidatedAction = (
|
||||
action: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>,
|
||||
) => {
|
||||
if (!payload.title) {
|
||||
setLocalError('请先填写作品标题');
|
||||
return;
|
||||
}
|
||||
setLocalError(null);
|
||||
void onPublish(payload);
|
||||
void action(payload);
|
||||
};
|
||||
const visibleError = localError ?? externalError;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-hidden"
|
||||
className="platform-remap-surface mx-auto flex min-h-0 w-full max-w-5xl flex-1 flex-col overflow-y-auto overscroll-y-contain pr-0.5"
|
||||
aria-label="汪汪声浪轻配置编辑器"
|
||||
>
|
||||
{showBackButton && onBack ? (
|
||||
@@ -101,7 +113,7 @@ export function BarkBattleConfigEditor({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{headingTitle ? (
|
||||
<div className="mb-3 shrink-0 sm:mb-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -116,9 +128,9 @@ export function BarkBattleConfigEditor({
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={`grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(17rem,0.88fr)] lg:grid-rows-1 ${isBusy ? 'opacity-55' : ''}`}
|
||||
className={`grid flex-1 gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(17rem,0.88fr)] ${isBusy ? 'opacity-55' : ''}`}
|
||||
>
|
||||
<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">
|
||||
<label className="block shrink-0">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
作品标题
|
||||
@@ -193,63 +205,88 @@ export function BarkBattleConfigEditor({
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
玩家狗狗
|
||||
玩家角色设定
|
||||
</span>
|
||||
<select
|
||||
<input
|
||||
value={playerDogSkinPreset}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
setPlayerDogSkinPreset(event.target.value)
|
||||
}
|
||||
onChange={(event) => setPlayerDogSkinPreset(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
aria-label="玩家狗狗"
|
||||
>
|
||||
{DOG_SKIN_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
aria-label="玩家角色设定"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
对手狗狗
|
||||
对手角色设定
|
||||
</span>
|
||||
<select
|
||||
<input
|
||||
value={opponentDogSkinPreset}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
setOpponentDogSkinPreset(event.target.value)
|
||||
}
|
||||
onChange={(event) => setOpponentDogSkinPreset(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-black text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
aria-label="对手狗狗"
|
||||
>
|
||||
{DOG_SKIN_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
aria-label="对手角色设定"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="flex shrink-0 items-center justify-between gap-3 rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-4 py-3 text-sm font-black text-[var(--platform-text-strong)] shadow-[inset_0_1px_0_rgba(255,255,255,0.76)]">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Trophy className="h-4 w-4 text-amber-500" />
|
||||
开启排行榜
|
||||
</span>
|
||||
<input
|
||||
aria-label="开启排行榜"
|
||||
type="checkbox"
|
||||
checked={leaderboardEnabled}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
setLeaderboardEnabled(event.target.checked)
|
||||
}
|
||||
className="h-5 w-5 accent-[#ff4f6a]"
|
||||
/>
|
||||
</label>
|
||||
<div className="grid shrink-0 gap-2.5 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
玩家形象
|
||||
</span>
|
||||
<input
|
||||
value={playerCharacterImageSrc}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setPlayerCharacterImageSrc(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
placeholder=""
|
||||
aria-label="玩家形象"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
对手形象
|
||||
</span>
|
||||
<input
|
||||
value={opponentCharacterImageSrc}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setOpponentCharacterImageSrc(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
placeholder=""
|
||||
aria-label="对手形象"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
UI背景
|
||||
</span>
|
||||
<input
|
||||
value={uiBackgroundImageSrc}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setUiBackgroundImageSrc(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
placeholder=""
|
||||
aria-label="UI背景"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
狗叫音效
|
||||
</span>
|
||||
<input
|
||||
value={barkSoundSrc}
|
||||
disabled={isBusy}
|
||||
onChange={(event) => setBarkSoundSrc(event.target.value)}
|
||||
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-sm font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
||||
placeholder=""
|
||||
aria-label="狗叫音效"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{visibleError ? (
|
||||
<div className="platform-banner platform-banner--danger shrink-0 rounded-2xl text-sm leading-6">
|
||||
@@ -262,20 +299,20 @@ export function BarkBattleConfigEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex shrink-0 justify-center pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:mt-3">
|
||||
<div className="mt-3 flex shrink-0 flex-wrap justify-center gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:mt-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={handlePublish}
|
||||
onClick={() => runValidatedAction(onPreview)}
|
||||
className={`platform-button platform-button--primary min-h-10 px-4 py-2 text-sm sm:min-h-11 sm:px-5 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
>
|
||||
<span className="inline-flex flex-wrap items-center justify-center gap-1.5 sm:gap-2">
|
||||
{isBusy ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<WandSparkles className="h-4 w-4" />
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
<span>{isBusy ? '发布中' : '发布并试玩'}</span>
|
||||
<span>{isBusy ? '处理中' : '生成草稿'}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user