328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
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 {
|
|
Search,
|
|
Plus,
|
|
Upload,
|
|
Network,
|
|
Mail,
|
|
Phone,
|
|
Eye,
|
|
Edit,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
|
import { UserDetailDialog } from './UserDetailDialog';
|
|
import { CsvImportDialog } from './CsvImportDialog';
|
|
import { LdapImportDialog } from './LdapImportDialog';
|
|
import { usePermission } from '@/hooks/usePermission';
|
|
|
|
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;
|
|
onResetPassword?: (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,
|
|
onResetPassword,
|
|
importDialogOpen,
|
|
setImportDialogOpen,
|
|
importPreview,
|
|
setImportPreview,
|
|
importing,
|
|
importResult,
|
|
setImportResult,
|
|
handleCsvUpload,
|
|
handleBatchImport,
|
|
ldapUsers,
|
|
ldapSelected,
|
|
setLdapSelected,
|
|
ldapLoading,
|
|
ldapImportOUs,
|
|
ldapImportOUDN,
|
|
setLdapImportOUDN,
|
|
loadLdapUsers,
|
|
handleLdapImport,
|
|
setLdapUsers,
|
|
}: SystemUserTabProps) {
|
|
const { hasPermission } = usePermission();
|
|
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">
|
|
{hasPermission('system:user') && (
|
|
<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>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<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>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<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>
|
|
{hasPermission('system:user') && (
|
|
<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>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<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>
|
|
|
|
{/* 用户详情 / 创建编辑 / CSV 导入 / LDAP 导入 弹窗 — 均由独立子组件承载 */}
|
|
<UserDetailDialog
|
|
selectedUser={selectedUser}
|
|
setSelectedUser={setSelectedUser}
|
|
getUserStatusBadge={getUserStatusBadge}
|
|
openEdit={openEdit}
|
|
onResetPassword={onResetPassword}
|
|
/>
|
|
|
|
<UserFormDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
editingUser={editingUser}
|
|
form={form}
|
|
setForm={setForm}
|
|
saving={saving}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
|
|
<CsvImportDialog
|
|
importDialogOpen={importDialogOpen}
|
|
setImportDialogOpen={setImportDialogOpen}
|
|
importPreview={importPreview}
|
|
setImportPreview={setImportPreview}
|
|
importing={importing}
|
|
importResult={importResult}
|
|
setImportResult={setImportResult}
|
|
handleCsvUpload={handleCsvUpload}
|
|
handleBatchImport={handleBatchImport}
|
|
/>
|
|
|
|
<LdapImportDialog
|
|
importDialogOpen={importDialogOpen}
|
|
setImportDialogOpen={setImportDialogOpen}
|
|
ldapUsers={ldapUsers}
|
|
setLdapUsers={setLdapUsers}
|
|
ldapSelected={ldapSelected}
|
|
setLdapSelected={setLdapSelected}
|
|
ldapLoading={ldapLoading}
|
|
ldapImportOUs={ldapImportOUs}
|
|
ldapImportOUDN={ldapImportOUDN}
|
|
setLdapImportOUDN={setLdapImportOUDN}
|
|
loadLdapUsers={loadLdapUsers}
|
|
handleLdapImport={handleLdapImport}
|
|
importing={importing}
|
|
importResult={importResult}
|
|
setImportResult={setImportResult}
|
|
/>
|
|
</>
|
|
);
|
|
}
|