feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
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
+21 -21
View File
@@ -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>
);
})}