feat: wire bark battle platform loop
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
161
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx
Normal file
161
src/components/bark-battle-creation/BarkBattleConfigEditor.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { BarkBattleConfigEditorPayload } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BarkBattleDifficultyPreset } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import { BarkBattlePreviewCard } from './BarkBattlePreviewCard';
|
||||
|
||||
export type BarkBattleConfigEditorProps = {
|
||||
isBusy?: boolean;
|
||||
onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
};
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ value: 'sunny-yard', label: '阳光院子' },
|
||||
{ value: 'neon-park', label: '霓虹公园' },
|
||||
{ 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: '标准' },
|
||||
{ value: 'hard', label: '硬核' },
|
||||
];
|
||||
|
||||
export function BarkBattleConfigEditor({
|
||||
isBusy = false,
|
||||
onPublish,
|
||||
onBack,
|
||||
}: BarkBattleConfigEditorProps) {
|
||||
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 [difficultyPreset, setDifficultyPreset] = useState<BarkBattleDifficultyPreset>('normal');
|
||||
const [leaderboardEnabled, setLeaderboardEnabled] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const payload = useMemo<BarkBattleConfigEditorPayload>(
|
||||
() => ({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
themePreset,
|
||||
playerDogSkinPreset,
|
||||
opponentDogSkinPreset,
|
||||
difficultyPreset,
|
||||
leaderboardEnabled,
|
||||
}),
|
||||
[
|
||||
title,
|
||||
description,
|
||||
themePreset,
|
||||
playerDogSkinPreset,
|
||||
opponentDogSkinPreset,
|
||||
difficultyPreset,
|
||||
leaderboardEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePublish = () => {
|
||||
if (!payload.title) {
|
||||
setError('请先填写作品标题');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
void onPublish(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="min-h-screen bg-slate-950 px-4 py-6 text-slate-50 sm:px-6" aria-label="Bark Battle 轻配置编辑器">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-5 lg:grid lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="rounded-3xl border border-cyan-300/20 bg-slate-900/90 p-5 shadow-2xl shadow-cyan-950/40">
|
||||
<div className="mb-5 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="mb-2 inline-flex rounded-full bg-cyan-300/10 px-3 py-1 text-xs font-bold text-cyan-100">轻配置作品</p>
|
||||
<h1 className="text-2xl font-black tracking-tight sm:text-3xl">汪汪声浪大作战</h1>
|
||||
<p className="mt-2 text-sm text-slate-300">配置展示、皮肤、难度和排行榜;公平性规则由后端固定裁决。</p>
|
||||
</div>
|
||||
{onBack ? (
|
||||
<button type="button" onClick={onBack} className="rounded-full border border-slate-600 px-3 py-2 text-sm text-slate-200">
|
||||
返回
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
作品标题
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-base text-white outline-none focus:border-cyan-300"
|
||||
maxLength={40}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
简介
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
className="min-h-[88px] rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-base text-white outline-none focus:border-cyan-300"
|
||||
maxLength={160}
|
||||
placeholder="一句话告诉玩家这场声浪对决的氛围"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
主题背景
|
||||
<select value={themePreset} onChange={(event) => setThemePreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
|
||||
{THEME_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
难度预设
|
||||
<select value={difficultyPreset} onChange={(event) => setDifficultyPreset(event.target.value as BarkBattleDifficultyPreset)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
|
||||
{DIFFICULTY_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
玩家狗狗
|
||||
<select value={playerDogSkinPreset} onChange={(event) => setPlayerDogSkinPreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
|
||||
{DOG_SKIN_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm font-semibold text-slate-200">
|
||||
对手狗狗
|
||||
<select value={opponentDogSkinPreset} onChange={(event) => setOpponentDogSkinPreset(event.target.value)} className="rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-white">
|
||||
{DOG_SKIN_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center justify-between gap-3 rounded-2xl border border-slate-700 bg-slate-950 px-4 py-3 text-sm font-semibold text-slate-100">
|
||||
<span>
|
||||
开启排行榜
|
||||
<span className="block text-xs font-normal text-slate-400">仅收录后端裁决胜利且未拒绝的成绩</span>
|
||||
</span>
|
||||
<input aria-label="开启排行榜" type="checkbox" checked={leaderboardEnabled} onChange={(event) => setLeaderboardEnabled(event.target.checked)} className="h-5 w-5" />
|
||||
</label>
|
||||
|
||||
{error ? <p className="rounded-2xl bg-rose-500/15 px-4 py-3 text-sm font-semibold text-rose-100">{error}</p> : null}
|
||||
|
||||
<button type="button" disabled={isBusy} onClick={handlePublish} className="rounded-full bg-cyan-200 px-5 py-3 text-sm font-black text-slate-950 disabled:opacity-50">
|
||||
{isBusy ? '发布中…' : '发布并试玩'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BarkBattlePreviewCard config={payload} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user