162 lines
7.4 KiB
TypeScript
162 lines
7.4 KiB
TypeScript
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>
|
||
);
|
||
}
|