feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
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';
|
||||
@@ -19,15 +18,19 @@ import {
|
||||
import { credentialApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
type CredentialType = 'ssh_key' | 'api_token' | 'password' | 'access_key';
|
||||
|
||||
const TYPE_CONFIG: Record<CredentialType, { label: string; icon: typeof Key; color: string }> = {
|
||||
ssh_key: { label: 'SSH 密钥', icon: Key, color: 'text-amber-400' },
|
||||
api_token: { label: 'API Token', icon: Globe, color: 'text-blue-400' },
|
||||
password: { label: '密码', icon: Shield, color: 'text-emerald-400' },
|
||||
access_key: { label: 'AK密钥对', icon: Key, color: 'text-purple-400' },
|
||||
ssh_key: { label: 'SSH 密钥', icon: Key, color: 'dg-text-yellow' },
|
||||
api_token: { label: 'API Token', icon: Globe, color: 'dg-text-blue' },
|
||||
password: { label: '密码', icon: Shield, color: 'dg-text-green' },
|
||||
access_key: { label: 'AK密钥对', icon: Key, color: 'dg-text-purple' },
|
||||
};
|
||||
|
||||
interface CredentialItem {
|
||||
@@ -139,10 +142,10 @@ export function CredentialManager() {
|
||||
const typeBadge = (type: CredentialType) => {
|
||||
const cfg = TYPE_CONFIG[type];
|
||||
const bgMap: Record<string, string> = {
|
||||
ssh_key: 'bg-amber-500/20 text-amber-400',
|
||||
api_token: 'bg-blue-500/20 text-blue-400',
|
||||
password: 'bg-emerald-500/20 text-emerald-400',
|
||||
access_key: 'bg-purple-500/20 text-purple-400',
|
||||
ssh_key: 'bg-amber-500/20 dg-text-yellow',
|
||||
api_token: 'bg-blue-500/20 dg-text-blue',
|
||||
password: 'bg-emerald-500/20 dg-text-green',
|
||||
access_key: 'bg-purple-500/20 dg-text-purple',
|
||||
};
|
||||
return (
|
||||
<Badge className={`border-0 text-xs ${bgMap[type] || ''}`}>
|
||||
@@ -152,34 +155,35 @@ export function CredentialManager() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">凭证配置</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">
|
||||
SSH 密钥、API Token、密码、AK密钥对等敏感信息统一加密管理
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
onClick={fetchList}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="凭证配置"
|
||||
subtitle="SSH 密钥、API Token、密码、AK密钥对等敏感信息统一加密管理"
|
||||
icon={<Key className="h-5 w-5" />}
|
||||
loading={loading}
|
||||
metrics={(
|
||||
<StatStrip items={[{ label: '凭据总数', value: credentials.length, color: 'blue' }]} />
|
||||
)}
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={fetchList}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />刷新
|
||||
</Button>
|
||||
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加凭据
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />添加凭据
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-0">
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{!loading && credentials.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<Key className="h-10 w-10" />}
|
||||
title="暂无凭据"
|
||||
description="点击「添加凭据」创建"
|
||||
action={<Button onClick={openCreate}><Plus className="h-4 w-4 mr-2" />添加凭据</Button>}
|
||||
/>
|
||||
) : (
|
||||
<ContentPanel noPadding>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
@@ -239,7 +243,7 @@ export function CredentialManager() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-red-400"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:dg-text-red"
|
||||
onClick={() => handleDelete(cred.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -251,8 +255,8 @@ export function CredentialManager() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ContentPanel>
|
||||
)}
|
||||
|
||||
{/* Form Dialog */}
|
||||
<Dialog open={formOpen} onOpenChange={setFormOpen}>
|
||||
@@ -268,7 +272,7 @@ export function CredentialManager() {
|
||||
{/* 名称 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
名称 <span className="text-red-400">*</span>
|
||||
名称 <span className="dg-text-red">*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder="如: 生产环境SSH密钥"
|
||||
@@ -291,22 +295,22 @@ export function CredentialManager() {
|
||||
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="ssh_key">
|
||||
<span className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4 text-amber-400" /> SSH 密钥
|
||||
<Key className="h-4 w-4 dg-text-yellow" /> SSH 密钥
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="api_token">
|
||||
<span className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-blue-400" /> API Token
|
||||
<Globe className="h-4 w-4 dg-text-blue" /> API Token
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="password">
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-emerald-400" /> 密码
|
||||
<Shield className="h-4 w-4 dg-text-green" /> 密码
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="access_key">
|
||||
<span className="flex items-center gap-2">
|
||||
<Key className="h-4 w-4 text-purple-400" /> AK密钥对
|
||||
<Key className="h-4 w-4 dg-text-purple" /> AK密钥对
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -317,7 +321,7 @@ export function CredentialManager() {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||
{formData.type === 'ssh_key' ? 'SSH 私钥' : formData.type === 'api_token' ? 'Token' : formData.type === 'access_key' ? 'AK/SK (每行一对,用空格或逗号分隔)' : '密码'}
|
||||
{!editingId && <span className="text-red-400"> *</span>}
|
||||
{!editingId && <span className="dg-text-red"> *</span>}
|
||||
{editingId && <span className="text-xs text-[var(--dg-text-muted)] ml-1">(留空不更新)</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -360,7 +364,7 @@ export function CredentialManager() {
|
||||
|
||||
{/* 安全提示 */}
|
||||
<div className="p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
|
||||
<p className="text-xs text-amber-400 flex items-center gap-2">
|
||||
<p className="text-xs dg-text-yellow flex items-center gap-2">
|
||||
<Shield className="h-3.5 w-3.5" />
|
||||
凭据内容使用 AES-256-GCM 加密存储,仅在服务器端解密使用
|
||||
</p>
|
||||
@@ -381,6 +385,6 @@ export function CredentialManager() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -46,13 +46,13 @@ export function CsvImportDialog({
|
||||
}: CsvImportDialogProps) {
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogContent className="dg-card max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 dg-text-blue" />
|
||||
批量导入用户(CSV)
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
<DialogDescription className="dg-text-muted">
|
||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -61,11 +61,11 @@ export function CsvImportDialog({
|
||||
<>
|
||||
<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()}>
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" 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">
|
||||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] cursor-pointer hover:bg-[var(--dg-bg-hover)] transition-colors">
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
选择 CSV 文件
|
||||
<input type="file" accept=".csv" className="hidden" onChange={handleCsvUpload} />
|
||||
@@ -73,31 +73,31 @@ export function CsvImportDialog({
|
||||
</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">
|
||||
<div className="rounded-lg border border-[var(--dg-border)] overflow-hidden">
|
||||
<div className="px-3 py-2 bg-[var(--dg-bg-tertiary)]/50 text-xs dg-text-secondary">
|
||||
预览:{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 className="border-[var(--dg-border)]">
|
||||
<TableHead className="dg-text-secondary text-xs">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">昵称</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary 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 key={i} className="border-[var(--dg-border)]">
|
||||
<TableCell className="text-[var(--dg-text-primary)] text-xs">{u.username}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] text-xs">{u.nickname}</TableCell>
|
||||
<TableCell className="dg-text-secondary text-xs">{u.email}</TableCell>
|
||||
<TableCell className="dg-text-secondary 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">
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={4} className="dg-text-muted text-xs text-center">
|
||||
还有 {importPreview.length - 5} 条...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -109,7 +109,7 @@ export function CsvImportDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||||
@@ -121,16 +121,16 @@ export function CsvImportDialog({
|
||||
<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" />
|
||||
<CheckCircle className="h-6 w-6 dg-text-green shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
<XCircle className="h-6 w-6 dg-text-red shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">{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">
|
||||
<div className="max-h-32 overflow-auto rounded-lg bg-[var(--dg-bg-tertiary)]/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 key={i} className="text-xs dg-text-red">{err}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -57,13 +57,13 @@ export function LdapImportDialog({
|
||||
}: LdapImportDialogProps) {
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogContent className="dg-card max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Network className="h-5 w-5 dg-text-cyan" />
|
||||
从 LDAP 导入用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
<DialogDescription className="dg-text-muted">
|
||||
从 LDAP 服务器检索用户列表,选择需要导入的用户
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -73,9 +73,9 @@ export function LdapImportDialog({
|
||||
<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>
|
||||
<span className="text-xs dg-text-muted 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"
|
||||
className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-[var(--dg-blue)]"
|
||||
value={ldapImportOUDN}
|
||||
onChange={(e) => {
|
||||
setLdapImportOUDN(e.target.value);
|
||||
@@ -88,22 +88,22 @@ export function LdapImportDialog({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} disabled={ldapLoading}>
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] 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">
|
||||
<p className="text-xs dg-text-muted">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||||
</p>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-[var(--dg-border)]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 sticky top-0 bg-slate-900">
|
||||
<TableHead className="w-10 text-slate-400 text-xs">
|
||||
<TableRow className="border-[var(--dg-border)] sticky top-0 bg-[var(--dg-bg-secondary)]">
|
||||
<TableHead className="w-10 dg-text-secondary text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
@@ -117,15 +117,15 @@ export function LdapImportDialog({
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<TableHead className="dg-text-secondary text-xs">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">显示名</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary text-xs">DN</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.map((u) => (
|
||||
<TableRow key={u.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableRow key={u.dn} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -139,10 +139,10 @@ export function LdapImportDialog({
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<TableCell className="text-[var(--dg-text-primary)] text-xs">{u.uid}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] text-xs">{u.cn}</TableCell>
|
||||
<TableCell className="dg-text-secondary text-xs">{u.mail}</TableCell>
|
||||
<TableCell className="dg-text-muted text-xs font-mono truncate max-w-[200px]">{u.dn}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -152,7 +152,7 @@ export function LdapImportDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||||
@@ -164,11 +164,11 @@ export function LdapImportDialog({
|
||||
<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" />
|
||||
<CheckCircle className="h-6 w-6 dg-text-green shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
<XCircle className="h-6 w-6 dg-text-red shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">{importResult.message}</span>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||||
|
||||
@@ -144,52 +144,52 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
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"
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-secondary)]/50 hover:bg-[var(--dg-bg-hover)] dg-text-secondary 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>
|
||||
<Users className="h-4 w-4 dg-text-yellow" />
|
||||
<span className="text-[var(--dg-text-secondary)]">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="bg-[var(--dg-bg-secondary)]/30 border-t border-[var(--dg-border)]/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">
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<Key className="h-3.5 w-3.5 mr-1" />
|
||||
账号验证
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">LDAP 账号验证测试</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">LDAP 账号验证测试</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">输入 LDAP 用户凭据进行认证测试</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">用户名</label>
|
||||
<label className="text-xs dg-text-muted 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"
|
||||
className="dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">密码</label>
|
||||
<label className="text-xs dg-text-muted 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"
|
||||
className="dg-input"
|
||||
/>
|
||||
</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'}`}>
|
||||
<div className={`p-3 rounded-lg text-sm ${ldapUserTestResult.success ? 'bg-emerald-500/10 dg-text-green' : 'bg-red-500/10 dg-text-red'}`}>
|
||||
{ldapUserTestResult.message}
|
||||
{ldapUserTestResult.user_info && (
|
||||
<pre className="mt-2 text-xs whitespace-pre-wrap">{JSON.stringify(ldapUserTestResult.user_info, null, 2)}</pre>
|
||||
@@ -208,17 +208,17 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200" onClick={loadLdapOUs}>
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]" 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">
|
||||
<SelectTrigger className="w-60 h-7 dg-input text-xs">
|
||||
<SelectValue placeholder="选择组织单元" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||
{ldapOUs.map((ou: JsonData) => (
|
||||
<SelectItem key={ou.dn} value={ou.dn}>
|
||||
@@ -231,28 +231,28 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
{/* 新建 OU */}
|
||||
<Dialog open={createOUOpen} onOpenChange={setCreateOUOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
新建 OU
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">新建组织单元</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的组织单元 (OU)</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建组织单元</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">在 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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">OU 名称 *</label>
|
||||
<Input value={ouForm.ou_name} onChange={(e) => setOuForm({ ...ouForm, ou_name: e.target.value })} placeholder="dev" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">父级 DN</label>
|
||||
<Input value={ouForm.parent_dn} onChange={(e) => setOuForm({ ...ouForm, parent_dn: e.target.value })} placeholder={ldapConfig.base_dn} className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">描述</label>
|
||||
<Input value={ouForm.description} onChange={(e) => setOuForm({ ...ouForm, description: e.target.value })} placeholder="开发团队" className="dg-input" />
|
||||
</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}
|
||||
@@ -265,36 +265,36 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
{/* 添加用户 */}
|
||||
<Dialog open={createUserOpen} onOpenChange={setCreateUserOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-slate-200">
|
||||
<Button variant="ghost" size="sm" className="dg-text-secondary hover:text-[var(--dg-text-primary)]">
|
||||
<UserPlus className="h-3.5 w-3.5 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">添加 LDAP 用户</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">在 LDAP 目录中创建新的用户账号</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">添加 LDAP 用户</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">在 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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">用户名 *</label>
|
||||
<Input value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} placeholder="zhangsan" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">密码 *</label>
|
||||
<Input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} placeholder="••••••" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">显示名称</label>
|
||||
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">邮箱</label>
|
||||
<Input value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} placeholder="zs@example.com" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">目标 OU</label>
|
||||
<Input value={newUser.ou_dn || selectedOUDN || ldapConfig.base_dn} onChange={(e) => setNewUser({ ...newUser, ou_dn: e.target.value })} className="dg-input" />
|
||||
</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}
|
||||
@@ -308,7 +308,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-blue-600/50 text-blue-400 hover:bg-blue-600/10 ml-auto"
|
||||
className="border-blue-600/50 dg-text-blue hover:bg-blue-600/10 ml-auto"
|
||||
onClick={handleLdapImport}
|
||||
disabled={ldapUsers.length === 0 || importing}
|
||||
>
|
||||
@@ -319,12 +319,12 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
|
||||
{importFeedback && (
|
||||
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||
<DialogContent className="max-w-sm bg-slate-900 border-slate-800">
|
||||
<DialogContent className="max-w-sm dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">导入结果</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">导入结果</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">LDAP 用户导入到本地用户库的执行结果</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-3 text-sm text-slate-300">
|
||||
<div className="py-3 text-sm text-[var(--dg-text-secondary)]">
|
||||
{importFeedback}
|
||||
</div>
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700 text-white" onClick={() => setImportResultOpen(false)}>
|
||||
@@ -335,39 +335,39 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
)}
|
||||
|
||||
{/* 用户表格 */}
|
||||
<div className="border border-slate-800 rounded-lg overflow-hidden">
|
||||
<div className="border border-[var(--dg-border)] rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400 w-8">
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="dg-text-secondary w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
className="rounded bg-[var(--dg-bg-tertiary)] border-[var(--dg-border-hover)]"
|
||||
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: JsonData) => 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>
|
||||
<TableHead className="dg-text-secondary">用户名</TableHead>
|
||||
<TableHead className="dg-text-secondary">显示名称</TableHead>
|
||||
<TableHead className="dg-text-secondary">邮箱</TableHead>
|
||||
<TableHead className="dg-text-secondary">DN</TableHead>
|
||||
<TableHead className="dg-text-secondary 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">
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableCell colSpan={6} className="text-center dg-text-muted py-8">
|
||||
点击「刷新」加载 LDAP 用户
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ldapUsers.map((user: JsonData) => (
|
||||
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableRow key={user.dn} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
className="rounded bg-[var(--dg-bg-tertiary)] border-[var(--dg-border-hover)]"
|
||||
checked={selectedLDAPUsers.includes(user.dn)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) setSelectedLDAPUsers([...selectedLDAPUsers, user.dn]);
|
||||
@@ -376,23 +376,23 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200 font-medium">{user.uid}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)] font-medium">{user.uid}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{user.cn}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{user.cn}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{user.mail || '-'}</span>
|
||||
<span className="text-sm dg-text-secondary">{user.mail || '-'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-500">{user.dn}</code>
|
||||
<code className="text-xs dg-text-muted">{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"
|
||||
className="h-6 w-6 dg-text-secondary hover:dg-text-blue hover:bg-blue-500/10"
|
||||
onClick={() => openEditLdapUser(user)}
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
@@ -400,7 +400,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
className="h-6 w-6 dg-text-red hover:dg-text-red hover:bg-red-500/10"
|
||||
onClick={() => handleDeleteLDAPEntry(user.dn)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
@@ -418,42 +418,42 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
|
||||
{/* 编辑 LDAP 用户 Dialog */}
|
||||
<Dialog open={!!editLdapUser} onOpenChange={(v) => !v && setEditLdapUser(null)}>
|
||||
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
|
||||
<DialogContent className="max-w-md dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Edit className="h-5 w-5 text-blue-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Edit className="h-5 w-5 dg-text-blue" />
|
||||
编辑 LDAP 用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
<DialogDescription className="dg-text-muted">
|
||||
修改 {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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">用户名(只读)</label>
|
||||
<Input value={editLdapUser.uid || ''} disabled className="dg-input dg-text-muted" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">显示名称 (cn)</label>
|
||||
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">邮箱</label>
|
||||
<Input value={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@example.com" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">电话</label>
|
||||
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" className="dg-input" />
|
||||
</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" />
|
||||
<label className="text-xs dg-text-muted mb-1 block">职位/头衔</label>
|
||||
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="dg-input" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setEditLdapUser(null)}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -200,7 +200,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
{/* 工具栏 — 与 TerminalDialog 一致 // Logan.Li */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-2 text-sm min-w-0 flex-wrap">
|
||||
<Monitor className="h-4 w-4 text-emerald-400 shrink-0" />
|
||||
<Monitor className="h-4 w-4 dg-text-green shrink-0" />
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">终端实时监控</span>
|
||||
{meta && (
|
||||
<span className="text-[var(--dg-text-muted)] truncate">
|
||||
@@ -208,18 +208,18 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
</span>
|
||||
)}
|
||||
{connected && (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10 text-xs">
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10 text-xs">
|
||||
● 在线
|
||||
</Badge>
|
||||
)}
|
||||
{!connected && !connectError && (
|
||||
<Badge variant="outline" className="border-yellow-500/30 text-yellow-400 bg-yellow-500/10 text-xs">
|
||||
<Badge variant="outline" className="border-yellow-500/30 dg-text-yellow bg-yellow-500/10 text-xs">
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
||||
连接中
|
||||
</Badge>
|
||||
)}
|
||||
{connectError && (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10 text-xs">
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 text-xs">
|
||||
连接失败
|
||||
</Badge>
|
||||
)}
|
||||
@@ -244,7 +244,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
|
||||
</div>
|
||||
|
||||
{connectError && (
|
||||
<p className="text-sm text-red-400 px-4 py-2 border-b border-[var(--dg-border)]">{connectError}</p>
|
||||
<p className="text-sm dg-text-red px-4 py-2 border-b border-[var(--dg-border)]">{connectError}</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -234,15 +234,15 @@ export function ReplayDialog({ open, onOpenChange, sessionId, initialSeekSec }:
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showOverlay={false}
|
||||
className="bg-slate-900 border-slate-800 p-0 gap-0 flex flex-col"
|
||||
className="dg-card p-0 gap-0 flex flex-col"
|
||||
style={{ maxWidth: '1100px', width: '92vw' }}
|
||||
>
|
||||
<DialogHeader className="px-4 py-3 border-b border-slate-800 shrink-0">
|
||||
<DialogTitle className="text-slate-100 text-base">
|
||||
<DialogHeader className="px-4 py-3 border-b border-[var(--dg-border)] shrink-0">
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] text-base">
|
||||
{t('audit.terminal.replayPlayer.title', { id: sessionId ?? '' })}
|
||||
</DialogTitle>
|
||||
{meta && (
|
||||
<div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-x-3 gap-y-0.5">
|
||||
<div className="text-xs dg-text-muted mt-1 flex flex-wrap gap-x-3 gap-y-0.5">
|
||||
<span>{meta.username} → {meta.server_name} ({meta.server_ip}:{meta.server_port})</span>
|
||||
{meta.client_ip && <span>{t('audit.terminal.replayPlayer.clientIp')}: {meta.client_ip}</span>}
|
||||
{meta.start_time && <span>{new Date(meta.start_time).toLocaleString()}</span>}
|
||||
@@ -256,47 +256,47 @@ export function ReplayDialog({ open, onOpenChange, sessionId, initialSeekSec }:
|
||||
<div className="flex-1 min-w-0 flex flex-col relative bg-[#0a0e14]">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/40">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-400" />
|
||||
<Loader2 className="h-6 w-6 animate-spin dg-text-blue" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/40">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<p className="dg-text-red text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="flex-1 min-h-0 p-1" />
|
||||
|
||||
{!legacyMode && totalTime > 0 && (
|
||||
<div className="shrink-0 border-t border-slate-800 bg-slate-900/95 px-3 py-2.5 space-y-2">
|
||||
<div className="shrink-0 border-t border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]/95 px-3 py-2.5 space-y-2">
|
||||
{/* 控制按钮 */}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => seekTo(0)} title={t('audit.terminal.replayPlayer.toStart')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={() => seekTo(0)} title={t('audit.terminal.replayPlayer.toStart')}>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => engineRef.current?.skip(-SKIP_SEC)}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={() => engineRef.current?.skip(-SKIP_SEC)}>
|
||||
<SkipBack className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-blue-400" onClick={togglePlay}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-blue" onClick={togglePlay}>
|
||||
{playing ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => engineRef.current?.skip(SKIP_SEC)}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={() => engineRef.current?.skip(SKIP_SEC)}>
|
||||
<SkipForward className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => seekTo(totalTime)} title={t('audit.terminal.replayPlayer.toEnd')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={() => seekTo(totalTime)} title={t('audit.terminal.replayPlayer.toEnd')}>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => { seekTo(0); engineRef.current?.play(); setPlaying(true); }} title={t('audit.terminal.replayPlayer.restart')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={() => { seekTo(0); engineRef.current?.play(); setPlaying(true); }} title={t('audit.terminal.replayPlayer.restart')}>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="w-px h-5 bg-slate-700 mx-1" />
|
||||
<div className="w-px h-5 bg-[var(--dg-bg-active)] mx-1" />
|
||||
|
||||
{SPEEDS.map((s) => (
|
||||
<Button
|
||||
key={s}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-7 px-2 text-xs ${speed === s ? 'text-blue-400 bg-blue-500/10' : 'text-slate-500'}`}
|
||||
className={`h-7 px-2 text-xs ${speed === s ? 'dg-text-blue bg-blue-500/10' : 'dg-text-muted'}`}
|
||||
onClick={() => setSpeed(s)}
|
||||
>
|
||||
{s}x
|
||||
@@ -305,15 +305,15 @@ export function ReplayDialog({ open, onOpenChange, sessionId, initialSeekSec }:
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<span className="text-xs text-slate-400 font-mono tabular-nums">
|
||||
<span className="text-xs dg-text-secondary font-mono tabular-nums">
|
||||
{formatReplayTime(displayTime)} / {formatReplayTime(totalTime)}
|
||||
</span>
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={handleExportCsv} disabled={!meta} title={t('audit.terminal.exportReportCsv')}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={handleExportCsv} disabled={!meta} title={t('audit.terminal.exportReportCsv')}>
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-slate-500" onClick={handleExportJson} disabled={!meta}>JSON</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={handleDownload} title={t('audit.terminal.downloadRecord')}>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs dg-text-muted" onClick={handleExportJson} disabled={!meta}>JSON</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-secondary" onClick={handleDownload} title={t('audit.terminal.downloadRecord')}>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -337,14 +337,14 @@ export function ReplayDialog({ open, onOpenChange, sessionId, initialSeekSec }:
|
||||
step={0.1}
|
||||
onValueChange={(v) => setScrubTime(v[0])}
|
||||
onValueCommit={(v) => seekTo(v[0])}
|
||||
className="[&_[data-slot=slider-track]]:bg-slate-700 [&_[data-slot=slider-range]]:bg-blue-500"
|
||||
className="[&_[data-slot=slider-track]]:bg-[var(--dg-bg-active)] [&_[data-slot=slider-range]]:bg-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{legacyMode && (
|
||||
<div className="shrink-0 border-t border-slate-800 px-3 py-2 text-xs text-slate-500">
|
||||
<div className="shrink-0 border-t border-[var(--dg-border)] px-3 py-2 text-xs dg-text-muted">
|
||||
{t('audit.terminal.replayPlayer.legacyFormat')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function ResetPasswordDialog({ user, onClose }: ResetPasswordDialogProps)
|
||||
</DialogHeader>
|
||||
|
||||
{isLdap ? (
|
||||
<p className="text-sm text-amber-400/90 py-2">LDAP 用户请在 LDAP 目录中管理密码,不支持本地重置。</p>
|
||||
<p className="text-sm dg-text-yellow/90 py-2">LDAP 用户请在 LDAP 目录中管理密码,不支持本地重置。</p>
|
||||
) : (
|
||||
<div className="space-y-3 py-1">
|
||||
<div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function RoleFormDialog({
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-purple-400" />
|
||||
<Shield className="h-5 w-5 dg-text-purple" />
|
||||
{editingRole ? '编辑角色' : '新建角色'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
@@ -139,7 +139,7 @@ export function RoleFormDialog({
|
||||
<Label className="text-xs text-[var(--dg-text-secondary)]">
|
||||
权限分配
|
||||
{form.permission_ids.length > 0 && (
|
||||
<span className="ml-2 text-purple-400">已选 {form.permission_ids.length} 项</span>
|
||||
<span className="ml-2 dg-text-purple">已选 {form.permission_ids.length} 项</span>
|
||||
)}
|
||||
</Label>
|
||||
<div className="max-h-[40vh] overflow-y-auto rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-3 space-y-3">
|
||||
@@ -163,7 +163,7 @@ export function RoleFormDialog({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleGroup(group.items, !allSelected)}
|
||||
className="text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
|
||||
className="text-[10px] dg-text-blue hover:text-blue-300 transition-colors"
|
||||
>
|
||||
{allSelected ? '取消全选' : '全选'}
|
||||
</button>
|
||||
|
||||
@@ -68,11 +68,11 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
|
||||
const getLogResultBadge = (result: OperationLog['result']) => {
|
||||
return result === 'success' ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
成功
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
失败
|
||||
</Badge>
|
||||
);
|
||||
@@ -83,17 +83,17 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<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"
|
||||
className="pl-9 h-9 dg-input"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={fetchLogs}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
@@ -102,55 +102,55 @@ export function SystemLogsTab({ searchQuery, setSearchQuery }: SystemLogsTabProp
|
||||
</div>
|
||||
|
||||
{/* Logs Table */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
<div className="border-t border-[var(--dg-border)]">
|
||||
{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>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<div className="flex flex-col items-center justify-center py-16 dg-text-muted">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无操作日志</p>
|
||||
</div>
|
||||
) : (
|
||||
<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">IP地址</TableHead>
|
||||
<TableHead className="text-slate-400">结果</TableHead>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="dg-text-secondary">时间</TableHead>
|
||||
<TableHead className="dg-text-secondary">用户</TableHead>
|
||||
<TableHead className="dg-text-secondary">模块</TableHead>
|
||||
<TableHead className="dg-text-secondary">操作</TableHead>
|
||||
<TableHead className="dg-text-secondary">资源</TableHead>
|
||||
<TableHead className="dg-text-secondary">IP地址</TableHead>
|
||||
<TableHead className="dg-text-secondary">结果</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredLogs.map((log) => (
|
||||
<TableRow key={log.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableRow key={log.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
<span className="text-sm dg-text-secondary">
|
||||
{log.createdAt ? new Date(log.createdAt).toLocaleString() : '--'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200">{log.username || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">{log.username || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.module || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{log.module || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{log.action || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{log.action || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs text-slate-400 bg-slate-800 px-2 py-0.5 rounded">
|
||||
<code className="text-xs dg-text-secondary bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
|
||||
{log.resource || '--'}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{log.ip || '--'}</span>
|
||||
<span className="text-sm dg-text-secondary">{log.ip || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>{getLogResultBadge(log.result)}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
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 { CheckCircle, XCircle, Settings } from 'lucide-react';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { userApi } from '@/api';
|
||||
import type { Role } from '@/types';
|
||||
import { SystemUserTab } from './SystemUserTab';
|
||||
@@ -231,34 +231,25 @@ export function SystemManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const pageTabs = [
|
||||
{ id: 'users', label: '用户管理', badge: users.length || undefined },
|
||||
{ id: 'roles', label: '角色权限' },
|
||||
{ id: 'settings', label: '系统设置' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">系统管理</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">用户、权限与系统配置</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]">
|
||||
<TabsTrigger value="users" className="data-[state=active]:bg-[var(--dg-bg-hover)]">
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
用户管理
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roles" className="data-[state=active]:bg-[var(--dg-bg-hover)]">
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
角色权限
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" className="data-[state=active]:bg-[var(--dg-bg-hover)]">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
系统设置
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users" className="mt-4 space-y-4">
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="系统管理"
|
||||
subtitle="用户、权限与系统配置"
|
||||
icon={<Settings className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
>
|
||||
<div className="w-full">
|
||||
{activeTab === 'users' && (
|
||||
<div className="space-y-4">
|
||||
<SystemUserTab
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
@@ -298,21 +289,22 @@ export function SystemManager() {
|
||||
handleLdapImport={ldap.handleLdapImport}
|
||||
setLdapUsers={ldap.setLdapUsers}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="roles" className="mt-4 space-y-4">
|
||||
{activeTab === 'roles' && (
|
||||
<SystemRolesTab
|
||||
selectedRole={selectedRole}
|
||||
setSelectedRole={setSelectedRole}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="settings" className="mt-4">
|
||||
{activeTab === 'settings' && (
|
||||
<SystemSettingsTab {...ldap.settingsTabProps} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ResetPasswordDialog user={resetPasswordUser} onClose={() => setResetPasswordUser(null)} />
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
<CardTitle className="text-base text-[var(--dg-text-primary)] flex items-center gap-1.5">
|
||||
{role.name}
|
||||
{role.isSystem && (
|
||||
<Badge className="bg-purple-500/15 text-purple-400 border-purple-500/30 text-[10px] px-1 py-0">
|
||||
<Badge className="bg-purple-500/15 dg-text-purple border-purple-500/30 text-[10px] px-1 py-0">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
</Badge>
|
||||
)}
|
||||
@@ -240,14 +240,14 @@ export function SystemRolesTab({ selectedRole, setSelectedRole }: SystemRolesTab
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-blue-400"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:dg-text-blue"
|
||||
onClick={() => openEdit(role)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:text-red-400"
|
||||
className="h-8 w-8 text-[var(--dg-text-muted)] hover:dg-text-red"
|
||||
onClick={() => handleDelete(role)}
|
||||
disabled={role.isSystem}
|
||||
title={role.isSystem ? '系统默认角色不可删除' : '删除角色'}
|
||||
@@ -301,10 +301,10 @@ function RoleDetailDialog({
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-purple-400" />
|
||||
<ShieldCheck className="h-5 w-5 dg-text-purple" />
|
||||
{role?.name}
|
||||
{isSystem && (
|
||||
<Badge className="bg-purple-500/15 text-purple-400 border-purple-500/30 text-[10px]">
|
||||
<Badge className="bg-purple-500/15 dg-text-purple border-purple-500/30 text-[10px]">
|
||||
<Lock className="h-2.5 w-2.5 mr-0.5" />系统
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -89,68 +89,69 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
handleLdapTest,
|
||||
} = props;
|
||||
|
||||
// 作为 SystemManager 子 Tab 渲染,外层已有 PageShell,此处仅输出配置内容 // Logan.Li
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-3xl mx-auto w-full space-y-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Lock className="h-5 w-5 text-blue-400" />
|
||||
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Lock className="h-5 w-5 dg-text-blue" />
|
||||
安全设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">密码策略与登录安全</CardDescription>
|
||||
<CardDescription className="dg-text-muted">密码策略与登录安全</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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">强制密码复杂度</p>
|
||||
<p className="text-xs dg-text-muted">要求密码包含大小写字母、数字和特殊字符</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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">登录失败锁定</p>
|
||||
<p className="text-xs dg-text-muted">连续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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">双因素认证</p>
|
||||
<p className="text-xs dg-text-muted">启用后需要手机验证码登录</p>
|
||||
</div>
|
||||
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<History className="h-5 w-5 text-emerald-400" />
|
||||
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<History className="h-5 w-5 dg-text-green" />
|
||||
审计设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">操作日志与审计策略</CardDescription>
|
||||
<CardDescription className="dg-text-muted">操作日志与审计策略</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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">记录所有操作</p>
|
||||
<p className="text-xs dg-text-muted">记录用户的所有操作行为</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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">日志保留时间</p>
|
||||
<p className="text-xs dg-text-muted">操作日志保留180天</p>
|
||||
</div>
|
||||
<Select defaultValue="180">
|
||||
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-32 h-8 dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="30">30天</SelectItem>
|
||||
<SelectItem value="90">90天</SelectItem>
|
||||
<SelectItem value="180">180天</SelectItem>
|
||||
@@ -161,25 +162,25 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Key className="h-5 w-5 text-amber-400" />
|
||||
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Key className="h-5 w-5 dg-text-yellow" />
|
||||
会话设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">会话超时与Token配置</CardDescription>
|
||||
<CardDescription className="dg-text-muted">会话超时与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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">会话超时时间</p>
|
||||
<p className="text-xs dg-text-muted">用户无操作30分钟后自动登出</p>
|
||||
</div>
|
||||
<Select defaultValue="30">
|
||||
<SelectTrigger className="w-32 h-8 bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="w-32 h-8 dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="15">15分钟</SelectItem>
|
||||
<SelectItem value="30">30分钟</SelectItem>
|
||||
<SelectItem value="60">1小时</SelectItem>
|
||||
@@ -189,103 +190,103 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
</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>
|
||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">单点登录</p>
|
||||
<p className="text-xs dg-text-muted">同一账号只能在一个设备登录</p>
|
||||
</div>
|
||||
<Switch className="data-[state=checked]:bg-blue-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-purple-400" />
|
||||
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 dg-text-purple" />
|
||||
系统信息
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500">版本与系统状态</CardDescription>
|
||||
<CardDescription className="dg-text-muted">版本与系统状态</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>
|
||||
<span className="text-sm dg-text-secondary">系统版本</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">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>
|
||||
<span className="text-sm dg-text-secondary">运行时间</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">30天 12小时</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* LDAP 配置 */}
|
||||
<Card className="bg-slate-900 border-slate-800 mt-4">
|
||||
<Card className="dg-card 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" />
|
||||
<CardTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<Network className="h-5 w-5 dg-text-cyan" />
|
||||
LDAP / Active Directory
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-500 mt-1">配置 LDAP 服务器,实现统一身份认证</CardDescription>
|
||||
<CardDescription className="dg-text-muted 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>
|
||||
<label className="text-xs dg-text-muted">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"
|
||||
className="dg-input"
|
||||
placeholder="ldap.example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">端口</label>
|
||||
<label className="text-xs dg-text-muted">端口</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"
|
||||
className="dg-input"
|
||||
placeholder="389"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">Base DN(用户搜索基础)</label>
|
||||
<label className="text-xs dg-text-muted">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"
|
||||
className="dg-input"
|
||||
placeholder="ou=users,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员 Bind DN</label>
|
||||
<label className="text-xs dg-text-muted">管理员 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"
|
||||
className="dg-input"
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-slate-500">管理员密码 {ldapConfig && '(留空不修改)'}</label>
|
||||
<label className="text-xs dg-text-muted">管理员密码 {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"
|
||||
className="dg-input"
|
||||
placeholder="******"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 col-span-2">
|
||||
<label className="text-xs text-slate-500">搜索过滤器(可选,用 %s 代表用户名)</label>
|
||||
<label className="text-xs dg-text-muted">搜索过滤器(可选,用 %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"
|
||||
className="dg-input"
|
||||
placeholder="(uid=%s)"
|
||||
/>
|
||||
</div>
|
||||
@@ -295,14 +296,14 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
onCheckedChange={(v) => setLdapForm({ ...ldapForm, use_ldaps: v })}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
/>
|
||||
<span className="text-sm text-slate-300">启用 LDAPS(SSL 加密)</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">启用 LDAPS(SSL 加密)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试/保存按钮 */}
|
||||
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-slate-800">
|
||||
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||
{ldapTestResult && (
|
||||
<span className={`text-xs ${ldapTestResult.includes('成功') ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
<span className={`text-xs ${ldapTestResult.includes('成功') ? 'dg-text-green' : 'dg-text-red'}`}>
|
||||
{ldapTestResult}
|
||||
</span>
|
||||
)}
|
||||
@@ -310,7 +311,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-slate-700 text-slate-300"
|
||||
className="dg-btn-secondary"
|
||||
onClick={handleLdapTest}
|
||||
disabled={ldapTesting}
|
||||
>
|
||||
@@ -319,7 +320,7 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
className="dg-btn-primary"
|
||||
onClick={handleLdapSave}
|
||||
disabled={ldapSaving}
|
||||
>
|
||||
@@ -336,6 +337,6 @@ export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
<LdapManagementSection {...props} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -63,20 +63,20 @@ export function TerminalSessionsTab() {
|
||||
const getStatusBadge = (status: string) => {
|
||||
if (status === 'active') {
|
||||
return (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
活动中
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
失败
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10">
|
||||
已关闭
|
||||
</Badge>
|
||||
);
|
||||
@@ -146,12 +146,12 @@ export function TerminalSessionsTab() {
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="relative w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索用户/服务器/IP..."
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="pl-9 h-9 dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -163,7 +163,7 @@ export function TerminalSessionsTab() {
|
||||
className={
|
||||
statusFilter === st
|
||||
? 'h-8 bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'h-8 border-slate-700 text-slate-400 hover:text-slate-100 hover:bg-slate-800'
|
||||
: 'h-8 border-[var(--dg-border)] dg-text-secondary hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}
|
||||
onClick={() => setStatusFilter(st)}
|
||||
>
|
||||
@@ -173,7 +173,7 @@ export function TerminalSessionsTab() {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
@@ -192,23 +192,23 @@ export function TerminalSessionsTab() {
|
||||
</div>
|
||||
|
||||
{/* Sessions Table */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-t border-slate-800">
|
||||
<div className="border-t border-[var(--dg-border)]">
|
||||
{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>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<div className="flex flex-col items-center justify-center py-16 dg-text-muted">
|
||||
<Search className="h-8 w-8 mb-3 opacity-40" />
|
||||
<p className="text-sm">暂无终端会话记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400 w-10">
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="dg-text-secondary w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allChecked}
|
||||
@@ -216,19 +216,19 @@ export function TerminalSessionsTab() {
|
||||
className="accent-blue-600 cursor-pointer"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400">用户</TableHead>
|
||||
<TableHead className="text-slate-400">服务器</TableHead>
|
||||
<TableHead className="text-slate-400">来源 IP</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>
|
||||
<TableHead className="dg-text-secondary">用户</TableHead>
|
||||
<TableHead className="dg-text-secondary">服务器</TableHead>
|
||||
<TableHead className="dg-text-secondary">来源 IP</TableHead>
|
||||
<TableHead className="dg-text-secondary">状态</TableHead>
|
||||
<TableHead className="dg-text-secondary">时长</TableHead>
|
||||
<TableHead className="dg-text-secondary">录制</TableHead>
|
||||
<TableHead className="dg-text-secondary">开始时间</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((s) => (
|
||||
<TableRow key={s.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableRow key={s.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -239,28 +239,28 @@ export function TerminalSessionsTab() {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-200">{s.username || '--'}</span>
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">{s.username || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-slate-200">{s.server_name || '--'}</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
<span className="text-sm text-[var(--dg-text-primary)]">{s.server_name || '--'}</span>
|
||||
<span className="text-xs dg-text-muted">
|
||||
{s.server_ip}:{s.server_port}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">{s.client_ip || '--'}</span>
|
||||
<span className="text-sm dg-text-secondary">{s.client_ip || '--'}</span>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(s.status)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-300">{formatDuration(s.duration)}</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{formatDuration(s.duration)}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs text-slate-500">{formatSize(s.record_size)}</span>
|
||||
<span className="text-xs dg-text-muted">{formatSize(s.record_size)}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
<span className="text-sm dg-text-secondary">
|
||||
{s.start_time ? new Date(s.start_time).toLocaleString() : '--'}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -269,7 +269,7 @@ export function TerminalSessionsTab() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800"
|
||||
className="h-8 w-8 dg-text-secondary hover:dg-text-blue hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => openReplay(s.id)}
|
||||
title="回放"
|
||||
>
|
||||
@@ -278,7 +278,7 @@ export function TerminalSessionsTab() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-400 hover:bg-slate-800"
|
||||
className="h-8 w-8 dg-text-secondary hover:dg-text-red hover:bg-[var(--dg-bg-hover)]"
|
||||
onClick={() => handleDelete(s.id)}
|
||||
title="删除"
|
||||
>
|
||||
|
||||
@@ -30,52 +30,52 @@ export function UserDetailDialog({
|
||||
}: UserDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogContent className="dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<UserIcon className="h-5 w-5 text-blue-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<UserIcon className="h-5 w-5 dg-text-blue" />
|
||||
{selectedUser?.nickname}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
<DialogDescription className="dg-text-muted">
|
||||
@{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 className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-[var(--dg-bg-tertiary)]/50">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">用户名</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.username}</p>
|
||||
<p className="text-xs dg-text-muted mb-1">用户名</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{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>
|
||||
<p className="text-xs dg-text-muted mb-1">昵称</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{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>
|
||||
<p className="text-xs dg-text-muted mb-1">邮箱</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{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>
|
||||
<p className="text-xs dg-text-muted mb-1">手机号</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{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>
|
||||
<p className="text-xs dg-text-muted mb-1">状态</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">{selectedUser && getUserStatusBadge(selectedUser.status)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">创建时间</p>
|
||||
<p className="text-sm text-slate-200">
|
||||
<p className="text-xs dg-text-muted mb-1">创建时间</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)]">
|
||||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">角色</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)] 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">
|
||||
<Badge key={role} variant="outline" className="border-[var(--dg-border)] dg-text-secondary">
|
||||
{role}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -86,7 +86,7 @@ export function UserDetailDialog({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-slate-700 text-slate-300"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
disabled={selectedUser?.authSource === 'ldap'}
|
||||
onClick={() => selectedUser && onResetPassword?.(selectedUser)}
|
||||
>
|
||||
|
||||
@@ -77,13 +77,13 @@ export function UserFormDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogContent className="dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-blue-400" />
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<User className="h-5 w-5 dg-text-blue" />
|
||||
{editingUser ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
<DialogDescription className="dg-text-muted">
|
||||
{editingUser ? '修改用户信息' : '创建新的系统用户'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -91,49 +91,49 @@ export function UserFormDialog({
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">用户名</label>
|
||||
<label className="text-xs dg-text-muted">用户名</label>
|
||||
<Input
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
disabled={!!editingUser}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="dg-input"
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">昵称</label>
|
||||
<label className="text-xs dg-text-muted">昵称</label>
|
||||
<Input
|
||||
value={form.nickname}
|
||||
onChange={(e) => setForm({ ...form, nickname: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="dg-input"
|
||||
placeholder="显示昵称"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">邮箱</label>
|
||||
<label className="text-xs dg-text-muted">邮箱</label>
|
||||
<Input
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="dg-input"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">手机号</label>
|
||||
<label className="text-xs dg-text-muted">手机号</label>
|
||||
<Input
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="dg-input"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">角色</label>
|
||||
<label className="text-xs dg-text-muted">角色</label>
|
||||
<Select value={form.role} onValueChange={(v) => setForm({ ...form, role: v })}>
|
||||
<SelectTrigger className="bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue placeholder="选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="dg-input">
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.id} value={role.name}>
|
||||
{role.name}
|
||||
@@ -143,15 +143,15 @@ export function UserFormDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">状态</label>
|
||||
<label className="text-xs dg-text-muted">状态</label>
|
||||
<Select
|
||||
value={String(form.status)}
|
||||
onValueChange={(v) => setForm({ ...form, status: Number(v) })}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectTrigger className="dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="1">正常</SelectItem>
|
||||
<SelectItem value="0">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -159,21 +159,21 @@ export function UserFormDialog({
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">
|
||||
<label className="text-xs dg-text-muted">
|
||||
密码{editingUser ? '(留空则不修改)' : ''}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
className="dg-input"
|
||||
placeholder={editingUser ? '留空则不修改' : '设置初始密码'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
|
||||
|
||||
@@ -24,24 +24,24 @@ export function ReplayCommandSidebar({ commands, activeOffset, onSeek }: Props)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-56 shrink-0 border-l border-slate-800 flex flex-col bg-slate-900/80">
|
||||
<div className="px-2.5 py-2 border-b border-slate-800">
|
||||
<div className="text-xs font-medium text-slate-300 mb-1.5">
|
||||
<div className="w-56 shrink-0 border-l border-[var(--dg-border)] flex flex-col bg-[var(--dg-bg-secondary)]">
|
||||
<div className="px-2.5 py-2 border-b border-[var(--dg-border)]">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-1.5">
|
||||
{t('audit.terminal.replayPlayer.commands')} ({commands.length})
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-slate-500" />
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 dg-text-muted" />
|
||||
<Input
|
||||
value={kw}
|
||||
onChange={(e) => setKw(e.target.value)}
|
||||
placeholder={t('audit.terminal.replayPlayer.searchCommand')}
|
||||
className="h-7 pl-7 text-xs bg-slate-800 border-slate-700"
|
||||
className="h-7 pl-7 text-xs dg-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="p-3 text-xs text-slate-500 text-center">{t('audit.terminal.replayPlayer.noCommands')}</div>
|
||||
<div className="p-3 text-xs dg-text-muted text-center">{t('audit.terminal.replayPlayer.noCommands')}</div>
|
||||
) : (
|
||||
<div className="p-1 space-y-0.5">
|
||||
{filtered.map((c) => {
|
||||
@@ -53,14 +53,14 @@ export function ReplayCommandSidebar({ commands, activeOffset, onSeek }: Props)
|
||||
onClick={() => onSeek(c.offsetSec)}
|
||||
className={cn(
|
||||
'w-full text-left rounded px-2 py-1.5 transition-colors',
|
||||
active ? 'bg-blue-500/15 border border-blue-500/30' : 'hover:bg-slate-800/60 border border-transparent',
|
||||
active ? 'bg-blue-500/15 border border-blue-500/30' : 'hover:bg-[var(--dg-bg-hover)]/60 border border-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-slate-500 font-mono shrink-0">{c.label}</span>
|
||||
{c.highRisk && <AlertTriangle className="w-3 h-3 text-red-400 shrink-0" />}
|
||||
<span className="text-[10px] dg-text-muted font-mono shrink-0">{c.label}</span>
|
||||
{c.highRisk && <AlertTriangle className="w-3 h-3 dg-text-red shrink-0" />}
|
||||
</div>
|
||||
<code className="text-[11px] text-slate-300 block truncate mt-0.5">{c.command}</code>
|
||||
<code className="text-[11px] text-[var(--dg-text-secondary)] block truncate mt-0.5">{c.command}</code>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user