Files
aiops/frontend/src/components/system/SystemManager.tsx
T

319 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { CheckCircle, XCircle, Users, Shield, Settings } from 'lucide-react';
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 type { UserFormData } from './UserFormDialog';
// 后端用户数据转换为前端兼容格式
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 confirm = useConfirm();
const location = useLocation();
// 侧边栏菜单路径映射到 Tab // Logan.Li
const pathToTab: Record<string, string> = {
'/users': 'users',
'/roles': 'roles',
'/settings': 'settings',
};
const [activeTab, setActiveTab] = useState(pathToTab[location.pathname] || 'users');
// URL 变化时同步 Tab(侧边栏菜单点击触发) // Logan.Li
useEffect(() => {
const tab = pathToTab[location.pathname];
if (tab && tab !== activeTab) {
setActiveTab(tab);
}
}, [location.pathname]);
const [searchQuery, setSearchQuery] = useState('');
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
const [resetPasswordUser, setResetPasswordUser] = useState<any | null>(null);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
// 创建/编辑用户弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<any | null>(null);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
// CSV 批量导入弹窗
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
const [importPreview, setImportPreview] = useState<any[]>([]);
const [importResult, setImportResult] = useState<any>(null);
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);
useEffect(() => {
loadUsers();
ldap.loadLdapConfig();
}, []);
// LDAP 导入 Dialog 打开时加载 OU 和用户
useEffect(() => {
if (importDialogOpen === 'ldap') {
ldap.loadImportDialogData();
}
}, [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' ? (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
<CheckCircle className="h-3 w-3 mr-1" />
正常
</Badge>
) : (
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
<XCircle className="h-3 w-3 mr-1" />
禁用
</Badge>
);
};
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<string, unknown> = {
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: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try {
await userApi.deleteUser(user.id);
await loadUsers();
} catch (error) {
console.error('Failed to delete user:', error);
}
};
// CSV 文件解析
const handleCsvUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
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<string, unknown>[] = [];
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 || '导入失败' });
} finally {
ldap.setImporting(false);
}
};
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">系统管理</h1>
<p className="text-sm text-slate-500 mt-1">用户、权限与系统配置</p>
</div>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="users" className="data-[state=active]:bg-slate-700">
<Users className="h-4 w-4 mr-2" />
用户管理
</TabsTrigger>
<TabsTrigger value="roles" className="data-[state=active]:bg-slate-700">
<Shield className="h-4 w-4 mr-2" />
角色权限
</TabsTrigger>
<TabsTrigger value="settings" className="data-[state=active]:bg-slate-700">
<Settings className="h-4 w-4 mr-2" />
系统设置
</TabsTrigger>
</TabsList>
<TabsContent value="users" className="mt-4 space-y-4">
<SystemUserTab
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filteredUsers={filteredUsers}
loading={loading}
selectedUser={selectedUser}
setSelectedUser={setSelectedUser}
getUserStatusBadge={getUserStatusBadge}
dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen}
editingUser={editingUser}
form={form}
setForm={setForm}
saving={saving}
handleSubmit={handleSubmit}
openCreate={openCreate}
openEdit={openEdit}
handleDelete={handleDelete}
onResetPassword={(u) => { 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}
setLdapUsers={ldap.setLdapUsers}
/>
</TabsContent>
<TabsContent value="roles" className="mt-4 space-y-4">
<SystemRolesTab
selectedRole={selectedRole}
setSelectedRole={setSelectedRole}
/>
</TabsContent>
<TabsContent value="settings" className="mt-4">
<SystemSettingsTab {...ldap.settingsTabProps} />
</TabsContent>
</Tabs>
<ResetPasswordDialog user={resetPasswordUser} onClose={() => setResetPasswordUser(null)} />
</div>
);
}