dvc890 commited on
Commit
21df6a2
·
verified ·
1 Parent(s): 0c25fe8

Upload 45 files

Browse files
Files changed (1) hide show
  1. pages/AchievementTeacher.tsx +128 -78
pages/AchievementTeacher.tsx CHANGED
@@ -36,18 +36,15 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
36
 
37
  useEffect(() => {
38
  loadData();
39
- }, [className]); // Reload when className changes
40
 
41
  const loadData = async () => {
42
  if (!homeroomClass) return;
43
  setLoading(true);
 
 
44
  try {
45
- const [cfg, stus, sysCfg] = await Promise.all([
46
- api.achievements.getConfig(homeroomClass),
47
- api.students.getAll(),
48
- api.config.getPublic() as Promise<SystemConfig>
49
- ]);
50
-
51
  // Filter students for homeroom & Sort by SeatNo > Name
52
  const sortedStudents = stus
53
  .filter((s: Student) => s.className === homeroomClass)
@@ -57,34 +54,65 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
57
  if (seatA !== seatB) return seatA - seatB;
58
  return a.name.localeCompare(b.name, 'zh-CN');
59
  });
60
-
61
  setStudents(sortedStudents);
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- setConfig(cfg || {
 
64
  schoolId: currentUser?.schoolId || '',
65
  className: homeroomClass,
66
  achievements: [],
67
  exchangeRules: []
68
- });
 
 
 
69
 
70
- setSemester(sysCfg.semester || '当前学期');
 
71
 
72
- } catch (e) { console.error(e); }
 
 
 
 
 
 
 
 
 
73
  finally { setLoading(false); }
74
  };
75
 
76
  const handleSaveConfig = async (newConfig: AchievementConfig) => {
 
 
77
  try {
78
  await api.achievements.saveConfig(newConfig);
79
- setConfig(newConfig);
80
- } catch (e) { alert('保存失败'); }
 
 
81
  };
82
 
83
  // --- Tab 1: Manage Achievements ---
84
  const addAchievement = () => {
85
- if (!config || !newAchieve.name) return;
 
 
86
  const newItem = { ...newAchieve, id: Date.now().toString() };
87
  const updated = { ...config, achievements: [...config.achievements, newItem] };
 
88
  handleSaveConfig(updated);
89
  setNewAchieve({ id: '', name: '', icon: '🏆', points: 1 });
90
  };
@@ -111,9 +139,12 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
111
 
112
  // --- Tab 3: Exchange Rules ---
113
  const addRule = () => {
114
- if (!config || !newRule.rewardName) return;
 
 
115
  const newItem = { ...newRule, id: Date.now().toString() };
116
  const updated = { ...config, exchangeRules: [...config.exchangeRules, newItem] };
 
117
  handleSaveConfig(updated);
118
  setNewRule({ id: '', cost: 10, rewardType: 'DRAW_COUNT', rewardName: '抽奖券x1', rewardValue: 1 });
119
  };
@@ -137,20 +168,23 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
137
  if (!homeroomClass) return <div className="p-10 text-center text-gray-500">您不是班主任,无法使用成就管理功能。</div>;
138
  if (loading) return <div className="flex justify-center p-10"><Loader2 className="animate-spin text-blue-600"/></div>;
139
 
 
 
 
140
  return (
141
  <div className="h-full flex flex-col bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
142
  {/* Header / Tabs */}
143
- <div className="flex border-b border-gray-100 bg-gray-50/50">
144
- <button onClick={() => setActiveTab('manage')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${activeTab === 'manage' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
145
  <Award size={18}/> 成就库管理
146
  </button>
147
- <button onClick={() => setActiveTab('grant')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${activeTab === 'grant' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
148
  <Gift size={18}/> 发放成就
149
  </button>
150
- <button onClick={() => setActiveTab('exchange')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${activeTab === 'exchange' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
151
  <Coins size={18}/> 兑换规则
152
  </button>
153
- <button onClick={() => setActiveTab('balance')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${activeTab === 'balance' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
154
  <Users size={18}/> 学生积分
155
  </button>
156
  </div>
@@ -162,35 +196,39 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
162
  <div className="bg-blue-50 p-4 rounded-xl border border-blue-100 flex flex-col md:flex-row gap-4 items-end">
163
  <div className="flex-1 w-full">
164
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">成就名称</label>
165
- <input className="w-full border rounded p-2" placeholder="如: 劳动小能手" value={newAchieve.name} onChange={e => setNewAchieve({...newAchieve, name: e.target.value})} />
166
  </div>
167
  <div className="w-full md:w-32">
168
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">分值 (小红花)</label>
169
- <input type="number" min={1} className="w-full border rounded p-2" value={newAchieve.points} onChange={e => setNewAchieve({...newAchieve, points: Number(e.target.value)})} />
170
  </div>
171
  <div className="w-full md:w-auto">
172
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">图标预设</label>
173
- <select className="w-full border rounded p-2 min-w-[100px]" value={newAchieve.icon} onChange={e => setNewAchieve({...newAchieve, icon: e.target.value})}>
174
  {PRESET_ICONS.map(p => <option key={p.label} value={p.icon}>{p.icon} {p.label}</option>)}
175
  </select>
176
  </div>
177
- <button onClick={addAchievement} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-blue-700 whitespace-nowrap">添加成就</button>
178
  </div>
179
 
180
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
181
- {config?.achievements.map(ach => (
182
- <div key={ach.id} className="border border-gray-200 rounded-xl p-4 flex flex-col items-center relative group hover:shadow-md transition-all bg-white">
183
- <div className="text-4xl mb-2"><Emoji symbol={ach.icon} /></div>
184
- <div className="font-bold text-gray-800 text-center">{ach.name}</div>
185
- <div className="text-xs text-amber-600 font-bold bg-amber-50 px-2 py-0.5 rounded-full mt-1 border border-amber-100">
186
- {ach.points} <Emoji symbol="🌺" size={14}/>
 
 
 
 
 
 
 
187
  </div>
188
- <button onClick={() => deleteAchievement(ach.id)} className="absolute top-2 right-2 text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
189
- <Trash2 size={16}/>
190
- </button>
191
- </div>
192
- ))}
193
- </div>
194
  </div>
195
  )}
196
 
@@ -203,42 +241,50 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
203
  <button onClick={() => setSelectedStudents(new Set(selectedStudents.size === students.length ? [] : students.map(s => s._id || String(s.id))))} className="text-xs text-blue-600 mb-2 text-left hover:underline">
204
  {selectedStudents.size === students.length ? '取消全选' : '全选所有'}
205
  </button>
206
- <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar max-h-[500px]">
207
- {students.map(s => (
208
- <div
209
- key={s._id}
210
- onClick={() => {
211
- const newSet = new Set(selectedStudents);
212
- const sid = s._id || String(s.id);
213
- if (newSet.has(sid)) newSet.delete(sid);
214
- else newSet.add(sid);
215
- setSelectedStudents(newSet);
216
- }}
217
- className={`p-2 rounded border cursor-pointer flex items-center justify-between ${selectedStudents.has(s._id || String(s.id)) ? 'bg-blue-50 border-blue-400' : 'bg-white border-gray-200 hover:bg-gray-50'}`}
218
- >
219
- <span className="text-sm font-medium">{s.seatNo ? s.seatNo+'.':''}{s.name}</span>
220
- {selectedStudents.has(s._id || String(s.id)) && <CheckCircle size={16} className="text-blue-500"/>}
221
- </div>
222
- ))}
223
- </div>
 
 
 
 
224
  </div>
225
 
226
  {/* Achievement Selector */}
227
  <div className="flex-1 flex flex-col">
228
  <h3 className="font-bold text-gray-700 mb-4">2. 选择要颁发的奖状</h3>
229
- <div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
230
- {config?.achievements.map(ach => (
231
- <div
232
- key={ach.id}
233
- onClick={() => setSelectedAchieveId(ach.id)}
234
- className={`p-4 rounded-xl border cursor-pointer text-center transition-all ${selectedAchieveId === ach.id ? 'bg-amber-50 border-amber-400 ring-2 ring-amber-200' : 'bg-white border-gray-200 hover:bg-gray-50'}`}
235
- >
236
- <div className="text-3xl mb-1"><Emoji symbol={ach.icon} /></div>
237
- <div className="font-bold text-sm text-gray-800">{ach.name}</div>
238
- <div className="text-xs text-gray-500">+{ach.points} <Emoji symbol="🌺" size={12}/></div>
239
- </div>
240
- ))}
241
- </div>
 
 
 
 
242
 
243
  <div className="mt-auto bg-gray-50 p-4 rounded-xl border border-gray-200">
244
  <div className="flex justify-between items-center mb-2">
@@ -259,28 +305,30 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
259
  <div className="bg-green-50 p-4 rounded-xl border border-green-100 flex flex-col md:flex-row gap-4 items-end">
260
  <div className="w-full md:w-32">
261
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">消耗小红花</label>
262
- <input type="number" className="w-full border rounded p-2" value={newRule.cost} onChange={e => setNewRule({...newRule,cost: Number(e.target.value)})}/>
263
  </div>
264
  <div className="w-full md:w-32">
265
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励类型</label>
266
- <select className="w-full border rounded p-2" value={newRule.rewardType} onChange={e => setNewRule({...newRule, rewardType: e.target.value as any})}>
267
  <option value="DRAW_COUNT">🎲 抽奖券</option>
268
  <option value="ITEM">🎁 实物/特权</option>
269
  </select>
270
  </div>
271
  <div className="flex-1 w-full">
272
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励名称</label>
273
- <input className="w-full border rounded p-2" placeholder="如: 免作业券" value={newRule.rewardName} onChange={e => setNewRule({...newRule, rewardName: e.target.value})}/>
274
  </div>
275
  <div className="w-24">
276
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">数量</label>
277
- <input type="number" min={1} className="w-full border rounded p-2" value={newRule.rewardValue} onChange={e => setNewRule({...newRule, rewardValue: Number(e.target.value)})}/>
278
  </div>
279
- <button onClick={addRule} className="bg-green-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-green-700 whitespace-nowrap">添加规则</button>
280
  </div>
281
 
282
  <div className="space-y-3">
283
- {config?.exchangeRules.map(rule => (
 
 
284
  <div key={rule.id} className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-xl hover:shadow-sm">
285
  <div className="flex items-center gap-4">
286
  <div className="bg-amber-100 text-amber-700 font-bold px-3 py-1 rounded-lg border border-amber-200">
@@ -314,7 +362,9 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
314
  </tr>
315
  </thead>
316
  <tbody className="divide-y divide-gray-100">
317
- {students.map(s => (
 
 
318
  <tr key={s._id} className="hover:bg-gray-50">
319
  <td className="p-4 font-bold text-gray-700">{s.seatNo ? s.seatNo+'.':''}{s.name}</td>
320
  <td className="p-4">
@@ -324,7 +374,7 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
324
  </td>
325
  <td className="p-4 text-right">
326
  <div className="flex justify-end gap-2">
327
- {config?.exchangeRules.map(r => (
328
  <button
329
  key={r.id}
330
  disabled={(s.flowerBalance || 0) < r.cost}
@@ -334,7 +384,7 @@ export const AchievementTeacher: React.FC<{className?: string}> = ({ className }
334
  >
335
  兑换 {r.rewardName} (-{r.cost})
336
  </button>
337
- ))}
338
  </div>
339
  </td>
340
  </tr>
 
36
 
37
  useEffect(() => {
38
  loadData();
39
+ }, [homeroomClass]); // Reload when homeroomClass changes
40
 
41
  const loadData = async () => {
42
  if (!homeroomClass) return;
43
  setLoading(true);
44
+
45
+ // 1. Load Students (Independent)
46
  try {
47
+ const stus = await api.students.getAll();
 
 
 
 
 
48
  // Filter students for homeroom & Sort by SeatNo > Name
49
  const sortedStudents = stus
50
  .filter((s: Student) => s.className === homeroomClass)
 
54
  if (seatA !== seatB) return seatA - seatB;
55
  return a.name.localeCompare(b.name, 'zh-CN');
56
  });
 
57
  setStudents(sortedStudents);
58
+ } catch (e) {
59
+ console.error("Failed to load students", e);
60
+ }
61
+
62
+ // 2. Load Config & System (Independent)
63
+ try {
64
+ // Use allSettled or catch individual promises to avoid blocking
65
+ const cfgPromise = api.achievements.getConfig(homeroomClass).catch(() => null);
66
+ const sysPromise = api.config.getPublic().catch(() => ({ semester: '当前学期' }));
67
+
68
+ const [cfg, sysCfg] = await Promise.all([cfgPromise, sysPromise]);
69
 
70
+ // Critical: Always ensure a config object exists, even if API returns null/error
71
+ const defaultConfig: AchievementConfig = {
72
  schoolId: currentUser?.schoolId || '',
73
  className: homeroomClass,
74
  achievements: [],
75
  exchangeRules: []
76
+ };
77
+
78
+ // If cfg is null or empty, use default
79
+ setConfig(cfg || defaultConfig);
80
 
81
+ // @ts-ignore
82
+ setSemester(sysCfg?.semester || '当前学期');
83
 
84
+ } catch (e) {
85
+ console.error("Failed to load config", e);
86
+ // Fallback just in case
87
+ setConfig({
88
+ schoolId: currentUser?.schoolId || '',
89
+ className: homeroomClass,
90
+ achievements: [],
91
+ exchangeRules: []
92
+ });
93
+ }
94
  finally { setLoading(false); }
95
  };
96
 
97
  const handleSaveConfig = async (newConfig: AchievementConfig) => {
98
+ // Optimistic update
99
+ setConfig(newConfig);
100
  try {
101
  await api.achievements.saveConfig(newConfig);
102
+ } catch (e) {
103
+ alert('保存失败,请检查网络');
104
+ loadData(); // Revert on fail
105
+ }
106
  };
107
 
108
  // --- Tab 1: Manage Achievements ---
109
  const addAchievement = () => {
110
+ if (!config) return;
111
+ if (!newAchieve.name) return alert('请输入成就名称');
112
+
113
  const newItem = { ...newAchieve, id: Date.now().toString() };
114
  const updated = { ...config, achievements: [...config.achievements, newItem] };
115
+
116
  handleSaveConfig(updated);
117
  setNewAchieve({ id: '', name: '', icon: '🏆', points: 1 });
118
  };
 
139
 
140
  // --- Tab 3: Exchange Rules ---
141
  const addRule = () => {
142
+ if (!config) return;
143
+ if (!newRule.rewardName) return alert('请输入奖励名称');
144
+
145
  const newItem = { ...newRule, id: Date.now().toString() };
146
  const updated = { ...config, exchangeRules: [...config.exchangeRules, newItem] };
147
+
148
  handleSaveConfig(updated);
149
  setNewRule({ id: '', cost: 10, rewardType: 'DRAW_COUNT', rewardName: '抽奖券x1', rewardValue: 1 });
150
  };
 
168
  if (!homeroomClass) return <div className="p-10 text-center text-gray-500">您不是班主任,无法使用成就管理功能。</div>;
169
  if (loading) return <div className="flex justify-center p-10"><Loader2 className="animate-spin text-blue-600"/></div>;
170
 
171
+ // Safety fallback render if config is somehow still null (shouldn't happen with new logic)
172
+ const safeConfig = config || { achievements: [], exchangeRules: [] };
173
+
174
  return (
175
  <div className="h-full flex flex-col bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
176
  {/* Header / Tabs */}
177
+ <div className="flex border-b border-gray-100 bg-gray-50/50 overflow-x-auto">
178
+ <button onClick={() => setActiveTab('manage')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${activeTab === 'manage' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
179
  <Award size={18}/> 成就库管理
180
  </button>
181
+ <button onClick={() => setActiveTab('grant')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${activeTab === 'grant' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
182
  <Gift size={18}/> 发放成就
183
  </button>
184
+ <button onClick={() => setActiveTab('exchange')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${activeTab === 'exchange' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
185
  <Coins size={18}/> 兑换规则
186
  </button>
187
+ <button onClick={() => setActiveTab('balance')} className={`px-6 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${activeTab === 'balance' ? 'border-blue-500 text-blue-600 bg-white' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
188
  <Users size={18}/> 学生积分
189
  </button>
190
  </div>
 
196
  <div className="bg-blue-50 p-4 rounded-xl border border-blue-100 flex flex-col md:flex-row gap-4 items-end">
197
  <div className="flex-1 w-full">
198
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">成就名称</label>
199
+ <input className="w-full border rounded p-2 text-sm" placeholder="如: 劳动小能手" value={newAchieve.name} onChange={e => setNewAchieve({...newAchieve, name: e.target.value})} />
200
  </div>
201
  <div className="w-full md:w-32">
202
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">分值 (小红花)</label>
203
+ <input type="number" min={1} className="w-full border rounded p-2 text-sm" value={newAchieve.points} onChange={e => setNewAchieve({...newAchieve, points: Number(e.target.value)})} />
204
  </div>
205
  <div className="w-full md:w-auto">
206
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">图标预设</label>
207
+ <select className="w-full border rounded p-2 min-w-[100px] text-sm bg-white" value={newAchieve.icon} onChange={e => setNewAchieve({...newAchieve, icon: e.target.value})}>
208
  {PRESET_ICONS.map(p => <option key={p.label} value={p.icon}>{p.icon} {p.label}</option>)}
209
  </select>
210
  </div>
211
+ <button onClick={addAchievement} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-blue-700 whitespace-nowrap text-sm">添加成就</button>
212
  </div>
213
 
214
+ {safeConfig.achievements.length === 0 ? (
215
+ <div className="text-center text-gray-400 py-10">暂无成就,请添加</div>
216
+ ) : (
217
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
218
+ {safeConfig.achievements.map(ach => (
219
+ <div key={ach.id} className="border border-gray-200 rounded-xl p-4 flex flex-col items-center relative group hover:shadow-md transition-all bg-white">
220
+ <div className="text-4xl mb-2"><Emoji symbol={ach.icon} /></div>
221
+ <div className="font-bold text-gray-800 text-center">{ach.name}</div>
222
+ <div className="text-xs text-amber-600 font-bold bg-amber-50 px-2 py-0.5 rounded-full mt-1 border border-amber-100">
223
+ {ach.points} <Emoji symbol="🌺" size={14}/>
224
+ </div>
225
+ <button onClick={() => deleteAchievement(ach.id)} className="absolute top-2 right-2 text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
226
+ <Trash2 size={16}/>
227
+ </button>
228
  </div>
229
+ ))}
230
+ </div>
231
+ )}
 
 
 
232
  </div>
233
  )}
234
 
 
241
  <button onClick={() => setSelectedStudents(new Set(selectedStudents.size === students.length ? [] : students.map(s => s._id || String(s.id))))} className="text-xs text-blue-600 mb-2 text-left hover:underline">
242
  {selectedStudents.size === students.length ? '取消全选' : '全选所有'}
243
  </button>
244
+ {students.length === 0 ? (
245
+ <div className="text-sm text-gray-400">班级暂无学生数据</div>
246
+ ) : (
247
+ <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar max-h-[500px]">
248
+ {students.map(s => (
249
+ <div
250
+ key={s._id}
251
+ onClick={() => {
252
+ const newSet = new Set(selectedStudents);
253
+ const sid = s._id || String(s.id);
254
+ if (newSet.has(sid)) newSet.delete(sid);
255
+ else newSet.add(sid);
256
+ setSelectedStudents(newSet);
257
+ }}
258
+ className={`p-2 rounded border cursor-pointer flex items-center justify-between transition-colors ${selectedStudents.has(s._id || String(s.id)) ? 'bg-blue-50 border-blue-400' : 'bg-white border-gray-200 hover:bg-gray-50'}`}
259
+ >
260
+ <span className="text-sm font-medium">{s.seatNo ? s.seatNo+'.':''}{s.name}</span>
261
+ {selectedStudents.has(s._id || String(s.id)) && <CheckCircle size={16} className="text-blue-500"/>}
262
+ </div>
263
+ ))}
264
+ </div>
265
+ )}
266
  </div>
267
 
268
  {/* Achievement Selector */}
269
  <div className="flex-1 flex flex-col">
270
  <h3 className="font-bold text-gray-700 mb-4">2. 选择要颁发的奖状</h3>
271
+ {safeConfig.achievements.length === 0 ? (
272
+ <div className="text-gray-400 text-sm mb-6">暂无成就,请先去“成就库管理”添加。</div>
273
+ ) : (
274
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6 max-h-[400px] overflow-y-auto custom-scrollbar p-1">
275
+ {safeConfig.achievements.map(ach => (
276
+ <div
277
+ key={ach.id}
278
+ onClick={() => setSelectedAchieveId(ach.id)}
279
+ className={`p-4 rounded-xl border cursor-pointer text-center transition-all ${selectedAchieveId === ach.id ? 'bg-amber-50 border-amber-400 ring-2 ring-amber-200' : 'bg-white border-gray-200 hover:bg-gray-50'}`}
280
+ >
281
+ <div className="text-3xl mb-1"><Emoji symbol={ach.icon} /></div>
282
+ <div className="font-bold text-sm text-gray-800">{ach.name}</div>
283
+ <div className="text-xs text-gray-500">+{ach.points} <Emoji symbol="🌺" size={12}/></div>
284
+ </div>
285
+ ))}
286
+ </div>
287
+ )}
288
 
289
  <div className="mt-auto bg-gray-50 p-4 rounded-xl border border-gray-200">
290
  <div className="flex justify-between items-center mb-2">
 
305
  <div className="bg-green-50 p-4 rounded-xl border border-green-100 flex flex-col md:flex-row gap-4 items-end">
306
  <div className="w-full md:w-32">
307
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">消耗小红花</label>
308
+ <input type="number" min={1} className="w-full border rounded p-2 text-sm" value={newRule.cost} onChange={e => setNewRule({...newRule,cost: Number(e.target.value)})}/>
309
  </div>
310
  <div className="w-full md:w-32">
311
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励类型</label>
312
+ <select className="w-full border rounded p-2 text-sm bg-white" value={newRule.rewardType} onChange={e => setNewRule({...newRule, rewardType: e.target.value as any})}>
313
  <option value="DRAW_COUNT">🎲 抽奖券</option>
314
  <option value="ITEM">🎁 实物/特权</option>
315
  </select>
316
  </div>
317
  <div className="flex-1 w-full">
318
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">奖励名称</label>
319
+ <input className="w-full border rounded p-2 text-sm" placeholder="如: 免作业券" value={newRule.rewardName} onChange={e => setNewRule({...newRule, rewardName: e.target.value})}/>
320
  </div>
321
  <div className="w-24">
322
  <label className="text-xs font-bold text-gray-500 uppercase mb-1 block">数量</label>
323
+ <input type="number" min={1} className="w-full border rounded p-2 text-sm" value={newRule.rewardValue} onChange={e => setNewRule({...newRule, rewardValue: Number(e.target.value)})}/>
324
  </div>
325
+ <button onClick={addRule} className="bg-green-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-green-700 whitespace-nowrap text-sm">添加规则</button>
326
  </div>
327
 
328
  <div className="space-y-3">
329
+ {safeConfig.exchangeRules.length === 0 ? (
330
+ <div className="text-center text-gray-400 py-4">暂无兑换规则</div>
331
+ ) : safeConfig.exchangeRules.map(rule => (
332
  <div key={rule.id} className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-xl hover:shadow-sm">
333
  <div className="flex items-center gap-4">
334
  <div className="bg-amber-100 text-amber-700 font-bold px-3 py-1 rounded-lg border border-amber-200">
 
362
  </tr>
363
  </thead>
364
  <tbody className="divide-y divide-gray-100">
365
+ {students.length === 0 ? (
366
+ <tr><td colSpan={3} className="p-4 text-center text-gray-400">暂无学生数据</td></tr>
367
+ ) : students.map(s => (
368
  <tr key={s._id} className="hover:bg-gray-50">
369
  <td className="p-4 font-bold text-gray-700">{s.seatNo ? s.seatNo+'.':''}{s.name}</td>
370
  <td className="p-4">
 
374
  </td>
375
  <td className="p-4 text-right">
376
  <div className="flex justify-end gap-2">
377
+ {safeConfig.exchangeRules.length > 0 ? safeConfig.exchangeRules.map(r => (
378
  <button
379
  key={r.id}
380
  disabled={(s.flowerBalance || 0) < r.cost}
 
384
  >
385
  兑换 {r.rewardName} (-{r.cost})
386
  </button>
387
+ )) : <span className="text-gray-300 text-xs">无兑换规则</span>}
388
  </div>
389
  </td>
390
  </tr>