Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
285 lines
12 KiB
TypeScript
285 lines
12 KiB
TypeScript
import { ArrowLeft, Loader2, Trophy, WandSparkles } from 'lucide-react';
|
|
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;
|
|
error?: string | null;
|
|
onPublish: (payload: BarkBattleConfigEditorPayload) => void | Promise<void>;
|
|
onBack?: () => void;
|
|
showBackButton?: boolean;
|
|
title?: string | null;
|
|
};
|
|
|
|
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,
|
|
error: externalError = null,
|
|
onPublish,
|
|
onBack,
|
|
showBackButton = true,
|
|
title: headingTitle = '汪汪声浪大作战',
|
|
}: 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 [localError, setLocalError] = 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) {
|
|
setLocalError('请先填写作品标题');
|
|
return;
|
|
}
|
|
setLocalError(null);
|
|
void onPublish(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"
|
|
aria-label="汪汪声浪轻配置编辑器"
|
|
>
|
|
{showBackButton && onBack ? (
|
|
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 sm:mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
disabled={isBusy}
|
|
className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
|
|
>
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
返回
|
|
</span>
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
{headingTitle ? (
|
|
<div className="mb-3 shrink-0 sm:mb-5">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h1 className="m-0 text-3xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
|
|
{headingTitle}
|
|
</h1>
|
|
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-black text-emerald-700">
|
|
轻配置
|
|
</span>
|
|
</div>
|
|
</div>
|
|
) : 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' : ''}`}
|
|
>
|
|
<div className="flex min-h-0 flex-col gap-3 overflow-y-auto pr-0 lg:pr-1">
|
|
<label className="block shrink-0">
|
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
|
作品标题
|
|
</span>
|
|
<input
|
|
value={title}
|
|
disabled={isBusy}
|
|
onChange={(event) => setTitle(event.target.value)}
|
|
className="h-11 w-full rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 text-base font-semibold text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
|
maxLength={40}
|
|
aria-label="作品标题"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block shrink-0">
|
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
|
简介
|
|
</span>
|
|
<textarea
|
|
value={description}
|
|
disabled={isBusy}
|
|
onChange={(event) => setDescription(event.target.value)}
|
|
className="h-[5.5rem] min-h-[5.5rem] w-full resize-none rounded-[1.05rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-rose-200 focus:bg-white focus:ring-2 focus:ring-rose-100"
|
|
maxLength={160}
|
|
placeholder=""
|
|
aria-label="简介"
|
|
/>
|
|
</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>
|
|
<select
|
|
value={themePreset}
|
|
disabled={isBusy}
|
|
onChange={(event) => setThemePreset(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="主题背景"
|
|
>
|
|
{THEME_OPTIONS.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
|
难度预设
|
|
</span>
|
|
<select
|
|
value={difficultyPreset}
|
|
disabled={isBusy}
|
|
onChange={(event) =>
|
|
setDifficultyPreset(
|
|
event.target.value as BarkBattleDifficultyPreset,
|
|
)
|
|
}
|
|
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="难度预设"
|
|
>
|
|
{DIFFICULTY_OPTIONS.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
|
玩家狗狗
|
|
</span>
|
|
<select
|
|
value={playerDogSkinPreset}
|
|
disabled={isBusy}
|
|
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>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
|
对手狗狗
|
|
</span>
|
|
<select
|
|
value={opponentDogSkinPreset}
|
|
disabled={isBusy}
|
|
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>
|
|
</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>
|
|
|
|
{visibleError ? (
|
|
<div className="platform-banner platform-banner--danger shrink-0 rounded-2xl text-sm leading-6">
|
|
{visibleError}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<BarkBattlePreviewCard config={payload} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 flex shrink-0 justify-center pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:mt-3">
|
|
<button
|
|
type="button"
|
|
disabled={isBusy}
|
|
onClick={handlePublish}
|
|
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" />
|
|
)}
|
|
<span>{isBusy ? '发布中' : '发布并试玩'}</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|