dvc890 commited on
Commit
fe20223
·
verified ·
1 Parent(s): 54156ef

Update pages/MyClass.tsx

Browse files
Files changed (1) hide show
  1. pages/MyClass.tsx +123 -118
pages/MyClass.tsx CHANGED
@@ -1,15 +1,13 @@
1
 
2
- import React, { useState, useEffect, useRef } from 'react';
3
  import { api } from '../services/api';
4
  import { Student, ClassInfo } from '../types';
5
  import {
6
  Users, Grid, ArrowLeftRight, Save, Wand2, Eye, EyeOff, Smile, Frown, Meh,
7
- Ruler, Search, Info, Edit, Upload, FileSpreadsheet, Loader2, AlertTriangle,
8
- Settings, CheckSquare, Square, Group, User, UserPlus
9
  } from 'lucide-react';
10
  import { Toast, ToastState } from '../components/Toast';
11
- import { ConfirmModal } from '../components/ConfirmModal';
12
- import { Emoji } from '../components/Emoji';
13
 
14
  // Algorithm Rule Types
15
  type SeatingRule = 'VISION' | 'HEIGHT' | 'GENDER_MIX' | 'CHAR_COMP' | 'CHAR_SEP' | 'SCORE_HELP';
@@ -23,8 +21,8 @@ export const MyClass: React.FC = () => {
23
  // Seating Configuration
24
  const [rows, setRows] = useState(6);
25
  const [cols, setCols] = useState(8);
26
- const [groupSize, setGroupSize] = useState<number>(2); // 1, 2, 3, 4
27
- const [activeRules, setActiveRules] = useState<Set<SeatingRule>>(new Set(['VISION', 'HEIGHT', 'GENDER_MIX']));
28
 
29
  const [draggedStudentId, setDraggedStudentId] = useState<string | null>(null);
30
 
@@ -68,8 +66,8 @@ export const MyClass: React.FC = () => {
68
  if (cls) {
69
  setMyClassInfo(cls);
70
  if (cls.seatingConfig) {
71
- setRows(cls.seatingConfig.rows);
72
- setCols(cls.seatingConfig.cols);
73
  }
74
  }
75
  } catch (e) { console.error(e); }
@@ -204,143 +202,149 @@ export const MyClass: React.FC = () => {
204
  };
205
 
206
  /**
207
- * CORE ALGORITHM: Smart Seating
 
 
208
  */
209
  const autoArrange = () => {
210
  if (!confirm('确定要自动重新排座吗?当前所有座位将被重置。')) return;
211
 
212
  let pool = [...students];
213
 
214
- // 1. Calculate Base Priority Score (Who sits in front?)
215
- // Lower score = Better Seat (Front)
216
- const getBaseScore = (s: Student) => {
217
  let score = 0;
218
- // Vision: Severe(-1000), Mild(-500) -> Ensures they are picked first
219
  if (activeRules.has('VISION')) {
220
- if (s.vision === 'Near-Severe') score -= 1000;
221
- if (s.vision === 'Near-Mild') score -= 500;
222
  }
223
- // Height: Short(-200 ~ -100)
 
 
224
  if (activeRules.has('HEIGHT') && s.height) {
225
- // Assuming height 100-200. We want shorter people first.
226
- // Height 120 -> Score +120. Height 180 -> Score +180.
227
- // So lower height = lower score = earlier pick.
228
- score += s.height;
229
  }
230
  return score;
231
  };
232
 
233
- // Sort pool by Base Priority (Ascending: Lowest score first)
234
- pool.sort((a, b) => getBaseScore(a) - getBaseScore(b));
235
 
236
  const arranged: Student[] = [];
237
  const totalSeats = rows * cols;
238
- const grid: (Student | null)[] = new Array(totalSeats).fill(null);
239
 
240
- // 2. Fill Grid Group by Group
241
  let currentIdx = 0;
242
 
243
  while (currentIdx < totalSeats && pool.length > 0) {
244
- // How many seats in this group? (Handle edge case of end of row)
245
- const currentRow = Math.floor(currentIdx / cols);
246
- const colInRow = currentIdx % cols;
247
- const remainingInRow = cols - colInRow;
248
- const currentGroupSize = Math.min(groupSize, remainingInRow, pool.length);
249
-
250
- // If it's a single seat group or only 1 student left, just pick the top priority
251
- if (currentGroupSize === 1) {
 
 
 
 
252
  const s = pool.shift()!;
253
- grid[currentIdx] = s;
254
  currentIdx++;
255
  continue;
256
  }
257
 
258
- // --- Pairing Logic for Groups (Size > 1) ---
259
- // Take the "Anchor" (highest priority student)
260
- const anchor = pool.shift()!;
261
- const groupMembers = [anchor];
 
262
 
263
- // Find best matches for the Anchor from the next few candidates
264
- // We look ahead a bit (e.g., next 12 students) to find a good match without ruining the height/vision order too much.
265
- const lookAhead = 12;
266
-
267
- for (let i = 0; i < currentGroupSize - 1; i++) {
268
- if (pool.length === 0) break;
269
 
270
- let bestMatchIdx = -1;
271
- let bestMatchScore = -Infinity;
272
 
273
- // Search window
274
- const searchLimit = Math.min(pool.length, lookAhead);
275
-
276
- for (let j = 0; j < searchLimit; j++) {
277
- const candidate = pool[j];
278
- let matchScore = 0;
279
-
280
- // Evaluate compatibility with ALL current group members
281
- for (const member of groupMembers) {
282
- // Gender Mix: Different gender = Good
283
- if (activeRules.has('GENDER_MIX') && member.gender !== candidate.gender) {
284
- matchScore += 50;
285
- }
286
- // Char Complementary: Active + Quiet = Good
287
- if (activeRules.has('CHAR_COMP')) {
288
- if ((member.character === 'Active' && candidate.character === 'Quiet') ||
289
- (member.character === 'Quiet' && candidate.character === 'Active')) {
290
- matchScore += 40;
 
 
 
 
 
 
 
 
 
 
291
  }
292
  }
293
- // Char Separation: Active + Active = Bad, Naughty + Naughty = Bad
294
- if (activeRules.has('CHAR_SEP')) {
295
- if (member.character === 'Active' && candidate.character === 'Active') matchScore -= 50;
296
- if (member.character === 'Naughty' && candidate.character === 'Naughty') matchScore -= 100;
297
- if ((member.character === 'Naughty' && candidate.character === 'Active') ||
298
- (member.character === 'Active' && candidate.character === 'Naughty')) matchScore -= 60;
299
- }
300
- // Score Help: High + Low = Good (Use flowerBalance)
301
- if (activeRules.has('SCORE_HELP')) {
302
- const diff = Math.abs((member.flowerBalance || 0) - (candidate.flowerBalance || 0));
303
- if (diff > 50) matchScore += 30; // Significant difference implies potential help
304
  }
305
  }
306
 
307
- // Penalize skipping ahead (keep height order roughly)
308
- matchScore -= (j * 5);
309
-
310
- if (matchScore > bestMatchScore) {
311
- bestMatchScore = matchScore;
312
- bestMatchIdx = j;
313
  }
314
  }
315
 
316
- // Pick the best match
317
- if (bestMatchIdx !== -1) {
318
- const partner = pool.splice(bestMatchIdx, 1)[0];
319
- groupMembers.push(partner);
320
- } else {
321
- // Fallback: just take next one
322
- groupMembers.push(pool.shift()!);
323
- }
 
 
324
  }
325
-
326
- // Assign to Grid
327
- groupMembers.forEach((m, i) => {
328
- grid[currentIdx + i] = m;
329
- });
330
- currentIdx += groupMembers.length;
331
  }
332
 
333
- // 3. Convert Grid Index to Row/Col
334
  const finalArrangement = students.map(s => {
335
- const idx = grid.findIndex(g => g?._id === s._id);
336
- if (idx === -1) return { ...s, seatRow: undefined, seatCol: undefined }; // Should not happen for pool members
337
- const r = Math.floor(idx / cols) + 1;
338
- const c = (idx % cols) + 1;
339
  return { ...s, seatRow: r, seatCol: c };
340
  });
341
 
342
  setStudents(finalArrangement);
343
- setToast({ show: true, message: '智能分组排座完成', type: 'success' });
344
  };
345
 
346
  const saveSeating = async () => {
@@ -504,19 +508,19 @@ export const MyClass: React.FC = () => {
504
  <div className="h-full flex flex-col md:flex-row bg-gray-100">
505
  {/* Left: Tools & Rules */}
506
  <div className="w-full md:w-80 bg-white border-r border-gray-200 flex flex-col shadow-sm z-20">
507
- <div className="p-4 border-b border-gray-100 flex-1 overflow-y-auto">
508
  <h3 className="font-bold text-gray-800 mb-3 flex items-center"><Settings size={18} className="mr-2 text-blue-600"/> 布局与规则</h3>
509
 
510
  {/* Layout Config */}
511
- <div className="mb-4 bg-gray-50 p-3 rounded-lg">
512
- <div className="flex gap-2 mb-2">
513
  <div className="flex-1">
514
  <label className="text-xs text-gray-500 mb-1 block">行数</label>
515
- <input className="w-full border rounded px-2 py-1 text-center font-bold text-sm" type="number" min={1} value={rows} onChange={e=>setRows(Number(e.target.value))}/>
516
  </div>
517
  <div className="flex-1">
518
  <label className="text-xs text-gray-500 mb-1 block">列数</label>
519
- <input className="w-full border rounded px-2 py-1 text-center font-bold text-sm" type="number" min={1} value={cols} onChange={e=>setCols(Number(e.target.value))}/>
520
  </div>
521
  </div>
522
  <div className="flex items-center justify-between">
@@ -549,19 +553,19 @@ export const MyClass: React.FC = () => {
549
  <div
550
  key={rule.key}
551
  onClick={() => toggleRule(rule.key as SeatingRule)}
552
- className={`flex items-center p-2 rounded cursor-pointer border transition-all ${activeRules.has(rule.key as SeatingRule) ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:border-gray-300'}`}
553
  >
554
- <div className={`w-4 h-4 rounded border flex items-center justify-center mr-2 ${activeRules.has(rule.key as SeatingRule) ? 'bg-blue-500 border-blue-500 text-white' : 'bg-white border-gray-300'}`}>
555
  {activeRules.has(rule.key as SeatingRule) && <CheckSquare size={10}/>}
556
  </div>
557
- <rule.icon size={14} className={`mr-2 ${activeRules.has(rule.key as SeatingRule) ? 'text-blue-600' : 'text-gray-400'}`}/>
558
- <span className={`text-xs font-medium ${activeRules.has(rule.key as SeatingRule) ? 'text-blue-700' : 'text-gray-600'}`}>{rule.label}</span>
559
  </div>
560
  ))}
561
  </div>
562
 
563
- <button onClick={autoArrange} className="w-full mt-4 bg-gradient-to-r from-purple-600 to-indigo-600 text-white py-2.5 rounded-lg font-bold text-sm shadow-md hover:shadow-lg transition-all flex items-center justify-center gap-2">
564
- <Wand2 size={16}/> 智能生成座位表
565
  </button>
566
  </div>
567
 
@@ -569,7 +573,7 @@ export const MyClass: React.FC = () => {
569
  <h4 className="text-xs font-bold text-gray-500 uppercase mb-2 flex justify-between">
570
  未排座 ({unseatedStudents.length})
571
  </h4>
572
- <div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto custom-scrollbar">
573
  {unseatedStudents.map(s => (
574
  <div
575
  key={s._id}
@@ -587,8 +591,8 @@ export const MyClass: React.FC = () => {
587
  {/* Center: Grid */}
588
  <div className="flex-1 p-8 overflow-auto flex flex-col items-center bg-gray-100/50">
589
  {/* Blackboard */}
590
- <div className="w-[60%] h-16 bg-gray-700 rounded-b-xl mb-12 flex items-center justify-center shadow-lg text-gray-300 font-bold tracking-[1em] text-lg border-b-4 border-gray-600 shrink-0">
591
- 讲台 (TEACHER)
592
  </div>
593
 
594
  <div
@@ -600,9 +604,10 @@ export const MyClass: React.FC = () => {
600
  const c = (idx % cols) + 1;
601
  const student = getStudentAt(r, c);
602
 
603
- // Grouping Logic for Visuals
 
604
  const isEndOfGroup = c % groupSize === 0 && c !== cols;
605
- const marginRight = isEndOfGroup ? '24px' : '0px';
606
 
607
  return (
608
  <div
@@ -635,7 +640,7 @@ export const MyClass: React.FC = () => {
635
  </div>
636
  </div>
637
  ) : (
638
- <span className="text-xs text-gray-300 font-bold">空</span>
639
  )}
640
  </div>
641
  );
 
1
 
2
+ import React, { useState, useEffect } from 'react';
3
  import { api } from '../services/api';
4
  import { Student, ClassInfo } from '../types';
5
  import {
6
  Users, Grid, ArrowLeftRight, Save, Wand2, Eye, EyeOff, Smile, Frown, Meh,
7
+ Ruler, Search, Edit, FileSpreadsheet, Loader2, AlertTriangle,
8
+ Settings, CheckSquare, Square, UserPlus, Zap
9
  } from 'lucide-react';
10
  import { Toast, ToastState } from '../components/Toast';
 
 
11
 
12
  // Algorithm Rule Types
13
  type SeatingRule = 'VISION' | 'HEIGHT' | 'GENDER_MIX' | 'CHAR_COMP' | 'CHAR_SEP' | 'SCORE_HELP';
 
21
  // Seating Configuration
22
  const [rows, setRows] = useState(6);
23
  const [cols, setCols] = useState(8);
24
+ const [groupSize, setGroupSize] = useState<number>(1); // Default 1
25
+ const [activeRules, setActiveRules] = useState<Set<SeatingRule>>(new Set(['VISION', 'HEIGHT']));
26
 
27
  const [draggedStudentId, setDraggedStudentId] = useState<string | null>(null);
28
 
 
66
  if (cls) {
67
  setMyClassInfo(cls);
68
  if (cls.seatingConfig) {
69
+ setRows(cls.seatingConfig.rows || 6);
70
+ setCols(cls.seatingConfig.cols || 8);
71
  }
72
  }
73
  } catch (e) { console.error(e); }
 
202
  };
203
 
204
  /**
205
+ * 🧠 CORE ALGORITHM: Smart Weighted Seating 🧠
206
+ * 1. Calculates a "Priority Score" for front-row necessity (Vision, Height).
207
+ * 2. Uses a "Greedy Lookahead" strategy to find best pairings based on constraints (Gender, Character).
208
  */
209
  const autoArrange = () => {
210
  if (!confirm('确定要自动重新排座吗?当前所有座位将被重置。')) return;
211
 
212
  let pool = [...students];
213
 
214
+ // 1. Calculate Priority Score (Higher = Needs to be closer to front)
215
+ const getPriorityScore = (s: Student) => {
 
216
  let score = 0;
217
+ // Vision: Severe(+200), Mild(+100)
218
  if (activeRules.has('VISION')) {
219
+ if (s.vision === 'Near-Severe') score += 200;
220
+ if (s.vision === 'Near-Mild') score += 100;
221
  }
222
+ // Height: Shorter (+Score)
223
+ // Assumed range 100-200. Shorter people need front.
224
+ // 120cm -> +80 pts. 180cm -> +20 pts.
225
  if (activeRules.has('HEIGHT') && s.height) {
226
+ score += (200 - s.height);
 
 
 
227
  }
228
  return score;
229
  };
230
 
231
+ // Sort pool by Priority Descending (Highest priority first)
232
+ pool.sort((a, b) => getPriorityScore(b) - getPriorityScore(a));
233
 
234
  const arranged: Student[] = [];
235
  const totalSeats = rows * cols;
236
+ const gridMap: Record<string, Student> = {}; // key: "r-c"
237
 
 
238
  let currentIdx = 0;
239
 
240
  while (currentIdx < totalSeats && pool.length > 0) {
241
+ // Determine grid position
242
+ const r = Math.floor(currentIdx / cols) + 1;
243
+ const c = (currentIdx % cols) + 1;
244
+ const key = `${r}-${c}`;
245
+
246
+ // Check if we are at the start of a group
247
+ // Columns are 1-based. (c-1) % groupSize === 0 means start of a group slot.
248
+ const isGroupStart = (c - 1) % groupSize === 0;
249
+ const slotsInGroup = Math.min(groupSize, cols - (c - 1)); // Handle end of row
250
+
251
+ // If groupSize is 1, just pick top priority
252
+ if (groupSize === 1) {
253
  const s = pool.shift()!;
254
+ gridMap[key] = s;
255
  currentIdx++;
256
  continue;
257
  }
258
 
259
+ // --- Multi-Student Pairing Logic ---
260
+ if (isGroupStart) {
261
+ // 1. Pick Anchor (Highest Priority Student)
262
+ const anchor = pool.shift()!;
263
+ const groupMembers = [anchor];
264
 
265
+ // 2. Find partners
266
+ // Search next 12 students to find best match without breaking priority too much
267
+ const searchLimit = Math.min(pool.length, 12);
268
+
269
+ for (let k = 0; k < slotsInGroup - 1; k++) {
270
+ if (pool.length === 0) break;
271
 
272
+ let bestMatchIdx = -1;
273
+ let bestMatchScore = -Infinity;
274
 
275
+ for (let j = 0; j < searchLimit; j++) {
276
+ const candidate = pool[j];
277
+ let matchScore = 0;
278
+
279
+ // Evaluate compatibility with ALL current group members
280
+ for (const member of groupMembers) {
281
+ // Rule: Gender Mix (+20)
282
+ if (activeRules.has('GENDER_MIX') && member.gender !== candidate.gender) {
283
+ matchScore += 20;
284
+ }
285
+ // Rule: Character Complementary (+15)
286
+ if (activeRules.has('CHAR_COMP')) {
287
+ if ((member.character === 'Active' && candidate.character === 'Quiet') ||
288
+ (member.character === 'Quiet' && candidate.character === 'Active')) {
289
+ matchScore += 15;
290
+ }
291
+ }
292
+ // Rule: Character Separation (-30)
293
+ if (activeRules.has('CHAR_SEP')) {
294
+ if (member.character === 'Active' && candidate.character === 'Active') matchScore -= 30;
295
+ if (member.character === 'Naughty' && candidate.character === 'Naughty') matchScore -= 50;
296
+ if ((member.character === 'Naughty' && candidate.character === 'Active') ||
297
+ (member.character === 'Active' && candidate.character === 'Naughty')) matchScore -= 30;
298
+ }
299
+ // Rule: Score Help (+10) (Mock logic using flowerBalance as proxy for "Score")
300
+ if (activeRules.has('SCORE_HELP')) {
301
+ const diff = Math.abs((member.flowerBalance || 0) - (candidate.flowerBalance || 0));
302
+ if (diff > 50) matchScore += 10;
303
  }
304
  }
305
+
306
+ // Penalty for skipping ahead (to maintain height/vision order)
307
+ matchScore -= (j * 2);
308
+
309
+ if (matchScore > bestMatchScore) {
310
+ bestMatchScore = matchScore;
311
+ bestMatchIdx = j;
 
 
 
 
312
  }
313
  }
314
 
315
+ // Pick match
316
+ if (bestMatchIdx !== -1) {
317
+ groupMembers.push(pool.splice(bestMatchIdx, 1)[0]);
318
+ } else {
319
+ // Fallback
320
+ groupMembers.push(pool.shift()!);
321
  }
322
  }
323
 
324
+ // Assign positions
325
+ groupMembers.forEach((m, idx) => {
326
+ gridMap[`${r}-${c + idx}`] = m;
327
+ });
328
+
329
+ // Skip indices for the filled group
330
+ currentIdx += groupMembers.length;
331
+ } else {
332
+ // Should not happen if loop logic is correct, but safe fallback
333
+ currentIdx++;
334
  }
 
 
 
 
 
 
335
  }
336
 
337
+ // Apply to state
338
  const finalArrangement = students.map(s => {
339
+ const entry = Object.entries(gridMap).find(([_, stu]) => stu._id === s._id);
340
+ if (!entry) return { ...s, seatRow: undefined, seatCol: undefined };
341
+ const [pos] = entry;
342
+ const [r, c] = pos.split('-').map(Number);
343
  return { ...s, seatRow: r, seatCol: c };
344
  });
345
 
346
  setStudents(finalArrangement);
347
+ setToast({ show: true, message: `智能排座完成!应用规则: ${Array.from(activeRules).length}项`, type: 'success' });
348
  };
349
 
350
  const saveSeating = async () => {
 
508
  <div className="h-full flex flex-col md:flex-row bg-gray-100">
509
  {/* Left: Tools & Rules */}
510
  <div className="w-full md:w-80 bg-white border-r border-gray-200 flex flex-col shadow-sm z-20">
511
+ <div className="p-4 border-b border-gray-100 flex-1 overflow-y-auto custom-scrollbar">
512
  <h3 className="font-bold text-gray-800 mb-3 flex items-center"><Settings size={18} className="mr-2 text-blue-600"/> 布局与规则</h3>
513
 
514
  {/* Layout Config */}
515
+ <div className="mb-4 bg-gray-50 p-3 rounded-lg border border-gray-200">
516
+ <div className="flex gap-2 mb-3">
517
  <div className="flex-1">
518
  <label className="text-xs text-gray-500 mb-1 block">行数</label>
519
+ <input className="w-full border rounded px-2 py-1.5 text-center font-bold text-sm bg-white" type="number" min={1} value={rows} onChange={e=>setRows(Number(e.target.value))}/>
520
  </div>
521
  <div className="flex-1">
522
  <label className="text-xs text-gray-500 mb-1 block">列数</label>
523
+ <input className="w-full border rounded px-2 py-1.5 text-center font-bold text-sm bg-white" type="number" min={1} value={cols} onChange={e=>setCols(Number(e.target.value))}/>
524
  </div>
525
  </div>
526
  <div className="flex items-center justify-between">
 
553
  <div
554
  key={rule.key}
555
  onClick={() => toggleRule(rule.key as SeatingRule)}
556
+ className={`flex items-center p-2.5 rounded cursor-pointer border transition-all ${activeRules.has(rule.key as SeatingRule) ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:border-gray-300'}`}
557
  >
558
+ <div className={`w-4 h-4 rounded border flex items-center justify-center mr-3 transition-colors ${activeRules.has(rule.key as SeatingRule) ? 'bg-blue-500 border-blue-500 text-white' : 'bg-white border-gray-300'}`}>
559
  {activeRules.has(rule.key as SeatingRule) && <CheckSquare size={10}/>}
560
  </div>
561
+ <rule.icon size={16} className={`mr-2 ${activeRules.has(rule.key as SeatingRule) ? 'text-blue-600' : 'text-gray-400'}`}/>
562
+ <span className={`text-xs font-medium ${activeRules.has(rule.key as SeatingRule) ? 'text-blue-800' : 'text-gray-600'}`}>{rule.label}</span>
563
  </div>
564
  ))}
565
  </div>
566
 
567
+ <button onClick={autoArrange} className="w-full mt-6 bg-gradient-to-r from-purple-600 to-indigo-600 text-white py-3 rounded-lg font-bold text-sm shadow-md hover:shadow-lg transition-all flex items-center justify-center gap-2 transform active:scale-95">
568
+ <Wand2 size={18}/> 智能生成座位表
569
  </button>
570
  </div>
571
 
 
573
  <h4 className="text-xs font-bold text-gray-500 uppercase mb-2 flex justify-between">
574
  未排座 ({unseatedStudents.length})
575
  </h4>
576
+ <div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto custom-scrollbar content-start">
577
  {unseatedStudents.map(s => (
578
  <div
579
  key={s._id}
 
591
  {/* Center: Grid */}
592
  <div className="flex-1 p-8 overflow-auto flex flex-col items-center bg-gray-100/50">
593
  {/* Blackboard */}
594
+ <div className="w-[60%] h-16 bg-gray-700 rounded-b-xl mb-12 flex items-center justify-center shadow-lg text-gray-300 font-bold tracking-[1em] text-lg border-b-4 border-gray-600 shrink-0 select-none">
595
+ 讲台 ( T E A C H E R )
596
  </div>
597
 
598
  <div
 
604
  const c = (idx % cols) + 1;
605
  const student = getStudentAt(r, c);
606
 
607
+ // Visual Grouping Logic
608
+ // Add significant margin to the right of the group
609
  const isEndOfGroup = c % groupSize === 0 && c !== cols;
610
+ const marginRight = isEndOfGroup ? '32px' : '0px';
611
 
612
  return (
613
  <div
 
640
  </div>
641
  </div>
642
  ) : (
643
+ <span className="text-xs text-gray-300 font-bold select-none">空</span>
644
  )}
645
  </div>
646
  );