feat: 用户管理列表批量删除(复选框多选+全选,当前登录用户保护)
This commit is contained in:
1 parent
67aeafac19
commit
7090c8efc2
1 file changed
+74
-1
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user