import { useState } from 'react'; import { Plus, Upload, Download, FileText } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; import { userApi } from '@/api/user'; import { toast } from '@/hooks/useToast'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { useQueryState, asString, asNumber } from '@/hooks/useQueryState'; import { useUsersList } from '@/features/rbac/hooks/useUsersList'; import { useUserSelection } from '@/features/rbac/hooks/useUserSelection'; import { exportUsersToCSV } from '@/features/rbac/utils'; import type { BatchActionKey } from '@/features/rbac/constants'; import type { UserItem } from '@/features/rbac/types'; import { UserFilters } from '@/features/rbac/components/UserFilters'; import { BatchActionBar } from '@/features/rbac/components/BatchActionBar'; import { UserTable } from '@/features/rbac/components/UserTable'; import { CreateUserModal } from '@/features/rbac/components/CreateUserModal'; import { EditUserModal } from '@/features/rbac/components/EditUserModal'; import { ResetPasswordModal } from '@/features/rbac/components/ResetPasswordModal'; import { UserDetailModal } from '@/features/rbac/components/UserDetailModal'; import { BatchImportModal } from '@/features/rbac/components/BatchImportModal'; /* ───────── 用户管理页(编排:筛选/表格/弹窗均拆分至 features/rbac) ───────── */ const PAGE_SIZE = 20; /** 列表接口参与的筛选键(空态「清除筛选条件」仅清这三项,保留来源与页码) */ const LIST_FILTER_KEYS = ['q', 'role', 'status'] as const; export default function UsersPage() { // 筛选/分页状态迁移到 URL query:刷新、分享链接、回退均可恢复 const [search, setSearch] = useQueryState('q', '', asString); const [roleFilter, setRoleFilter] = useQueryState('role', '', asString); const [statusFilter, setStatusFilter] = useQueryState('status', '', asString); const [sourceFilter, setSourceFilter] = useQueryState('source', '', asString); const [page, setPage] = useQueryState('page', 1, asNumber); // 注意:react-router 的 setSearchParams 函数式更新基于渲染期快照, // 同一事件内多次调用会互相覆盖,批量重置必须合并为一次导航 const [, setSearchParams] = useSearchParams(); const [showCreate, setShowCreate] = useState(false); const [showDetail, setShowDetail] = useState(null); const [showEdit, setShowEdit] = useState(null); const [showResetPwd, setShowResetPwd] = useState(null); const [showImport, setShowImport] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState<{msg: string; onConfirm: () => void} | null>(null); const { users, total, loading, fetchUsers } = useUsersList({ page, pageSize: PAGE_SIZE, search, roleFilter, statusFilter, }); // 来源筛选在前端执行(不参与列表请求,保持原行为) const filtered = users.filter(u => { if (sourceFilter && u.auth_source !== sourceFilter) return false; return true; }); const { selectedIds, setSelectedIds, allSelected, toggleSelectAll, toggleSelect } = useUserSelection(filtered); /* ───────── 筛选重置(单次导航,等价于原「清空全部筛选并回到第 1 页」) ───────── */ const resetFilters = () => setSearchParams({}, { replace: true }); /* ───────── 空态「清除筛选条件」:仅清关键字/角色/状态,保留来源与页码 ───────── */ const clearListFilters = () => setSearchParams(prev => { const params = new URLSearchParams(prev); LIST_FILTER_KEYS.forEach(key => params.delete(key)); return params; }, { replace: true }); const handleToggleActive = async (user: UserItem) => { try { await userApi.update(user.id, { is_active: !user.is_active }); toast('success', `用户已${!user.is_active ? '启用' : '禁用'}`); fetchUsers(); } catch { toast('error', '操作失败'); } }; const handleDelete = (id: number) => { setDeleteConfirm({msg: '确定删除该用户?此操作不可恢复。', onConfirm: async () => { try { await userApi.delete(id); setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; }); toast('success', '用户已删除'); fetchUsers(); } catch { toast('error', '删除失败'); } }}); }; const handleBatchAction = async (action: BatchActionKey) => { const ids = Array.from(selectedIds); if (ids.length === 0) return; if (action === 'delete') { setDeleteConfirm({ msg: `确定删除选中的 ${ids.length} 个用户?此操作不可恢复。`, onConfirm: async () => { try { await Promise.all(ids.map(id => userApi.delete(id))); toast('success', `已删除 ${ids.length} 个用户`); setSelectedIds(new Set()); fetchUsers(); } catch { toast('error', '批量删除失败'); } }, }); return; } try { if (action === 'enable') { await Promise.all(ids.map(id => userApi.update(id, { is_active: true }))); toast('success', `已启用 ${ids.length} 个用户`); } else if (action === 'disable') { await Promise.all(ids.map(id => userApi.update(id, { is_active: false }))); toast('success', `已禁用 ${ids.length} 个用户`); } setSelectedIds(new Set()); fetchUsers(); } catch { toast('error', '批量操作失败'); } }; const handleDownloadTemplate = async () => { try { const blob = await userApi.downloadTemplate() as any; const url = URL.createObjectURL(new Blob([blob], { type: 'text/csv' })); const a = document.createElement('a'); a.href = url; a.download = 'users_import_template.csv'; a.click(); URL.revokeObjectURL(url); } catch { toast('error', '下载模板失败'); } }; return (
{/* Header */}

用户管理

管理系统用户,分配角色和数据库访问权限

{/* Filters */} {/* Batch Actions Bar */} {selectedIds.size > 0 && ( )} {/* User Table */} {/* Create Modal */} {showCreate && ( setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchUsers(); }} /> )} {/* Detail Modal */} {showDetail && ( setShowDetail(null)} /> )} {/* Edit Modal */} {showEdit && ( setShowEdit(null)} onUpdated={() => { setShowEdit(null); fetchUsers(); }} /> )} {/* Reset Password Modal */} {showResetPwd && ( setShowResetPwd(null)} /> )} {/* Batch Import Modal */} {showImport && ( setShowImport(false)} onImported={() => { setShowImport(false); fetchUsers(); }} /> )} { deleteConfirm?.onConfirm(); setDeleteConfirm(null); }} onCancel={() => setDeleteConfirm(null)} />
); }