dvc890 commited on
Commit
2159fd9
·
verified ·
1 Parent(s): c9a1ebd

Update pages/TeacherDashboard.tsx

Browse files
Files changed (1) hide show
  1. pages/TeacherDashboard.tsx +263 -90
pages/TeacherDashboard.tsx CHANGED
@@ -1,9 +1,10 @@
1
 
2
  import React, { useEffect, useState } from 'react';
3
  import { api } from '../services/api';
4
- import { Schedule, Student, Attendance } from '../types';
5
- import { Calendar, UserX, AlertTriangle, Activity, Coffee, Plus, X, PaintBucket, ToggleLeft } from 'lucide-react';
6
  import { TodoList } from '../components/TodoList';
 
7
 
8
  const COLORS = ['#dbeafe', '#dcfce7', '#fef9c3', '#fee2e2', '#f3e8ff', '#ffedd5', '#e0e7ff', '#ecfccb'];
9
  const stringToColor = (str: string) => {
@@ -12,18 +13,42 @@ const stringToColor = (str: string) => {
12
  return COLORS[Math.abs(hash) % COLORS.length];
13
  };
14
 
 
 
 
 
 
 
 
 
 
 
 
15
  export const TeacherDashboard: React.FC = () => {
16
  const [loading, setLoading] = useState(true);
17
  const [schedules, setSchedules] = useState<Schedule[]>([]);
18
  const [students, setStudents] = useState<Student[]>([]);
19
  const [attendance, setAttendance] = useState<Attendance[]>([]);
20
 
 
 
 
 
21
  // Schedule State
22
  const [showSchedule, setShowSchedule] = useState(false);
23
- const [weekType, setWeekType] = useState<'ALL' | 'ODD' | 'EVEN'>('ALL'); // Currently displaying
 
 
 
24
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
25
  const [editForm, setEditForm] = useState({ subject: '', teacherName: '', weekType: 'ALL' });
26
- const [periodCount, setPeriodCount] = useState(8); // Default, could be config
 
 
 
 
 
 
27
 
28
  const currentUser = api.auth.getCurrentUser();
29
  const homeroomClass = currentUser?.homeroomClass;
@@ -37,26 +62,35 @@ export const TeacherDashboard: React.FC = () => {
37
  try {
38
  const todayStr = new Date().toISOString().split('T')[0];
39
 
40
- const [stus, scheds, atts] = await Promise.all([
41
  api.students.getAll(),
42
  homeroomClass ? api.schedules.get({ className: homeroomClass }) : Promise.resolve([]),
43
- homeroomClass ? api.attendance.get({ className: homeroomClass, date: todayStr }) : Promise.resolve([])
 
 
 
44
  ]);
45
 
 
 
 
 
 
46
  if (homeroomClass) {
47
  setStudents(stus.filter((s: Student) => s.className === homeroomClass));
48
  setSchedules(scheds);
49
  setAttendance(atts);
50
- } else {
51
- // Non-homeroom teacher view logic could be added here
52
  }
 
 
 
53
  } catch (e) { console.error(e); }
54
  finally { setLoading(false); }
55
  };
56
 
57
  const handleSaveSchedule = async () => {
58
  if (!homeroomClass || !editingCell) return;
59
- if (!editForm.subject) return alert('请输入科目');
60
  try {
61
  await api.schedules.save({
62
  className: homeroomClass,
@@ -67,17 +101,68 @@ export const TeacherDashboard: React.FC = () => {
67
  weekType: editForm.weekType as any
68
  });
69
  setEditingCell(null);
70
- // Refresh
71
  const updated = await api.schedules.get({ className: homeroomClass });
72
  setSchedules(updated);
73
  } catch(e) { alert('保存失败'); }
74
  };
75
 
76
  const handleDeleteSchedule = async (s: Schedule) => {
77
- if(!confirm('确定删除此课程?')) return;
78
- await api.schedules.delete({ className: s.className, dayOfWeek: s.dayOfWeek, period: s.period });
79
- const updated = await api.schedules.get({ className: homeroomClass });
80
- setSchedules(updated);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  };
82
 
83
  // Stats
@@ -91,6 +176,15 @@ export const TeacherDashboard: React.FC = () => {
91
 
92
  return (
93
  <div className="space-y-6">
 
 
 
 
 
 
 
 
 
94
  <div className="flex flex-col md:flex-row justify-between items-center bg-white p-6 rounded-xl shadow-sm border border-gray-100">
95
  <div>
96
  <h1 className="text-2xl font-bold text-gray-800">你好,{currentUser?.trueName || currentUser?.username} 老师</h1>
@@ -104,9 +198,8 @@ export const TeacherDashboard: React.FC = () => {
104
  </div>
105
 
106
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
107
- {/* Left Column: Stats & Todo */}
108
  <div className="lg:col-span-2 space-y-6">
109
- {/* Attendance Stats */}
110
  {homeroomClass && (
111
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
112
  <h3 className="font-bold text-gray-800 mb-4 flex items-center"><Activity className="mr-2 text-blue-500"/> 今日考勤概览</h3>
@@ -131,7 +224,6 @@ export const TeacherDashboard: React.FC = () => {
131
  </div>
132
  )}
133
 
134
- {/* Warnings */}
135
  {homeroomClass && (absentCount > 0 || leaveCount > 0) && (
136
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
137
  <h3 className="font-bold text-gray-800 mb-4 flex items-center"><AlertTriangle className="mr-2 text-amber-500"/> 缺勤/请假名单</h3>
@@ -167,80 +259,84 @@ export const TeacherDashboard: React.FC = () => {
167
  {showSchedule && homeroomClass && (
168
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
169
  <div className="bg-white rounded-xl w-full max-w-6xl h-[90vh] flex flex-col shadow-2xl animate-in zoom-in-95">
170
- <div className="p-4 border-b flex justify-between items-center">
171
  <div className="flex items-center gap-4">
172
- <h3 className="text-xl font-bold flex items-center"><Calendar className="mr-2 text-blue-600"/> 智能课表 ({homeroomClass})</h3>
173
- <div className="flex bg-gray-100 p-1 rounded-lg text-xs font-bold">
174
  {['ALL','ODD','EVEN'].map(t => (
175
  <button
176
  key={t}
177
  onClick={() => setWeekType(t as any)}
178
- className={`px-3 py-1.5 rounded transition-all ${weekType === t ? 'bg-white shadow text-blue-600' : 'text-gray-500'}`}
179
  >
180
  {t === 'ALL' ? '全周' : t === 'ODD' ? '单周' : '双周'}
181
  </button>
182
  ))}
183
  </div>
184
  </div>
185
- <button onClick={() => setShowSchedule(false)}><X size={24} className="text-gray-400 hover:text-gray-600"/></button>
 
 
 
 
 
186
  </div>
187
 
188
  <div className="flex-1 overflow-auto bg-gray-50 p-4">
189
- <table className="w-full border-collapse bg-white shadow-sm rounded-lg overflow-hidden">
190
  <thead>
191
- <tr className="bg-gray-100 text-gray-500 text-sm">
192
- <th className="p-3 border w-20">节次</th>
193
- {['周一','周二','周三','周四','周五'].map(d=><th key={d} className="p-3 border w-[18%]">{d}</th>)}
194
  </tr>
195
  </thead>
196
  <tbody>
197
- {Array.from({length: periodCount}).map((_, idx) => {
198
- const period = idx + 1;
199
- return (
200
- <tr key={period}>
201
- <td className="border p-3 text-center text-sm font-bold text-gray-600 bg-gray-50">第{period}节</td>
202
- {[1,2,3,4,5].map(day => {
203
- // Filter relevant schedules based on week type
204
- const slotItems = schedules.filter(s =>
205
- s.dayOfWeek === day &&
206
- s.period === period &&
207
- (s.weekType === 'ALL' || s.weekType === weekType || weekType === 'ALL')
208
- );
209
-
210
- return (
211
- <td key={day} className="border p-1 align-top h-24 relative group hover:bg-blue-50 transition-colors" onClick={() => {
212
- setEditingCell({day, period});
213
- setEditForm({subject:'', teacherName:'', weekType: weekType === 'ALL' ? 'ALL' : weekType});
214
- }}>
215
- <div className="flex flex-col gap-1 h-full">
216
- {slotItems.map(item => (
217
- <div
218
- key={item._id}
219
- className="p-1 rounded text-xs border relative group/item"
220
- style={{backgroundColor: stringToColor(item.subject), borderColor: 'rgba(0,0,0,0.1)'}}
221
- onClick={(e) => { e.stopPropagation(); }}
222
- >
223
- <div className="font-bold text-gray-800">{item.subject}</div>
224
- <div className="flex justify-between items-center text-[10px] text-gray-600 mt-0.5">
225
- <span>{item.teacherName}</span>
226
- {item.weekType !== 'ALL' && <span className="bg-white/50 px-1 rounded">{item.weekType==='ODD'?'单':'双'}</span>}
227
- </div>
228
- <button
229
- onClick={(e) => { e.stopPropagation(); handleDeleteSchedule(item); }}
230
- className="absolute top-0 right-0 p-0.5 text-red-500 opacity-0 group-hover/item:opacity-100"
231
- >
232
- <X size={12}/>
233
- </button>
234
  </div>
235
- ))}
236
- {slotItems.length === 0 && <div className="h-full flex items-center justify-center opacity-0 group-hover:opacity-50"><Plus size={20} className="text-blue-300"/></div>}
237
- </div>
238
- </td>
239
- );
240
- })}
241
- </tr>
242
- );
243
- })}
 
 
 
 
 
 
244
  </tbody>
245
  </table>
246
  </div>
@@ -248,36 +344,113 @@ export const TeacherDashboard: React.FC = () => {
248
  </div>
249
  )}
250
 
251
- {/* Edit Modal */}
252
  {editingCell && (
253
- <div className="fixed inset-0 bg-black/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
254
- <div className="bg-white p-6 rounded-xl shadow-2xl w-80 animate-in zoom-in-95">
255
- <h4 className="font-bold mb-4 text-gray-800">编辑课程</h4>
256
- <div className="space-y-3">
257
  <div>
258
- <label className="text-xs font-bold text-gray-500">科目</label>
259
- <input className="w-full border rounded p-2 text-sm" value={editForm.subject} onChange={e=>setEditForm({...editForm, subject: e.target.value})} autoFocus/>
 
 
 
 
 
 
 
 
260
  </div>
261
  <div>
262
- <label className="text-xs font-bold text-gray-500">教师 (可选)</label>
263
- <input className="w-full border rounded p-2 text-sm" value={editForm.teacherName} onChange={e=>setEditForm({...editForm, teacherName: e.target.value})}/>
 
 
 
 
 
 
 
264
  </div>
265
  <div>
266
- <label className="text-xs font-bold text-gray-500">周次类型</label>
267
- <select className="w-full border rounded p-2 text-sm" value={editForm.weekType} onChange={e=>setEditForm({...editForm, weekType: e.target.value})}>
268
- <option value="ALL">每周</option>
269
- <option value="ODD">单周</option>
270
- <option value="EVEN">双周</option>
271
- </select>
 
 
 
 
 
 
 
 
272
  </div>
273
- <div className="flex gap-2 pt-2">
274
- <button onClick={handleSaveSchedule} className="flex-1 bg-blue-600 text-white py-2 rounded font-bold">保存</button>
275
- <button onClick={()=>setEditingCell(null)} className="flex-1 bg-gray-100 text-gray-700 py-2 rounded">取消</button>
276
  </div>
277
  </div>
278
  </div>
279
  </div>
280
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  </div>
282
  );
283
  };
 
1
 
2
  import React, { useEffect, useState } from 'react';
3
  import { api } from '../services/api';
4
+ import { Schedule, Student, Attendance, PeriodConfig, Subject, User } 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) => {
 
13
  return COLORS[Math.abs(hash) % COLORS.length];
14
  };
15
 
16
+ const DEFAULT_PERIODS: PeriodConfig[] = [
17
+ { period: 1, name: '第一节' },
18
+ { period: 2, name: '第二节' },
19
+ { period: 3, name: '第三节' },
20
+ { period: 4, name: '第四节' },
21
+ { period: 5, name: '第五节' },
22
+ { period: 6, name: '第六节' },
23
+ { period: 7, name: '第七节' },
24
+ { period: 8, name: '第八节' },
25
+ ];
26
+
27
  export const TeacherDashboard: React.FC = () => {
28
  const [loading, setLoading] = useState(true);
29
  const [schedules, setSchedules] = useState<Schedule[]>([]);
30
  const [students, setStudents] = useState<Student[]>([]);
31
  const [attendance, setAttendance] = useState<Attendance[]>([]);
32
 
33
+ // Resources for Select Inputs
34
+ const [subjects, setSubjects] = useState<Subject[]>([]);
35
+ const [teachers, setTeachers] = useState<User[]>([]);
36
+
37
  // Schedule State
38
  const [showSchedule, setShowSchedule] = useState(false);
39
+ const [periodConfig, setPeriodConfig] = useState<PeriodConfig[]>(DEFAULT_PERIODS);
40
+ const [weekType, setWeekType] = useState<'ALL' | 'ODD' | 'EVEN'>('ALL');
41
+
42
+ // Modals & Editing
43
  const [editingCell, setEditingCell] = useState<{day: number, period: number} | null>(null);
44
  const [editForm, setEditForm] = useState({ subject: '', teacherName: '', weekType: 'ALL' });
45
+ const [isPeriodSettingsOpen, setIsPeriodSettingsOpen] = useState(false);
46
+ const [tempPeriodConfig, setTempPeriodConfig] = useState<PeriodConfig[]>([]);
47
+
48
+ // Confirm Modal State
49
+ const [confirmModal, setConfirmModal] = useState<{isOpen: boolean, title: string, message: string, onConfirm: () => void, isDanger?: boolean}>({
50
+ isOpen: false, title: '', message: '', onConfirm: () => {}
51
+ });
52
 
53
  const currentUser = api.auth.getCurrentUser();
54
  const homeroomClass = currentUser?.homeroomClass;
 
62
  try {
63
  const todayStr = new Date().toISOString().split('T')[0];
64
 
65
+ const [stus, scheds, atts, subs, usrs, cfg] = await Promise.all([
66
  api.students.getAll(),
67
  homeroomClass ? api.schedules.get({ className: homeroomClass }) : Promise.resolve([]),
68
+ homeroomClass ? api.attendance.get({ className: homeroomClass, date: todayStr }) : Promise.resolve([]),
69
+ api.subjects.getAll(),
70
+ api.users.getAll({ role: 'TEACHER' }),
71
+ api.config.getPublic()
72
  ]);
73
 
74
+ // Load Global Config for Periods
75
+ if (cfg && cfg.periodConfig && cfg.periodConfig.length > 0) {
76
+ setPeriodConfig(cfg.periodConfig);
77
+ }
78
+
79
  if (homeroomClass) {
80
  setStudents(stus.filter((s: Student) => s.className === homeroomClass));
81
  setSchedules(scheds);
82
  setAttendance(atts);
 
 
83
  }
84
+ setSubjects(subs);
85
+ setTeachers(usrs);
86
+
87
  } catch (e) { console.error(e); }
88
  finally { setLoading(false); }
89
  };
90
 
91
  const handleSaveSchedule = async () => {
92
  if (!homeroomClass || !editingCell) return;
93
+ if (!editForm.subject) return alert('请选择科目');
94
  try {
95
  await api.schedules.save({
96
  className: homeroomClass,
 
101
  weekType: editForm.weekType as any
102
  });
103
  setEditingCell(null);
 
104
  const updated = await api.schedules.get({ className: homeroomClass });
105
  setSchedules(updated);
106
  } catch(e) { alert('保存失败'); }
107
  };
108
 
109
  const handleDeleteSchedule = async (s: Schedule) => {
110
+ setConfirmModal({
111
+ isOpen: true,
112
+ title: '删除课程',
113
+ message: `确定要删除 ${s.subject} 吗?`,
114
+ isDanger: true,
115
+ onConfirm: async () => {
116
+ await api.schedules.delete({ className: s.className, dayOfWeek: s.dayOfWeek, period: s.period });
117
+ const updated = await api.schedules.get({ className: homeroomClass });
118
+ setSchedules(updated);
119
+ }
120
+ });
121
+ };
122
+
123
+ // --- Period Config Logic ---
124
+ const openPeriodSettings = () => {
125
+ setTempPeriodConfig(JSON.parse(JSON.stringify(periodConfig))); // Deep copy
126
+ setIsPeriodSettingsOpen(true);
127
+ };
128
+
129
+ const savePeriodSettings = async () => {
130
+ // Validate
131
+ if (tempPeriodConfig.some(p => !p.name.trim())) return alert('节次名称不能为空');
132
+
133
+ try {
134
+ // Fetch current config to merge, avoiding overwrite other fields
135
+ const currentConfig = await api.config.get();
136
+ await api.config.save({
137
+ ...currentConfig,
138
+ periodConfig: tempPeriodConfig
139
+ });
140
+ setPeriodConfig(tempPeriodConfig);
141
+ setIsPeriodSettingsOpen(false);
142
+ alert('节次设置已保存 (全校生效)');
143
+ } catch (e) {
144
+ alert('保存失败');
145
+ }
146
+ };
147
+
148
+ const updateTempPeriod = (index: number, field: keyof PeriodConfig, value: any) => {
149
+ const newConfig = [...tempPeriodConfig];
150
+ newConfig[index] = { ...newConfig[index], [field]: value };
151
+ setTempPeriodConfig(newConfig);
152
+ };
153
+
154
+ const addPeriod = () => {
155
+ const nextPeriodNum = tempPeriodConfig.length + 1;
156
+ setTempPeriodConfig([...tempPeriodConfig, { period: nextPeriodNum, name: `第${nextPeriodNum}节` }]);
157
+ };
158
+
159
+ const removePeriod = (index: number) => {
160
+ const newConfig = tempPeriodConfig.filter((_, i) => i !== index);
161
+ // Re-index period numbers strictly? Or keep IDs?
162
+ // Usually cleaner to re-index for display consistency, but keeping ID is safer for DB.
163
+ // Here we re-index purely for the logic of "Period 1, 2, 3..."
164
+ const reIndexed = newConfig.map((p, i) => ({ ...p, period: i + 1 }));
165
+ setTempPeriodConfig(reIndexed);
166
  };
167
 
168
  // Stats
 
176
 
177
  return (
178
  <div className="space-y-6">
179
+ <ConfirmModal
180
+ isOpen={confirmModal.isOpen}
181
+ title={confirmModal.title}
182
+ message={confirmModal.message}
183
+ onClose={()=>setConfirmModal({...confirmModal, isOpen: false})}
184
+ onConfirm={confirmModal.onConfirm}
185
+ isDanger={confirmModal.isDanger}
186
+ />
187
+
188
  <div className="flex flex-col md:flex-row justify-between items-center bg-white p-6 rounded-xl shadow-sm border border-gray-100">
189
  <div>
190
  <h1 className="text-2xl font-bold text-gray-800">你好,{currentUser?.trueName || currentUser?.username} 老师</h1>
 
198
  </div>
199
 
200
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
201
+ {/* Left Column: Stats */}
202
  <div className="lg:col-span-2 space-y-6">
 
203
  {homeroomClass && (
204
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
205
  <h3 className="font-bold text-gray-800 mb-4 flex items-center"><Activity className="mr-2 text-blue-500"/> 今日考勤概览</h3>
 
224
  </div>
225
  )}
226
 
 
227
  {homeroomClass && (absentCount > 0 || leaveCount > 0) && (
228
  <div className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm">
229
  <h3 className="font-bold text-gray-800 mb-4 flex items-center"><AlertTriangle className="mr-2 text-amber-500"/> 缺勤/请假名单</h3>
 
259
  {showSchedule && homeroomClass && (
260
  <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
261
  <div className="bg-white rounded-xl w-full max-w-6xl h-[90vh] flex flex-col shadow-2xl animate-in zoom-in-95">
262
+ <div className="p-4 border-b flex justify-between items-center bg-gray-50 rounded-t-xl">
263
  <div className="flex items-center gap-4">
264
+ <h3 className="text-xl font-bold flex items-center text-gray-800"><Calendar className="mr-2 text-blue-600"/> 智能课表 ({homeroomClass})</h3>
265
+ <div className="flex bg-white border p-1 rounded-lg text-xs font-bold shadow-sm">
266
  {['ALL','ODD','EVEN'].map(t => (
267
  <button
268
  key={t}
269
  onClick={() => setWeekType(t as any)}
270
+ className={`px-3 py-1.5 rounded transition-all ${weekType === t ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:bg-gray-50'}`}
271
  >
272
  {t === 'ALL' ? '全周' : t === 'ODD' ? '单周' : '双周'}
273
  </button>
274
  ))}
275
  </div>
276
  </div>
277
+ <div className="flex gap-2">
278
+ <button onClick={openPeriodSettings} className="flex items-center px-3 py-2 bg-white border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 shadow-sm transition-colors">
279
+ <Settings size={16} className="mr-1"/> 节次设置
280
+ </button>
281
+ <button onClick={() => setShowSchedule(false)} className="text-gray-400 hover:text-gray-600 hover:bg-gray-200 p-2 rounded-lg transition-colors"><X size={24}/></button>
282
+ </div>
283
  </div>
284
 
285
  <div className="flex-1 overflow-auto bg-gray-50 p-4">
286
+ <table className="w-full border-collapse bg-white shadow-sm rounded-lg overflow-hidden ring-1 ring-gray-200">
287
  <thead>
288
+ <tr className="bg-gray-100 text-gray-600 text-sm uppercase tracking-wider">
289
+ <th className="p-3 border-b border-r w-24 bg-gray-50 font-bold">节次</th>
290
+ {['周一','周二','周三','周四','周五'].map(d=><th key={d} className="p-3 border-b border-r w-[18%] font-bold">{d}</th>)}
291
  </tr>
292
  </thead>
293
  <tbody>
294
+ {periodConfig.map((pConfig) => (
295
+ <tr key={pConfig.period}>
296
+ <td className="border-b border-r p-3 text-center text-sm font-bold text-gray-700 bg-gray-50/50">
297
+ {pConfig.name}
298
+ <div className="text-[10px] text-gray-400 font-normal mt-1">{pConfig.startTime ? `${pConfig.startTime}-${pConfig.endTime}` : ''}</div>
299
+ </td>
300
+ {[1,2,3,4,5].map(day => {
301
+ const slotItems = schedules.filter(s =>
302
+ s.dayOfWeek === day &&
303
+ s.period === pConfig.period &&
304
+ (s.weekType === 'ALL' || s.weekType === weekType || weekType === 'ALL')
305
+ );
306
+
307
+ return (
308
+ <td key={day} className="border-b border-r p-1 align-top h-24 relative group hover:bg-blue-50/50 transition-colors cursor-pointer" onClick={() => {
309
+ setEditingCell({day, period: pConfig.period});
310
+ setEditForm({subject:'', teacherName:'', weekType: weekType === 'ALL' ? 'ALL' : weekType});
311
+ }}>
312
+ <div className="flex flex-col gap-1 h-full">
313
+ {slotItems.map(item => (
314
+ <div
315
+ key={item._id}
316
+ className="p-1.5 rounded-md text-xs border shadow-sm relative group/item transition-transform hover:scale-[1.02]"
317
+ style={{backgroundColor: stringToColor(item.subject), borderColor: 'rgba(0,0,0,0.05)'}}
318
+ onClick={(e) => { e.stopPropagation(); }}
319
+ >
320
+ <div className="font-bold text-gray-800 truncate">{item.subject}</div>
321
+ <div className="flex justify-between items-center text-[10px] text-gray-600 mt-0.5">
322
+ <span className="truncate max-w-[60px]">{item.teacherName}</span>
323
+ {item.weekType !== 'ALL' && <span className="bg-white/60 px-1 rounded text-[9px] font-bold border border-black/5">{item.weekType==='ODD'?'单':'双'}</span>}
 
 
 
 
 
 
 
324
  </div>
325
+ <button
326
+ onClick={(e) => { e.stopPropagation(); handleDeleteSchedule(item); }}
327
+ className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full p-0.5 opacity-0 group-hover/item:opacity-100 shadow-sm transition-opacity"
328
+ >
329
+ <X size={10}/>
330
+ </button>
331
+ </div>
332
+ ))}
333
+ {slotItems.length === 0 && <div className="h-full flex items-center justify-center opacity-0 group-hover:opacity-40 text-blue-400"><Plus size={24}/></div>}
334
+ </div>
335
+ </td>
336
+ );
337
+ })}
338
+ </tr>
339
+ ))}
340
  </tbody>
341
  </table>
342
  </div>
 
344
  </div>
345
  )}
346
 
347
+ {/* Edit Schedule Modal */}
348
  {editingCell && (
349
+ <div className="fixed inset-0 bg-black/30 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
350
+ <div className="bg-white p-6 rounded-xl shadow-2xl w-full max-w-sm animate-in zoom-in-95">
351
+ <h4 className="font-bold mb-4 text-gray-800 text-lg border-b pb-2">编辑课程信息</h4>
352
+ <div className="space-y-4">
353
  <div>
354
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-1">科目</label>
355
+ <select
356
+ 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"
357
+ value={editForm.subject}
358
+ onChange={e=>setEditForm({...editForm, subject: e.target.value})}
359
+ autoFocus
360
+ >
361
+ <option value="">-- 请选择科目 --</option>
362
+ {subjects.map(s => <option key={s._id} value={s.name}>{s.name}</option>)}
363
+ </select>
364
  </div>
365
  <div>
366
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-1">任课教师 (可选)</label>
367
+ <select
368
+ 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"
369
+ value={editForm.teacherName}
370
+ onChange={e=>setEditForm({...editForm, teacherName: e.target.value})}
371
+ >
372
+ <option value="">-- 未指定 --</option>
373
+ {teachers.map(t => <option key={t._id} value={t.trueName || t.username}>{t.trueName || t.username} {t.teachingSubject ? `(${t.teachingSubject})` : ''}</option>)}
374
+ </select>
375
  </div>
376
  <div>
377
+ <label className="text-xs font-bold text-gray-500 uppercase block mb-1">周次类型</label>
378
+ <div className="flex gap-2 bg-gray-100 p-1 rounded-lg">
379
+ {[
380
+ {v:'ALL', l:'每周'}, {v:'ODD', l:'单周'}, {v:'EVEN', l:'双周'}
381
+ ].map(opt => (
382
+ <button
383
+ key={opt.v}
384
+ onClick={() => setEditForm({...editForm, weekType: opt.v})}
385
+ className={`flex-1 py-1.5 text-xs rounded font-medium transition-all ${editForm.weekType===opt.v ? 'bg-white shadow text-blue-600' : 'text-gray-500'}`}
386
+ >
387
+ {opt.l}
388
+ </button>
389
+ ))}
390
+ </div>
391
  </div>
392
+ <div className="flex gap-3 pt-4">
393
+ <button onClick={()=>setEditingCell(null)} className="flex-1 bg-gray-100 text-gray-700 py-2.5 rounded-lg hover:bg-gray-200 font-medium transition-colors">取消</button>
394
+ <button onClick={handleSaveSchedule} className="flex-1 bg-blue-600 text-white py-2.5 rounded-lg hover:bg-blue-700 font-bold shadow-md transition-colors">保存</button>
395
  </div>
396
  </div>
397
  </div>
398
  </div>
399
  )}
400
+
401
+ {/* Period Settings Modal */}
402
+ {isPeriodSettingsOpen && (
403
+ <div className="fixed inset-0 bg-black/30 z-[110] flex items-center justify-center p-4 backdrop-blur-sm">
404
+ <div className="bg-white rounded-xl w-full max-w-md shadow-2xl animate-in zoom-in-95 flex flex-col max-h-[80vh]">
405
+ <div className="p-4 border-b flex justify-between items-center bg-gray-50 rounded-t-xl">
406
+ <h3 className="font-bold text-gray-800">节次与作息设置</h3>
407
+ <button onClick={()=>setIsPeriodSettingsOpen(false)}><X size={20} className="text-gray-400 hover:text-gray-600"/></button>
408
+ </div>
409
+ <div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
410
+ <div className="space-y-2">
411
+ {tempPeriodConfig.map((p, idx) => (
412
+ <div key={idx} className="flex items-center gap-2 mb-2 p-2 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors bg-white">
413
+ <div className="w-6 text-center text-xs font-bold text-gray-400">{idx + 1}</div>
414
+ <input
415
+ className="flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:border-blue-500 outline-none"
416
+ value={p.name}
417
+ onChange={e => updateTempPeriod(idx, 'name', e.target.value)}
418
+ placeholder="节次名称"
419
+ />
420
+ {/* Optional Time Inputs */}
421
+ <div className="flex items-center gap-1">
422
+ <Clock size={12} className="text-gray-400"/>
423
+ <input
424
+ type="time"
425
+ className="w-16 border border-gray-300 rounded px-1 py-0.5 text-xs text-gray-600"
426
+ value={p.startTime || ''}
427
+ onChange={e => updateTempPeriod(idx, 'startTime', e.target.value)}
428
+ />
429
+ <span className="text-gray-300">-</span>
430
+ <input
431
+ type="time"
432
+ className="w-16 border border-gray-300 rounded px-1 py-0.5 text-xs text-gray-600"
433
+ value={p.endTime || ''}
434
+ onChange={e => updateTempPeriod(idx, 'endTime', e.target.value)}
435
+ />
436
+ </div>
437
+ <button onClick={() => removePeriod(idx)} className="text-gray-300 hover:text-red-500 p-1"><Trash2 size={16}/></button>
438
+ </div>
439
+ ))}
440
+ </div>
441
+ <button onClick={addPeriod} className="w-full mt-4 py-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 font-medium transition-colors flex items-center justify-center gap-2">
442
+ <Plus size={16}/> 添加新节次
443
+ </button>
444
+ </div>
445
+ <div className="p-4 border-t bg-gray-50 rounded-b-xl flex justify-end gap-3">
446
+ <button onClick={()=>setIsPeriodSettingsOpen(false)} className="px-4 py-2 text-gray-600 hover:bg-gray-200 rounded-lg text-sm font-medium transition-colors">取消</button>
447
+ <button onClick={savePeriodSettings} className="px-6 py-2 bg-blue-600 text-white rounded-lg text-sm font-bold hover:bg-blue-700 shadow-md transition-colors flex items-center">
448
+ <Save size={16} className="mr-2"/> 保存设置
449
+ </button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ )}
454
  </div>
455
  );
456
  };