import React, { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { api } from '../services/api'; import { Student, GameSession, GameTeam, AchievementConfig } from '../types'; import { Loader2, User, Users, Play, Pause, Gift, CheckCircle, XCircle, Award, Volume2, Settings, Maximize, Minimize, UserX, RotateCcw, Repeat } from 'lucide-react'; import { Emoji } from '../components/Emoji'; export const GameRandom: React.FC<{className?: string}> = ({ className }) => { const [loading, setLoading] = useState(true); const [students, setStudents] = useState([]); const [teams, setTeams] = useState([]); const [achConfig, setAchConfig] = useState(null); // Game State const [mode, setMode] = useState<'STUDENT' | 'TEAM'>('STUDENT'); const [scopeTeamId, setScopeTeamId] = useState('ALL'); // 'ALL' or specific team ID const [isRunning, setIsRunning] = useState(false); const [highlightIndex, setHighlightIndex] = useState(null); const [selectedResult, setSelectedResult] = useState(null); const [showResultModal, setShowResultModal] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); // Avoidance / No Repeat State const [avoidRepeat, setAvoidRepeat] = useState(false); const [pickedIds, setPickedIds] = useState>(new Set()); // Filter State const [excludedStudentIds, setExcludedStudentIds] = useState>(new Set()); const [isFilterOpen, setIsFilterOpen] = useState(false); // Reward State const [rewardType, setRewardType] = useState<'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT'>('DRAW_COUNT'); const [rewardId, setRewardId] = useState(''); // Achievement ID or Item Name const [rewardCount, setRewardCount] = useState(1); const timerRef = useRef(null); const speedRef = useRef(50); const currentUser = api.auth.getCurrentUser(); const homeroomClass = className || currentUser?.homeroomClass; useEffect(() => { loadData(); return () => stopAnimation(); }, [homeroomClass]); // Clear picked IDs when mode changes to avoid ID confusion useEffect(() => { setPickedIds(new Set()); }, [mode, scopeTeamId]); const loadData = async () => { if (!homeroomClass) { setLoading(false); return; } setLoading(true); // 1. Load Students (Critical) try { const allStus = await api.students.getAll(); const classStudents = allStus.filter((s: Student) => s.className === homeroomClass); // Sort by Seat No classStudents.sort((a: Student, b: Student) => { const seatA = parseInt(a.seatNo || '99999'); const seatB = parseInt(b.seatNo || '99999'); if (seatA !== seatB) return seatA - seatB; return a.name.localeCompare(b.name, 'zh-CN'); }); setStudents(classStudents); // 2. Load Teams (Optional - Fallback to auto-gen if missing) try { const session = await api.games.getMountainSession(homeroomClass); if (session && session.teams && session.teams.length > 0) { setTeams(session.teams); } else { // Generate temporary teams if none exist generateTempTeams(classStudents); } } catch (e) { console.warn('Failed to load teams, generating defaults', e); generateTempTeams(classStudents); } // 3. Load Achievements (Optional) try { const ac = await api.achievements.getConfig(homeroomClass); setAchConfig(ac); } catch (e) { console.warn('Failed to load achievements', e); } } catch (e) { console.error('Failed to load student list', e); } finally { setLoading(false); } }; const generateTempTeams = (stus: Student[]) => { // Simple logic: 6 students per team const teamSize = 6; const teamCount = Math.ceil(stus.length / teamSize); const tempTeams: GameTeam[] = []; const avatars = ['🐯','🦁','🐺','🐻','🐨','🐼','🐸','🐙']; const colors = ['#ef4444', '#f97316', '#eab308', '#84cc16', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899']; for (let i = 0; i < teamCount; i++) { const members = stus.slice(i * teamSize, (i + 1) * teamSize).map(s => s._id || String(s.id)); tempTeams.push({ id: `temp-${i}`, name: `${i+1}组`, score: 0, avatar: avatars[i % avatars.length], color: colors[i % colors.length], members }); } setTeams(tempTeams); }; const getTargetList = () => { let baseList: any[] = []; if (mode === 'TEAM') { baseList = teams; } else { baseList = students; if (scopeTeamId !== 'ALL') { const team = teams.find(t => t.id === scopeTeamId); if (team) { baseList = students.filter(s => team.members.includes(s._id || String(s.id))); } } // Filter out excluded (absent) students baseList = baseList.filter(s => !excludedStudentIds.has(s._id || String(s.id))); } // Filter out previously picked if mode is on if (avoidRepeat) { baseList = baseList.filter(item => !pickedIds.has(mode === 'TEAM' ? item.id : (item._id || String(item.id)))); } return baseList; }; const startRandom = () => { const list = getTargetList(); if (list.length === 0) { if (avoidRepeat && pickedIds.size > 0) { if (confirm('本轮所有候选对象已全部点完!是否重置记录并重新开始?')) { setPickedIds(new Set()); } return; } return alert('当前范围内没有可选对象'); } setIsRunning(true); setShowResultModal(false); setSelectedResult(null); speedRef.current = 50; const run = () => { setHighlightIndex(prev => { let next = Math.floor(Math.random() * list.length); // 视觉优化:避免连续两次选中同一个学生,增加跳跃感 if (list.length > 1 && prev !== null) { while (next === prev) { next = Math.floor(Math.random() * list.length); } } return next; }); // Slow down logic speedRef.current = Math.min(speedRef.current * 1.1, 600); if (speedRef.current < 500) { timerRef.current = setTimeout(run, speedRef.current); } else { // Stop stopAnimation(); setTimeout(() => finalizeSelection(list), 500); } }; run(); }; const finalizeSelection = (list: any[]) => { setHighlightIndex(prev => { if (prev === null) return 0; const result = list[prev]; setSelectedResult(result); setShowResultModal(true); // Add to picked history if (avoidRepeat) { const id = mode === 'TEAM' ? result.id : (result._id || String(result.id)); setPickedIds(prev => new Set(prev).add(id)); } return prev; }); }; const stopAnimation = () => { if (timerRef.current) clearTimeout(timerRef.current); setIsRunning(false); }; const handleGrantReward = async (shouldReward: boolean) => { if (!selectedResult || !shouldReward) { setShowResultModal(false); return; } let targets: Student[] = []; if (mode === 'STUDENT') { targets = [selectedResult as Student]; } else { // Team mode: find all students in team const team = selectedResult as GameTeam; // IMPORTANT: Exclude absent students from team reward targets = students.filter(s => team.members.includes(s._id || String(s.id)) && !excludedStudentIds.has(s._id || String(s.id))); } if (targets.length === 0) { alert('没有符合条件的学生可发放奖励'); setShowResultModal(false); return; } const rewardName = rewardType === 'ACHIEVEMENT' ? achConfig?.achievements.find(a => a.id === rewardId)?.name : (rewardId || (rewardType === 'DRAW_COUNT' ? '抽奖券' : '奖励')); try { const promises = targets.map(s => { if (rewardType === 'ACHIEVEMENT' && rewardId) { return api.achievements.grant({ studentId: s._id || String(s.id), achievementId: rewardId, semester: '当前学期' // Ideally fetch from config }); } else { return api.games.grantReward({ studentId: s._id || String(s.id), count: rewardCount, rewardType, name: rewardName }); } }); await Promise.all(promises); } catch (e) { console.error(e); alert('发放失败'); } setShowResultModal(false); }; if (loading) return
; const targetList = getTargetList(); const GameContent = (
{/* Floating Fullscreen Toggle Button */} {/* Header Config */}
{mode === 'STUDENT' && ( )} {/* Avoid Repeat Logic */}
setAvoidRepeat(e.target.checked)} className="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500 cursor-pointer"/>
{avoidRepeat && pickedIds.size > 0 && ( <>
已点{pickedIds.size} )}
{rewardType === 'ACHIEVEMENT' ? ( ) : rewardType === 'ITEM' ? ( setRewardId(e.target.value)}/> ) : null} x setRewardCount(Number(e.target.value))}/>
{/* Grid Area */}
{targetList.length === 0 ? (

暂无数据

该班级下没有找到{mode === 'STUDENT' ? '学生' : '小组'}数据

) : (
{targetList.map((item, idx) => { const isHighlighted = idx === highlightIndex; const isTeam = mode === 'TEAM'; // @ts-ignore const name = isTeam ? item.name : item.name; // @ts-ignore const avatar = isTeam ? (item.avatar || '🚩') : (item.seatNo || 'User'); // @ts-ignore const subText = isTeam ? `${item.members.length}人` : item.studentNo; // @ts-ignore const color = isTeam ? (item.color || '#3b82f6') : '#3b82f6'; return (
{isHighlighted &&
}
{mode === 'STUDENT' ? (
{name[0]}
) : }
{name}
{subText}
); })}
)}
{/* Start Button */}
{/* Student Filter Modal */} {isFilterOpen && (

排除请假/缺勤学生

{students.length === 0 ? (
暂无学生数据
) : (
{students.map(s => { const isExcluded = excludedStudentIds.has(s._id || String(s.id)); return (
{ const newSet = new Set(excludedStudentIds); if (isExcluded) newSet.delete(s._id || String(s.id)); else newSet.add(s._id || String(s.id)); setExcludedStudentIds(newSet); }} className={`p-2 rounded border cursor-pointer text-center text-sm transition-all select-none ${isExcluded ? 'bg-red-50 border-red-300 text-red-500 opacity-60' : 'bg-white border-gray-200 hover:border-blue-300'}`} >
{s.name}
{s.seatNo}
{isExcluded &&
已排除
}
) })}
)}
)} {/* Result Modal */} {showResultModal && selectedResult && (

选中对象

{/* @ts-ignore */} {selectedResult.name}
{/* @ts-ignore */}
{mode==='STUDENT' ? `座号: ${selectedResult.seatNo || '-'}` : `${selectedResult.members.length} 位成员`}
)}
); if (isFullscreen) { return createPortal(GameContent, document.body); } return GameContent; };