Spaces:
Sleeping
Sleeping
Upload 24 files
Browse files- pages/GameMonster.tsx +7 -3
- pages/GameRandom.tsx +51 -40
pages/GameMonster.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
import React, { useState, useEffect, useRef } from 'react';
|
| 3 |
import { api } from '../services/api';
|
| 4 |
import { StudentReward } from '../types';
|
| 5 |
-
import { Play, Pause, Settings, Volume2, Mic, RotateCcw, Award, Keyboard, AlertTriangle } from 'lucide-react';
|
| 6 |
|
| 7 |
export const GameMonster: React.FC = () => {
|
| 8 |
// Game State
|
|
@@ -12,6 +12,7 @@ export const GameMonster: React.FC = () => {
|
|
| 12 |
const [monsterPos, setMonsterPos] = useState(100); // 0 (Left/Loss) to 100 (Right/Start)
|
| 13 |
const [currentVolume, setCurrentVolume] = useState(0);
|
| 14 |
const [attackCount, setAttackCount] = useState(0);
|
|
|
|
| 15 |
|
| 16 |
// Config State
|
| 17 |
const [duration, setDuration] = useState(300);
|
|
@@ -181,7 +182,7 @@ export const GameMonster: React.FC = () => {
|
|
| 181 |
};
|
| 182 |
|
| 183 |
return (
|
| 184 |
-
<div className=
|
| 185 |
{/* Background */}
|
| 186 |
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?q=80&w=2544&auto=format&fit=crop')] bg-cover bg-center opacity-30"></div>
|
| 187 |
|
|
@@ -205,6 +206,9 @@ export const GameMonster: React.FC = () => {
|
|
| 205 |
</div>
|
| 206 |
|
| 207 |
<div className="flex gap-2">
|
|
|
|
|
|
|
|
|
|
| 208 |
<button onClick={() => setIsConfigOpen(true)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors"><Settings size={20}/></button>
|
| 209 |
<button onClick={() => setIsPlaying(!isPlaying)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors">
|
| 210 |
{isPlaying ? <Pause size={20}/> : <Play size={20}/>}
|
|
@@ -303,7 +307,7 @@ export const GameMonster: React.FC = () => {
|
|
| 303 |
|
| 304 |
{/* Result Modal */}
|
| 305 |
{gameState !== 'IDLE' && gameState !== 'PLAYING' && (
|
| 306 |
-
<div className="absolute inset-0 bg-black/80 z-
|
| 307 |
<div className="text-center">
|
| 308 |
<div className="text-9xl mb-4 animate-bounce">
|
| 309 |
{gameState === 'VICTORY' ? '🏆' : '💀'}
|
|
|
|
| 2 |
import React, { useState, useEffect, useRef } from 'react';
|
| 3 |
import { api } from '../services/api';
|
| 4 |
import { StudentReward } from '../types';
|
| 5 |
+
import { Play, Pause, Settings, Volume2, Mic, RotateCcw, Award, Keyboard, AlertTriangle, Maximize, Minimize } from 'lucide-react';
|
| 6 |
|
| 7 |
export const GameMonster: React.FC = () => {
|
| 8 |
// Game State
|
|
|
|
| 12 |
const [monsterPos, setMonsterPos] = useState(100); // 0 (Left/Loss) to 100 (Right/Start)
|
| 13 |
const [currentVolume, setCurrentVolume] = useState(0);
|
| 14 |
const [attackCount, setAttackCount] = useState(0);
|
| 15 |
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 16 |
|
| 17 |
// Config State
|
| 18 |
const [duration, setDuration] = useState(300);
|
|
|
|
| 182 |
};
|
| 183 |
|
| 184 |
return (
|
| 185 |
+
<div className={`${isFullscreen ? 'fixed inset-0 z-[100] h-screen w-screen' : 'h-full'} flex flex-col bg-slate-900 relative overflow-hidden select-none transition-all duration-300`} onClick={handleManualInput}>
|
| 186 |
{/* Background */}
|
| 187 |
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?q=80&w=2544&auto=format&fit=crop')] bg-cover bg-center opacity-30"></div>
|
| 188 |
|
|
|
|
| 206 |
</div>
|
| 207 |
|
| 208 |
<div className="flex gap-2">
|
| 209 |
+
<button onClick={() => setIsFullscreen(!isFullscreen)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors" title={isFullscreen?"退出全屏":"全屏显示"}>
|
| 210 |
+
{isFullscreen ? <Minimize size={20}/> : <Maximize size={20}/>}
|
| 211 |
+
</button>
|
| 212 |
<button onClick={() => setIsConfigOpen(true)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors"><Settings size={20}/></button>
|
| 213 |
<button onClick={() => setIsPlaying(!isPlaying)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-md transition-colors">
|
| 214 |
{isPlaying ? <Pause size={20}/> : <Play size={20}/>}
|
|
|
|
| 307 |
|
| 308 |
{/* Result Modal */}
|
| 309 |
{gameState !== 'IDLE' && gameState !== 'PLAYING' && (
|
| 310 |
+
<div className="absolute inset-0 bg-black/80 z-[110] flex items-center justify-center p-4 backdrop-blur-md animate-in zoom-in">
|
| 311 |
<div className="text-center">
|
| 312 |
<div className="text-9xl mb-4 animate-bounce">
|
| 313 |
{gameState === 'VICTORY' ? '🏆' : '💀'}
|
pages/GameRandom.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
import React, { useState, useEffect, useRef } from 'react';
|
| 3 |
import { api } from '../services/api';
|
| 4 |
import { Student, GameSession, GameTeam, AchievementConfig } from '../types';
|
| 5 |
-
import { Loader2, User, Users, Play, Pause, Gift, CheckCircle, XCircle, Award, Volume2, Settings } from 'lucide-react';
|
| 6 |
|
| 7 |
export const GameRandom: React.FC = () => {
|
| 8 |
const [loading, setLoading] = useState(true);
|
|
@@ -17,6 +17,7 @@ export const GameRandom: React.FC = () => {
|
|
| 17 |
const [highlightIndex, setHighlightIndex] = useState<number | null>(null);
|
| 18 |
const [selectedResult, setSelectedResult] = useState<Student | GameTeam | null>(null);
|
| 19 |
const [showResultModal, setShowResultModal] = useState(false);
|
|
|
|
| 20 |
|
| 21 |
// Reward State
|
| 22 |
const [rewardType, setRewardType] = useState<'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT'>('DRAW_COUNT');
|
|
@@ -162,49 +163,59 @@ export const GameRandom: React.FC = () => {
|
|
| 162 |
const targetList = getTargetList();
|
| 163 |
|
| 164 |
return (
|
| 165 |
-
<div className=
|
| 166 |
{/* Header Config */}
|
| 167 |
<div className="bg-white p-4 shadow-sm border-b border-gray-200 z-10 flex flex-wrap gap-4 items-center justify-between shrink-0">
|
| 168 |
-
<div className="flex
|
| 169 |
-
<
|
| 170 |
-
<
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
<
|
| 174 |
-
|
| 175 |
-
|
|
|
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
</select>
|
| 192 |
-
{rewardType === 'ACHIEVEMENT' ? (
|
| 193 |
-
<select className="text-sm border-gray-300 rounded w-32 bg-white" value={rewardId} onChange={e=>setRewardId(e.target.value)}>
|
| 194 |
-
<option value="">选择成就</option>
|
| 195 |
-
{achConfig?.achievements.map(a => <option key={a.id} value={a.id}>{a.icon} {a.name}</option>)}
|
| 196 |
</select>
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
</div>
|
| 204 |
|
| 205 |
{/* Grid Area */}
|
| 206 |
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
| 207 |
-
<div className={`grid gap-4 transition-all ${mode==='STUDENT' ? 'grid-cols-4 sm:grid-cols-6 md:grid-cols-8' : 'grid-cols-2 md:grid-cols-3'}`}>
|
| 208 |
{targetList.map((item, idx) => {
|
| 209 |
const isHighlighted = idx === highlightIndex;
|
| 210 |
const isTeam = mode === 'TEAM';
|
|
@@ -224,14 +235,14 @@ export const GameRandom: React.FC = () => {
|
|
| 224 |
: 'bg-white border-gray-100 shadow-sm opacity-80'
|
| 225 |
}`}>
|
| 226 |
{isHighlighted && <div className="absolute inset-0 bg-yellow-400 opacity-20 animate-pulse rounded-xl"></div>}
|
| 227 |
-
<div className=
|
| 228 |
{mode === 'STUDENT' ? (
|
| 229 |
-
<div className=
|
| 230 |
{name[0]}
|
| 231 |
</div>
|
| 232 |
) : avatar}
|
| 233 |
</div>
|
| 234 |
-
<div className=
|
| 235 |
<div className="text-xs text-gray-400">{subText}</div>
|
| 236 |
</div>
|
| 237 |
);
|
|
@@ -251,14 +262,14 @@ export const GameRandom: React.FC = () => {
|
|
| 251 |
|
| 252 |
{/* Result Modal */}
|
| 253 |
{showResultModal && selectedResult && (
|
| 254 |
-
<div className="fixed inset-0 bg-black/60 z-
|
| 255 |
<div className="bg-white rounded-3xl p-8 w-full max-w-md text-center shadow-2xl relative animate-in zoom-in-95">
|
| 256 |
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-24 h-24 bg-yellow-400 rounded-full flex items-center justify-center border-4 border-white shadow-lg">
|
| 257 |
<span className="text-5xl">✨</span>
|
| 258 |
</div>
|
| 259 |
|
| 260 |
-
<h2 className="mt-10 text-gray-500 text-sm font-bold uppercase tracking-widest"
|
| 261 |
-
<div className="text-
|
| 262 |
{/* @ts-ignore */}
|
| 263 |
{selectedResult.name}
|
| 264 |
</div>
|
|
|
|
| 2 |
import React, { useState, useEffect, useRef } from 'react';
|
| 3 |
import { api } from '../services/api';
|
| 4 |
import { Student, GameSession, GameTeam, AchievementConfig } from '../types';
|
| 5 |
+
import { Loader2, User, Users, Play, Pause, Gift, CheckCircle, XCircle, Award, Volume2, Settings, Maximize, Minimize } from 'lucide-react';
|
| 6 |
|
| 7 |
export const GameRandom: React.FC = () => {
|
| 8 |
const [loading, setLoading] = useState(true);
|
|
|
|
| 17 |
const [highlightIndex, setHighlightIndex] = useState<number | null>(null);
|
| 18 |
const [selectedResult, setSelectedResult] = useState<Student | GameTeam | null>(null);
|
| 19 |
const [showResultModal, setShowResultModal] = useState(false);
|
| 20 |
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 21 |
|
| 22 |
// Reward State
|
| 23 |
const [rewardType, setRewardType] = useState<'DRAW_COUNT' | 'ITEM' | 'ACHIEVEMENT'>('DRAW_COUNT');
|
|
|
|
| 163 |
const targetList = getTargetList();
|
| 164 |
|
| 165 |
return (
|
| 166 |
+
<div className={`${isFullscreen ? 'fixed inset-0 z-[100] h-screen w-screen' : 'h-full'} flex flex-col bg-slate-50 relative overflow-hidden transition-all duration-300`}>
|
| 167 |
{/* Header Config */}
|
| 168 |
<div className="bg-white p-4 shadow-sm border-b border-gray-200 z-10 flex flex-wrap gap-4 items-center justify-between shrink-0">
|
| 169 |
+
<div className="flex flex-wrap gap-4 items-center">
|
| 170 |
+
<div className="flex gap-2 bg-gray-100 p-1 rounded-lg">
|
| 171 |
+
<button onClick={()=>{setMode('STUDENT'); setHighlightIndex(null);}} className={`px-4 py-2 rounded-md text-sm font-bold transition-all ${mode==='STUDENT'?'bg-white shadow text-blue-600':'text-gray-500'}`}>
|
| 172 |
+
<User size={16} className="inline mr-1"/> 点名学生
|
| 173 |
+
</button>
|
| 174 |
+
<button onClick={()=>{setMode('TEAM'); setHighlightIndex(null);}} className={`px-4 py-2 rounded-md text-sm font-bold transition-all ${mode==='TEAM'?'bg-white shadow text-purple-600':'text-gray-500'}`}>
|
| 175 |
+
<Users size={16} className="inline mr-1"/> 随机小组
|
| 176 |
+
</button>
|
| 177 |
+
</div>
|
| 178 |
|
| 179 |
+
{mode === 'STUDENT' && (
|
| 180 |
+
<select className="border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white" value={scopeTeamId} onChange={e=>setScopeTeamId(e.target.value)}>
|
| 181 |
+
<option value="ALL">全班范围</option>
|
| 182 |
+
{teams.map(t => <option key={t.id} value={t.id}>{t.name} (组)</option>)}
|
| 183 |
+
</select>
|
| 184 |
+
)}
|
| 185 |
|
| 186 |
+
<div className="flex items-center gap-2 bg-amber-50 px-3 py-2 rounded-lg border border-amber-100">
|
| 187 |
+
<Gift size={16} className="text-amber-500"/>
|
| 188 |
+
<label className="text-sm font-bold text-gray-700">奖励:</label>
|
| 189 |
+
<select className="text-sm border-gray-300 rounded bg-white" value={rewardType} onChange={e=>setRewardType(e.target.value as any)}>
|
| 190 |
+
<option value="DRAW_COUNT">抽奖券</option>
|
| 191 |
+
<option value="ITEM">实物</option>
|
| 192 |
+
<option value="ACHIEVEMENT">成就</option>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
</select>
|
| 194 |
+
{rewardType === 'ACHIEVEMENT' ? (
|
| 195 |
+
<select className="text-sm border-gray-300 rounded w-32 bg-white" value={rewardId} onChange={e=>setRewardId(e.target.value)}>
|
| 196 |
+
<option value="">选择成就</option>
|
| 197 |
+
{achConfig?.achievements.map(a => <option key={a.id} value={a.id}>{a.icon} {a.name}</option>)}
|
| 198 |
+
</select>
|
| 199 |
+
) : rewardType === 'ITEM' ? (
|
| 200 |
+
<input className="text-sm border-gray-300 rounded w-24 p-1" placeholder="奖品名" value={rewardId} onChange={e=>setRewardId(e.target.value)}/>
|
| 201 |
+
) : null}
|
| 202 |
+
<span className="text-xs text-gray-400">x</span>
|
| 203 |
+
<input type="number" min={1} className="w-12 text-sm border-gray-300 rounded p-1 text-center" value={rewardCount} onChange={e=>setRewardCount(Number(e.target.value))}/>
|
| 204 |
+
</div>
|
| 205 |
</div>
|
| 206 |
+
|
| 207 |
+
<button
|
| 208 |
+
onClick={() => setIsFullscreen(!isFullscreen)}
|
| 209 |
+
className="p-2 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg transition-colors"
|
| 210 |
+
title={isFullscreen ? "退出全屏" : "全屏显示"}
|
| 211 |
+
>
|
| 212 |
+
{isFullscreen ? <Minimize size={20}/> : <Maximize size={20}/>}
|
| 213 |
+
</button>
|
| 214 |
</div>
|
| 215 |
|
| 216 |
{/* Grid Area */}
|
| 217 |
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
| 218 |
+
<div className={`grid gap-4 transition-all ${mode==='STUDENT' ? 'grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4'}`}>
|
| 219 |
{targetList.map((item, idx) => {
|
| 220 |
const isHighlighted = idx === highlightIndex;
|
| 221 |
const isTeam = mode === 'TEAM';
|
|
|
|
| 235 |
: 'bg-white border-gray-100 shadow-sm opacity-80'
|
| 236 |
}`}>
|
| 237 |
{isHighlighted && <div className="absolute inset-0 bg-yellow-400 opacity-20 animate-pulse rounded-xl"></div>}
|
| 238 |
+
<div className={`mb-2 font-bold ${isFullscreen ? 'text-5xl' : 'text-3xl'}`} style={{color: isTeam ? color : '#64748b'}}>
|
| 239 |
{mode === 'STUDENT' ? (
|
| 240 |
+
<div className={`rounded-full bg-blue-100 flex items-center justify-center text-blue-600 ${isFullscreen ? 'w-20 h-20 text-3xl' : 'w-12 h-12 text-lg'}`}>
|
| 241 |
{name[0]}
|
| 242 |
</div>
|
| 243 |
) : avatar}
|
| 244 |
</div>
|
| 245 |
+
<div className={`font-bold text-gray-800 text-center truncate w-full px-2 ${isFullscreen ? 'text-xl' : 'text-base'}`}>{name}</div>
|
| 246 |
<div className="text-xs text-gray-400">{subText}</div>
|
| 247 |
</div>
|
| 248 |
);
|
|
|
|
| 262 |
|
| 263 |
{/* Result Modal */}
|
| 264 |
{showResultModal && selectedResult && (
|
| 265 |
+
<div className="fixed inset-0 bg-black/60 z-[110] flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in">
|
| 266 |
<div className="bg-white rounded-3xl p-8 w-full max-w-md text-center shadow-2xl relative animate-in zoom-in-95">
|
| 267 |
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-24 h-24 bg-yellow-400 rounded-full flex items-center justify-center border-4 border-white shadow-lg">
|
| 268 |
<span className="text-5xl">✨</span>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
+
<h2 className="mt-10 text-gray-500 text-sm font-bold uppercase tracking-widest">选中对象</h2>
|
| 272 |
+
<div className="text-5xl font-black text-gray-800 my-6">
|
| 273 |
{/* @ts-ignore */}
|
| 274 |
{selectedResult.name}
|
| 275 |
</div>
|