diff --git a/server/models/SudokuRecord.ts b/server/models/SudokuRecord.ts index 93e3d11..d6aa084 100644 --- a/server/models/SudokuRecord.ts +++ b/server/models/SudokuRecord.ts @@ -7,6 +7,7 @@ export interface ISudokuRecord extends Document { username: string; fullname: string; difficulty: SudokuDifficulty; + gameMode: 'standard' | 'irregular'; timeSeconds: number; won: boolean; createdAt: Date; @@ -17,6 +18,7 @@ export interface ISudokuLeaderboardRecord { userId: mongoose.Types.ObjectId; username: string; fullname: string; + gameMode: 'standard' | 'irregular'; bestTime: number; totalGames: number; wonGames: number; @@ -43,6 +45,12 @@ const sudokuRecordSchema = new Schema({ enum: ['easy', 'medium', 'hard'], required: true }, + gameMode: { + type: String, + enum: ['standard', 'irregular'], + required: true, + default: 'standard' + }, timeSeconds: { type: Number, required: true, @@ -56,25 +64,41 @@ const sudokuRecordSchema = new Schema({ timestamps: true }); -sudokuRecordSchema.index({ userId: 1, difficulty: 1 }); -sudokuRecordSchema.index({ difficulty: 1, won: 1, timeSeconds: 1 }); +sudokuRecordSchema.index({ userId: 1, difficulty: 1, gameMode: 1 }); +sudokuRecordSchema.index({ difficulty: 1, won: 1, gameMode: 1, timeSeconds: 1 }); sudokuRecordSchema.statics.getLeaderboard = function( difficulty: SudokuDifficulty, + gameMode: 'standard' | 'irregular' | 'all', skipCount: number, pageSize: number ): mongoose.Aggregate { + const match: any = { + difficulty + }; + + if (gameMode === 'standard') { + match.$or = [ + { gameMode: 'standard' }, + { gameMode: { $exists: false } } + ]; + } else if (gameMode === 'irregular') { + match.gameMode = 'irregular'; + } + return this.aggregate([ { - $match: { - difficulty - } + $match: match }, { $group: { - _id: '$userId', + _id: { + userId: '$userId', + gameMode: { $ifNull: ['$gameMode', 'standard'] } + }, username: { $first: '$username' }, fullname: { $first: '$fullname' }, + gameMode: { $first: { $ifNull: ['$gameMode', 'standard'] } }, bestTime: { $min: { $cond: [{ $eq: ['$won', true] }, '$timeSeconds', null] @@ -92,7 +116,7 @@ sudokuRecordSchema.statics.getLeaderboard = function( }, { $addFields: { - userId: '$_id', + userId: '$_id.userId', winRate: { $multiply: [ { $cond: [{ $eq: ['$totalGames', 0] }, 0, { $divide: ['$wonGames', '$totalGames'] }] }, diff --git a/server/routes/sudoku.ts b/server/routes/sudoku.ts index 60b4553..0730e9c 100644 --- a/server/routes/sudoku.ts +++ b/server/routes/sudoku.ts @@ -51,34 +51,71 @@ router.post('/record', auth, async (req: Request, res: Response) => { router.get('/leaderboard/:difficulty', async (req: Request, res: Response) => { try { const { difficulty } = req.params; - const { page = '1', limit = '10' } = req.query; + const { page = '1', limit = '10', mode = 'all' } = req.query; const validDifficulties: SudokuDifficulty[] = ['easy', 'medium', 'hard']; + const validModes = ['all', 'standard', 'irregular']; + if (!validDifficulties.includes(difficulty as SudokuDifficulty)) { return res.status(400).json({ error: '无效的难度级别' }); } + if (!validModes.includes(mode as string)) { + return res.status(400).json({ error: '无效的游戏模式' }); + } + const pageNum = parseInt(page as string, 10); const pageSize = parseInt(limit as string, 10); const skipCount = (pageNum - 1) * pageSize; + const gameMode = mode as 'standard' | 'irregular' | 'all'; const records = await SudokuRecord.getLeaderboard( difficulty as SudokuDifficulty, + gameMode, skipCount, pageSize ); - const totalUsers = await SudokuRecord.distinct('userId', { + const totalMatch: any = { difficulty, won: true - }); + }; + + if (gameMode === 'standard') { + totalMatch.$or = [ + { gameMode: 'standard' }, + { gameMode: { $exists: false } } + ]; + } else if (gameMode === 'irregular') { + totalMatch.gameMode = 'irregular'; + } + + const totalCountResult = await SudokuRecord.aggregate([ + { + $match: totalMatch + }, + { + $group: { + _id: { + userId: '$userId', + gameMode: { $ifNull: ['$gameMode', 'standard'] } + } + } + }, + { + $count: 'count' + } + ]); + + const total = totalCountResult[0]?.count || 0; res.json({ records, - total: totalUsers.length, + total, currentPage: pageNum, - totalPages: Math.ceil(totalUsers.length / pageSize), - difficulty + totalPages: Math.ceil(total / pageSize), + difficulty, + mode: gameMode }); } catch (error) { console.error('获取数独排行榜失败:', error); diff --git a/src/components/SudokuGame.tsx b/src/components/SudokuGame.tsx index fb2ac12..d739e80 100644 --- a/src/components/SudokuGame.tsx +++ b/src/components/SudokuGame.tsx @@ -158,6 +158,7 @@ const SudokuGame: React.FC = () => { }, body: JSON.stringify({ difficulty, + gameMode, timeSeconds, won }) diff --git a/src/components/SudokuLeaderboard.tsx b/src/components/SudokuLeaderboard.tsx index 7449a3b..62b596e 100644 --- a/src/components/SudokuLeaderboard.tsx +++ b/src/components/SudokuLeaderboard.tsx @@ -19,11 +19,13 @@ import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import { API_BASE_URL } from '../config'; type Difficulty = 'easy' | 'medium' | 'hard'; +type GameMode = 'all' | 'standard' | 'irregular'; interface LeaderboardRecord { userId: string; username: string; fullname: string; + gameMode: 'standard' | 'irregular'; bestTime: number; totalGames: number; wonGames: number; @@ -47,6 +49,7 @@ const DIFFICULTY_LABELS: Record = { const SudokuLeaderboard: React.FC = () => { const [difficulty, setDifficulty] = useState('hard'); + const [gameModeFilter, setGameModeFilter] = useState('all'); const [records, setRecords] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); @@ -55,14 +58,14 @@ const SudokuLeaderboard: React.FC = () => { useEffect(() => { fetchLeaderboard(); - }, [difficulty, page]); + }, [difficulty, page, gameModeFilter]); const fetchLeaderboard = async () => { setLoading(true); setError(null); try { const response = await fetch( - `${API_BASE_URL}/api/sudoku/leaderboard/${difficulty}?page=${page}&limit=10` + `${API_BASE_URL}/api/sudoku/leaderboard/${difficulty}?page=${page}&limit=10&mode=${gameModeFilter}` ); if (!response.ok) { @@ -85,6 +88,11 @@ const SudokuLeaderboard: React.FC = () => { setPage(1); }; + const handleGameModeChange = (_event: React.SyntheticEvent, newValue: GameMode) => { + setGameModeFilter(newValue); + setPage(1); + }; + const handlePageChange = (_event: React.ChangeEvent, value: number) => { setPage(value); }; @@ -139,12 +147,27 @@ const SudokuLeaderboard: React.FC = () => { + + + + + + + + 排名 姓名 + 模式 最佳时间 总游戏次数 获胜次数 @@ -155,7 +178,7 @@ const SudokuLeaderboard: React.FC = () => { {records.length === 0 ? ( - + 暂无排行榜数据 @@ -167,7 +190,7 @@ const SudokuLeaderboard: React.FC = () => { const badge = page === 1 ? getRankBadge(rank) : null; return ( @@ -181,6 +204,13 @@ const SudokuLeaderboard: React.FC = () => { {record.fullname || record.username} + + +