From 48ac08d19d29956e75dd66aed125cc3406126a70 Mon Sep 17 00:00:00 2001 From: Trae Assistant Date: Mon, 5 Oct 2026 08:27:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E4=B9=89=E5=B7=A5=E5=8F=B7/=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=BB=84=E4=B8=A2=E5=A4=B1=20+=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 批量解析:移除义工号强制纯数字校验(原 volunteerLooksLikeNo 逻辑错误导致 VOL-003、2024-A 等全部置空);改为列数驱动:3列时智能探测是否工作组命中,4+列保留义工号原值、剩余段统一合并为工作组解析 - 后端 batch:user_groups 插入语句改为事务内按用户编译;义工号判断改为 null/undefined 严格检查,避免真值判断丢弃空字符串或 0 开头的值 - 新增 DELETE /users/batch:事务性批量删除部员(含 user_groups/hours 级联),返回实际删除 count - 前端列表新增复选框列(表头 indeterminate 半选态),选中 1+ 条后工具栏显示「删除选中(N)」红色按钮,确认弹窗二次校验 --- client/src/api/index.ts | 6 ++ client/src/pages/admin/Users.tsx | 128 ++++++++++++++++++++++++++----- server/src/routes/users.ts | 41 +++++++++- 3 files changed, 151 insertions(+), 24 deletions(-) diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 91bc3bd..6becb56 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -65,6 +65,12 @@ export const usersApi = { failed: number; results: Array<{ idx: number; ok: boolean; id?: number; error?: string; item: any }>; }>('/users/batch', { items }).then(r => r.data), + batchRemove: (ids: number[]) => + api.request<{ ok: boolean; deleted: number; total: number }>({ + url: '/users/batch', + method: 'DELETE', + data: { ids }, + }).then(r => r.data), }; export const activitiesApi = { diff --git a/client/src/pages/admin/Users.tsx b/client/src/pages/admin/Users.tsx index 72064af..e244955 100644 --- a/client/src/pages/admin/Users.tsx +++ b/client/src/pages/admin/Users.tsx @@ -103,6 +103,8 @@ export default function Users() { const [batchText, setBatchText] = useState(''); const [batchErrors, setBatchErrors] = useState(''); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const load = async () => { setLoading(true); try { @@ -136,6 +138,41 @@ export default function Users() { ); }); + const filteredIds = filtered.map((u) => u.id); + const allSelected = filteredIds.length > 0 && filteredIds.every((id) => selectedIds.has(id)); + const someSelected = filteredIds.some((id) => selectedIds.has(id)); + const selectedCount = selectedIds.size; + + const toggleSelectAll = () => { + const next = new Set(selectedIds); + if (allSelected) { + for (const id of filteredIds) next.delete(id); + } else { + for (const id of filteredIds) next.add(id); + } + setSelectedIds(next); + }; + const toggleSelectOne = (id: number) => { + const next = new Set(selectedIds); + if (next.has(id)) next.delete(id); + else next.add(id); + setSelectedIds(next); + }; + const clearSelection = () => setSelectedIds(new Set()); + + const batchDelete = async () => { + if (selectedCount === 0) return; + if (!confirm(`确认删除选中的 ${selectedCount} 名部员?相关的时长登记也会一并删除,此操作不可撤销。`)) return; + try { + const result = await usersApi.batchRemove(Array.from(selectedIds)); + alert(`批量删除完成:共提交 ${result.total} 条,实际删除 ${result.deleted} 条`); + clearSelection(); + load(); + } catch (e) { + alert(errMsg(e)); + } + }; + const openNew = () => { setForm(emptyForm); setEditId(null); @@ -218,6 +255,11 @@ export default function Users() { return result; }; + const allGroupMatch = (names: string[]): boolean => { + if (names.length === 0) return false; + return names.every((n) => groupMap.has(n)); + }; + for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line.trim()) continue; @@ -227,28 +269,42 @@ export default function Users() { } else { parts = line.split(/[,,;;]/).map((p) => p.trim()); } - const [cls = '', name = '', volunteer_no = '', ...rest] = parts; - const groupsStr = rest.join(','); + const nonEmptyParts = parts.filter((p) => p.length > 0); + + let cls = ''; + let name = ''; + let volunteer_no = ''; + let groupsRaw = ''; + + if (nonEmptyParts.length >= 2) { + cls = parts[0] || ''; + name = parts[1] || ''; + if (nonEmptyParts.length === 2) { + volunteer_no = ''; + groupsRaw = ''; + } else if (nonEmptyParts.length === 3) { + const third = parts[2] || ''; + const probe = splitGroupNames(third); + if (allGroupMatch(probe)) { + volunteer_no = ''; + groupsRaw = third; + } else { + volunteer_no = third; + groupsRaw = ''; + } + } else { + volunteer_no = parts[2] || ''; + groupsRaw = parts.slice(3).join(','); + } + } + const errors: string[] = []; if (!cls) errors.push('缺少班级'); if (!name) errors.push('缺少姓名'); const groupIds: number[] = []; const groupNames: string[] = []; - 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) { + const rawNames = splitGroupNames(groupsRaw); + for (const n of rawNames) { if (!n) continue; const gid = groupMap.get(n); if (gid === undefined) { @@ -261,12 +317,11 @@ export default function Users() { } } } - const vol = volunteerLooksLikeNo ? volunteer_no : ''; rows.push({ idx: i, - class: cls || '', - name: name || '', - volunteer_no: vol, + class: cls, + name: name, + volunteer_no: volunteer_no, group_ids: groupIds, group_names: groupNames, raw: line, @@ -331,6 +386,13 @@ export default function Users() { className="md:max-w-md" />
+ {selectedCount > 0 && ( + + )}