'use client'; import { AnimatePresence, motion } from 'motion/react'; import { useRouter } from 'next/navigation'; import { useEffect, useMemo, useRef, useState } from 'react'; import { apiRequest, ensureLogin } from '../lib/api'; import { chooseMatch, createBracket, firstOpenMatch } from '../lib/bracket'; import type { BracketState, ChoiceSide, TestDetail, TestResult } from '../lib/types'; type Mode = 'single' | 'multi' | 'swipe'; const MODES: readonly { id: Mode; label: string }[] = [ { id: 'single', label: '一对' }, { id: 'multi', label: '多对' }, { id: 'swipe', label: '滑动' } ]; export default function TestArena({ slug }: { slug: string }) { const router = useRouter(); const [test, setTest] = useState(null); const [bracket, setBracket] = useState(null); const [mode, setMode] = useState('single'); const [error, setError] = useState(''); const [saving, setSaving] = useState(false); const pointerStart = useRef(null); useEffect(() => { apiRequest(`/api/v1/tests/${slug}`).then(data => { setTest(data); setBracket(createBracket(data.candidates)); }).catch((error: Error) => setError(error.message)); }, [slug]); const total = test ? test.candidates.length - 1 : 1; const progress = bracket ? Math.round(bracket.decisions.length / total * 100) : 0; const currentIndex = bracket ? firstOpenMatch(bracket) : -1; const current = bracket && currentIndex >= 0 ? bracket.matches[currentIndex] : null; const visibleMatches = useMemo(() => bracket?.matches.slice(bracket.offset, bracket.offset + 4) || [], [bracket]); async function complete(finalState: BracketState) { setSaving(true); const champion = finalState.pool[0]; const finalDecision = finalState.decisions.at(-1); try { await ensureLogin(); if (!champion || !finalDecision) throw new Error('晋级结果不完整'); const runnerUp = finalDecision.left_id === champion.id ? finalDecision.right_id : finalDecision.left_id; const result = await apiRequest(`/api/v1/tests/${slug}/runs`, { method: 'POST', body: { app_key: 'hub', champion_id: champion.id, runner_up_id: runnerUp, ranking_ids: [champion, ...finalState.eliminated].map(item => item.id), decisions: finalState.decisions } }); sessionStorage.setItem('pk_last_result', JSON.stringify(result)); router.replace(`/results/${result.id}`); } catch (error) { setError(error instanceof Error ? error.message : '保存失败'); setSaving(false); } } function select(matchIndex: number, side: ChoiceSide) { if (saving || !bracket) return; const outcome = chooseMatch(bracket, matchIndex, side); let next = outcome.state; if (!outcome.finished && mode === 'multi') { const block = next.matches.slice(next.offset, next.offset + 4); if (block.length && block.every(match => match.winner)) next = { ...next, offset: next.offset + 4 }; } setBracket(next); if (outcome.finished) complete(next); } function pointerUp(event: React.PointerEvent) { if (mode !== 'swipe' || pointerStart.current === null) return; const delta = event.clientX - pointerStart.current; pointerStart.current = null; if (Math.abs(delta) >= 55) select(currentIndex, delta < 0 ? 'left' : 'right'); } if (error) return

暂时无法继续

{error}

; if (!test || !bracket) return
正在抽签…
; if (!current && mode !== 'multi') return
正在生成晋级图…
; return (
第 {bracket.round} 轮 {bracket.decisions.length} / {total}
{MODES.map(item => )}
{mode === 'multi' ? (
{visibleMatches.map((match, index) => { const matchIndex = bracket.offset + index; return ( {match.winner ? 晋级
{match.winner.name}
: <>VS}
); })}
) : (
{ pointerStart.current = event.clientX; }} onPointerUp={pointerUp}> mode !== 'swipe' && select(currentIndex, 'left')} className="choiceCard warm">A{current!.left.name}{mode === 'swipe' ? '向左滑选它' : '点按晋级'} VS mode !== 'swipe' && select(currentIndex, 'right')} className="choiceCard cool">B{current!.right!.name}{mode === 'swipe' ? '向右滑选它' : '点按晋级'}
)} {saving ?
正在保存你的冠军…
: null}
); }