Files
aiops/frontend/src/components/system/SystemUserTab.tsx
T
attacker 72bfb0e12d fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换:
- 翻译词典: 22 namespaces, 3600+ keys
- 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy)
- 语言切换器在 Header 中可用 (🇨🇳/🇺🇸)
- tsc 零错误, Vite 构建成功
2026-07-03 02:02:29 +08:00

385 lines
15 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 { Checkbox } from '@/components/ui/checkbox';
import { UserFormDialog, type UserFormData } from './UserFormDialog';
import { UserDetailDialog } from './UserDetailDialog';
import { CsvImportDialog } from './CsvImportDialog';
import { LdapImportDialog } from './LdapImportDialog';
import { usePermission } from '@/hooks/usePermission';
import { useTranslation } from 'react-i18next';
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;
handleBatchDelete: () => void;
selectedUserIds: number[];
setSelectedUserIds: React.Dispatch<React.SetStateAction<number[]>>;
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,
handleBatchDelete,
selectedUserIds,
setSelectedUserIds,
onResetPassword,
importDialogOpen,
setImportDialogOpen,
importPreview,
setImportPreview,
importing,
importResult,
setImportResult,
handleCsvUpload,
handleBatchImport,
ldapUsers,
ldapSelected,
setLdapSelected,
ldapLoading,
ldapImportOUs,
ldapImportOUDN,
setLdapImportOUDN,
loadLdapUsers,
handleLdapImport,
setLdapUsers,
}: SystemUserTabProps) {
const { t } = useTranslation();
const { hasPermission } = usePermission();
const canManage = hasPermission('system:user');
const toggleSelect = (id: number) => {
setSelectedUserIds((prev) =>
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
);
};
const allSelected = filteredUsers.length > 0 && selectedUserIds.length === filteredUsers.length;
const toggleSelectAll = () => {
setSelectedUserIds(allSelected ? [] : filteredUsers.map((u) => u.id));
};
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-[var(--dg-text-muted)]" />
<Input
placeholder={t('system.searchUserDot')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="flex items-center gap-2">
{hasPermission('system:user') && (
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('csv')}>
<Upload className="h-4 w-4 mr-2" />
{t('common.batchImport')}
</Button>
)}
{hasPermission('system:user') && (
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('ldap')}>
<Network className="h-4 w-4 mr-2" />
{t('system.ldapImport')}
</Button>
)}
{hasPermission('system:user') && (
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white" onClick={openCreate}>
<Plus className="h-4 w-4 mr-2" />
新建用户
</Button>
)}
</div>
</div>
{canManage && selectedUserIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
<span className="text-sm dg-text-secondary">
已选择 {selectedUserIds.length} 项 / 共 {filteredUsers.length} 项
</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedUserIds([])}>
{t('cloud.clearSelection')}
</Button>
<Button variant="outline" size="sm" className="dg-btn-danger dg-btn-sm" onClick={handleBatchDelete}>
<Trash2 className="w-4 h-4 mr-2" />
{t('system.terminalBatchDelete')}
</Button>
</div>
</div>
)}
{/* Users Table */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-0">
<div className="border-t border-[var(--dg-border)]">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
{canManage && (
<TableHead className="w-10 text-[var(--dg-text-secondary)]">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
</TableHead>
)}
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userTitleField')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.contactInfo')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userRole')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.source')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userStatus')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.lastLogin')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)] text-right">{t('system.auditAction')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={canManage ? 8 : 7} className="text-center py-12">
<div className="flex items-center justify-center">
<div className="h-6 w-6 border-2 border-[var(--dg-border)] border-t-[var(--dg-blue)] rounded-full animate-spin" />
</div>
</TableCell>
</TableRow>
) : filteredUsers.length === 0 ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={canManage ? 8 : 7} className="text-center py-12 text-sm text-[var(--dg-text-muted)]">
暂无用户数据
</TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]/50">
{canManage && (
<TableCell>
<Checkbox
checked={selectedUserIds.includes(user.id)}
onCheckedChange={() => toggleSelect(user.id)}
className="border-[var(--dg-border)]"
/>
</TableCell>
)}
<TableCell>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[var(--dg-blue)] 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-[var(--dg-text-primary)]">{user.nickname || user.username}</p>
<p className="text-xs text-[var(--dg-text-muted)]">@{user.username}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-1 text-sm text-[var(--dg-text-secondary)]">
<Mail className="h-3.5 w-3.5" />
{user.email || '--'}
</div>
{user.phone && (
<div className="flex items-center gap-1 text-sm text-[var(--dg-text-secondary)]">
<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-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{role}
</Badge>
))}
</div>
</TableCell>
<TableCell>
{user.authSource === 'ldap' ? (
<Badge variant="outline" className="border-[var(--dg-cyan)]/30 text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10">
LDAP
</Badge>
) : (
<Badge variant="outline" className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10">
本地
</Badge>
)}
</TableCell>
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
<TableCell>
<span className="text-sm text-[var(--dg-text-secondary)]">
{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-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={() => setSelectedUser(user)}
>
<Eye className="h-4 w-4" />
</Button>
{hasPermission('system:user') && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={() => openEdit(user)}
>
<Edit className="h-4 w-4" />
</Button>
)}
{hasPermission('system:user') && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-red)]"
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}
/>
</>
);
}