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 && ( + + )}