refactor: 全局代码模块化重构 — 所有文件<700行
后端 handler 拆分: - automation.go(1067→4文件), server.go(1011→5文件), ldap.go(815→3文件), user.go(704→2文件) 前端组件拆分: - SystemManager(1782→569), AlertManager(1133→220), K8sClusters(1062→4文件) - Dashboard(941→4文件), ServerManager(905→640), ApprovalFlow(830→548) - AlertCenter(775→693), AIPredictiveAnalytics(760→670) - CloudBilling(728→663), GrafanaDashboard(717→643), sidebar(726→689) Mock 数据模块化: - mocks/data.ts(2552行→barrel+11个domain模块) 新增 CLAUDE.md 项目全局开发规范 修复: AlertCenter/LogManager ListResponse.data→.list 类型不匹配 验证: go build ✅ + tsc --noEmit ✅ + npm run build ✅
This commit is contained in:
1 parent
e8dbe65955
commit
73b5a48df9
37 files changed
+5148
-5237
No files matched your search
@@ -0,0 +1,462 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
ChevronDown,
|
||||
Users,
|
||||
Key,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
UserPlus,
|
||||
Download,
|
||||
Edit,
|
||||
Trash2,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface LdapManagementSectionProps {
|
||||
ldapConfig: any;
|
||||
ldapMgmtOpen: boolean;
|
||||
setLdapMgmtOpen: (v: boolean) => void;
|
||||
ldapOUs: any[];
|
||||
selectedOUDN: string;
|
||||
setSelectedOUDN: (v: string) => void;
|
||||
ldapUsers: any[];
|
||||
selectedLDAPUsers: string[];
|
||||
setSelectedLDAPUsers: (v: string[]) => void;
|
||||
loadLdapOUs: () => void;
|
||||
loadLdapOUUsers: (ouDn?: string) => void;
|
||||
handleLdapImport: () => void;
|
||||
importing: boolean;
|
||||
importFeedback: string | null;
|
||||
importResultOpen: boolean;
|
||||
setImportResultOpen: (v: boolean) => void;
|
||||
// Create OU
|
||||
createOUOpen: boolean;
|
||||
setCreateOUOpen: (v: boolean) => void;
|
||||
ouForm: { ou_name: string; parent_dn: string; description: string };
|
||||
setOuForm: React.Dispatch<React.SetStateAction<{ ou_name: string; parent_dn: string; description: string }>>;
|
||||
handleCreateOU: () => void;
|
||||
ldapSaving: boolean;
|
||||
// Create LDAP user
|
||||
createUserOpen: boolean;
|
||||
setCreateUserOpen: (v: boolean) => void;
|
||||
newUser: { username: string; password: string; display_name: string; email: string; ou_dn: string };
|
||||
setNewUser: React.Dispatch<React.SetStateAction<{ username: string; password: string; display_name: string; email: string; ou_dn: string }>>;
|
||||
handleCreateLDAPUser: () => void;
|
||||
// LDAP user test
|
||||
ldapUserTestOpen: boolean;
|
||||
setLdapUserTestOpen: (v: boolean) => void;
|
||||
ldapTestUser: string;
|
||||
setLdapTestUser: (v: string) => void;
|
||||
ldapTestPass: string;
|
||||
setLdapTestPass: (v: string) => void;
|
||||
ldapUserTesting: boolean;
|
||||
ldapUserTestResult: any;
|
||||
handleLdapUserTest: () => void;
|
||||
// Edit LDAP user
|
||||
editLdapUser: any | null;
|
||||
setEditLdapUser: (u: any | null) => void;
|
||||
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||
handleUpdateLdapUser: () => void;
|
||||
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||
openEditLdapUser: (user: any) => void;
|
||||
}
|
||||
|
||||
export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
const {
|
||||
ldapConfig,
|
||||
ldapMgmtOpen,
|
||||
setLdapMgmtOpen,
|
||||
ldapOUs,
|
||||
selectedOUDN,
|
||||
setSelectedOUDN,
|
||||
ldapUsers,
|
||||
selectedLDAPUsers,
|
||||
setSelectedLDAPUsers,
|
||||
loadLdapOUs,
|
||||
loadLdapOUUsers,
|
||||
handleLdapImport,
|
||||
importing,
|
||||
importFeedback,
|
||||
importResultOpen,
|
||||
setImportResultOpen,
|
||||
createOUOpen,
|
||||
setCreateOUOpen,
|
||||
ouForm,
|
||||
setOuForm,
|
||||
handleCreateOU,
|
||||
ldapSaving,
|
||||
createUserOpen,
|
||||
setCreateUserOpen,
|
||||
newUser,
|
||||
setNewUser,
|
||||
handleCreateLDAPUser,
|
||||
ldapUserTestOpen,
|
||||
setLdapUserTestOpen,
|
||||
ldapTestUser,
|
||||
setLdapTestUser,
|
||||
ldapTestPass,
|
||||
setLdapTestPass,
|
||||
ldapUserTesting,
|
||||
ldapUserTestResult,
|
||||
handleLdapUserTest,
|
||||
editLdapUser,
|
||||
setEditLdapUser,
|
||||
editLdapForm,
|
||||
setEditLdapForm,
|
||||
handleUpdateLdapUser,
|
||||
handleDeleteLDAPEntry,
|
||||
openEditLdapUser,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 折叠/展开按钮 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const open = !ldapMgmtOpen;
|
||||
setLdapMgmtOpen(open);
|
||||
if (open && ldapOUs.length === 0) loadLdapOUs();
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 bg-slate-900/50 hover:bg-slate-800/50 text-slate-400 text-sm transition-colors"
|
||||
>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${ldapMgmtOpen ? '' : '-rotate-90'}`} />
|
||||
<Users className="h-4 w-4 text-amber-400" />
|
||||
<span className="text-slate-300">LDAP 账号管理</span>
|
||||
</button>
|
||||
|
||||
{ldapMgmtOpen && (
|
||||
<div className="bg-slate-900/30 border-t border-slate-800/50 px-4 py-4 space-y-4">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* 账号验证测试 */}
|
||||
<Dialog open={ldapUserTestOpen} onOpenChange={setLdapUserTestOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Key className="h-3.5 w-3.5 mr-1" />
|
||||
账号验证
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">LDAP 账号验证测试</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名</label>
|
||||
<Input
|
||||
value={ldapTestUser}
|
||||
onChange={(e) => setLdapTestUser(e.target.value)}
|
||||
placeholder="zhangsan"
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">密码</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapTestPass}
|
||||
onChange={(e) => setLdapTestPass(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLdapUserTest()}
|
||||
placeholder="••••••"
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
{ldapUserTestResult && (
|
||||
<div className={`p-3 rounded-lg text-sm ${ldapUserTestResult.success ? 'bg-emerald-500/10 text-emerald-300' : 'bg-red-500/10 text-red-300'}`}>
|
||||
{ldapUserTestResult.message}
|
||||
{ldapUserTestResult.user_info && (
|
||||
<pre className="mt-2 text-xs whitespace-pre-wrap">{JSON.stringify(ldapUserTestResult.user_info, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleLdapUserTest}
|
||||
disabled={ldapUserTesting || !ldapTestUser}
|
||||
>
|
||||
{ldapUserTesting ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
验证
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200" onClick={loadLdapOUs}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
|
||||
{/* OU 选择 */}
|
||||
<Select value={selectedOUDN} onValueChange={(v) => { setSelectedOUDN(v); loadLdapOUUsers(v); }}>
|
||||
<SelectTrigger className="w-60 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs">
|
||||
<SelectValue placeholder="选择组织单元" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||
{ldapOUs.map((ou: any) => (
|
||||
<SelectItem key={ou.dn} value={ou.dn}>
|
||||
{ou.description ? `${ou.name} (${ou.description})` : ou.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 新建 OU */}
|
||||
<Dialog open={createOUOpen} onOpenChange={setCreateOUOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
新建 OU
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">新建组织单元</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的组织单元 (OU)</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">OU 名称 *</label>
|
||||
<Input value={ouForm.ou_name} onChange={(e) => setOuForm({ ...ouForm, ou_name: e.target.value })} placeholder="dev" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">父级 DN</label>
|
||||
<Input value={ouForm.parent_dn} onChange={(e) => setOuForm({ ...ouForm, parent_dn: e.target.value })} placeholder={ldapConfig.base_dn} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">描述</label>
|
||||
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder="开发团队" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateOU} disabled={!ouForm.ou_name || ldapSaving}>
|
||||
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
创建
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 添加用户 */}
|
||||
<Dialog open={createUserOpen} onOpenChange={setCreateUserOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<UserPlus className="h-3.5 w-3.5 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">添加 LDAP 用户</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的用户账号</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名 *</label>
|
||||
<Input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="zhangsan" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">密码 *</label>
|
||||
<Input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="••••••" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">显示名称</label>
|
||||
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
|
||||
<Input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="zs@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">目标 OU</label>
|
||||
<Input value={newUser.ou_dn || selectedOUDN || ldapConfig.base_dn} onChange={(e) => setNewUser({ ...newUser, ou_dn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={handleCreateLDAPUser} disabled={!newUser.username || !newUser.password || ldapSaving}>
|
||||
{ldapSaving ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : null}
|
||||
创建
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 导入到本地 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-blue-600/50 text-blue-400 hover:bg-blue-600/10 ml-auto"
|
||||
onClick={handleLdapImport}
|
||||
disabled={ldapUsers.length === 0 || importing}
|
||||
>
|
||||
{importing ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <Download className="h-3.5 w-3.5 mr-1" />}
|
||||
{selectedLDAPUsers.length > 0 ? `导入 (${selectedLDAPUsers.length})` : `导入全部 (${ldapUsers.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{importFeedback && (
|
||||
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||
<DialogContent className="max-w-sm bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">导入结果</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 text-sm text-slate-300">
|
||||
{importFeedback}
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => setImportResultOpen(false)}>
|
||||
确定
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* 用户表格 */}
|
||||
<div className="border border-slate-800 rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400 w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: any) => u.dn) : [])}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400">用户名</TableHead>
|
||||
<TableHead className="text-slate-400">显示名称</TableHead>
|
||||
<TableHead className="text-slate-400">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400">DN</TableHead>
|
||||
<TableHead className="text-slate-400 w-12">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.length === 0 ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center text-slate-500 py-8">
|
||||
点击「刷新」加载 LDAP 用户
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ldapUsers.map((user: any) => (
|
||||
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
checked={selectedLDAPUsers.includes(user.dn)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) setSelectedLDAPUsers([...selectedLDAPUsers, user.dn]);
|
||||
else setSelectedLDAPUsers(selectedLDAPUsers.filter((d: string) => d !== user.dn));
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200 font-medium">{user.uid}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{user.cn}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{user.mail || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-500">{user.dn}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-slate-400 hover:text-blue-400 hover:bg-blue-500/10"
|
||||
onClick={() => openEditLdapUser(user)}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => handleDeleteLDAPEntry(user.dn)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑 LDAP 用户 Dialog */}
|
||||
<Dialog open={!!editLdapUser} onOpenChange={(v) => !v && setEditLdapUser(null)}>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Edit className="h-5 w-5 text-blue-400" />
|
||||
编辑 LDAP 用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
修改 {editLdapUser?.uid} 的属性
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editLdapUser && (
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名(只读)</label>
|
||||
<Input value={editLdapUser.uid || ''} disabled className="bg-slate-800/50 border-slate-700 text-slate-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">显示名称 (cn)</label>
|
||||
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
|
||||
<Input value={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">电话</label>
|
||||
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">职位/头衔</label>
|
||||
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Shield, Edit, Eye } from 'lucide-react';
|
||||
import { roles } from '@/mocks/data';
|
||||
import { userApi } from '@/services/api';
|
||||
import type { Role } from '@/types';
|
||||
|
||||
interface SystemRolesTabProps {
|
||||
@@ -10,7 +11,43 @@ interface SystemRolesTabProps {
|
||||
setSelectedRole: (role: Role | null) => void;
|
||||
}
|
||||
|
||||
// 将后端角色数据规范化为前端 Role 类型 // Logan.Li
|
||||
function normalizeRole(raw: any): Role {
|
||||
return {
|
||||
id: String(raw.id ?? ''),
|
||||
name: raw.name ?? '',
|
||||
code: raw.code ?? '',
|
||||
description: raw.description ?? '',
|
||||
// 后端可能不返回 permissions/createdAt,给予默认值以避免渲染崩溃
|
||||
permissions: Array.isArray(raw.permissions) ? raw.permissions : [],
|
||||
createdAt: raw.createdAt ?? raw.created_at ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTabProps) {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await userApi.getRoles();
|
||||
if (cancelled) return;
|
||||
const list = resp?.list ?? [];
|
||||
setRoles(list.map(normalizeRole));
|
||||
} catch (err) {
|
||||
if (!cancelled) setRoles([]);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
@@ -20,53 +57,64 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{roles.map((role) => (
|
||||
<Card key={role.id} className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center">
|
||||
<Shield className="h-5 w-5 text-purple-400" />
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : roles.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<Shield className="h-10 w-10 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无角色数据</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{roles.map((role) => (
|
||||
<Card key={role.id} className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center">
|
||||
<Shield className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base text-slate-100">{role.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">{role.code}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base text-slate-100">{role.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">{role.code}</CardDescription>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedRole(role)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedRole(role)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-slate-400 mb-3">{role.description}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{role.permissions.slice(0, 5).map((permission) => (
|
||||
<Badge key={permission} variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
{permission}
|
||||
</Badge>
|
||||
))}
|
||||
{role.permissions.length > 5 && (
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
+{role.permissions.length - 5}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-slate-400 mb-3">{role.description}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{role.permissions.slice(0, 5).map((permission) => (
|
||||
<Badge key={permission} variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
{permission}
|
||||
</Badge>
|
||||
))}
|
||||
{role.permissions.length > 5 && (
|
||||
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
|
||||
+{role.permissions.length - 5}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { LdapManagementSection } from './LdapManagementSection';
|
||||
|
||||
export interface LdapFormData {
|
||||
ldap_host: string;
|
||||
ldap_port: number;
|
||||
use_ldaps: boolean;
|
||||
admin_dn: string;
|
||||
admin_password: string;
|
||||
base_dn: string;
|
||||
search_filter: string;
|
||||
}
|
||||
|
||||
interface SystemSettingsTabProps {
|
||||
// LDAP config
|
||||
ldapConfig: any;
|
||||
ldapForm: LdapFormData;
|
||||
setLdapForm: React.Dispatch<React.SetStateAction<LdapFormData>>;
|
||||
ldapSaving: boolean;
|
||||
ldapTesting: boolean;
|
||||
ldapTestResult: string | null;
|
||||
handleLdapSave: () => void;
|
||||
handleLdapTest: () => void;
|
||||
// LDAP management - pass-through to LdapManagementSection
|
||||
ldapMgmtOpen: boolean;
|
||||
setLdapMgmtOpen: (v: boolean) => void;
|
||||
ldapOUs: any[];
|
||||
selectedOUDN: string;
|
||||
setSelectedOUDN: (v: string) => void;
|
||||
ldapUsers: any[];
|
||||
selectedLDAPUsers: string[];
|
||||
setSelectedLDAPUsers: (v: string[]) => void;
|
||||
loadLdapOUs: () => void;
|
||||
loadLdapOUUsers: (ouDn?: string) => void;
|
||||
handleLdapImport: () => void;
|
||||
importing: boolean;
|
||||
importFeedback: string | null;
|
||||
importResultOpen: boolean;
|
||||
setImportResultOpen: (v: boolean) => void;
|
||||
createOUOpen: boolean;
|
||||
setCreateOUOpen: (v: boolean) => void;
|
||||
ouForm: { ou_name: string; parent_dn: string; description: string };
|
||||
setOuForm: React.Dispatch<React.SetStateAction<{ ou_name: string; parent_dn: string; description: string }>>;
|
||||
handleCreateOU: () => void;
|
||||
createUserOpen: boolean;
|
||||
setCreateUserOpen: (v: boolean) => void;
|
||||
newUser: { username: string; password: string; display_name: string; email: string; ou_dn: string };
|
||||
setNewUser: React.Dispatch<React.SetStateAction<{ username: string; password: string; display_name: string; email: string; ou_dn: string }>>;
|
||||
handleCreateLDAPUser: () => void;
|
||||
ldapUserTestOpen: boolean;
|
||||
setLdapUserTestOpen: (v: boolean) => void;
|
||||
ldapTestUser: string;
|
||||
setLdapTestUser: (v: string) => void;
|
||||
ldapTestPass: string;
|
||||
setLdapTestPass: (v: string) => void;
|
||||
ldapUserTesting: boolean;
|
||||
ldapUserTestResult: any;
|
||||
handleLdapUserTest: () => void;
|
||||
editLdapUser: any | null;
|
||||
setEditLdapUser: (u: any | null) => void;
|
||||
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||
handleUpdateLdapUser: () => void;
|
||||
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||
openEditLdapUser: (user: any) => void;
|
||||
}
|
||||
|
||||
export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
const {
|
||||
ldapConfig,
|
||||
ldapForm,
|
||||
setLdapForm,
|
||||
ldapSaving,
|
||||
ldapTesting,
|
||||
ldapTestResult,
|
||||
handleLdapSave,
|
||||
handleLdapTest,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Lock className="h-5 w-5 text-blue-400" />
|
||||
安全设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">密码策略与登录安全</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">强制密码复杂度</p>
|
||||
<p className="text-xs text-slate-500">要求密码包含大小写字母、数字和特殊字符</p>
|
||||
</div>
|
||||
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">登录失败锁定</p>
|
||||
<p className="text-xs text-slate-500">连续5次登录失败锁定账户30分钟</p>
|
||||
</div>
|
||||
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">双因素认证</p>
|
||||
<p className="text-xs text-slate-500">启用后需要手机验证码登录</p>
|
||||
</div>
|
||||
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<History className="h-5 w-5 text-emerald-400" />
|
||||
审计设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">操作日志与审计策略</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">记录所有操作</p>
|
||||
<p className="text-xs text-slate-500">记录用户的所有操作行为</p>
|
||||
</div>
|
||||
<Switch defaultChecked className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">日志保留时间</p>
|
||||
<p className="text-xs text-slate-500">操作日志保留180天</p>
|
||||
</div>
|
||||
<Select defaultValue="180">
|
||||
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="30">30天</SelectItem>
|
||||
<SelectItem value="90">90天</SelectItem>
|
||||
<SelectItem value="180">180天</SelectItem>
|
||||
<SelectItem value="365">1年</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-amber-400" />
|
||||
会话设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">会话超时与Token配置</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">会话超时时间</p>
|
||||
<p className="text-xs text-slate-500">用户无操作30分钟后自动登出</p>
|
||||
</div>
|
||||
<Select defaultValue="30">
|
||||
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="15">15分钟</SelectItem>
|
||||
<SelectItem value="30">30分钟</SelectItem>
|
||||
<SelectItem value="60">1小时</SelectItem>
|
||||
<SelectItem value="120">2小时</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">单点登录</p>
|
||||
<p className="text-xs text-slate-500">同一账号只能在一个设备登录</p>
|
||||
</div>
|
||||
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-purple-400" />
|
||||
系统信息
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">版本与系统状态</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-400">系统版本</span>
|
||||
<span className="text-sm text-slate-200">v2.5.1</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-400">运行时间</span>
|
||||
<span className="text-sm text-slate-200">30天 12小时</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* LDAP 配置 */}
|
||||
<Card className="bg-slate-900 border-slate-800 mt-4">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
LDAP / Active Directory
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500 mt-1">配置 LDAP 服务器,实现统一身份认证</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">LDAP 服务器地址</label>
|
||||
<Input
|
||||
value={ldapForm.ldap_host}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, ldap_host: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="ldap.example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">端口</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={ldapForm.ldap_port}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, ldap_port: Number(e.target.value) })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200"
|
||||
placeholder="389"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">Base DN(用户搜索基础)</label>
|
||||
<Input
|
||||
value={ldapForm.base_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, base_dn: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员 Bind DN</label>
|
||||
<Input
|
||||
value={ldapForm.admin_dn}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, admin_dn: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员密码 {ldapConfig && '(留空不修改)'}</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={ldapForm.admin_password}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, admin_password: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="******"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">搜索过滤器(可选,用 %s 代表用户名)</label>
|
||||
<Input
|
||||
value={ldapForm.search_filter}
|
||||
onChange={(e) => setLdapForm({ ...ldapForm, search_filter: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
|
||||
placeholder="(uid=%s)"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 col-span-2">
|
||||
<Switch
|
||||
checked={ldapForm.use_ldaps}
|
||||
onCheckedChange={(v) => setLdapForm({ ...ldapForm, use_ldaps: v })}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
/>
|
||||
<span className="text-sm text-slate-300">启用 LDAPS(SSL 加密)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试/保存按钮 */}
|
||||
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-slate-800">
|
||||
{ldapTestResult && (
|
||||
<span className={`text-xs ${ldapTestResult.includes('成功') ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{ldapTestResult}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-slate-700 text-slate-300"
|
||||
onClick={handleLdapTest}
|
||||
disabled={ldapTesting}
|
||||
>
|
||||
{ldapTesting ? <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 mr-1" />}
|
||||
测试连接
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleLdapSave}
|
||||
disabled={ldapSaving}
|
||||
>
|
||||
{ldapSaving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* LDAP 账号管理 - 可折叠 */}
|
||||
{ldapConfig && (
|
||||
<div className="mt-4">
|
||||
<LdapManagementSection {...props} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { User } from 'lucide-react';
|
||||
import { roles } from '@/mocks/data';
|
||||
import { userApi } from '@/services/api';
|
||||
|
||||
export interface UserFormData {
|
||||
username: string;
|
||||
@@ -47,6 +48,32 @@ export function UserFormDialog({
|
||||
saving,
|
||||
onSubmit,
|
||||
}: UserFormDialogProps) {
|
||||
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
// 对话框打开时拉取角色列表 // Logan.Li
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await userApi.getRoles();
|
||||
if (cancelled) return;
|
||||
const list = resp?.list ?? [];
|
||||
setRoles(
|
||||
list.map((r: any) => ({
|
||||
id: String(r.id ?? ''),
|
||||
name: r.name ?? r.code ?? '',
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
if (!cancelled) setRoles([]);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
|
||||
Reference in new issue
Block a user