feat: 用户管理列表批量删除(复选框多选+全选,当前登录用户保护)
This commit is contained in:
1 parent
67aeafac19
commit
7090c8efc2
1 file changed
+74
-1
@@ -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<Set<string>>(new Set());
|
||||
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 { 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() {
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
新建用户
|
||||
</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>
|
||||
|
||||
@@ -280,6 +335,14 @@ export default function UserManage() {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<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) => (
|
||||
<th key={h} className="text-left px-4 py-2 text-[11px] font-semibold uppercase tracking-wider" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{h}
|
||||
@@ -294,8 +357,18 @@ export default function UserManage() {
|
||||
const isAdmin = u.role === 'admin';
|
||||
return (
|
||||
<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">
|
||||
<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 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>
|
||||
|
||||
Reference in new issue
Block a user