- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
(getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
approveInstance→approveProcess, rejectInstance→rejectProcess)
+ 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
// Logan.Li
589 lines
25 KiB
TypeScript
589 lines
25 KiB
TypeScript
import { Card, CardContent } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog';
|
||
import {
|
||
Search,
|
||
Plus,
|
||
Upload,
|
||
Network,
|
||
Mail,
|
||
Phone,
|
||
CheckCircle,
|
||
XCircle,
|
||
Eye,
|
||
Edit,
|
||
Trash2,
|
||
Download,
|
||
Key,
|
||
Loader2,
|
||
RefreshCw,
|
||
User as UserIcon,
|
||
} from 'lucide-react';
|
||
import { userApi } from '@/services/api';
|
||
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
||
|
||
interface SystemUserTabProps {
|
||
// Search
|
||
searchQuery: string;
|
||
setSearchQuery: (v: string) => void;
|
||
// Users
|
||
filteredUsers: any[];
|
||
loading: boolean;
|
||
selectedUser: any | null;
|
||
setSelectedUser: (u: any | null) => void;
|
||
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
|
||
// Create / Edit
|
||
dialogOpen: boolean;
|
||
setDialogOpen: (open: boolean) => void;
|
||
editingUser: any | null;
|
||
form: UserFormData;
|
||
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
|
||
saving: boolean;
|
||
handleSubmit: () => void;
|
||
openCreate: () => void;
|
||
openEdit: (user: any) => void;
|
||
handleDelete: (user: any) => void;
|
||
// CSV import
|
||
importDialogOpen: 'csv' | 'ldap' | null;
|
||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||
importPreview: any[];
|
||
setImportPreview: (p: any[]) => void;
|
||
importing: boolean;
|
||
importResult: any;
|
||
setImportResult: (r: any) => void;
|
||
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||
handleBatchImport: () => void;
|
||
// LDAP import
|
||
ldapUsers: any[];
|
||
ldapSelected: Set<string>;
|
||
setLdapSelected: (s: Set<string>) => void;
|
||
ldapLoading: boolean;
|
||
ldapImportOUs: any[];
|
||
ldapImportOUDN: string;
|
||
setLdapImportOUDN: (v: string) => void;
|
||
loadLdapUsers: (ouDn?: string) => void;
|
||
handleLdapImport: () => void;
|
||
setLdapUsers: (u: any[]) => void;
|
||
}
|
||
|
||
export function SystemUserTab({
|
||
searchQuery,
|
||
setSearchQuery,
|
||
filteredUsers,
|
||
loading,
|
||
selectedUser,
|
||
setSelectedUser,
|
||
getUserStatusBadge,
|
||
dialogOpen,
|
||
setDialogOpen,
|
||
editingUser,
|
||
form,
|
||
setForm,
|
||
saving,
|
||
handleSubmit,
|
||
openCreate,
|
||
openEdit,
|
||
handleDelete,
|
||
importDialogOpen,
|
||
setImportDialogOpen,
|
||
importPreview,
|
||
setImportPreview,
|
||
importing,
|
||
importResult,
|
||
setImportResult,
|
||
handleCsvUpload,
|
||
handleBatchImport,
|
||
ldapUsers,
|
||
ldapSelected,
|
||
setLdapSelected,
|
||
ldapLoading,
|
||
ldapImportOUs,
|
||
ldapImportOUDN,
|
||
setLdapImportOUDN,
|
||
loadLdapUsers,
|
||
handleLdapImport,
|
||
setLdapUsers,
|
||
}: SystemUserTabProps) {
|
||
return (
|
||
<>
|
||
{/* Filters */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="relative w-64">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||
<Input
|
||
placeholder="搜索用户..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('csv')}>
|
||
<Upload className="h-4 w-4 mr-2" />
|
||
批量导入
|
||
</Button>
|
||
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('ldap')}>
|
||
<Network className="h-4 w-4 mr-2" />
|
||
LDAP 导入
|
||
</Button>
|
||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
新建用户
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Users Table */}
|
||
<Card className="bg-slate-900 border-slate-800">
|
||
<CardContent className="p-0">
|
||
<div className="border-t border-slate-800">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||
<TableHead className="text-slate-400">用户信息</TableHead>
|
||
<TableHead className="text-slate-400">联系方式</TableHead>
|
||
<TableHead className="text-slate-400">角色</TableHead>
|
||
<TableHead className="text-slate-400">来源</TableHead>
|
||
<TableHead className="text-slate-400">状态</TableHead>
|
||
<TableHead className="text-slate-400">最后登录</TableHead>
|
||
<TableHead className="text-slate-400 text-right">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
<TableRow className="border-slate-800">
|
||
<TableCell colSpan={7} className="text-center py-12">
|
||
<div className="flex items-center justify-center">
|
||
<div className="h-6 w-6 border-2 border-slate-700 border-t-blue-500 rounded-full animate-spin" />
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : filteredUsers.length === 0 ? (
|
||
<TableRow className="border-slate-800">
|
||
<TableCell colSpan={7} className="text-center py-12 text-sm text-slate-500">
|
||
暂无用户数据
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
filteredUsers.map((user) => (
|
||
<TableRow key={user.id} className="border-slate-800 hover:bg-slate-800/50">
|
||
<TableCell>
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
|
||
{(user.nickname || user.username || '?').charAt(0)}
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-200">{user.nickname || user.username}</p>
|
||
<p className="text-xs text-slate-500">@{user.username}</p>
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||
<Mail className="h-3.5 w-3.5" />
|
||
{user.email || '--'}
|
||
</div>
|
||
{user.phone && (
|
||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||
<Phone className="h-3.5 w-3.5" />
|
||
{user.phone}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex flex-wrap gap-1">
|
||
{(user.roles || []).map((role: string) => (
|
||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||
{role}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
{user.authSource === 'ldap' ? (
|
||
<Badge variant="outline" className="border-cyan-500/30 text-cyan-400 bg-cyan-500/10">
|
||
LDAP
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="outline" className="border-slate-600/30 text-slate-400 bg-slate-600/10">
|
||
本地
|
||
</Badge>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
|
||
<TableCell>
|
||
<span className="text-sm text-slate-400">
|
||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '--'}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||
onClick={() => setSelectedUser(user)}
|
||
>
|
||
<Eye className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||
onClick={() => openEdit(user)}
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 text-slate-400 hover:text-red-400"
|
||
onClick={() => handleDelete(user)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* User Detail Dialog */}
|
||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||
<DialogContent className="bg-slate-900 border-slate-800">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||
<UserIcon className="h-5 w-5 text-blue-400" />
|
||
{selectedUser?.nickname}
|
||
</DialogTitle>
|
||
<DialogDescription className="text-slate-500">
|
||
@{selectedUser?.username} · {selectedUser?.email}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">用户名</p>
|
||
<p className="text-sm text-slate-200">{selectedUser?.username}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">昵称</p>
|
||
<p className="text-sm text-slate-200">{selectedUser?.nickname}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">邮箱</p>
|
||
<p className="text-sm text-slate-200">{selectedUser?.email}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">手机号</p>
|
||
<p className="text-sm text-slate-200">{selectedUser?.phone || '--'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||
<p className="text-sm text-slate-200">{selectedUser && getUserStatusBadge(selectedUser.status)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">创建时间</p>
|
||
<p className="text-sm text-slate-200">
|
||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="text-sm font-medium text-slate-300 mb-2">角色</h4>
|
||
<div className="flex flex-wrap gap-2">
|
||
{selectedUser?.roles.map((role: string) => (
|
||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||
{role}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" className="border-slate-700 text-slate-300">
|
||
<Key className="h-4 w-4 mr-2" />
|
||
重置密码
|
||
</Button>
|
||
<Button
|
||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||
onClick={() => {
|
||
if (selectedUser) {
|
||
setSelectedUser(null);
|
||
openEdit(selectedUser);
|
||
}
|
||
}}
|
||
>
|
||
<Edit className="h-4 w-4 mr-2" />
|
||
编辑
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Create / Edit User Dialog */}
|
||
<UserFormDialog
|
||
open={dialogOpen}
|
||
onOpenChange={setDialogOpen}
|
||
editingUser={editingUser}
|
||
form={form}
|
||
setForm={setForm}
|
||
saving={saving}
|
||
onSubmit={handleSubmit}
|
||
/>
|
||
|
||
{/* CSV 批量导入 Dialog */}
|
||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||
<Upload className="h-5 w-5 text-blue-400" />
|
||
批量导入用户(CSV)
|
||
</DialogTitle>
|
||
<DialogDescription className="text-slate-500">
|
||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{!importResult ? (
|
||
<>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => userApi.downloadTemplate()}>
|
||
<Download className="h-4 w-4 mr-1.5" />
|
||
下载模板
|
||
</Button>
|
||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-slate-800 border border-slate-700 text-slate-200 cursor-pointer hover:bg-slate-700 transition-colors">
|
||
<Upload className="h-4 w-4 mr-1.5" />
|
||
选择 CSV 文件
|
||
<input type="file" accept=".csv" className="hidden" onChange={handleCsvUpload} />
|
||
</label>
|
||
</div>
|
||
|
||
{importPreview.length > 0 && (
|
||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||
<div className="px-3 py-2 bg-slate-800/50 text-xs text-slate-400">
|
||
预览:{importPreview.length} 条数据
|
||
</div>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="border-slate-800">
|
||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">昵称</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">手机</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{importPreview.slice(0, 5).map((u, i) => (
|
||
<TableRow key={i} className="border-slate-800">
|
||
<TableCell className="text-slate-200 text-xs">{u.username}</TableCell>
|
||
<TableCell className="text-slate-300 text-xs">{u.nickname}</TableCell>
|
||
<TableCell className="text-slate-400 text-xs">{u.email}</TableCell>
|
||
<TableCell className="text-slate-400 text-xs">{u.phone}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
{importPreview.length > 5 && (
|
||
<TableRow className="border-slate-800">
|
||
<TableCell colSpan={4} className="text-slate-500 text-xs text-center">
|
||
还有 {importPreview.length - 5} 条...
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||
取消
|
||
</Button>
|
||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||
{importing ? '导入中...' : `确认导入 (${importPreview.length})`}
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
) : (
|
||
<div className="space-y-4 py-2">
|
||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||
{importResult.code === 200 ? (
|
||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||
) : (
|
||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||
)}
|
||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||
</div>
|
||
{importResult.errors?.length > 0 && (
|
||
<div className="max-h-32 overflow-auto rounded-lg bg-slate-800/50 p-3 space-y-1">
|
||
{importResult.errors.map((err: string, i: number) => (
|
||
<div key={i} className="text-xs text-red-400">{err}</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setImportPreview([]); setImportResult(null); }}>
|
||
完成
|
||
</Button>
|
||
</DialogFooter>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* LDAP 导入 Dialog */}
|
||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||
<Network className="h-5 w-5 text-cyan-400" />
|
||
从 LDAP 导入用户
|
||
</DialogTitle>
|
||
<DialogDescription className="text-slate-500">
|
||
从 LDAP 服务器检索用户列表,选择需要导入的用户
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{!importResult ? (
|
||
<>
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-slate-500 whitespace-nowrap">选择 OU</span>
|
||
<select
|
||
className="bg-slate-800 border border-slate-700 text-slate-200 text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
value={ldapImportOUDN}
|
||
onChange={(e) => {
|
||
setLdapImportOUDN(e.target.value);
|
||
loadLdapUsers(e.target.value);
|
||
}}
|
||
>
|
||
<option value="">根 (全部)</option>
|
||
{ldapImportOUs.map((ou) => (
|
||
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} disabled={ldapLoading}>
|
||
{ldapLoading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
|
||
<p className="text-xs text-slate-500">
|
||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||
</p>
|
||
|
||
{ldapUsers.length > 0 && (
|
||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="border-slate-800 sticky top-0 bg-slate-900">
|
||
<TableHead className="w-10 text-slate-400 text-xs">
|
||
<input
|
||
type="checkbox"
|
||
className="accent-blue-600"
|
||
checked={ldapSelected.size === ldapUsers.length && ldapUsers.length > 0}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
setLdapSelected(new Set(ldapUsers.map((u) => u.dn)));
|
||
} else {
|
||
setLdapSelected(new Set());
|
||
}
|
||
}}
|
||
/>
|
||
</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">显示名</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||
<TableHead className="text-slate-400 text-xs">DN</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{ldapUsers.map((u) => (
|
||
<TableRow key={u.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||
<TableCell>
|
||
<input
|
||
type="checkbox"
|
||
className="accent-blue-600"
|
||
checked={ldapSelected.has(u.dn)}
|
||
onChange={(e) => {
|
||
const next = new Set(ldapSelected);
|
||
if (e.target.checked) next.add(u.dn);
|
||
else next.delete(u.dn);
|
||
setLdapSelected(next);
|
||
}}
|
||
/>
|
||
</TableCell>
|
||
<TableCell className="text-slate-200 text-xs">{u.uid}</TableCell>
|
||
<TableCell className="text-slate-300 text-xs">{u.cn}</TableCell>
|
||
<TableCell className="text-slate-400 text-xs">{u.mail}</TableCell>
|
||
<TableCell className="text-slate-500 text-xs font-mono truncate max-w-[200px]">{u.dn}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<DialogFooter>
|
||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||
取消
|
||
</Button>
|
||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
||
</Button>
|
||
</DialogFooter>
|
||
</>
|
||
) : (
|
||
<div className="space-y-4 py-2">
|
||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||
{importResult.code === 200 ? (
|
||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||
) : (
|
||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||
)}
|
||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||
完成
|
||
</Button>
|
||
</DialogFooter>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
}
|