/* eslint-disable @typescript-eslint/no-explicit-any */ import { useState, useEffect } from "react"; import { useLocation } from "react-router-dom"; import { useConfirm } from "@/components/ui/confirm-dialog"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { CheckCircle, XCircle, Settings, Users, Shield } from "lucide-react"; import { PageShell } from "@/components/layout/PageShell"; import { userApi } from "@/api"; import type { Role } from "@/types"; import { SystemUserTab } from "./SystemUserTab"; import { SystemRolesTab } from "./SystemRolesTab"; import { SystemSettingsTab } from "./SystemSettingsTab"; import { ResetPasswordDialog } from "./ResetPasswordDialog"; import { useLdapManagement } from "./useLdapManagement"; import { useAuditSettings } from "./useAuditSettings"; import type { UserFormData } from "./UserFormDialog"; import { useTranslation } from "react-i18next"; // 后端用户数据转换为前端兼容格式 function mapUser(u: any) { return { ...u, status: u.status === 1 || u.status === "active" ? ("active" as const) : ("inactive" as const), roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [], lastLoginAt: u.last_login ?? u.lastLoginAt, createdAt: u.created_at ?? u.createdAt, authSource: u.auth_source ?? u.authSource ?? "local", }; } const emptyForm: UserFormData = { username: "", nickname: "", email: "", phone: "", role: "", password: "", status: 1 as number, }; export function SystemManager() { const { t } = useTranslation(); const confirm = useConfirm(); const location = useLocation(); // 侧边栏菜单路径映射到 Tab // Logan.Li // 侧边栏已有独立菜单项,按路由直接渲染对应页面,不再重复 Tab // Logan.Li const pathToTab: Record = { "/users": "users", "/roles": "roles", "/settings": "settings", }; const activeTab = pathToTab[location.pathname] || "users"; const pageMeta: Record< string, { title: string; subtitle: string; icon: React.ReactNode } > = { users: { title: t("system.users"), subtitle: t("system.localUserDesc"), icon: , }, roles: { title: t("system.roles"), subtitle: t("system.roleBindingHint"), icon: , }, settings: { title: t("system.settings"), subtitle: t("system.generalSubtitle"), icon: , }, }; const meta = pageMeta[activeTab]; const [searchQuery, setSearchQuery] = useState(""); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [selectedUser, setSelectedUser] = useState(null); const [resetPasswordUser, setResetPasswordUser] = useState(null); const [selectedRole, setSelectedRole] = useState(null); // 创建/编辑用户弹窗 const [dialogOpen, setDialogOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); const [saving, setSaving] = useState(false); const [form, setForm] = useState({ ...emptyForm }); // CSV 批量导入弹窗 const [importDialogOpen, setImportDialogOpen] = useState< "csv" | "ldap" | null >(null); const [importPreview, setImportPreview] = useState([]); const [importResult, setImportResult] = useState(null); const [selectedUserIds, setSelectedUserIds] = useState([]); const loadUsers = async () => { setLoading(true); try { const res = await userApi.getUsers({ page: 1, page_size: 100 }); const rawUsers = res?.list ?? []; setUsers(rawUsers.map(mapUser)); } catch (error) { console.error("Failed to fetch users:", error); setUsers([]); } finally { setLoading(false); } }; // LDAP 状态全部由 hook 管理(在 loadUsers 之后调用避免 TDZ) // Logan.Li const ldap = useLdapManagement(loadUsers); const audit = useAuditSettings(); useEffect(() => { loadUsers(); ldap.loadLdapConfig(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // LDAP 导入 Dialog 打开时加载 OU 和用户 useEffect(() => { if (importDialogOpen === "ldap") { ldap.loadImportDialogData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [importDialogOpen]); const filteredUsers = users.filter( (u) => (u.username || "").toLowerCase().includes(searchQuery.toLowerCase()) || (u.nickname || "").toLowerCase().includes(searchQuery.toLowerCase()) || (u.email || "").toLowerCase().includes(searchQuery.toLowerCase()), ); const getUserStatusBadge = (status: "active" | "inactive") => { return status === "active" ? ( {t("common.normal")} ) : ( {t("system.userInactive")} ); }; const openCreate = () => { setEditingUser(null); setForm({ ...emptyForm }); setDialogOpen(true); }; const openEdit = (user: any) => { setEditingUser(user); setForm({ username: user.username || "", nickname: user.nickname || "", email: user.email || "", phone: user.phone || "", role: Array.isArray(user.roles) ? user.roles[0] || "" : user.role || "", password: "", status: user.status === "active" ? 1 : 0, }); setDialogOpen(true); }; const handleSubmit = async () => { setSaving(true); try { const payload: Record = { username: form.username, nickname: form.nickname, email: form.email, phone: form.phone, role: form.role, status: form.status, }; if (form.password) { payload.password = form.password; } if (editingUser) { await userApi.updateUser(editingUser.id, payload); } else { await userApi.createUser(payload); } setDialogOpen(false); await loadUsers(); } catch (error) { console.error("Failed to save user:", error); } finally { setSaving(false); } }; const handleDelete = async (user: any) => { const ok = await confirm({ title: t("system.deleteUser"), description: t('system.deleteUserConfirmDesc', { name: user.nickname || user.username }), confirmText: t("common.delete"), variant: "destructive", }); if (!ok) return; try { await userApi.deleteUser(user.id); setSelectedUserIds((prev) => prev.filter((id) => id !== user.id)); await loadUsers(); toast.success(t("system.userDeleted")); } catch (error: unknown) { toast.error( error instanceof Error ? error.message : t("common.deleteFailed"), ); } }; const handleBatchDelete = async () => { if (selectedUserIds.length === 0) return; const ok = await confirm({ title: t("system.batchDeleteUsers"), description: t("system.batchDeleteUsersConfirmDesc", { count: selectedUserIds.length }), confirmText: t("system.terminalBatchDelete"), variant: "destructive", }); if (!ok) return; try { const res = (await userApi.batchDelete(selectedUserIds)) as { message?: string; }; toast.success(res?.message || t('system.batchDeleteUsers', { count: selectedUserIds.length })); setSelectedUserIds([]); await loadUsers(); } catch (error: unknown) { toast.error( error instanceof Error ? error.message : t("cloud.batchDeleteFailed"), ); } }; // CSV 文件解析 const handleCsvUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const text = ev.target?.result as string; const lines = text .split("\n") .filter((l) => l.trim() && !l.startsWith("\xEF\xBB\xBF")); if (lines.length < 2) return; // Find column indices from header const headers = lines[0].split(",").map((h) => h.trim().toLowerCase()); const idxUsername = headers.findIndex( (h) => h.includes("用户名") || h.includes("username"), ); const idxNickname = headers.findIndex( (h) => h.includes("昵称") || h.includes("name") || h.includes("显示名"), ); const idxEmail = headers.findIndex( (h) => h.includes("邮箱") || h.includes("email") || h.includes("mail"), ); const idxPhone = headers.findIndex( (h) => h.includes("手机") || h.includes("phone"), ); const idxRole = headers.findIndex( (h) => h.includes("角色") || h.includes("role"), ); const idxPassword = headers.findIndex( (h) => h.includes("密码") || h.includes("password"), ); const parsed: Record[] = []; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split(",").map((c) => c.trim()); parsed.push({ username: idxUsername >= 0 ? cols[idxUsername] : "", nickname: idxNickname >= 0 ? cols[idxNickname] : "", email: idxEmail >= 0 ? cols[idxEmail] : "", phone: idxPhone >= 0 ? cols[idxPhone] : "", role: idxRole >= 0 ? cols[idxRole] : "", password: idxPassword >= 0 ? cols[idxPassword] : "", }); } setImportPreview(parsed); setImportResult(null); }; reader.readAsText(file, "UTF-8"); }; // 执行 CSV 批量导入 const handleBatchImport = async () => { ldap.setImporting(true); try { const res = await userApi.batchImport({ users: importPreview, skip_existing: true, }); setImportResult(res); await loadUsers(); } catch (error) { setImportResult({ code: 500, message: error?.message || t("system.importFailed"), }); } finally { ldap.setImporting(false); } }; return (
{activeTab === "users" && (
{ setSelectedUser(null); setResetPasswordUser(u); }} importDialogOpen={importDialogOpen} setImportDialogOpen={setImportDialogOpen} importPreview={importPreview} setImportPreview={setImportPreview} importing={ldap.importing} importResult={importResult} setImportResult={setImportResult} handleCsvUpload={handleCsvUpload} handleBatchImport={handleBatchImport} ldapUsers={ldap.ldapUsers} ldapSelected={ldap.ldapSelected} setLdapSelected={ldap.setLdapSelected} ldapLoading={ldap.ldapLoading} ldapImportOUs={ldap.ldapImportOUs} ldapImportOUDN={ldap.ldapImportOUDN} setLdapImportOUDN={ldap.setLdapImportOUDN} loadLdapUsers={ldap.loadLdapUsers} handleLdapImport={() => ldap.handleLdapImport({ setResult: setImportResult }) } setLdapUsers={ldap.setLdapUsers} />
)} {activeTab === "roles" && ( )} {activeTab === "settings" && ( )}
setResetPasswordUser(null)} />
); }