diff --git a/client/src/pages/admin/Users.tsx b/client/src/pages/admin/Users.tsx index e8133e3..72064af 100644 --- a/client/src/pages/admin/Users.tsx +++ b/client/src/pages/admin/Users.tsx @@ -112,6 +112,11 @@ export default function Users() { group_ids: x.group_ids ?? [], group_names: x.group_names ?? [], })); + filled.sort((a, b) => { + const c = (a.class || '').localeCompare(b.class || '', 'zh-Hans-CN', { numeric: true }); + if (c !== 0) return c; + return (a.name || '').localeCompare(b.name || '', 'zh-Hans-CN'); + }); setList(filled); setGroups(g); } finally { @@ -188,36 +193,80 @@ export default function Users() { const rows: BatchParsedRow[] = []; const lines = batchText.split(/\r?\n/); const groupMap = new Map(); - for (const g of groups) groupMap.set(g.name.trim(), g.id); + const groupNameNormalized = new Map(); + for (const g of groups) { + const key = g.name.trim(); + groupMap.set(key, g.id); + groupNameNormalized.set(key, g.name); + groupMap.set(g.name, g.id); + groupNameNormalized.set(g.name, g.name); + const noSpace = key.replace(/\s+/g, ''); + if (noSpace !== key) { + groupMap.set(noSpace, g.id); + groupNameNormalized.set(noSpace, g.name); + } + } + + const splitGroupNames = (s: string): string[] => { + const result: string[] = []; + if (!s) return result; + const segs = s.split(/[\/、||,,;;\s\u3000]+/); + for (const seg of segs) { + const n = seg.trim().replace(/\s+/g, ''); + if (n) result.push(n); + } + return result; + }; + for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line.trim()) continue; - const parts = line.split(/[,,\t|;;]/).map((p) => p.trim()); - const [cls, name, volunteer_no = '', groupsStr = ''] = parts; + let parts: string[]; + if (line.includes('\t')) { + parts = line.split(/\t/).map((p) => p.trim()); + } else { + parts = line.split(/[,,;;]/).map((p) => p.trim()); + } + const [cls = '', name = '', volunteer_no = '', ...rest] = parts; + const groupsStr = rest.join(','); const errors: string[] = []; if (!cls) errors.push('缺少班级'); if (!name) errors.push('缺少姓名'); const groupIds: number[] = []; const groupNames: string[] = []; - if (groupsStr) { - const rawNames = groupsStr.split(/[/、|\s]+/).map((x) => x.trim()).filter(Boolean); - for (const n of rawNames) { - const gid = groupMap.get(n); - if (gid === undefined) { - errors.push(`工作组「${n}」不存在`); - } else { - if (!groupIds.includes(gid)) { - groupIds.push(gid); - groupNames.push(n); - } + const rawNames = splitGroupNames([volunteer_no, groupsStr].filter(Boolean).join('/')); + const volunteerLooksLikeNo = /^\d+$/.test(volunteer_no.trim()); + const realRawNames: string[] = []; + if (volunteer_no.trim() && !volunteerLooksLikeNo) { + const probe = splitGroupNames(volunteer_no); + const allMatch = probe.length > 0 && probe.every((n) => groupMap.has(n)); + if (allMatch) { + realRawNames.push(...probe); + } else { + realRawNames.push(...splitGroupNames(groupsStr)); + } + } else { + realRawNames.push(...splitGroupNames(groupsStr)); + } + for (const n of realRawNames) { + if (!n) continue; + const gid = groupMap.get(n); + if (gid === undefined) { + errors.push(`工作组「${n}」不存在`); + } else { + if (!groupIds.includes(gid)) { + groupIds.push(gid); + const display = groupNameNormalized.get(n) || n; + if (!groupNames.includes(display)) groupNames.push(display); } } } + const vol = volunteerLooksLikeNo ? volunteer_no : ''; rows.push({ idx: i, class: cls || '', name: name || '', - volunteer_no, + volunteer_no: vol, group_ids: groupIds, group_names: groupNames, raw: line,