dvc890 commited on
Commit
842b4cd
·
verified ·
1 Parent(s): 07d35d2

Upload 53 files

Browse files
components/ConfirmModal.tsx CHANGED
@@ -1,6 +1,6 @@
1
 
2
  import React from 'react';
3
- import { X, AlertTriangle } from 'lucide-react';
4
 
5
  interface ConfirmModalProps {
6
  isOpen: boolean;
@@ -11,38 +11,37 @@ interface ConfirmModalProps {
11
  confirmText?: string;
12
  cancelText?: string;
13
  isDanger?: boolean;
 
14
  }
15
 
16
  export const ConfirmModal: React.FC<ConfirmModalProps> = ({
17
- isOpen, onClose, onConfirm, title = "确认操作", message,
18
- confirmText = "确定", cancelText = "取消", isDanger = false
19
  }) => {
20
  if (!isOpen) return null;
21
 
22
  return (
23
- <div className="fixed inset-0 bg-black/60 z-[9999] flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in">
24
- <div className="bg-white rounded-xl shadow-2xl max-w-sm w-full overflow-hidden animate-in zoom-in-95">
25
- <div className="p-5">
26
- <div className="flex items-start gap-4">
27
- <div className={`p-3 rounded-full shrink-0 ${isDanger ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'}`}>
28
- <AlertTriangle size={24} />
29
- </div>
30
- <div>
31
- <h3 className="text-lg font-bold text-gray-900 mb-1">{title}</h3>
32
- <p className="text-sm text-gray-500 leading-relaxed">{message}</p>
33
- </div>
34
  </div>
 
 
35
  </div>
36
- <div className="bg-gray-50 px-5 py-3 flex justify-end gap-3">
 
 
 
 
 
 
 
 
37
  <button
38
- onClick={onClose}
39
- className="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-200 transition-colors"
40
- >
41
- {cancelText}
42
- </button>
43
- <button
44
- onClick={() => { onConfirm(); onClose(); }}
45
- className={`px-4 py-2 rounded-lg text-sm font-bold text-white shadow-sm transition-colors ${isDanger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'}`}
46
  >
47
  {confirmText}
48
  </button>
 
1
 
2
  import React from 'react';
3
+ import { X, AlertTriangle, Info } from 'lucide-react';
4
 
5
  interface ConfirmModalProps {
6
  isOpen: boolean;
 
11
  confirmText?: string;
12
  cancelText?: string;
13
  isDanger?: boolean;
14
+ type?: 'confirm' | 'alert'; // Added type
15
  }
16
 
17
  export const ConfirmModal: React.FC<ConfirmModalProps> = ({
18
+ isOpen, onClose, onConfirm, title = "提示", message,
19
+ confirmText = "确定", cancelText = "取消", isDanger = false, type = 'confirm'
20
  }) => {
21
  if (!isOpen) return null;
22
 
23
  return (
24
+ <div className="fixed inset-0 bg-black/60 z-[99999] flex items-center justify-center p-4 backdrop-blur-sm animate-in fade-in">
25
+ <div className="bg-white rounded-2xl shadow-2xl max-w-sm w-full overflow-hidden animate-in zoom-in-95 border border-gray-100">
26
+ <div className="p-6 text-center">
27
+ <div className={`mx-auto w-12 h-12 rounded-full flex items-center justify-center mb-4 ${isDanger ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'}`}>
28
+ {isDanger ? <AlertTriangle size={24} /> : <Info size={24} />}
 
 
 
 
 
 
29
  </div>
30
+ <h3 className="text-lg font-bold text-gray-900 mb-2">{title}</h3>
31
+ <p className="text-sm text-gray-500 leading-relaxed">{message}</p>
32
  </div>
33
+ <div className="bg-gray-50 px-6 py-4 flex gap-3">
34
+ {type === 'confirm' && (
35
+ <button
36
+ onClick={onClose}
37
+ className="flex-1 py-2.5 rounded-xl text-sm font-bold text-gray-600 hover:bg-gray-200 transition-colors border border-gray-200 bg-white"
38
+ >
39
+ {cancelText}
40
+ </button>
41
+ )}
42
  <button
43
+ onClick={() => { onConfirm(); if(type==='alert') onClose(); }} // Close automatically for alert, parent handles confirm for logic
44
+ className={`flex-1 py-2.5 rounded-xl text-sm font-bold text-white shadow-md transition-colors ${isDanger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'}`}
 
 
 
 
 
 
45
  >
46
  {confirmText}
47
  </button>
components/NumberInput.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { Minus, Plus } from 'lucide-react';
4
+
5
+ interface NumberInputProps {
6
+ value: number;
7
+ onChange: (val: number) => void;
8
+ min?: number;
9
+ max?: number;
10
+ className?: string;
11
+ }
12
+
13
+ export const NumberInput: React.FC<NumberInputProps> = ({ value, onChange, min = 0, max = 9999, className = '' }) => {
14
+ const handleDec = (e: React.MouseEvent) => {
15
+ e.preventDefault();
16
+ e.stopPropagation();
17
+ if (value > min) onChange(value - 1);
18
+ };
19
+ const handleInc = (e: React.MouseEvent) => {
20
+ e.preventDefault();
21
+ e.stopPropagation();
22
+ if (value < max) onChange(value + 1);
23
+ };
24
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25
+ let val = parseInt(e.target.value);
26
+ if (isNaN(val)) val = min;
27
+ if (val < min) val = min;
28
+ if (val > max) val = max;
29
+ onChange(val);
30
+ };
31
+
32
+ return (
33
+ <div className={`flex items-center border border-gray-300 rounded-lg overflow-hidden bg-white shadow-sm shrink-0 ${className}`}>
34
+ <button
35
+ type="button"
36
+ onClick={handleDec}
37
+ disabled={value <= min}
38
+ className="p-2 md:p-1.5 bg-gray-50 text-gray-600 hover:bg-gray-100 disabled:opacity-30 border-r border-gray-200 active:bg-gray-200 transition-colors touch-manipulation"
39
+ >
40
+ <Minus size={14} strokeWidth={3} />
41
+ </button>
42
+ <input
43
+ type="number"
44
+ className="w-10 md:w-12 text-center text-sm font-bold text-gray-800 outline-none p-1 appearance-none bg-transparent"
45
+ value={value}
46
+ onChange={handleChange}
47
+ style={{MozAppearance: 'textfield'}}
48
+ />
49
+ <button
50
+ type="button"
51
+ onClick={handleInc}
52
+ disabled={value >= max}
53
+ className="p-2 md:p-1.5 bg-gray-50 text-gray-600 hover:bg-gray-100 disabled:opacity-30 border-l border-gray-200 active:bg-gray-200 transition-colors touch-manipulation"
54
+ >
55
+ <Plus size={14} strokeWidth={3} />
56
+ </button>
57
+ </div>
58
+ );
59
+ };
components/Toast.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect } from 'react';
3
+ import { CheckCircle, AlertCircle, X } from 'lucide-react';
4
+
5
+ export interface ToastState {
6
+ show: boolean;
7
+ message: string;
8
+ type: 'success' | 'error';
9
+ }
10
+
11
+ interface ToastProps {
12
+ message: string;
13
+ type?: 'success' | 'error';
14
+ onClose: () => void;
15
+ }
16
+
17
+ export const Toast: React.FC<ToastProps> = ({ message, type = 'success', onClose }) => {
18
+ useEffect(() => {
19
+ const timer = setTimeout(onClose, 3000);
20
+ return () => clearTimeout(timer);
21
+ }, [onClose]);
22
+
23
+ return (
24
+ <div className="fixed top-20 left-1/2 -translate-x-1/2 z-[99999] animate-in slide-in-from-top-5 fade-in duration-300 w-max max-w-[90vw]">
25
+ <div className={`flex items-center gap-3 px-6 py-3 rounded-xl shadow-2xl border backdrop-blur-md ${type === 'success' ? 'bg-white/95 border-green-200 text-green-800' : 'bg-white/95 border-red-200 text-red-800'}`}>
26
+ {type === 'success' ? <CheckCircle size={20} className="text-green-500 shrink-0" /> : <AlertCircle size={20} className="text-red-500 shrink-0" />}
27
+ <span className="font-bold text-sm">{message}</span>
28
+ <button onClick={onClose} className="ml-2 opacity-50 hover:opacity-100 p-1"><X size={16}/></button>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
pages/GameMountain.tsx CHANGED
@@ -5,6 +5,9 @@ import { api } from '../services/api';
5
  import { GameSession, GameTeam, Student, GameRewardConfig, AchievementConfig, ClassInfo } from '../types';
6
  import { Settings, Plus, Minus, Users, CheckSquare, Loader2, Trash2, X, Flag, Gift, Star, Trophy, Maximize, Minimize, Lock } from 'lucide-react';
7
  import { Emoji } from '../components/Emoji';
 
 
 
8
 
9
  // ... (Styles and Animations kept same)
10
  const styles = `
@@ -53,7 +56,6 @@ const CelebrationEffects = () => (
53
  </div>
54
  );
55
 
56
- // ... (MountainStage Component - Keep mostly same, update display for count)
57
  const MountainStage = ({ team, index, rewardsConfig, maxSteps, onScoreChange, isFullscreen }: {
58
  team: GameTeam,
59
  index: number,
@@ -141,21 +143,8 @@ const MountainStage = ({ team, index, rewardsConfig, maxSteps, onScoreChange, is
141
  );
142
  };
143
 
144
- // ... (GameToast - Keep same)
145
- const GameToast = ({ title, message, type, onClose }: { title: string, message: string, type: 'success' | 'info', onClose: () => void }) => {
146
- useEffect(() => { const timer = setTimeout(onClose, 3000); return () => clearTimeout(timer); }, [onClose]);
147
- return (
148
- <div className="fixed top-20 left-1/2 -translate-x-1/2 z-[10000] animate-in slide-in-from-top-5 fade-in duration-300">
149
- <div className={`flex items-center gap-3 px-6 py-4 rounded-xl shadow-2xl border-2 backdrop-blur-md ${type === 'success' ? 'bg-yellow-50/95 border-yellow-400 text-yellow-900' : 'bg-white/95 border-blue-200 text-slate-800'}`}>
150
- <div className={`p-2 rounded-full ${type === 'success' ? 'bg-yellow-400 text-white' : 'bg-blue-500 text-white'}`}>{type === 'success' ? <Trophy size={24} /> : <Gift size={24} />}</div>
151
- <div><h4 className="font-black text-lg leading-tight">{title}</h4><p className="text-sm opacity-90 font-medium">{message}</p></div>
152
- </div>
153
- </div>
154
- );
155
- };
156
-
157
  export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
158
- // ... (State - Keep same)
159
  const [session, setSession] = useState<GameSession | null>(null);
160
  const [loading, setLoading] = useState(true);
161
  const [students, setStudents] = useState<Student[]>([]);
@@ -164,7 +153,9 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
164
  const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
165
  const [achConfig, setAchConfig] = useState<AchievementConfig | null>(null);
166
  const [canEdit, setCanEdit] = useState(false);
167
- const [toast, setToast] = useState<{title: string, message: string, type: 'success' | 'info'} | null>(null);
 
 
168
 
169
  const currentUser = api.auth.getCurrentUser();
170
  const isTeacher = currentUser?.role === 'TEACHER';
@@ -247,9 +238,9 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
247
  if (delta > 0 && newScore > t.score) {
248
  const reward = session.rewardsConfig.find(r => r.scoreThreshold === newScore);
249
  if (newScore === session.maxSteps) {
250
- setToast({ title: `🏆 巅峰时刻!`, message: `恭喜 [${t.name}] 成功登顶!`, type: 'success' });
251
  } else if (reward) {
252
- setToast({ title: `🎉 触发奖励!`, message: `[${t.name}] 获得:${reward.rewardName} x${reward.rewardValue || 1}`, type: 'info' });
253
  }
254
 
255
  if (reward) {
@@ -269,7 +260,7 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
269
  studentName: stu.name,
270
  rewardType: reward.rewardType as any,
271
  name: reward.rewardName,
272
- count: reward.rewardValue || 1, // USE REWARD VALUE
273
  status: 'PENDING',
274
  source: `群岳争锋 - ${t.name} ${newScore}步`
275
  });
@@ -287,7 +278,7 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
287
  try {
288
  await api.games.saveMountainSession(newSession);
289
  } catch(e) {
290
- alert('保存失败:您可能没有权限修改此班级数据');
291
  loadData();
292
  }
293
  };
@@ -297,7 +288,8 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
297
  try {
298
  await api.games.saveMountainSession(session);
299
  setIsSettingsOpen(false);
300
- } catch(e) { alert('保存失败'); }
 
301
  }
302
  };
303
 
@@ -316,19 +308,31 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
316
  setSession({ ...session, teams: newTeams });
317
  };
318
 
 
 
 
 
 
 
 
 
 
 
 
319
  if (loading) return <div className="h-full flex items-center justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
320
  if (!session) return <div className="h-full flex items-center justify-center text-gray-400">暂无数据</div>;
321
 
322
  const GameContent = (
323
  <div className={`${isFullscreen ? 'fixed inset-0 z-[9999] w-screen h-screen' : 'h-full w-full relative'} flex flex-col bg-gradient-to-b from-sky-300 via-sky-100 to-emerald-50 overflow-hidden`}>
324
  <style>{styles}</style>
325
- {/* Background */}
326
  <div className="absolute inset-0 pointer-events-none overflow-hidden">
327
  <div className="absolute top-10 right-20 w-24 h-24 bg-yellow-300 rounded-full blur-xl opacity-60 animate-pulse"></div>
328
  <div className="absolute top-16 -left-20 text-white/60 text-9xl select-none animate-drift-slow opacity-80" style={{filter: 'blur(2px)'}}><Emoji symbol="☁️"/></div>
329
  <div className="absolute top-32 -left-40 text-white/40 text-8xl select-none animate-drift-medium opacity-60" style={{animationDelay: '5s'}}><Emoji symbol="☁️"/></div>
330
  </div>
331
- {toast && <GameToast title={toast.title} message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
 
 
332
  {/* Toolbar */}
333
  <div className="absolute top-4 right-4 z-50 flex gap-2">
334
  {!canEdit && isTeacher && <div className="px-3 py-1.5 bg-gray-100/80 backdrop-blur rounded-full text-xs font-bold text-gray-500 flex items-center border border-gray-200"><Lock size={14} className="mr-1"/> 只读模式 (非班主任)</div>}
@@ -359,39 +363,32 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
359
  <section className="bg-white p-5 rounded-xl border border-gray-200 shadow-sm">
360
  <h4 className="font-bold text-gray-700 mb-4 flex items-center text-sm uppercase tracking-wide"><Flag size={16} className="mr-2 text-indigo-500"/> 基础规则</h4>
361
  <div className="flex items-center gap-4">
362
- <div>
363
- <label className="text-xs text-gray-500 block mb-1">山峰高度 (步数)</label>
364
- <input type="number" className="border rounded-lg px-3 py-2 w-24 text-center font-bold text-lg focus:ring-2 focus:ring-blue-500 outline-none" value={session.maxSteps} onChange={e => setSession({...session, maxSteps: Number(e.target.value)})} min={5} max={50}/>
365
- </div>
366
  </div>
367
  </section>
368
- {/* Rewards Config - ADDED QUANTITY INPUT */}
369
  <section className="bg-white p-5 rounded-xl border border-gray-200 shadow-sm row-span-2">
370
  <h4 className="font-bold text-gray-700 mb-4 flex items-center text-sm uppercase tracking-wide"><Gift size={16} className="mr-2 text-amber-500"/> 奖励节点</h4>
371
  <div className="space-y-2 max-h-64 overflow-y-auto pr-1 custom-scrollbar">
372
  {session.rewardsConfig.sort((a,b) => a.scoreThreshold - b.scoreThreshold).map((rc, idx) => (
373
  <div key={idx} className="flex flex-col gap-2 bg-gray-50 p-2 rounded-lg border border-gray-100 group hover:border-blue-200 transition-colors">
374
  <div className="flex items-center gap-2">
375
- <div className="flex items-center bg-white border px-2 py-1 rounded">
376
- <span className="text-xs text-gray-400 mr-1">第</span>
377
- <input type="number" min={1} max={session.maxSteps} className="w-8 text-center font-bold text-sm outline-none" value={rc.scoreThreshold} onChange={e => {
378
- const newArr = [...session.rewardsConfig]; newArr[idx].scoreThreshold = Number(e.target.value); setSession({...session, rewardsConfig: newArr});
379
- }}/>
380
- <span className="text-xs text-gray-400 ml-1">步</span>
381
  </div>
382
- <select className="border-none bg-transparent text-sm font-medium text-gray-700 focus:ring-0 flex-1" value={rc.rewardType} onChange={e => {
383
  const newArr = [...session.rewardsConfig]; newArr[idx].rewardType = e.target.value as any; setSession({...session, rewardsConfig: newArr});
384
  }}>
385
  <option value="DRAW_COUNT">🎲 抽奖券</option>
386
  <option value="ITEM">🎁 实物</option>
387
- <option value="ACHIEVEMENT">🏆 奖状/成就</option>
388
  </select>
389
- {/* QUANTITY INPUT */}
390
- <div className="flex items-center gap-1 bg-white border px-2 py-1 rounded">
391
  <span className="text-xs text-gray-400">x</span>
392
- <input type="number" min={1} className="w-8 text-center font-bold text-sm outline-none" value={rc.rewardValue || 1} onChange={e => {
393
- const newArr = [...session.rewardsConfig]; newArr[idx].rewardValue = Number(e.target.value); setSession({...session, rewardsConfig: newArr});
394
- }}/>
395
  </div>
396
  <button onClick={() => {
397
  const newArr = session.rewardsConfig.filter((_, i) => i !== idx); setSession({...session, rewardsConfig: newArr});
@@ -440,10 +437,7 @@ export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
440
  const updated = session.teams.map(tm => tm.id === t.id ? {...tm, name: e.target.value} : tm);
441
  setSession({...session, teams: updated});
442
  }} className="bg-transparent font-bold text-gray-800 w-24 outline-none border-b border-transparent focus:border-blue-300 hover:border-gray-300" onClick={e=>e.stopPropagation()}/>
443
- <button onClick={(e) => {
444
- e.stopPropagation();
445
- if(confirm('删除队伍?')) setSession({...session, teams: session.teams.filter(tm => tm.id !== t.id)});
446
- }} className="text-gray-300 hover:text-red-500"><Trash2 size={14}/></button>
447
  </div>
448
  <div className="flex gap-2 pl-3 items-center">
449
  <input type="color" value={t.color} onChange={e => {
 
5
  import { GameSession, GameTeam, Student, GameRewardConfig, AchievementConfig, ClassInfo } from '../types';
6
  import { Settings, Plus, Minus, Users, CheckSquare, Loader2, Trash2, X, Flag, Gift, Star, Trophy, Maximize, Minimize, Lock } from 'lucide-react';
7
  import { Emoji } from '../components/Emoji';
8
+ import { NumberInput } from '../components/NumberInput';
9
+ import { Toast, ToastState } from '../components/Toast';
10
+ import { ConfirmModal } from '../components/ConfirmModal';
11
 
12
  // ... (Styles and Animations kept same)
13
  const styles = `
 
56
  </div>
57
  );
58
 
 
59
  const MountainStage = ({ team, index, rewardsConfig, maxSteps, onScoreChange, isFullscreen }: {
60
  team: GameTeam,
61
  index: number,
 
143
  );
144
  };
145
 
146
+ // Replaced local GameToast with global Toast component state
 
 
 
 
 
 
 
 
 
 
 
 
147
  export const GameMountain: React.FC<{className?: string}> = ({ className }) => {
 
148
  const [session, setSession] = useState<GameSession | null>(null);
149
  const [loading, setLoading] = useState(true);
150
  const [students, setStudents] = useState<Student[]>([]);
 
153
  const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
154
  const [achConfig, setAchConfig] = useState<AchievementConfig | null>(null);
155
  const [canEdit, setCanEdit] = useState(false);
156
+
157
+ const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' });
158
+ const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void}>({ isOpen: false, title: '', message: '', onConfirm: () => {} });
159
 
160
  const currentUser = api.auth.getCurrentUser();
161
  const isTeacher = currentUser?.role === 'TEACHER';
 
238
  if (delta > 0 && newScore > t.score) {
239
  const reward = session.rewardsConfig.find(r => r.scoreThreshold === newScore);
240
  if (newScore === session.maxSteps) {
241
+ setToast({ show: true, title: `🏆 巅峰时刻!`, message: `恭喜 [${t.name}] 成功登顶!`, type: 'success' } as any);
242
  } else if (reward) {
243
+ setToast({ show: true, title: `🎉 触发奖励!`, message: `[${t.name}] 获得:${reward.rewardName} x${reward.rewardValue || 1}`, type: 'success' } as any);
244
  }
245
 
246
  if (reward) {
 
260
  studentName: stu.name,
261
  rewardType: reward.rewardType as any,
262
  name: reward.rewardName,
263
+ count: reward.rewardValue || 1,
264
  status: 'PENDING',
265
  source: `群岳争锋 - ${t.name} ${newScore}步`
266
  });
 
278
  try {
279
  await api.games.saveMountainSession(newSession);
280
  } catch(e) {
281
+ setToast({ show: true, message: '保存失败:您可能没有权限修改此班级数据', type: 'error' });
282
  loadData();
283
  }
284
  };
 
288
  try {
289
  await api.games.saveMountainSession(session);
290
  setIsSettingsOpen(false);
291
+ setToast({ show: true, message: '游戏设置已保存', type: 'success' });
292
+ } catch(e) { setToast({ show: true, message: '保存失败', type: 'error' }); }
293
  }
294
  };
295
 
 
308
  setSession({ ...session, teams: newTeams });
309
  };
310
 
311
+ const deleteTeam = (id: string) => {
312
+ setConfirmModal({
313
+ isOpen: true,
314
+ title: '删除队伍',
315
+ message: '确定要删除这个队伍吗?',
316
+ onConfirm: () => {
317
+ if (session) setSession({...session, teams: session.teams.filter(tm => tm.id !== id)});
318
+ }
319
+ });
320
+ };
321
+
322
  if (loading) return <div className="h-full flex items-center justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
323
  if (!session) return <div className="h-full flex items-center justify-center text-gray-400">暂无数据</div>;
324
 
325
  const GameContent = (
326
  <div className={`${isFullscreen ? 'fixed inset-0 z-[9999] w-screen h-screen' : 'h-full w-full relative'} flex flex-col bg-gradient-to-b from-sky-300 via-sky-100 to-emerald-50 overflow-hidden`}>
327
  <style>{styles}</style>
 
328
  <div className="absolute inset-0 pointer-events-none overflow-hidden">
329
  <div className="absolute top-10 right-20 w-24 h-24 bg-yellow-300 rounded-full blur-xl opacity-60 animate-pulse"></div>
330
  <div className="absolute top-16 -left-20 text-white/60 text-9xl select-none animate-drift-slow opacity-80" style={{filter: 'blur(2px)'}}><Emoji symbol="☁️"/></div>
331
  <div className="absolute top-32 -left-40 text-white/40 text-8xl select-none animate-drift-medium opacity-60" style={{animationDelay: '5s'}}><Emoji symbol="☁️"/></div>
332
  </div>
333
+ {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})} />}
334
+ <ConfirmModal isOpen={confirmModal.isOpen} title={confirmModal.title} message={confirmModal.message} onClose={()=>setConfirmModal({...confirmModal, isOpen: false})} onConfirm={confirmModal.onConfirm}/>
335
+
336
  {/* Toolbar */}
337
  <div className="absolute top-4 right-4 z-50 flex gap-2">
338
  {!canEdit && isTeacher && <div className="px-3 py-1.5 bg-gray-100/80 backdrop-blur rounded-full text-xs font-bold text-gray-500 flex items-center border border-gray-200"><Lock size={14} className="mr-1"/> 只读模式 (非班主任)</div>}
 
363
  <section className="bg-white p-5 rounded-xl border border-gray-200 shadow-sm">
364
  <h4 className="font-bold text-gray-700 mb-4 flex items-center text-sm uppercase tracking-wide"><Flag size={16} className="mr-2 text-indigo-500"/> 基础规则</h4>
365
  <div className="flex items-center gap-4">
366
+ <label className="text-xs text-gray-500 block">山峰高度 (步数)</label>
367
+ <NumberInput value={session.maxSteps} onChange={v => setSession({...session, maxSteps: v})} min={5} max={50} />
 
 
368
  </div>
369
  </section>
370
+ {/* Rewards Config */}
371
  <section className="bg-white p-5 rounded-xl border border-gray-200 shadow-sm row-span-2">
372
  <h4 className="font-bold text-gray-700 mb-4 flex items-center text-sm uppercase tracking-wide"><Gift size={16} className="mr-2 text-amber-500"/> 奖励节点</h4>
373
  <div className="space-y-2 max-h-64 overflow-y-auto pr-1 custom-scrollbar">
374
  {session.rewardsConfig.sort((a,b) => a.scoreThreshold - b.scoreThreshold).map((rc, idx) => (
375
  <div key={idx} className="flex flex-col gap-2 bg-gray-50 p-2 rounded-lg border border-gray-100 group hover:border-blue-200 transition-colors">
376
  <div className="flex items-center gap-2">
377
+ <div className="flex items-center gap-1 bg-white border px-1 py-1 rounded">
378
+ <span className="text-xs text-gray-400">第</span>
379
+ <NumberInput value={rc.scoreThreshold} onChange={v => { const newArr = [...session.rewardsConfig]; newArr[idx].scoreThreshold = v; setSession({...session, rewardsConfig: newArr}); }} min={1} max={session.maxSteps} />
380
+ <span className="text-xs text-gray-400">步</span>
 
 
381
  </div>
382
+ <select className="border-none bg-transparent text-sm font-medium text-gray-700 focus:ring-0 flex-1 w-20" value={rc.rewardType} onChange={e => {
383
  const newArr = [...session.rewardsConfig]; newArr[idx].rewardType = e.target.value as any; setSession({...session, rewardsConfig: newArr});
384
  }}>
385
  <option value="DRAW_COUNT">🎲 抽奖券</option>
386
  <option value="ITEM">🎁 实物</option>
387
+ <option value="ACHIEVEMENT">🏆 奖状</option>
388
  </select>
389
+ <div className="flex items-center gap-1 bg-white border px-1 py-1 rounded">
 
390
  <span className="text-xs text-gray-400">x</span>
391
+ <NumberInput value={rc.rewardValue || 1} onChange={v => { const newArr = [...session.rewardsConfig]; newArr[idx].rewardValue = v; setSession({...session, rewardsConfig: newArr}); }} min={1} />
 
 
392
  </div>
393
  <button onClick={() => {
394
  const newArr = session.rewardsConfig.filter((_, i) => i !== idx); setSession({...session, rewardsConfig: newArr});
 
437
  const updated = session.teams.map(tm => tm.id === t.id ? {...tm, name: e.target.value} : tm);
438
  setSession({...session, teams: updated});
439
  }} className="bg-transparent font-bold text-gray-800 w-24 outline-none border-b border-transparent focus:border-blue-300 hover:border-gray-300" onClick={e=>e.stopPropagation()}/>
440
+ <button onClick={(e) => { e.stopPropagation(); deleteTeam(t.id); }} className="text-gray-300 hover:text-red-500"><Trash2 size={14}/></button>
 
 
 
441
  </div>
442
  <div className="flex gap-2 pl-3 items-center">
443
  <input type="color" value={t.color} onChange={e => {
pages/Settings.tsx CHANGED
@@ -3,6 +3,8 @@ import React, { useState, useEffect } from 'react';
3
  import { Save, Bell, Lock, Database, Loader2, Plus, X, Trash2, Globe, Edit, Calendar, GraduationCap } from 'lucide-react';
4
  import { api } from '../services/api';
5
  import { SystemConfig, UserRole } from '../types';
 
 
6
 
7
  export const Settings: React.FC = () => {
8
  const [loading, setLoading] = useState(false);
@@ -25,6 +27,9 @@ export const Settings: React.FC = () => {
25
  const [editVal, setEditVal] = useState('');
26
  const [teacherFollows, setTeacherFollows] = useState(false);
27
  const [promoting, setPromoting] = useState(false);
 
 
 
28
 
29
  const currentUser = api.auth.getCurrentUser();
30
  const isAdmin = currentUser?.role === UserRole.ADMIN;
@@ -45,8 +50,10 @@ export const Settings: React.FC = () => {
45
  setLoading(true);
46
  try {
47
  await api.config.save(config);
48
- alert('全局系统配置已保存');
49
- } catch (e) { alert('保存失败'); } finally { setLoading(false); }
 
 
50
  };
51
 
52
  const addSemester = () => {
@@ -61,7 +68,16 @@ export const Settings: React.FC = () => {
61
 
62
  const removeSemester = (idx: number) => {
63
  const list = [...(config.semesters || [])];
64
- if (config.semester === list[idx]) return alert('无法删除当前正在使用的学期。');
 
 
 
 
 
 
 
 
 
65
  list.splice(idx, 1);
66
  setConfig(prev => ({ ...prev, semesters: list }));
67
  };
@@ -81,19 +97,36 @@ export const Settings: React.FC = () => {
81
  };
82
 
83
  const handlePromotion = async () => {
84
- if (!confirm(`⚠️ 高风险操作警告!\n\n1. 所有学生年级将+1 (如: 一年级&rarr;二年级)\n2. 六年级/初三/高三学生将标记为“毕业”\n3. ${teacherFollows ? '班主任将随班升级 (继续带该班)' : '班主任将留在原年级 (班级变空)'}\n\n确定要执行全校升学吗?`)) return;
85
- setPromoting(true);
86
- try {
87
- const res = await api.students.promote({ teacherFollows });
88
- alert(`升学操作完成!\n共处理学生: ${res.count} 人。`);
89
- } catch (e: any) { alert('操作失败: ' + e.message); }
90
- finally { setPromoting(false); }
 
 
 
 
 
 
 
91
  };
92
 
93
  if (fetching) return <div className="p-10 text-center text-gray-500">加载配置中...</div>;
94
 
95
  return (
96
  <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
97
  <div className="flex items-center space-x-2 bg-blue-50 text-blue-800 p-4 rounded-xl border border-blue-100">
98
  <Globe size={20}/>
99
  <span className="font-bold">全局系统配置中心</span>
@@ -278,4 +311,4 @@ export const Settings: React.FC = () => {
278
  </div>
279
  </div>
280
  );
281
- };
 
3
  import { Save, Bell, Lock, Database, Loader2, Plus, X, Trash2, Globe, Edit, Calendar, GraduationCap } from 'lucide-react';
4
  import { api } from '../services/api';
5
  import { SystemConfig, UserRole } from '../types';
6
+ import { Toast, ToastState } from '../components/Toast';
7
+ import { ConfirmModal } from '../components/ConfirmModal';
8
 
9
  export const Settings: React.FC = () => {
10
  const [loading, setLoading] = useState(false);
 
27
  const [editVal, setEditVal] = useState('');
28
  const [teacherFollows, setTeacherFollows] = useState(false);
29
  const [promoting, setPromoting] = useState(false);
30
+
31
+ const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' });
32
+ const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void, type?: 'confirm' | 'alert'}>({ isOpen: false, title: '', message: '', onConfirm: () => {} });
33
 
34
  const currentUser = api.auth.getCurrentUser();
35
  const isAdmin = currentUser?.role === UserRole.ADMIN;
 
50
  setLoading(true);
51
  try {
52
  await api.config.save(config);
53
+ setToast({ show: true, message: '全局系统配置已保存', type: 'success' });
54
+ } catch (e) {
55
+ setToast({ show: true, message: '保存失败', type: 'error' });
56
+ } finally { setLoading(false); }
57
  };
58
 
59
  const addSemester = () => {
 
68
 
69
  const removeSemester = (idx: number) => {
70
  const list = [...(config.semesters || [])];
71
+ if (config.semester === list[idx]) {
72
+ setConfirmModal({
73
+ isOpen: true,
74
+ title: '操作无法完成',
75
+ message: '无法删除当前正在使用的学期。请先切换学期后再试。',
76
+ type: 'alert',
77
+ onConfirm: () => {}
78
+ });
79
+ return;
80
+ }
81
  list.splice(idx, 1);
82
  setConfig(prev => ({ ...prev, semesters: list }));
83
  };
 
97
  };
98
 
99
  const handlePromotion = async () => {
100
+ setConfirmModal({
101
+ isOpen: true,
102
+ title: '高风险操作警告',
103
+ message: `1. 所有学生年级将+1 (如: 一年级→二年级)\n2. 六年级/初三/高三学生将标记为“毕业”\n3. ${teacherFollows ? '班主任将随班升级 (继续带该班)' : '班主任将留在原年级 (班级变空)'}\n\n确定要执行全校升学吗?`,
104
+ onConfirm: async () => {
105
+ setPromoting(true);
106
+ try {
107
+ const res = await api.students.promote({ teacherFollows });
108
+ setToast({ show: true, message: `升学操作完成!共处理学生: ${res.count} 人。`, type: 'success' });
109
+ } catch (e: any) {
110
+ setToast({ show: true, message: '操作失败: ' + e.message, type: 'error' });
111
+ } finally { setPromoting(false); }
112
+ }
113
+ });
114
  };
115
 
116
  if (fetching) return <div className="p-10 text-center text-gray-500">加载配置中...</div>;
117
 
118
  return (
119
  <div className="space-y-6">
120
+ {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})}/>}
121
+ <ConfirmModal
122
+ isOpen={confirmModal.isOpen}
123
+ title={confirmModal.title}
124
+ message={confirmModal.message}
125
+ onClose={()=>setConfirmModal({...confirmModal, isOpen: false})}
126
+ onConfirm={confirmModal.onConfirm}
127
+ type={confirmModal.type || 'confirm'}
128
+ />
129
+
130
  <div className="flex items-center space-x-2 bg-blue-50 text-blue-800 p-4 rounded-xl border border-blue-100">
131
  <Globe size={20}/>
132
  <span className="font-bold">全局系统配置中心</span>
 
311
  </div>
312
  </div>
313
  );
314
+ };
pages/TeacherDashboard.tsx CHANGED
@@ -2,9 +2,10 @@
2
  import React, { useEffect, useState } from 'react';
3
  import { api } from '../services/api';
4
  import { Schedule, Student, Attendance, PeriodConfig, Subject, User, ClassInfo } from '../types';
5
- import { Calendar, UserX, AlertTriangle, Activity, Plus, X, Trash2, Settings, Save, Clock, ChevronDown, ChevronUp } from 'lucide-react';
6
  import { TodoList } from '../components/TodoList';
7
  import { ConfirmModal } from '../components/ConfirmModal';
 
8
 
9
  const COLORS = ['#dbeafe', '#dcfce7', '#fef9c3', '#fee2e2', '#f3e8ff', '#ffedd5', '#e0e7ff', '#ecfccb'];
10
  const stringToColor = (str: string) => {
@@ -45,13 +46,16 @@ export const TeacherDashboard: React.FC = () => {
45
  // Modals & Editing
46
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
47
  const [editForm, setEditForm] = useState<{ _id?: string, subject: string, teacherName: string, weekType: string }>({ subject: '', teacherName: '', weekType: 'ALL' });
 
 
48
  const [isPeriodSettingsOpen, setIsPeriodSettingsOpen] = useState(false);
49
  const [tempPeriodConfig, setTempPeriodConfig] = useState<PeriodConfig[]>([]);
50
 
51
- // Confirm Modal State
52
  const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void, isDanger?: boolean}>({
53
  isOpen: false, title: '', message: '', onConfirm: () => {}
54
  });
 
55
 
56
  const currentUser = api.auth.getCurrentUser();
57
  const homeroomClass = currentUser?.homeroomClass;
@@ -71,7 +75,7 @@ export const TeacherDashboard: React.FC = () => {
71
  homeroomClass ? api.attendance.get({ className: homeroomClass, date: todayStr }) : Promise.resolve([]),
72
  api.subjects.getAll(),
73
  api.users.getAll({ role: 'TEACHER' }),
74
- api.classes.getAll() // Fetch all classes to find ID and specific config
75
  ]);
76
 
77
  if (homeroomClass) {
@@ -79,8 +83,6 @@ export const TeacherDashboard: React.FC = () => {
79
  setSchedules(scheds);
80
  setAttendance(atts);
81
 
82
- // Find current class to get its specific period config
83
- // Normalize names to match safely
84
  const matchedClass = (allClassData as ClassInfo[]).find(c =>
85
  (c.grade + c.className) === homeroomClass || c.className === homeroomClass
86
  );
@@ -114,19 +116,26 @@ export const TeacherDashboard: React.FC = () => {
114
  teacherName: specificSchedule.teacherName,
115
  weekType: specificSchedule.weekType || 'ALL'
116
  });
 
 
 
117
  } else {
118
- // Add new schedule (default to current view mode, or ALL if in ALL view)
119
  setEditForm({
120
  subject: '',
121
  teacherName: '',
122
  weekType: weekType === 'ALL' ? 'ALL' : weekType
123
  });
 
124
  }
125
  };
126
 
127
  const handleSaveSchedule = async () => {
128
  if (!homeroomClass || !editingCell) return;
129
- if (!editForm.subject) return alert('请选择科目');
 
 
 
130
 
131
  try {
132
  const payload = {
@@ -156,7 +165,10 @@ export const TeacherDashboard: React.FC = () => {
156
  setEditingCell(null);
157
  const updated = await api.schedules.get({ className: homeroomClass });
158
  setSchedules(updated);
159
- } catch(e) { alert('保存失败'); }
 
 
 
160
  };
161
 
162
  const handleDeleteSchedule = async (s: Schedule) => {
@@ -174,6 +186,7 @@ export const TeacherDashboard: React.FC = () => {
174
  }
175
  const updated = await api.schedules.get({ className: homeroomClass });
176
  setSchedules(updated);
 
177
  }
178
  });
179
  };
@@ -185,12 +198,16 @@ export const TeacherDashboard: React.FC = () => {
185
  };
186
 
187
  const savePeriodSettings = async () => {
188
- // Validate
189
- if (tempPeriodConfig.some(p => !p.name.trim())) return alert('节次名称不能为空');
190
- if (!currentClassId) return alert('无法保存:找不到当前班级ID');
 
 
 
 
 
191
 
192
  try {
193
- // Update the SPECIFIC CLASS, not the global config
194
  await fetch(`/api/classes/${currentClassId}`, {
195
  method: 'PUT',
196
  headers: {
@@ -202,9 +219,9 @@ export const TeacherDashboard: React.FC = () => {
202
 
203
  setPeriodConfig(tempPeriodConfig);
204
  setIsPeriodSettingsOpen(false);
205
- alert('班级节次设置已保存');
206
  } catch (e) {
207
- alert('保存失败');
208
  }
209
  };
210
 
@@ -237,6 +254,7 @@ export const TeacherDashboard: React.FC = () => {
237
 
238
  return (
239
  <div className="space-y-6">
 
240
  <ConfirmModal
241
  isOpen={confirmModal.isOpen}
242
  title={confirmModal.title}
@@ -259,7 +277,6 @@ export const TeacherDashboard: React.FC = () => {
259
  </div>
260
 
261
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
262
- {/* Left Column: Stats */}
263
  <div className="lg:col-span-2 space-y-6">
264
  {homeroomClass && (
265
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
@@ -310,7 +327,6 @@ export const TeacherDashboard: React.FC = () => {
310
  )}
311
  </div>
312
 
313
- {/* Right Column: Todo List */}
314
  <div className="h-[500px]">
315
  <TodoList />
316
  </div>
@@ -359,7 +375,6 @@ export const TeacherDashboard: React.FC = () => {
359
  <div className="text-[10px] text-gray-400 font-normal mt-1">{pConfig.startTime ? `${pConfig.startTime}-${pConfig.endTime}` : ''}</div>
360
  </td>
361
  {[1,2,3,4,5].map(day => {
362
- // Enhanced filter: support displaying simultaneous odd/even records
363
  const slotItems = schedules.filter(s =>
364
  s.dayOfWeek === day &&
365
  s.period === pConfig.period
@@ -412,16 +427,35 @@ export const TeacherDashboard: React.FC = () => {
412
  <h4 className="font-bold mb-4 text-gray-800 text-lg border-b pb-2">{editForm._id ? '编辑课程信息' : '新增课程信息'}</h4>
413
  <div className="space-y-4">
414
  <div>
415
- <label className="text-xs font-bold text-gray-500 uppercase block mb-1">科目</label>
416
- <select
417
- className="w-full border border-gray-300 rounded-lg p-2.5 text-sm bg-white focus:ring-2 focus:ring-blue-500 outline-none"
418
- value={editForm.subject}
419
- onChange={e=>setEditForm({...editForm, subject: e.target.value})}
420
- autoFocus
421
- >
422
- <option value="">-- 请选择科目 --</option>
423
- {subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
424
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  </div>
426
  <div>
427
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">任课教师 (可选)</label>
@@ -478,7 +512,6 @@ export const TeacherDashboard: React.FC = () => {
478
  onChange={e => updateTempPeriod(idx, 'name', e.target.value)}
479
  placeholder="节次名称"
480
  />
481
- {/* Optional Time Inputs */}
482
  <div className="flex items-center gap-1">
483
  <Clock size={12} className="text-gray-400"/>
484
  <input
 
2
  import React, { useEffect, useState } from 'react';
3
  import { api } from '../services/api';
4
  import { Schedule, Student, Attendance, PeriodConfig, Subject, User, ClassInfo } from '../types';
5
+ import { Calendar, UserX, AlertTriangle, Activity, Plus, X, Trash2, Settings, Save, Clock, ChevronDown, ChevronUp, Edit2 } from 'lucide-react';
6
  import { TodoList } from '../components/TodoList';
7
  import { ConfirmModal } from '../components/ConfirmModal';
8
+ import { Toast, ToastState } from '../components/Toast';
9
 
10
  const COLORS = ['#dbeafe', '#dcfce7', '#fef9c3', '#fee2e2', '#f3e8ff', '#ffedd5', '#e0e7ff', '#ecfccb'];
11
  const stringToColor = (str: string) => {
 
46
  // Modals & Editing
47
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
48
  const [editForm, setEditForm] = useState<{ _id?: string, subject: string, teacherName: string, weekType: string }>({ subject: '', teacherName: '', weekType: 'ALL' });
49
+ const [isCustomSubject, setIsCustomSubject] = useState(false); // Toggle for custom input
50
+
51
  const [isPeriodSettingsOpen, setIsPeriodSettingsOpen] = useState(false);
52
  const [tempPeriodConfig, setTempPeriodConfig] = useState<PeriodConfig[]>([]);
53
 
54
+ // Confirm Modal & Toast
55
  const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void, isDanger?: boolean}>({
56
  isOpen: false, title: '', message: '', onConfirm: () => {}
57
  });
58
+ const [toast, setToast] = useState<ToastState>({ show: false, message: '', type: 'success' });
59
 
60
  const currentUser = api.auth.getCurrentUser();
61
  const homeroomClass = currentUser?.homeroomClass;
 
75
  homeroomClass ? api.attendance.get({ className: homeroomClass, date: todayStr }) : Promise.resolve([]),
76
  api.subjects.getAll(),
77
  api.users.getAll({ role: 'TEACHER' }),
78
+ api.classes.getAll()
79
  ]);
80
 
81
  if (homeroomClass) {
 
83
  setSchedules(scheds);
84
  setAttendance(atts);
85
 
 
 
86
  const matchedClass = (allClassData as ClassInfo[]).find(c =>
87
  (c.grade + c.className) === homeroomClass || c.className === homeroomClass
88
  );
 
116
  teacherName: specificSchedule.teacherName,
117
  weekType: specificSchedule.weekType || 'ALL'
118
  });
119
+ // Check if subject is in standard list
120
+ const exists = subjects.some(s => s.name === specificSchedule.subject);
121
+ setIsCustomSubject(!exists);
122
  } else {
123
+ // Add new schedule
124
  setEditForm({
125
  subject: '',
126
  teacherName: '',
127
  weekType: weekType === 'ALL' ? 'ALL' : weekType
128
  });
129
+ setIsCustomSubject(false);
130
  }
131
  };
132
 
133
  const handleSaveSchedule = async () => {
134
  if (!homeroomClass || !editingCell) return;
135
+ if (!editForm.subject) {
136
+ setToast({ show: true, message: '请填写科目名称', type: 'error' });
137
+ return;
138
+ }
139
 
140
  try {
141
  const payload = {
 
165
  setEditingCell(null);
166
  const updated = await api.schedules.get({ className: homeroomClass });
167
  setSchedules(updated);
168
+ setToast({ show: true, message: '课程保存成功', type: 'success' });
169
+ } catch(e) {
170
+ setToast({ show: true, message: '保存失败', type: 'error' });
171
+ }
172
  };
173
 
174
  const handleDeleteSchedule = async (s: Schedule) => {
 
186
  }
187
  const updated = await api.schedules.get({ className: homeroomClass });
188
  setSchedules(updated);
189
+ setToast({ show: true, message: '课程已删除', type: 'success' });
190
  }
191
  });
192
  };
 
198
  };
199
 
200
  const savePeriodSettings = async () => {
201
+ if (tempPeriodConfig.some(p => !p.name.trim())) {
202
+ setToast({ show: true, message: '节次名称不能为空', type: 'error' });
203
+ return;
204
+ }
205
+ if (!currentClassId) {
206
+ setToast({ show: true, message: '无法保存:找不到当前班级ID', type: 'error' });
207
+ return;
208
+ }
209
 
210
  try {
 
211
  await fetch(`/api/classes/${currentClassId}`, {
212
  method: 'PUT',
213
  headers: {
 
219
 
220
  setPeriodConfig(tempPeriodConfig);
221
  setIsPeriodSettingsOpen(false);
222
+ setToast({ show: true, message: '班级节次设置已保存', type: 'success' });
223
  } catch (e) {
224
+ setToast({ show: true, message: '保存失败', type: 'error' });
225
  }
226
  };
227
 
 
254
 
255
  return (
256
  <div className="space-y-6">
257
+ {toast.show && <Toast message={toast.message} type={toast.type} onClose={()=>setToast({...toast, show: false})}/>}
258
  <ConfirmModal
259
  isOpen={confirmModal.isOpen}
260
  title={confirmModal.title}
 
277
  </div>
278
 
279
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
 
280
  <div className="lg:col-span-2 space-y-6">
281
  {homeroomClass && (
282
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
 
327
  )}
328
  </div>
329
 
 
330
  <div className="h-[500px]">
331
  <TodoList />
332
  </div>
 
375
  <div className="text-[10px] text-gray-400 font-normal mt-1">{pConfig.startTime ? `${pConfig.startTime}-${pConfig.endTime}` : ''}</div>
376
  </td>
377
  {[1,2,3,4,5].map(day => {
 
378
  const slotItems = schedules.filter(s =>
379
  s.dayOfWeek === day &&
380
  s.period === pConfig.period
 
427
  <h4 className="font-bold mb-4 text-gray-800 text-lg border-b pb-2">{editForm._id ? '编辑课程信息' : '新增课程信息'}</h4>
428
  <div className="space-y-4">
429
  <div>
430
+ <div className="flex justify-between items-center mb-1">
431
+ <label className="text-xs font-bold text-gray-500 uppercase">科目</label>
432
+ <button
433
+ onClick={() => { setIsCustomSubject(!isCustomSubject); setEditForm({...editForm, subject: ''}); }}
434
+ className="text-[10px] text-blue-600 hover:underline flex items-center"
435
+ >
436
+ {isCustomSubject ? '选择现有科目' : '自定义输入'} <Edit2 size={10} className="ml-1"/>
437
+ </button>
438
+ </div>
439
+
440
+ {isCustomSubject ? (
441
+ <input
442
+ className="w-full border border-blue-300 rounded-lg p-2.5 text-sm bg-blue-50 focus:ring-2 focus:ring-blue-500 outline-none placeholder-blue-300"
443
+ placeholder="输入自定义科目 (如: 体育)"
444
+ value={editForm.subject}
445
+ onChange={e=>setEditForm({...editForm, subject: e.target.value})}
446
+ autoFocus
447
+ />
448
+ ) : (
449
+ <select
450
+ className="w-full border border-gray-300 rounded-lg p-2.5 text-sm bg-white focus:ring-2 focus:ring-blue-500 outline-none"
451
+ value={editForm.subject}
452
+ onChange={e=>setEditForm({...editForm, subject: e.target.value})}
453
+ autoFocus
454
+ >
455
+ <option value="">-- 请选择科目 --</option>
456
+ {subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
457
+ </select>
458
+ )}
459
  </div>
460
  <div>
461
  <label className="text-xs font-bold text-gray-500 uppercase block mb-1">任课教师 (可选)</label>
 
512
  onChange={e => updateTempPeriod(idx, 'name', e.target.value)}
513
  placeholder="节次名称"
514
  />
 
515
  <div className="flex items-center gap-1">
516
  <Clock size={12} className="text-gray-400"/>
517
  <input