724 lines
22 KiB
TypeScript
724 lines
22 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
||
import UndoIcon from '@mui/icons-material/Undo';
|
||
import RedoIcon from '@mui/icons-material/Redo';
|
||
import { API_BASE_URL } from '../config';
|
||
import {
|
||
GameBoard,
|
||
Difficulty,
|
||
GameMode,
|
||
RegionMap,
|
||
generatePuzzleForMode,
|
||
getRegionId,
|
||
getRegionNeighbors,
|
||
} from './sudokuEngine';
|
||
|
||
type HighlightType = 'none' | 'row' | 'col' | 'box' | 'sameNumber';
|
||
type SudokuRound = {
|
||
board: GameBoard;
|
||
regionMap: RegionMap;
|
||
};
|
||
|
||
const CELL_SIZE = 80;
|
||
const CELL_FONT_SIZE = 36;
|
||
|
||
const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound => {
|
||
const { puzzle, regionMap } = generatePuzzleForMode(difficulty, mode);
|
||
return {
|
||
board: puzzle,
|
||
regionMap,
|
||
};
|
||
};
|
||
|
||
const cloneGameBoard = (board: GameBoard): GameBoard => board.map(row => [...row]);
|
||
|
||
const SudokuGame: React.FC = () => {
|
||
// 游戏难度
|
||
const [difficulty, setDifficulty] = useState<Difficulty>('hard');
|
||
|
||
// 游戏模式:标准或不规则
|
||
const [gameMode, setGameMode] = useState<GameMode>('standard');
|
||
|
||
const [round, setRound] = useState<SudokuRound>(() => createSudokuRound('hard', 'standard'));
|
||
const { board, regionMap } = round;
|
||
const [undoStack, setUndoStack] = useState<GameBoard[]>([]);
|
||
const [redoStack, setRedoStack] = useState<GameBoard[]>([]);
|
||
|
||
const setBoard = (nextBoard: GameBoard) => {
|
||
setRound((currentRound) => ({
|
||
...currentRound,
|
||
board: nextBoard,
|
||
}));
|
||
};
|
||
|
||
const applyBoardChange = (nextBoard: GameBoard) => {
|
||
setUndoStack((previousStack) => [...previousStack, cloneGameBoard(board)]);
|
||
setRedoStack([]);
|
||
setBoard(nextBoard);
|
||
};
|
||
|
||
const [fixedCells, setFixedCells] = useState<boolean[][]>(() =>
|
||
board.map(row => row.map(cell => cell !== 0))
|
||
);
|
||
|
||
// 选中的格子 [row, col]
|
||
const [selectedCell, setSelectedCell] = useState<[number, number] | null>(null);
|
||
|
||
// 游戏完成状态
|
||
const [isGameComplete, setIsGameComplete] = useState(false);
|
||
|
||
// 完成对话框显示状态
|
||
const [showCompleteDialog, setShowCompleteDialog] = useState(false);
|
||
|
||
const [timeElapsed, setTimeElapsed] = useState(0);
|
||
const [isTimerRunning, setIsTimerRunning] = useState(true);
|
||
|
||
// 配置选项
|
||
const [config, setConfig] = useState({
|
||
highlightRegion: true, // 突出显示区域(行、列、宫)
|
||
highlightSameNumbers: true, // 突出显示相同数字
|
||
showRemainingCount: true, // 显示剩余数字数量
|
||
});
|
||
|
||
// 检查在位置 (row, col) 放置数字 num 是否有效
|
||
const isValid = (row: number, col: number, num: number): boolean => {
|
||
for (let c = 0; c < 9; c++) {
|
||
const cell = board[row][c];
|
||
if (c !== col && typeof cell === 'number' && cell === num) return false;
|
||
}
|
||
|
||
for (let r = 0; r < 9; r++) {
|
||
const cell = board[r][col];
|
||
if (r !== row && typeof cell === 'number' && cell === num) return false;
|
||
}
|
||
|
||
const targetRegion = getRegionId(regionMap, row, col);
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
const cell = board[r][c];
|
||
if ((r !== row || c !== col) && getRegionId(regionMap, r, c) === targetRegion && typeof cell === 'number' && cell === num) return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
// 检查数独是否完成
|
||
const isComplete = (): boolean => {
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
const cell = board[r][c];
|
||
if (typeof cell !== 'number') return false;
|
||
if (!isValid(r, c, cell)) return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const getCellValue = (cell: number): number | null => {
|
||
return cell === 0 ? null : cell;
|
||
};
|
||
|
||
const formatTime = (seconds: number): string => {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
const saveGameRecord = async (won: boolean, timeSeconds: number) => {
|
||
try {
|
||
const token = window.localStorage.getItem('token');
|
||
if (!token) {
|
||
console.log('未登录,不保存数独记录');
|
||
return;
|
||
}
|
||
|
||
const response = await fetch(`${API_BASE_URL}/api/sudoku/record`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({
|
||
difficulty,
|
||
timeSeconds,
|
||
won
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
console.log('数独游戏记录保存成功');
|
||
} else {
|
||
console.warn('保存数独记录失败', response.statusText);
|
||
}
|
||
} catch (error) {
|
||
console.error('保存数独记录失败:', error);
|
||
}
|
||
};
|
||
|
||
const handleNumberInput = (num: number) => {
|
||
if (!selectedCell || isGameComplete) return;
|
||
const [row, col] = selectedCell;
|
||
if (fixedCells[row][col]) return;
|
||
if (board[row][col] === num) return;
|
||
|
||
const newBoard = board.map(row => [...row]);
|
||
newBoard[row][col] = num;
|
||
applyBoardChange(newBoard);
|
||
|
||
// 检查是否完成
|
||
setTimeout(() => {
|
||
if (checkIsComplete(newBoard)) {
|
||
setIsGameComplete(true);
|
||
setIsTimerRunning(false);
|
||
setShowCompleteDialog(true);
|
||
saveGameRecord(true, timeElapsed);
|
||
}
|
||
}, 0);
|
||
};
|
||
|
||
const checkIsComplete = (boardToCheck: GameBoard): boolean => {
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
const cell = boardToCheck[r][c];
|
||
if (cell === 0) return false;
|
||
if (!isValidInBoard(r, c, cell, boardToCheck)) return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const isValidInBoard = (row: number, col: number, num: number, boardToCheck: GameBoard): boolean => {
|
||
for (let c = 0; c < 9; c++) {
|
||
if (c !== col && boardToCheck[row][c] === num) return false;
|
||
}
|
||
for (let r = 0; r < 9; r++) {
|
||
if (r !== row && boardToCheck[r][col] === num) return false;
|
||
}
|
||
// 使用 regionMap 检查不规则区域
|
||
const targetRegion = getRegionId(regionMap, row, col);
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
if ((r !== row || c !== col) && getRegionId(regionMap, r, c) === targetRegion && boardToCheck[r][c] === num) return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const handleClear = () => {
|
||
if (!selectedCell || isGameComplete) return;
|
||
const [row, col] = selectedCell;
|
||
if (fixedCells[row][col] || board[row][col] === 0) return;
|
||
|
||
const newBoard = board.map(row => [...row]);
|
||
newBoard[row][col] = 0;
|
||
applyBoardChange(newBoard);
|
||
};
|
||
|
||
const handleUndo = () => {
|
||
if (isGameComplete || undoStack.length === 0) return;
|
||
|
||
const previousBoard = undoStack[undoStack.length - 1];
|
||
setUndoStack(undoStack.slice(0, -1));
|
||
setRedoStack([cloneGameBoard(board), ...redoStack]);
|
||
setBoard(cloneGameBoard(previousBoard));
|
||
};
|
||
|
||
const handleRedo = () => {
|
||
if (isGameComplete || redoStack.length === 0) return;
|
||
|
||
const nextBoard = redoStack[0];
|
||
setRedoStack(redoStack.slice(1));
|
||
setUndoStack([...undoStack, cloneGameBoard(board)]);
|
||
setBoard(cloneGameBoard(nextBoard));
|
||
};
|
||
|
||
const canUndo = !isGameComplete && undoStack.length > 0;
|
||
const canRedo = !isGameComplete && redoStack.length > 0;
|
||
|
||
const getHistoryButtonStyle = (enabled: boolean): React.CSSProperties => ({
|
||
padding: '10px 16px',
|
||
fontSize: '16px',
|
||
cursor: enabled ? 'pointer' : 'not-allowed',
|
||
backgroundColor: enabled ? '#f0f0f0' : '#f7f7f7',
|
||
color: enabled ? '#333' : '#aaa',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
opacity: enabled ? 1 : 0.65,
|
||
});
|
||
|
||
const createNewGame = (level: Difficulty, mode: GameMode = gameMode) => {
|
||
const nextRound = createSudokuRound(level, mode);
|
||
setRound(nextRound);
|
||
setFixedCells(nextRound.board.map(row => row.map(cell => cell !== 0)));
|
||
setUndoStack([]);
|
||
setRedoStack([]);
|
||
setSelectedCell(null);
|
||
setIsGameComplete(false);
|
||
setShowCompleteDialog(false);
|
||
setTimeElapsed(0);
|
||
setIsTimerRunning(true);
|
||
};
|
||
|
||
const handleNewGame = () => {
|
||
createNewGame(difficulty);
|
||
};
|
||
|
||
const handleDifficultyChange = (level: Difficulty) => {
|
||
setDifficulty(level);
|
||
createNewGame(level);
|
||
};
|
||
|
||
const handleModeChange = (mode: GameMode) => {
|
||
setGameMode(mode);
|
||
createNewGame(difficulty, mode);
|
||
};
|
||
|
||
const handleCellClick = (row: number, col: number) => {
|
||
setSelectedCell([row, col]);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
const key = e.key.toLowerCase();
|
||
const isModifierKey = e.ctrlKey || e.metaKey;
|
||
|
||
if (isModifierKey && key === 'z') {
|
||
e.preventDefault();
|
||
if (e.shiftKey) {
|
||
handleRedo();
|
||
} else {
|
||
handleUndo();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (isModifierKey && key === 'y') {
|
||
e.preventDefault();
|
||
handleRedo();
|
||
return;
|
||
}
|
||
|
||
if (!selectedCell) return;
|
||
|
||
const [row, col] = selectedCell;
|
||
|
||
switch (e.key) {
|
||
case 'ArrowUp':
|
||
e.preventDefault();
|
||
setSelectedCell([Math.max(0, row - 1), col]);
|
||
break;
|
||
case 'ArrowDown':
|
||
e.preventDefault();
|
||
setSelectedCell([Math.min(8, row + 1), col]);
|
||
break;
|
||
case 'ArrowLeft':
|
||
e.preventDefault();
|
||
setSelectedCell([row, Math.max(0, col - 1)]);
|
||
break;
|
||
case 'ArrowRight':
|
||
e.preventDefault();
|
||
setSelectedCell([row, Math.min(8, col + 1)]);
|
||
break;
|
||
case 'Backspace':
|
||
case 'Delete':
|
||
case ' ':
|
||
e.preventDefault();
|
||
handleClear();
|
||
break;
|
||
case '1':
|
||
case '2':
|
||
case '3':
|
||
case '4':
|
||
case '5':
|
||
case '6':
|
||
case '7':
|
||
case '8':
|
||
case '9':
|
||
e.preventDefault();
|
||
handleNumberInput(Number(e.key));
|
||
break;
|
||
}
|
||
};
|
||
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, [selectedCell, board, undoStack, redoStack, isGameComplete, fixedCells, timeElapsed, regionMap, difficulty]);
|
||
|
||
useEffect(() => {
|
||
if (!isTimerRunning) return;
|
||
|
||
const intervalId = window.setInterval(() => {
|
||
setTimeElapsed((prev) => prev + 1);
|
||
}, 1000);
|
||
|
||
return () => window.clearInterval(intervalId);
|
||
}, [isTimerRunning]);
|
||
|
||
const getCellHighlightType = (row: number, col: number): HighlightType => {
|
||
if (!selectedCell) return 'none';
|
||
|
||
const [selRow, selCol] = selectedCell;
|
||
const cell = board[row][col];
|
||
const selectedValue = board[selRow][selCol];
|
||
|
||
if (config.highlightRegion) {
|
||
const inSameRow = row === selRow;
|
||
const inSameCol = col === selCol;
|
||
const inSameBox = getRegionId(regionMap, row, col) === getRegionId(regionMap, selRow, selCol);
|
||
|
||
if (inSameBox) return 'box';
|
||
if (inSameRow) return 'row';
|
||
if (inSameCol) return 'col';
|
||
}
|
||
|
||
if (config.highlightSameNumbers && selectedValue !== 0) {
|
||
if (cell === selectedValue) {
|
||
return 'sameNumber';
|
||
}
|
||
}
|
||
|
||
return 'none';
|
||
};
|
||
|
||
const highlightedCells = useMemo(() => {
|
||
const highlighted = new Set<string>();
|
||
if (!selectedCell) return highlighted;
|
||
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
if (getCellHighlightType(r, c) !== 'none') {
|
||
highlighted.add(`${r}-${c}`);
|
||
}
|
||
}
|
||
}
|
||
return highlighted;
|
||
}, [board, selectedCell, config, regionMap]);
|
||
|
||
// 计算剩余未填写的数字数量
|
||
const remainingCount = useMemo(() => {
|
||
let count = 0;
|
||
for (let r = 0; r < 9; r++) {
|
||
for (let c = 0; c < 9; c++) {
|
||
if (board[r][c] === 0) count++;
|
||
}
|
||
}
|
||
return count;
|
||
}, [board]);
|
||
|
||
// 获取单元格样式 - 支持不规则区域的边框显示
|
||
const getCellStyle = (row: number, col: number): React.CSSProperties => {
|
||
const cell = board[row][col];
|
||
const isFixed = fixedCells[row][col];
|
||
const highlightType = getCellHighlightType(row, col);
|
||
const isSelected = selectedCell && selectedCell[0] === row && selectedCell[1] === col;
|
||
const displayValue = getCellValue(cell);
|
||
|
||
// 基础背景色 - 根据高亮类型设置不同颜色
|
||
let backgroundColor = 'white';
|
||
if (isSelected) {
|
||
backgroundColor = '#69b1ff'; // 选中时的深蓝色
|
||
} else if (highlightType === 'box') {
|
||
backgroundColor = '#d9f7be'; // 宫 - 浅绿色
|
||
} else if (highlightType === 'row') {
|
||
backgroundColor = '#bae7ff'; // 行 - 浅蓝色
|
||
} else if (highlightType === 'col') {
|
||
backgroundColor = '#ffe58f'; // 列 - 浅黄色
|
||
} else if (highlightType === 'sameNumber') {
|
||
backgroundColor = '#ff4444'; // 相同数字 - 更醒目的红色
|
||
} else if (isFixed) {
|
||
backgroundColor = '#f0f0f0'; // 初始固定格子
|
||
}
|
||
|
||
const thickBorder = '4px solid #666';
|
||
const thinBorder = '2px solid #bbb';
|
||
|
||
// 使用 getRegionNeighbors 判断边框粗细,支持不规则区域
|
||
const neighbors = getRegionNeighbors(regionMap, row, col);
|
||
|
||
const borderRight = col < 8 ? (neighbors.right ? thickBorder : thinBorder) : thickBorder;
|
||
const borderBottom = row < 8 ? (neighbors.bottom ? thickBorder : thinBorder) : thickBorder;
|
||
const borderLeft = col > 0 ? (neighbors.left ? thickBorder : thinBorder) : thickBorder;
|
||
const borderTop = row > 0 ? (neighbors.top ? thickBorder : thinBorder) : thickBorder;
|
||
|
||
return {
|
||
width: `${CELL_SIZE}px`,
|
||
height: `${CELL_SIZE}px`,
|
||
backgroundColor,
|
||
borderRight,
|
||
borderBottom,
|
||
borderLeft,
|
||
borderTop,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: `${CELL_FONT_SIZE}px`,
|
||
cursor: 'pointer',
|
||
color: isFixed ? '#333' : '#1890ff',
|
||
position: 'relative' as const,
|
||
boxSizing: 'border-box',
|
||
};
|
||
};
|
||
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', padding: '20px' }}>
|
||
{/* 顶部控制和配置区 */}
|
||
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||
<div style={{ display: 'flex', gap: '10px' }}>
|
||
<button
|
||
onClick={handleNewGame}
|
||
style={{
|
||
padding: '10px 20px',
|
||
fontSize: '16px',
|
||
cursor: 'pointer',
|
||
backgroundColor: '#1890ff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
}}
|
||
>
|
||
新游戏
|
||
</button>
|
||
<button
|
||
onClick={handleClear}
|
||
style={{
|
||
padding: '10px 20px',
|
||
fontSize: '16px',
|
||
cursor: 'pointer',
|
||
backgroundColor: '#ff4d4f',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
}}
|
||
>
|
||
清除
|
||
</button>
|
||
<button
|
||
onClick={handleUndo}
|
||
disabled={!canUndo}
|
||
aria-label="撤销"
|
||
title="撤销 (Ctrl/Cmd+Z)"
|
||
style={getHistoryButtonStyle(canUndo)}
|
||
>
|
||
<UndoIcon fontSize="small" />
|
||
撤销
|
||
</button>
|
||
<button
|
||
onClick={handleRedo}
|
||
disabled={!canRedo}
|
||
aria-label="重做"
|
||
title="重做 (Ctrl/Cmd+Y)"
|
||
style={getHistoryButtonStyle(canRedo)}
|
||
>
|
||
<RedoIcon fontSize="small" />
|
||
重做
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '5px', alignItems: 'center' }}>
|
||
<span>难度:</span>
|
||
{(['easy', 'medium', 'hard'] as const).map((d) => (
|
||
<button
|
||
key={d}
|
||
onClick={() => handleDifficultyChange(d)}
|
||
style={{
|
||
padding: '5px 10px',
|
||
fontSize: '14px',
|
||
cursor: 'pointer',
|
||
backgroundColor: difficulty === d ? '#1890ff' : '#f0f0f0',
|
||
color: difficulty === d ? 'white' : '#333',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
}}
|
||
>
|
||
{d === 'easy' ? '简单' : d === 'medium' ? '中等' : '困难'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '5px', alignItems: 'center' }}>
|
||
<span>模式:</span>
|
||
{(['standard', 'irregular'] as const).map((m) => (
|
||
<button
|
||
key={m}
|
||
onClick={() => handleModeChange(m)}
|
||
style={{
|
||
padding: '5px 10px',
|
||
fontSize: '14px',
|
||
cursor: 'pointer',
|
||
backgroundColor: gameMode === m ? '#52c41a' : '#f0f0f0',
|
||
color: gameMode === m ? 'white' : '#333',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
}}
|
||
>
|
||
{m === 'standard' ? '标准' : '不规则'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 配置选项区 */}
|
||
<div style={{ display: 'flex', gap: '15px', flexWrap: 'wrap', justifyContent: 'center', padding: '10px', backgroundColor: '#fafafa', borderRadius: '8px' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={config.highlightRegion}
|
||
onChange={(e) => setConfig({ ...config, highlightRegion: e.target.checked })}
|
||
/>
|
||
突出显示区域
|
||
</label>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={config.highlightSameNumbers}
|
||
onChange={(e) => setConfig({ ...config, highlightSameNumbers: e.target.checked })}
|
||
/>
|
||
突出显示相同数字
|
||
</label>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={config.showRemainingCount}
|
||
onChange={(e) => setConfig({ ...config, showRemainingCount: e.target.checked })}
|
||
/>
|
||
显示剩余数量
|
||
</label>
|
||
</div>
|
||
|
||
{/* 数独棋盘 */}
|
||
<div style={{ width: '100%', overflowX: 'auto', display: 'flex', justifyContent: 'center', paddingBottom: '4px' }}>
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: `repeat(9, ${CELL_SIZE}px)`,
|
||
gridTemplateRows: `repeat(9, ${CELL_SIZE}px)`,
|
||
border: '4px solid #333',
|
||
backgroundColor: '#fff',
|
||
flex: '0 0 auto',
|
||
}}
|
||
>
|
||
{board.map((row, rowIndex) =>
|
||
row.map((cell, colIndex) => {
|
||
const displayValue = getCellValue(cell);
|
||
const cellStyle = getCellStyle(rowIndex, colIndex);
|
||
|
||
return (
|
||
<div
|
||
key={`${rowIndex}-${colIndex}`}
|
||
onClick={() => handleCellClick(rowIndex, colIndex)}
|
||
style={cellStyle}
|
||
>
|
||
{displayValue !== null ? displayValue : ''}
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '30px', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||
<div style={{ padding: '15px 25px', backgroundColor: '#f5f5f5', borderRadius: '8px', textAlign: 'center' }}>
|
||
<span style={{ fontSize: '14px', color: '#666', display: 'block', marginBottom: '8px' }}>
|
||
{selectedCell ? '按键盘 1-9 输入数字' : '选择一个格子后输入数字'}
|
||
</span>
|
||
<span style={{ fontSize: '16px', color: '#666', display: 'block', marginBottom: '8px' }}>
|
||
用时:<strong style={{ color: '#1890ff', fontSize: '20px' }}>{formatTime(timeElapsed)}</strong>
|
||
</span>
|
||
{config.showRemainingCount && (
|
||
<span style={{ fontSize: '16px', color: '#666' }}>
|
||
剩余:<strong style={{ color: '#1890ff', fontSize: '20px' }}>{remainingCount}</strong> 格
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 快捷键提示 */}
|
||
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f9f9f9', borderRadius: '4px', fontSize: '12px', color: '#666' }}>
|
||
<strong>快捷键:</strong> 1-9 输入数字 | 空格/Backspace 清除 | Ctrl/Cmd+Z 撤销 | Ctrl/Cmd+Y 重做 | ↑↓←→ 移动选择
|
||
</div>
|
||
|
||
{/* 完成成功对话框 */}
|
||
{showCompleteDialog && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1000,
|
||
}}>
|
||
<div style={{
|
||
backgroundColor: 'white',
|
||
borderRadius: '8px',
|
||
padding: '40px',
|
||
textAlign: 'center',
|
||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||
maxWidth: '400px',
|
||
}}>
|
||
<div style={{
|
||
fontSize: '48px',
|
||
marginBottom: '20px',
|
||
}}>
|
||
🎉
|
||
</div>
|
||
<h2 style={{
|
||
margin: '0 0 20px 0',
|
||
color: '#1890ff',
|
||
fontSize: '24px',
|
||
}}>
|
||
恭喜!完成成功
|
||
</h2>
|
||
<p style={{
|
||
margin: '0 0 30px 0',
|
||
color: '#666',
|
||
fontSize: '16px',
|
||
lineHeight: '1.6',
|
||
}}>
|
||
你已经正确完成了这个数独谜题!
|
||
所有格子已锁定,无法再修改。
|
||
</p>
|
||
<button
|
||
onClick={() => setShowCompleteDialog(false)}
|
||
style={{
|
||
padding: '12px 32px',
|
||
fontSize: '16px',
|
||
cursor: 'pointer',
|
||
backgroundColor: '#1890ff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
marginRight: '10px',
|
||
}}
|
||
>
|
||
关闭
|
||
</button>
|
||
<button
|
||
onClick={handleNewGame}
|
||
style={{
|
||
padding: '12px 32px',
|
||
fontSize: '16px',
|
||
cursor: 'pointer',
|
||
backgroundColor: '#52c41a',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
}}
|
||
>
|
||
新游戏
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SudokuGame;
|