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;
+}