mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35:55 +08:00
feat(frontend): 对局视图 + 回放视图 + 计时器 hook
This commit is contained in:
@@ -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 (
|
||||
<div className="game-controls">
|
||||
<button onClick={handleUndo} disabled={status === 'game_over'}>
|
||||
{t('game.undo')}
|
||||
</button>
|
||||
<button onClick={onBackToMenu}>{t('game.new_game')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <div className="game-info">{text}</div>;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="game-view">
|
||||
<GameInfo />
|
||||
<div className="board-container">
|
||||
<BoardCanvas />
|
||||
</div>
|
||||
<TimerDisplay />
|
||||
<GameControls onBackToMenu={onBackToMenu} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={`timer-display ${time <= 10 ? 'timer-warning' : ''}`}>
|
||||
{Math.floor(time / 60)}:{(time % 60).toString().padStart(2, '0')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="replay-controls">
|
||||
<button onClick={onPrev}>{t('replay.prev')}</button>
|
||||
<button onClick={onTogglePlay}>{isPlaying ? t('replay.pause') : t('replay.play')}</button>
|
||||
<button onClick={onNext}>{t('replay.next')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="replay-view">
|
||||
<div className="board-container">
|
||||
<BoardCanvas />
|
||||
</div>
|
||||
<StepSlider current={step} total={moves.length} onChange={setStep} />
|
||||
<div>{t('replay.step', { current: step, total: moves.length })}</div>
|
||||
<ReplayControls
|
||||
isPlaying={isPlaying}
|
||||
onTogglePlay={() => setIsPlaying(!isPlaying)}
|
||||
onPrev={() => setStep(Math.max(0, step - 1))}
|
||||
onNext={() => setStep(Math.min(moves.length, step + 1))}
|
||||
/>
|
||||
<button onClick={onBackToMenu}>返回菜单</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface Props {
|
||||
current: number;
|
||||
total: number;
|
||||
onChange: (step: number) => void;
|
||||
}
|
||||
|
||||
export default function StepSlider({ current, total, onChange }: Props) {
|
||||
return (
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={total}
|
||||
value={current}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="step-slider"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user