feat: 用户管理列表批量删除(复选框多选+全选,当前登录用户保护)

This commit is contained in:
Logan.Li committed 2026-10-27 15:56:31 +08:00
1 parent 67aeafac19
commit 7090c8efc2
1 file changed
+74 -1
+74 -1
View File
@@ -12,7 +12,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -71,6 +71,47 @@ export default function UserManage() {
const [ldapOpen, setLdapOpen] = useState(false); const [ldapOpen, setLdapOpen] = useState(false);
const [selected, setSelected] = useState<Set<string>>(new Set()); const [selected, setSelected] = useState<Set<string>>(new Set());
const [importResult, setImportResult] = useState<ImportResult | null>(null); const [importResult, setImportResult] = useState<ImportResult | null>(null);
// 批量删除
const [batchIds, setBatchIds] = useState<Set<number>>(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 usersQuery = useQuery({ queryKey: ['users'], queryFn: apiClient.listUsers });
const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings });
@@ -80,6 +121,9 @@ export default function UserManage() {
const users: any[] = usersQuery.data ?? []; 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'] }); const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: ['users'] });
// LDAP 预览列表(仅弹窗打开且未进入结果态时请求) // LDAP 预览列表(仅弹窗打开且未进入结果态时请求)
@@ -255,6 +299,17 @@ export default function UserManage() {
<Plus className="w-3.5 h-3.5" /> <Plus className="w-3.5 h-3.5" />
新建用户 新建用户
</button> </button>
{batchIds.size > 0 && (
<button
onClick={batchDelete}
disabled={batchDeleting}
className="flex items-center gap-1.5 px-3 py-[6px] text-[12px] font-medium rounded-md border disabled:opacity-60"
style={{ background: '#ffebe9', borderColor: '#cf222e', color: '#cf222e' }}
>
{batchDeleting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
批量删除({batchIds.size})
</button>
)}
</div> </div>
</div> </div>
@@ -280,6 +335,14 @@ export default function UserManage() {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr style={{ background: 'var(--panel-bg-secondary)' }}> <tr style={{ background: 'var(--panel-bg-secondary)' }}>
<th className="w-9 px-3 py-2">
<input
type="checkbox"
checked={allChecked}
onChange={(e) => setBatchIds(e.target.checked ? new Set(selectableUsers.map((u: any) => u.id)) : new Set())}
title="全选(不含当前登录用户)"
/>
</th>
{['用户名', '昵称', '邮箱', '角色', '来源', '创建时间', '操作'].map((h) => ( {['用户名', '昵称', '邮箱', '角色', '来源', '创建时间', '操作'].map((h) => (
<th key={h} className="text-left px-4 py-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: 'var(--text-tertiary)' }}> <th key={h} className="text-left px-4 py-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: 'var(--text-tertiary)' }}>
{h} {h}
@@ -294,8 +357,18 @@ export default function UserManage() {
const isAdmin = u.role === 'admin'; const isAdmin = u.role === 'admin';
return ( return (
<tr key={u.id} className="border-t hover:bg-black/[0.02] transition-colors" style={{ borderColor: 'var(--border-color-light)' }}> <tr key={u.id} className="border-t hover:bg-black/[0.02] transition-colors" style={{ borderColor: 'var(--border-color-light)' }}>
<td className="px-3 py-3">
{u.username === currentUsername ? (
<input type="checkbox" disabled title="当前登录用户不可删除" />
) : (
<input type="checkbox" checked={batchIds.has(u.id)} onChange={() => toggleBatch(u.id)} />
)}
</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="text-[13px] font-medium font-mono" style={{ color: 'var(--text-primary)' }}>{u.username}</span> <span className="text-[13px] font-medium font-mono" style={{ color: 'var(--text-primary)' }}>{u.username}</span>
{u.username === currentUsername && (
<span className="ml-1.5 text-[10px]" style={{ color: 'var(--text-tertiary)' }}>(当前登录)</span>
)}
</td> </td>
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{u.nickname || '—'}</td> <td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{u.nickname || '—'}</td>
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{u.email || '—'}</td> <td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{u.email || '—'}</td>