From 8af733a27441f22d8ef0c0e9e5d4d3d0e7ca1127 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Fri, 15 May 2026 22:00:36 +0800 Subject: [PATCH] add undo/redo feather --- src/components/SudokuGame.tsx | 164 +++++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 33 deletions(-) diff --git a/src/components/SudokuGame.tsx b/src/components/SudokuGame.tsx index fcd839b..b0f118a 100644 --- a/src/components/SudokuGame.tsx +++ b/src/components/SudokuGame.tsx @@ -1,4 +1,6 @@ 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, @@ -16,6 +18,9 @@ type SudokuRound = { 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 { @@ -24,6 +29,8 @@ const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound }; }; +const cloneGameBoard = (board: GameBoard): GameBoard => board.map(row => [...row]); + const SudokuGame: React.FC = () => { // 游戏难度 const [difficulty, setDifficulty] = useState('hard'); @@ -33,6 +40,8 @@ const SudokuGame: React.FC = () => { const [round, setRound] = useState(() => createSudokuRound('hard', 'standard')); const { board, regionMap } = round; + const [undoStack, setUndoStack] = useState([]); + const [redoStack, setRedoStack] = useState([]); const setBoard = (nextBoard: GameBoard) => { setRound((currentRound) => ({ @@ -41,6 +50,12 @@ const SudokuGame: React.FC = () => { })); }; + const applyBoardChange = (nextBoard: GameBoard) => { + setUndoStack((previousStack) => [...previousStack, cloneGameBoard(board)]); + setRedoStack([]); + setBoard(nextBoard); + }; + const [fixedCells, setFixedCells] = useState(() => board.map(row => row.map(cell => cell !== 0)) ); @@ -144,10 +159,11 @@ const SudokuGame: React.FC = () => { 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; - setBoard(newBoard); + applyBoardChange(newBoard); // 检查是否完成 setTimeout(() => { @@ -191,15 +207,54 @@ const SudokuGame: React.FC = () => { 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; - setBoard(newBoard); + 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); @@ -227,6 +282,25 @@ const SudokuGame: React.FC = () => { 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; @@ -271,7 +345,7 @@ const SudokuGame: React.FC = () => { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedCell, board]); + }, [selectedCell, board, undoStack, redoStack, isGameComplete, fixedCells, timeElapsed, regionMap, difficulty]); useEffect(() => { if (!isTimerRunning) return; @@ -358,8 +432,8 @@ const SudokuGame: React.FC = () => { backgroundColor = '#f0f0f0'; // 初始固定格子 } - const thickBorder = '2px solid #666'; - const thinBorder = '1px solid #bbb'; + const thickBorder = '4px solid #666'; + const thinBorder = '2px solid #bbb'; // 使用 getRegionNeighbors 判断边框粗细,支持不规则区域 const neighbors = getRegionNeighbors(regionMap, row, col); @@ -370,8 +444,8 @@ const SudokuGame: React.FC = () => { const borderTop = row > 0 ? (neighbors.top ? thickBorder : thinBorder) : thickBorder; return { - width: '40px', - height: '40px', + width: `${CELL_SIZE}px`, + height: `${CELL_SIZE}px`, backgroundColor, borderRight, borderBottom, @@ -380,10 +454,11 @@ const SudokuGame: React.FC = () => { display: 'flex', alignItems: 'center', justifyContent: 'center', - fontSize: '18px', + fontSize: `${CELL_FONT_SIZE}px`, cursor: 'pointer', color: isFixed ? '#333' : '#1890ff', position: 'relative' as const, + boxSizing: 'border-box', }; }; @@ -420,6 +495,26 @@ const SudokuGame: React.FC = () => { > 清除 + +
@@ -494,31 +589,34 @@ const SudokuGame: React.FC = () => {
{/* 数独棋盘 */} -
- {board.map((row, rowIndex) => - row.map((cell, colIndex) => { - const displayValue = getCellValue(cell); - const cellStyle = getCellStyle(rowIndex, colIndex); +
+
+ {board.map((row, rowIndex) => + row.map((cell, colIndex) => { + const displayValue = getCellValue(cell); + const cellStyle = getCellStyle(rowIndex, colIndex); - return ( -
handleCellClick(rowIndex, colIndex)} - style={cellStyle} - > - {displayValue !== null ? displayValue : ''} -
- ); - }) - )} + return ( +
handleCellClick(rowIndex, colIndex)} + style={cellStyle} + > + {displayValue !== null ? displayValue : ''} +
+ ); + }) + )} +
@@ -539,7 +637,7 @@ const SudokuGame: React.FC = () => { {/* 快捷键提示 */}
- 快捷键: 1-9 输入数字 | 空格/Backspace 清除 | ↑↓←→ 移动选择 + 快捷键: 1-9 输入数字 | 空格/Backspace 清除 | Ctrl/Cmd+Z 撤销 | Ctrl/Cmd+Y 重做 | ↑↓←→ 移动选择
{/* 完成成功对话框 */}