From 0138d80f2a447951aa99199e2f43a79fb6a9ffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 31 May 2026 00:25:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E5=AF=B9=E5=B1=80=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=20+=20=E5=9B=9E=E6=94=BE=E8=A7=86=E5=9B=BE=20+=20?= =?UTF-8?q?=E8=AE=A1=E6=97=B6=E5=99=A8=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/game/GameControls.tsx | 26 ++++++++++++++ src/components/game/GameInfo.tsx | 20 +++++++++++ src/components/game/GameView.tsx | 21 +++++++++++ src/components/game/TimerDisplay.tsx | 29 ++++++++++++++++ src/components/replay/ReplayControls.tsx | 19 ++++++++++ src/components/replay/ReplayView.tsx | 44 ++++++++++++++++++++++++ src/components/replay/StepSlider.tsx | 18 ++++++++++ src/hooks/useGame.ts | 13 +++++++ src/hooks/useTimer.ts | 19 ++++++++++ 9 files changed, 209 insertions(+) create mode 100644 src/components/game/GameControls.tsx create mode 100644 src/components/game/GameInfo.tsx create mode 100644 src/components/game/GameView.tsx create mode 100644 src/components/game/TimerDisplay.tsx create mode 100644 src/components/replay/ReplayControls.tsx create mode 100644 src/components/replay/ReplayView.tsx create mode 100644 src/components/replay/StepSlider.tsx create mode 100644 src/hooks/useGame.ts create mode 100644 src/hooks/useTimer.ts diff --git a/src/components/game/GameControls.tsx b/src/components/game/GameControls.tsx new file mode 100644 index 0000000..8e9e50f --- /dev/null +++ b/src/components/game/GameControls.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; +import { useGameStore } from '../../store/gameStore'; + +interface Props { + onBackToMenu: () => void; +} + +export default function GameControls({ onBackToMenu }: Props) { + const { t } = useTranslation(); + const undo = useGameStore((s) => s.undo); + const mode = useGameStore((s) => s.mode); + const status = useGameStore((s) => s.status); + + const handleUndo = () => { + undo(1); + }; + + return ( +
+ + +
+ ); +} diff --git a/src/components/game/GameInfo.tsx b/src/components/game/GameInfo.tsx new file mode 100644 index 0000000..1a7bf14 --- /dev/null +++ b/src/components/game/GameInfo.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next'; +import { useGameStore } from '../../store/gameStore'; + +export default function GameInfo() { + const { t } = useTranslation(); + const currentColor = useGameStore((s) => s.currentColor); + const status = useGameStore((s) => s.status); + const winner = useGameStore((s) => s.winner); + + let text = ''; + if (status === 'game_over' && winner) { + text = winner === 'Black' ? t('game.black_win') : t('game.white_win'); + } else if (status === 'ai_thinking') { + text = t('game.ai_thinking'); + } else if (status === 'playing') { + text = currentColor === 'Black' ? t('game.black_turn') : t('game.white_turn'); + } + + return
{text}
; +} diff --git a/src/components/game/GameView.tsx b/src/components/game/GameView.tsx new file mode 100644 index 0000000..c698138 --- /dev/null +++ b/src/components/game/GameView.tsx @@ -0,0 +1,21 @@ +import BoardCanvas from '../board/BoardCanvas'; +import GameInfo from './GameInfo'; +import GameControls from './GameControls'; +import TimerDisplay from './TimerDisplay'; + +interface Props { + onBackToMenu: () => void; +} + +export default function GameView({ onBackToMenu }: Props) { + return ( +
+ +
+ +
+ + +
+ ); +} diff --git a/src/components/game/TimerDisplay.tsx b/src/components/game/TimerDisplay.tsx new file mode 100644 index 0000000..c384d09 --- /dev/null +++ b/src/components/game/TimerDisplay.tsx @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; +import { useGameStore } from '../../store/gameStore'; + +export default function TimerDisplay() { + const config = useGameStore((s) => s.config); + const currentColor = useGameStore((s) => s.currentColor); + const status = useGameStore((s) => s.status); + const [time, setTime] = useState(config.timeLimitSecs); + + useEffect(() => { + if (!config.useTimer || status !== 'playing') return; + setTime(config.timeLimitSecs); + const timer = setInterval(() => { + setTime((t) => { + if (t <= 1) { clearInterval(timer); return 0; } + return t - 1; + }); + }, 1000); + return () => clearInterval(timer); + }, [currentColor, config.useTimer, config.timeLimitSecs, status]); + + if (!config.useTimer) return null; + + return ( +
+ {Math.floor(time / 60)}:{(time % 60).toString().padStart(2, '0')} +
+ ); +} diff --git a/src/components/replay/ReplayControls.tsx b/src/components/replay/ReplayControls.tsx new file mode 100644 index 0000000..f4aaf3b --- /dev/null +++ b/src/components/replay/ReplayControls.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from 'react-i18next'; + +interface Props { + isPlaying: boolean; + onTogglePlay: () => void; + onPrev: () => void; + onNext: () => void; +} + +export default function ReplayControls({ isPlaying, onTogglePlay, onPrev, onNext }: Props) { + const { t } = useTranslation(); + return ( +
+ + + +
+ ); +} diff --git a/src/components/replay/ReplayView.tsx b/src/components/replay/ReplayView.tsx new file mode 100644 index 0000000..4ab121d --- /dev/null +++ b/src/components/replay/ReplayView.tsx @@ -0,0 +1,44 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGameStore } from '../../store/gameStore'; +import BoardCanvas from '../board/BoardCanvas'; +import StepSlider from './StepSlider'; +import ReplayControls from './ReplayControls'; + +interface Props { + onBackToMenu: () => void; +} + +export default function ReplayView({ onBackToMenu }: Props) { + const { t } = useTranslation(); + const moves = useGameStore((s) => s.moves); + const [step, setStep] = useState(moves.length); + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + if (!isPlaying) return; + if (step >= moves.length) { + setIsPlaying(false); + return; + } + const timer = setInterval(() => setStep((s) => s + 1), 500); + return () => clearInterval(timer); + }, [isPlaying, step, moves.length]); + + return ( +
+
+ +
+ +
{t('replay.step', { current: step, total: moves.length })}
+ setIsPlaying(!isPlaying)} + onPrev={() => setStep(Math.max(0, step - 1))} + onNext={() => setStep(Math.min(moves.length, step + 1))} + /> + +
+ ); +} diff --git a/src/components/replay/StepSlider.tsx b/src/components/replay/StepSlider.tsx new file mode 100644 index 0000000..abeedb7 --- /dev/null +++ b/src/components/replay/StepSlider.tsx @@ -0,0 +1,18 @@ +interface Props { + current: number; + total: number; + onChange: (step: number) => void; +} + +export default function StepSlider({ current, total, onChange }: Props) { + return ( + onChange(Number(e.target.value))} + className="step-slider" + /> + ); +} diff --git a/src/hooks/useGame.ts b/src/hooks/useGame.ts new file mode 100644 index 0000000..58548e6 --- /dev/null +++ b/src/hooks/useGame.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; +import { useGameStore } from '../store/gameStore'; +import type { GameConfig, GameModeType } from '../core/types'; + +export function useGame() { + const store = useGameStore(); + + const startGame = useCallback(async (mode: GameModeType, config: GameConfig) => { + await store.startGame(mode, config); + }, [store]); + + return { ...store, startGame }; +} diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts new file mode 100644 index 0000000..221d38e --- /dev/null +++ b/src/hooks/useTimer.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +export function useTimer(seconds: number, active: boolean, onTimeout: () => void) { + const [time, setTime] = useState(seconds); + + useEffect(() => { + if (!active) return; + setTime(seconds); + const timer = setInterval(() => { + setTime((t) => { + if (t <= 1) { clearInterval(timer); onTimeout(); return 0; } + return t - 1; + }); + }, 1000); + return () => clearInterval(timer); + }, [active, seconds, onTimeout]); + + return time; +}