feat(frontend): 菜单组件 — 主菜单/本地双人/AI设置/网络/加载棋谱

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 00:25:19 +08:00
parent 6d8a62eca5
commit a4b3b5c380
5 changed files with 214 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGameStore } from '../../store/gameStore';
import type { Color, GameConfig } from '../../core/types';
interface Props {
onBack: () => void;
onStart: () => void;
}
export default function AiGameSetup({ onBack, onStart }: Props) {
const { t } = useTranslation();
const startGame = useGameStore((s) => s.startGame);
const [difficulty, setDifficulty] = useState(3);
const [playerColor, setPlayerColor] = useState<Color>('Black');
const [useForbidden, setUseForbidden] = useState(true);
const handleStart = async () => {
const config: GameConfig = {
boardSize: 15,
useForbiddenRules: useForbidden,
useTimer: false,
timeLimitSecs: 60,
aiDifficulty: difficulty,
playerColor,
isServer: false,
};
await startGame('VsAi', config);
onStart();
};
return (
<div className="setup-panel">
<h2>{t('menu.ai_game')}</h2>
<label>
{t('settings.difficulty')}:
<select value={difficulty} onChange={(e) => setDifficulty(Number(e.target.value))}>
{[1, 2, 3, 4, 5].map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
</label>
<label>
:
<select value={playerColor} onChange={(e) => setPlayerColor(e.target.value as Color)}>
<option value="Black"> ()</option>
<option value="White"> ()</option>
</select>
</label>
<label>
<input type="checkbox" checked={useForbidden} onChange={(e) => setUseForbidden(e.target.checked)} />
{t('settings.forbidden_rules')}
</label>
<div className="setup-actions">
<button onClick={handleStart}>{t('game.new_game')}</button>
<button onClick={onBack}></button>
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import { useTranslation } from 'react-i18next';
import { useGameStore } from '../../store/gameStore';
import { useRef } from 'react';
import type { CellState, Move } from '../../core/types';
interface Props { onBack: () => void; onStart: () => void; }
export default function LoadReplay({ onBack, onStart }: Props) {
const { t } = useTranslation();
const loadReplayBoard = useGameStore((s) => s.loadReplayBoard);
const fileRef = useRef<HTMLInputElement>(null);
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const json = JSON.parse(reader.result as string);
const board: CellState[][] = Array.from({ length: json.board_size }, () =>
Array(json.board_size).fill(0)
);
const moves: Move[] = [];
for (const m of json.moves) {
board[m.x][m.y] = (m.color === 'Black' ? 1 : 2) as CellState;
moves.push({ position: { x: m.x, y: m.y }, color: m.color, turn: m.turn });
}
loadReplayBoard(board, moves);
onStart();
} catch {
alert('无效的棋谱文件');
}
};
reader.readAsText(file);
};
return (
<div className="setup-panel">
<h2>{t('menu.load_replay')}</h2>
<input ref={fileRef} type="file" accept=".json" onChange={handleFile} />
<button onClick={onBack} style={{ marginTop: 12 }}></button>
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { useTranslation } from 'react-i18next';
import { useGameStore } from '../../store/gameStore';
import type { GameConfig } from '../../core/types';
interface Props {
onBack: () => void;
onStart: () => void;
}
export default function LocalGameSetup({ onBack, onStart }: Props) {
const { t } = useTranslation();
const startGame = useGameStore((s) => s.startGame);
const handleStart = async () => {
const config: GameConfig = {
boardSize: 15,
useForbiddenRules: true,
useTimer: false,
timeLimitSecs: 60,
aiDifficulty: 3,
playerColor: 'Black',
isServer: false,
};
await startGame('Local', config);
onStart();
};
return (
<div className="setup-panel">
<h2>{t('menu.local_game')}</h2>
<div className="setup-actions">
<button onClick={handleStart}>{t('game.new_game')}</button>
<button onClick={onBack}></button>
</div>
</div>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import LocalGameSetup from './LocalGameSetup';
import AiGameSetup from './AiGameSetup';
import OnlineSetup from './OnlineSetup';
import LoadReplay from './LoadReplay';
type View = 'main' | 'local' | 'ai' | 'online' | 'replay';
interface Props {
onGameStart: () => void;
}
export default function MainMenu({ onGameStart }: Props) {
const { t } = useTranslation();
const [view, setView] = useState<View>('main');
if (view === 'local') return <LocalGameSetup onBack={() => setView('main')} onStart={onGameStart} />;
if (view === 'ai') return <AiGameSetup onBack={() => setView('main')} onStart={onGameStart} />;
if (view === 'online') return <OnlineSetup onBack={() => setView('main')} onStart={onGameStart} />;
if (view === 'replay') return <LoadReplay onBack={() => setView('main')} onStart={onGameStart} />;
return (
<div className="main-menu">
<h1 className="menu-title">{t('app.title')}</h1>
<div className="menu-buttons">
<button onClick={() => setView('local')}>{t('menu.local_game')}</button>
<button onClick={() => setView('ai')}>{t('menu.ai_game')}</button>
<button onClick={() => setView('online')}>{t('menu.online_game')}</button>
<button onClick={() => setView('replay')}>{t('menu.load_replay')}</button>
</div>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next';
import { useGameStore } from '../../store/gameStore';
import { useState } from 'react';
import type { GameConfig } from '../../core/types';
interface Props { onBack: () => void; onStart: () => void; }
export default function OnlineSetup({ onBack, onStart }: Props) {
const { t } = useTranslation();
const startGame = useGameStore((s) => s.startGame);
const [ip, setIp] = useState('');
const baseConfig: GameConfig = {
boardSize: 15, useForbiddenRules: true, useTimer: false,
timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', isServer: false,
};
const handleHost = async () => {
await startGame('Online', { ...baseConfig, isServer: true });
onStart();
};
const handleJoin = async () => {
await startGame('Online', { ...baseConfig });
onStart();
};
return (
<div className="setup-panel">
<h2>{t('menu.online_game')}</h2>
<button onClick={handleHost}></button>
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<input value={ip} onChange={(e) => setIp(e.target.value)} placeholder="IP:端口" />
<button onClick={handleJoin} disabled={!ip}></button>
</div>
<button onClick={onBack} style={{ marginTop: 12 }}></button>
</div>
);
}