From 7090c8efc22736b6635df7098ce7dbff455e9f8d Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Tue, 27 Oct 2026 15:56:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4=EF=BC=88?= =?UTF-8?q?=E5=A4=8D=E9=80=89=E6=A1=86=E5=A4=9A=E9=80=89+=E5=85=A8?= =?UTF-8?q?=E9=80=89=EF=BC=8C=E5=BD=93=E5=89=8D=E7=99=BB=E5=BD=95=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=9D=E6=8A=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/UserManage.tsx | 75 ++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/UserManage.tsx b/frontend/src/pages/UserManage.tsx index 54bf779..298ea56 100644 --- a/frontend/src/pages/UserManage.tsx +++ b/frontend/src/pages/UserManage.tsx @@ -12,7 +12,7 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api'; +import { apiClient, auth } from '@/lib/api'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; @@ -71,6 +71,47 @@ export default function UserManage() { const [ldapOpen, setLdapOpen] = useState(false); const [selected, setSelected] = useState>(new Set()); const [importResult, setImportResult] = useState(null); + // 批量删除 + const [batchIds, setBatchIds] = useState>(new Set()); + const [batchDeleting, setBatchDeleting] = useState(false); + const currentUsername = (() => { + try { + const token = auth.getToken() || ''; + const raw = atob(token.replace(/-/g, '+').replace(/_/g, '/')); + return raw.split('|')[0]; + } catch { return ''; } + })(); + + const toggleBatch = (id: number) => { + setBatchIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + const batchDelete = async () => { + const targets = users.filter((u: any) => batchIds.has(u.id)); + if (targets.length === 0) return; + const preview = targets.slice(0, 5).map((u: any) => u.username).join('、') + (targets.length > 5 ? ` 等 ${targets.length} 个` : ''); + if (!window.confirm(`确定删除 ${preview} 吗?`)) return; + setBatchDeleting(true); + let ok = 0; + const failed: string[] = []; + for (const u of targets) { + try { + await apiClient.deleteUser(u.id); + ok++; + } catch (err) { + failed.push(`${u.username}(${(err as Error).message})`); + } + } + setBatchDeleting(false); + setBatchIds(new Set()); + invalidateUsers(); + if (failed.length === 0) toast.success(`已删除 ${ok} 个用户`); + else toast.warning(`删除完成:成功 ${ok},失败 ${failed.length}`, { description: failed.join(';') }); + }; const usersQuery = useQuery({ queryKey: ['users'], queryFn: apiClient.listUsers }); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); @@ -80,6 +121,9 @@ export default function UserManage() { const users: any[] = usersQuery.data ?? []; + const selectableUsers = users.filter((u: any) => u.username !== currentUsername); + const allChecked = selectableUsers.length > 0 && selectableUsers.every((u: any) => batchIds.has(u.id)); + const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: ['users'] }); // LDAP 预览列表(仅弹窗打开且未进入结果态时请求) @@ -255,6 +299,17 @@ export default function UserManage() { 新建用户 + {batchIds.size > 0 && ( + + )} @@ -280,6 +335,14 @@ export default function UserManage() { + {['用户名', '昵称', '邮箱', '角色', '来源', '创建时间', '操作'].map((h) => ( +
+ setBatchIds(e.target.checked ? new Set(selectableUsers.map((u: any) => u.id)) : new Set())} + title="全选(不含当前登录用户)" + /> + {h} @@ -294,8 +357,18 @@ export default function UserManage() { const isAdmin = u.role === 'admin'; return (
+ {u.username === currentUsername ? ( + + ) : ( + toggleBatch(u.id)} /> + )} + {u.username} + {u.username === currentUsername && ( + (当前登录) + )} {u.nickname || '—'} {u.email || '—'}