优化页面

This commit is contained in:
dushixiang committed 2025-11-21 22:45:29 +08:00
1 parent 1f365604a5
commit 4cf4186b83
6 files changed
+579 -736

No files matched your search

+261 -128
View File
@@ -1,21 +1,35 @@
import {useEffect, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
import type {MenuProps} from 'antd';
import {App, Button, Card, Col, Descriptions, Dropdown, Row, Space, Spin, Tag} from 'antd';
import {Activity, ArrowLeft, Clock, RefreshCw, Shield, Terminal} from 'lucide-react';
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
import type {MenuProps, TabsProps} from 'antd';
import {App, Button, Card, Col, Descriptions, Dropdown, Row, Space, Spin, Tag, Tabs, Alert, Statistic} from 'antd';
import {
Activity,
ArrowLeft,
Clock,
RefreshCw,
Shield,
Terminal,
CheckCircle,
XCircle,
AlertTriangle,
TrendingUp
} from 'lucide-react';
import {getAgentForAdmin, getAuditResult, sendAuditCommand, type VPSAuditResult} from '../../api/agent';
import type {Agent} from '../../types';
import dayjs from 'dayjs';
import {getErrorMessage} from '../../lib/utils';
import AuditResultView from './AuditResultView';
const AgentDetail = () => {
const {id} = useParams<{ id: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const {message: messageApi} = App.useApp();
const [loading, setLoading] = useState(false);
const [agent, setAgent] = useState<Agent | null>(null);
const [auditResult, setAuditResult] = useState<VPSAuditResult | null>(null);
const [auditing, setAuditing] = useState(false);
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info');
const fetchData = async () => {
if (!id) return;
@@ -39,6 +53,12 @@ const AgentDetail = () => {
const handleStartAudit = async () => {
if (!id) return;
// 检查是否为 Linux 系统
if (!agent?.os.toLowerCase().includes('linux')) {
messageApi.warning('安全审计功能仅支持 Linux 系统');
return;
}
setAuditing(true);
try {
await sendAuditCommand(id);
@@ -59,6 +79,11 @@ const AgentDetail = () => {
fetchData();
}, [id]);
useEffect(() => {
// 同步 activeTab 到 URL
setSearchParams({tab: activeTab});
}, [activeTab]);
if (loading && !agent) {
return (
<div className="text-center py-24">
@@ -67,7 +92,7 @@ const AgentDetail = () => {
);
}
// 命令菜单配置 - 方便扩展新命令
// 命令菜单配置
const commandMenuItems: MenuProps['items'] = [
{
key: 'audit',
@@ -84,19 +109,200 @@ const AgentDetail = () => {
label: '刷新数据',
onClick: fetchData,
},
// 可以在这里添加更多命令
// {
// key: 'restart',
// icon: <RotateCw size={16} />,
// label: '重启探针',
// onClick: handleRestartAgent,
// },
// {
// key: 'update',
// icon: <Download size={16} />,
// label: '更新探针',
// onClick: handleUpdateAgent,
// },
];
// 计算审计统计
const passCount = auditResult?.securityChecks.filter(c => c.status === 'pass').length || 0;
const failCount = auditResult?.securityChecks.filter(c => c.status === 'fail').length || 0;
const warnCount = auditResult?.securityChecks.filter(c => c.status === 'warn').length || 0;
const totalCount = auditResult?.securityChecks.length || 0;
// Tab 项配置
const tabItems: TabsProps['items'] = [
{
key: 'info',
label: (
<span className="flex items-center gap-2">
<Activity size={16}/>
基本信息
</span>
),
children: (
<Descriptions column={{xs: 1, sm: 2}} bordered>
<Descriptions.Item label="探针名称">{agent?.name}</Descriptions.Item>
<Descriptions.Item label="探针ID">{agent?.id}</Descriptions.Item>
<Descriptions.Item label="主机名">{agent?.hostname}</Descriptions.Item>
<Descriptions.Item label="IP地址">{agent?.ip}</Descriptions.Item>
<Descriptions.Item label="操作系统">
<Tag color="blue">{agent?.os}</Tag>
</Descriptions.Item>
<Descriptions.Item label="系统架构">
<Tag>{agent?.arch}</Tag>
</Descriptions.Item>
<Descriptions.Item label="平台">
{agent?.platform ? <Tag color="purple">{agent.platform}</Tag> : '-'}
</Descriptions.Item>
<Descriptions.Item label="位置">
{agent?.location ? <Tag color="blue">{agent.location}</Tag> : '-'}
</Descriptions.Item>
<Descriptions.Item label="到期时间">
{agent?.expireTime ? (
<div className="flex flex-col gap-1">
<div>{new Date(agent.expireTime).toLocaleDateString('zh-CN')}</div>
{(() => {
const expireDate = new Date(agent.expireTime);
const now = new Date();
const isExpired = expireDate < now;
const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (isExpired) {
return <Tag color="red" bordered={false}>已过期</Tag>;
} else if (daysLeft <= 7) {
return <Tag color="orange" bordered={false}>{daysLeft}天后到期</Tag>;
} else if (daysLeft <= 30) {
return <Tag color="gold" bordered={false}>{daysLeft}天后到期</Tag>;
}
return null;
})()}
</div>
) : '-'}
</Descriptions.Item>
<Descriptions.Item label="探针版本">{agent?.version}</Descriptions.Item>
<Descriptions.Item label="最后活跃时间">
<Space>
<Clock size={14}/>
{agent?.lastSeenAt && dayjs(agent.lastSeenAt).format('YYYY-MM-DD HH:mm:ss')}
</Space>
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{agent?.createdAt && dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{agent?.updatedAt && dayjs(agent.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
</Descriptions>
),
},
{
key: 'audit',
label: (
<span className="flex items-center gap-2">
<Shield size={16}/>
安全审计
{auditResult && failCount > 0 && (
<Tag color="error" style={{marginLeft: 4}}>{failCount}</Tag>
)}
</span>
),
children: (
<Space direction="vertical">
{/* 非 Linux 系统提示 */}
{agent && !agent.os.toLowerCase().includes('linux') && (
<Alert
message="功能限制"
description="安全审计功能仅支持 Linux 系统。当前系统为 Windows 或其他系统,无法使用此功能。"
type="warning"
showIcon
/>
)}
{!auditResult ? (
agent?.os.toLowerCase().includes('linux') ? (
<Alert
message="暂无审计结果"
description={
<Space direction="vertical">
<span>该探针还没有进行过安全审计。点击右上角"下发命令"按钮,选择"安全审计"来执行首次审计。</span>
<Button
type="primary"
icon={<Shield size={16}/>}
onClick={handleStartAudit}
loading={auditing}
>
立即开始审计
</Button>
</Space>
}
type="info"
showIcon
/>
) : null
) : (
<>
{/* 审计概览 */}
<Card bordered={false}>
<Row gutter={[16, 16]}>
<Col xs={12} sm={6}>
<Statistic
title="总检查项"
value={totalCount}
prefix={<Shield size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="通过"
value={passCount}
valueStyle={{color: '#52c41a'}}
prefix={<CheckCircle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="失败"
value={failCount}
valueStyle={{color: '#ff4d4f'}}
prefix={<XCircle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="警告"
value={warnCount}
valueStyle={{color: '#faad14'}}
prefix={<AlertTriangle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={12}>
<Statistic
title="风险评分"
value={auditResult.riskScore}
suffix="/ 100"
valueStyle={{
color: auditResult.riskScore >= 80 ? '#ff4d4f' :
auditResult.riskScore >= 50 ? '#faad14' :
auditResult.riskScore >= 20 ? '#1890ff' : '#52c41a'
}}
prefix={<Activity size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={12}>
<div className="text-sm text-gray-500 mb-2">威胁等级</div>
<Tag
color={
auditResult.threatLevel === 'critical' || auditResult.threatLevel === 'high' ? 'error' :
auditResult.threatLevel === 'medium' ? 'warning' : 'success'
}
className="text-base px-3 py-1"
>
{auditResult.threatLevel === 'critical' ? '严重风险' :
auditResult.threatLevel === 'high' ? '高风险' :
auditResult.threatLevel === 'medium' ? '中风险' : '低风险'}
</Tag>
</Col>
</Row>
<div className="mt-4 text-sm text-gray-500">
最近审计时间: {dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm:ss')}
</div>
</Card>
{/* 详细审计结果 */}
<AuditResultView result={auditResult}/>
</>
)}
</Space>
),
},
];
return (
@@ -112,16 +318,6 @@ const AgentDetail = () => {
</Button>
<Space size={8}>
{auditResult && (
<Button
icon={<Shield size={16}/>}
onClick={() => navigate(`/admin/agents/${id}/audit`)}
size="middle"
>
<span className="hidden sm:inline">查看审计结果</span>
<span className="sm:hidden">审计</span>
</Button>
)}
<Dropdown
menu={{items: commandMenuItems}}
placement="bottomRight"
@@ -140,110 +336,47 @@ const AgentDetail = () => {
</Space>
</div>
{/* 基本信息 */}
<div>
<Card
title={
<Space>
<Activity size={20}/>
<span className="font-semibold">探针信息</span>
{agent?.status === 1 && <Tag color="success">在线</Tag>}
{agent?.status !== 1 && <Tag color="error">离线</Tag>}
</Space>
}
className="rounded-lg border border-gray-200 mt-0"
>
<Descriptions column={{xs: 1, sm: 2}} bordered>
<Descriptions.Item label="探针名称">{agent?.name}</Descriptions.Item>
<Descriptions.Item label="探针ID">{agent?.id}</Descriptions.Item>
<Descriptions.Item label="主机名">{agent?.hostname}</Descriptions.Item>
<Descriptions.Item label="IP地址">{agent?.ip}</Descriptions.Item>
<Descriptions.Item label="操作系统">
<Tag color="blue">{agent?.os}</Tag>
</Descriptions.Item>
<Descriptions.Item label="系统架构">
<Tag>{agent?.arch}</Tag>
</Descriptions.Item>
<Descriptions.Item label="探针版本">{agent?.version}</Descriptions.Item>
<Descriptions.Item label="最后活跃时间">
<Space>
<Clock size={14}/>
{agent?.lastSeenAt && dayjs(agent.lastSeenAt).format('YYYY-MM-DD HH:mm:ss')}
{/* 探针状态卡片 */}
<Card variant={'outlined'}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-4">
<Activity size={32} className="text-blue-500"/>
<div>
<h2 className="text-xl font-semibold m-0">{agent?.name || agent?.hostname}</h2>
<Space className="mt-1">
<span className="text-gray-500">{agent?.hostname}</span>
{agent?.status === 1 ? (
<Tag color="success">在线</Tag>
) : (
<Tag color="error">离线</Tag>
)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{agent?.createdAt && dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{agent?.updatedAt && dayjs(agent.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
</Descriptions>
</Card>
</div>
</div>
</div>
{auditResult && (
<div className="text-center">
<div className="text-sm text-gray-500">安全评分</div>
<div className={`text-3xl font-bold ${
auditResult.riskScore >= 80 ? 'text-red-500' :
auditResult.riskScore >= 50 ? 'text-orange-500' :
auditResult.riskScore >= 20 ? 'text-blue-500' : 'text-green-500'
}`}>
{100 - auditResult.riskScore}
</div>
</div>
)}
</div>
</Card>
{/* 审计结果预览 */}
<div>
{auditResult && (
<Card
title={
<Space>
<Shield size={20}/>
<span className="font-semibold">最近审计结果</span>
<Tag color="blue">
{dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm')}
</Tag>
</Space>
}
className="rounded-lg border border-gray-200 mt-4 lg:mt-6"
extra={
<Button
type="link"
onClick={() => navigate(`/admin/agents/${id}/audit`)}
>
查看详情
</Button>
}
>
<Row gutter={[16, 16]}>
<Col xs={12} sm={6}>
<div className="text-center">
<div className="text-2xl font-semibold text-gray-900">
{auditResult.securityChecks.length}
</div>
<div className="text-sm text-gray-500 mt-1">总检查项</div>
</div>
</Col>
<Col xs={12} sm={6}>
<div className="text-center">
<div className="text-2xl font-semibold text-emerald-600">
{auditResult.securityChecks.filter(c => c.status === 'pass').length}
</div>
<div className="text-sm text-gray-500 mt-1">通过</div>
</div>
</Col>
<Col xs={12} sm={6}>
<div className="text-center">
<div className="text-2xl font-semibold text-rose-600">
{auditResult.securityChecks.filter(c => c.status === 'fail').length}
</div>
<div className="text-sm text-gray-500 mt-1">失败</div>
</div>
</Col>
<Col xs={12} sm={6}>
<div className="text-center">
<div className="text-2xl font-semibold text-orange-600">
{auditResult.securityChecks.filter(c => c.status === 'warn').length}
</div>
<div className="text-sm text-gray-500 mt-1">警告</div>
</div>
</Col>
</Row>
</Card>
)}
</div>
{/* Tabs 内容 */}
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
size="large"
/>
</div>
);
};
export default AgentDetail;
+19 -2
View File
@@ -3,7 +3,7 @@ import {useNavigate} from 'react-router-dom';
import type {ActionType, ProColumns} from '@ant-design/pro-components';
import {ProTable} from '@ant-design/pro-components';
import {App, Button, DatePicker, Divider, Form, Input, Modal, Space, Tag} from 'antd';
import {Edit, Eye, RefreshCw} from 'lucide-react';
import {Edit, Eye, RefreshCw, Plus, Shield} from 'lucide-react';
import {getAgentPaging, updateAgentInfo} from '../../api/agent';
import type {Agent} from '../../types';
import {getErrorMessage} from '../../lib/utils';
@@ -31,6 +31,7 @@ const AgentList = () => {
setEditModalVisible(true);
};
// 保存探针信息
const handleSave = async () => {
if (!currentAgent) return;
@@ -167,10 +168,19 @@ const AgentList = () => {
title: '操作',
key: 'action',
valueType: 'option',
width: 150,
width: 220,
fixed: 'right',
render: (_, record) => (
<Space>
<Button
type="link"
icon={<Shield size={14}/>}
onClick={() => navigate(`/admin/agents/${record.id}?tab=audit`)}
style={{padding: 0, margin: 0, color: '#1890ff'}}
title="安全审计"
>
审计
</Button>
<Button
type="link"
icon={<Edit size={14}/>}
@@ -199,6 +209,13 @@ const AgentList = () => {
title="探针管理"
description="管理和监控系统探针状态"
actions={[
{
key: 'register',
label: '注册探针',
icon: <Plus size={16}/>,
onClick: () => navigate('/admin/agents/deploy-guide'),
type: 'primary',
},
{
key: 'refresh',
label: '刷新',
-483
View File
@@ -1,483 +0,0 @@
import { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Button,
Space,
Spin,
App,
Descriptions,
Tag,
List,
Collapse,
Alert,
Row,
Col,
Statistic
} from 'antd';
import {
ArrowLeft,
Shield,
CheckCircle,
XCircle,
AlertTriangle,
MinusCircle,
RefreshCw,
FileText,
Hash,
GitBranch,
Clock,
Activity
} from 'lucide-react';
import { getAuditResult, sendAuditCommand, listAuditResults } from '../../api/agent';
import type { VPSAuditResult, AuditResultSummary } from '../../api/agent';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
dayjs.extend(duration);
const { Panel } = Collapse;
const AuditResult = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { message: messageApi } = App.useApp();
const [loading, setLoading] = useState(false);
const [auditing, setAuditing] = useState(false);
const [result, setResult] = useState<VPSAuditResult | null>(null);
const [historyList, setHistoryList] = useState<AuditResultSummary[]>([]);
const fetchData = async () => {
if (!id) return;
setLoading(true);
try {
const [resultRes, historyRes] = await Promise.all([
getAuditResult(id).catch(() => ({ data: null })),
listAuditResults(id),
]);
setResult(resultRes.data);
setHistoryList(historyRes.data.items || []);
} catch (error: any) {
// 如果没有审计结果,不显示错误
if (error.response?.status !== 404) {
messageApi.error(error.response?.data?.message || '获取审计结果失败');
}
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [id]);
const handleStartAudit = async () => {
if (!id) return;
setAuditing(true);
try {
await sendAuditCommand(id);
messageApi.success('安全审计已启动,请稍后查看结果');
// 5秒后刷新结果
setTimeout(() => {
fetchData();
}, 5000);
} catch (error: any) {
messageApi.error(error.response?.data?.message || '启动审计失败');
} finally {
setAuditing(false);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'pass':
return <CheckCircle size={16} className="text-green-500" />;
case 'fail':
return <XCircle size={16} className="text-red-500" />;
case 'warn':
return <AlertTriangle size={16} className="text-yellow-500" />;
case 'skip':
return <MinusCircle size={16} className="text-gray-400" />;
default:
return null;
}
};
const getStatusTag = (status: string) => {
const configs = {
pass: { color: 'success', text: '通过' },
fail: { color: 'error', text: '失败' },
warn: { color: 'warning', text: '警告' },
skip: { color: 'default', text: '跳过' },
};
const config = configs[status as keyof typeof configs] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
};
const getCategoryName = (category: string) => {
const names: Record<string, string> = {
'non_root_user': '非Root用户',
'ufw_security': 'UFW防火墙',
'ssh_security': 'SSH安全',
'access_control': '访问控制',
'port_security': '端口安全',
'unattended_upgrades': '自动更新',
'fail2ban': 'Fail2ban',
'rootkit_detection': 'Rootkit检测',
'suspicious_processes': '可疑进程检测',
'listening_ports': '端口监听检查',
'cron_jobs': '定时任务检查',
'suspicious_files': '可疑文件检查',
'system_accounts': '系统账户检查',
'network_connections': '网络连接检查',
'file_integrity': '文件完整性检查',
'login_history': '登录历史检查',
'immutable_files': '不可变文件检查',
'suspicious_env_vars': '环境变量检查',
};
return names[category] || category;
};
const getThreatLevelConfig = (level: string) => {
const configs = {
low: { color: 'success', text: '低风险', icon: CheckCircle },
medium: { color: 'warning', text: '中风险', icon: AlertTriangle },
high: { color: 'error', text: '高风险', icon: XCircle },
critical: { color: 'error', text: '严重风险', icon: XCircle },
};
return configs[level as keyof typeof configs] || configs.low;
};
const formatUptime = (seconds: number) => {
const d = dayjs.duration(seconds, 'seconds');
const days = Math.floor(d.asDays());
const hours = d.hours();
const minutes = d.minutes();
return `${days}天 ${hours}小时 ${minutes}分钟`;
};
if (loading) {
return (
<div className="text-center py-24">
<Spin size="large" />
</div>
);
}
const passCount = result?.securityChecks.filter(c => c.status === 'pass').length || 0;
const failCount = result?.securityChecks.filter(c => c.status === 'fail').length || 0;
const warnCount = result?.securityChecks.filter(c => c.status === 'warn').length || 0;
const totalCount = result?.securityChecks.length || 0;
return (
<Space direction={'vertical'} className={'w-full'}>
{/* 页面头部 */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<Button
icon={<ArrowLeft size={16} />}
onClick={() => navigate(`/admin/agents/${id}`)}
size="middle"
>
返回详情
</Button>
<Button
type="primary"
icon={<RefreshCw size={16} />}
onClick={handleStartAudit}
loading={auditing}
size="middle"
>
{auditing ? '审计中...' : '开始审计'}
</Button>
</div>
{!result && (
<Alert
message="暂无审计结果"
description='点击上方"开始审计"按钮执行VPS安全审计'
type="info"
showIcon
className="rounded-lg"
/>
)}
{result && (
<>
{/* 统计信息 */}
<Card className="rounded-lg border border-gray-200 mt-0">
<Row gutter={[16, 16]}>
<Col xs={12} sm={6} md={4}>
<Statistic
title="总检查项"
value={totalCount}
prefix={<Shield size={18} className="lg:w-5 lg:h-5" />}
/>
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic
title="通过"
value={passCount}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircle size={18} className="lg:w-5 lg:h-5" />}
/>
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic
title="失败"
value={failCount}
valueStyle={{ color: '#ff4d4f' }}
prefix={<XCircle size={18} className="lg:w-5 lg:h-5" />}
/>
</Col>
<Col xs={12} sm={6} md={4}>
<Statistic
title="警告"
value={warnCount}
valueStyle={{ color: '#faad14' }}
prefix={<AlertTriangle size={18} className="lg:w-5 lg:h-5" />}
/>
</Col>
<Col xs={12} sm={12} md={4}>
<Statistic
title="风险评分"
value={result.riskScore}
suffix="/ 100"
valueStyle={{
color: result.riskScore >= 80 ? '#ff4d4f' :
result.riskScore >= 50 ? '#faad14' :
result.riskScore >= 20 ? '#1890ff' : '#52c41a'
}}
prefix={<Activity size={18} className="lg:w-5 lg:h-5" />}
/>
</Col>
<Col xs={12} sm={12} md={4}>
<div className="text-sm text-gray-500 mb-2">威胁等级</div>
<Tag
color={getThreatLevelConfig(result.threatLevel).color}
className="text-base px-3 py-1"
>
{getThreatLevelConfig(result.threatLevel).text}
</Tag>
</Col>
</Row>
</Card>
{/* 系统信息 */}
<Card
title={<span className="font-semibold">系统信息</span>}
className="rounded-lg border border-gray-200 mt-4 lg:mt-6"
>
<Descriptions column={{ xs: 1, sm: 2 }} bordered>
<Descriptions.Item label="主机名">
{result.systemInfo.hostname}
</Descriptions.Item>
<Descriptions.Item label="操作系统">
{result.systemInfo.os}
</Descriptions.Item>
<Descriptions.Item label="内核版本">
{result.systemInfo.kernelVersion}
</Descriptions.Item>
<Descriptions.Item label="运行时长">
{formatUptime(result.systemInfo.uptime)}
</Descriptions.Item>
{result.systemInfo.publicIP && (
<Descriptions.Item label="公网IP" span={2}>
{result.systemInfo.publicIP}
</Descriptions.Item>
)}
<Descriptions.Item label="审计时间" span={2}>
{dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')}
<span className="ml-2 text-gray-500">
(耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒)
</span>
</Descriptions.Item>
</Descriptions>
</Card>
{/* 安全检查结果 */}
<Card
title={<span className="font-semibold">安全检查结果</span>}
className="rounded-lg border border-gray-200 mt-4 lg:mt-6"
>
<Collapse accordion>
{result.securityChecks.map((check, index) => (
<Panel
key={index}
header={
<Space>
{getStatusIcon(check.status)}
<span className="font-medium">
{getCategoryName(check.category)}
</span>
{getStatusTag(check.status)}
<span className="text-gray-500 text-sm">
{check.message}
</span>
</Space>
}
>
{check.details && check.details.length > 0 && (
<List
size="small"
dataSource={check.details}
renderItem={(detail) => (
<List.Item className="flex-col items-start">
<Space className="w-full">
{getStatusIcon(detail.status)}
<span className="font-mono text-sm">
{detail.name}
</span>
{getStatusTag(detail.status)}
<span className="text-gray-600 flex-1">
{detail.message}
</span>
</Space>
{detail.evidence && (
<div className="ml-6 mt-2 p-3 bg-gray-50 rounded text-xs w-full">
<div className="font-semibold mb-2 flex items-center gap-2">
<FileText size={14} />
证据信息
{detail.evidence.riskLevel && (
<Tag
color={
detail.evidence.riskLevel === 'high' ? 'error' :
detail.evidence.riskLevel === 'medium' ? 'warning' : 'default'
}
className="ml-2"
>
{detail.evidence.riskLevel === 'high' ? '高危' :
detail.evidence.riskLevel === 'medium' ? '中危' : '低危'}
</Tag>
)}
</div>
{detail.evidence.filePath && (
<div className="mb-1">
<span className="font-medium">文件路径: </span>
<code className="bg-white px-1">{detail.evidence.filePath}</code>
</div>
)}
{detail.evidence.fileHash && (
<div className="mb-1 flex items-start gap-1">
<Hash size={12} className="mt-0.5" />
<span className="font-medium">SHA256: </span>
<code className="bg-white px-1 break-all">{detail.evidence.fileHash}</code>
</div>
)}
{detail.evidence.timestamp && (
<div className="mb-1 flex items-center gap-1">
<Clock size={12} />
<span className="font-medium">时间: </span>
{dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')}
</div>
)}
{detail.evidence.networkConn && (
<div className="mb-1">
<span className="font-medium">网络连接: </span>
<code className="bg-white px-1">{detail.evidence.networkConn}</code>
</div>
)}
{detail.evidence.processTree && detail.evidence.processTree.length > 0 && (
<div className="mt-2">
<div className="font-medium mb-1 flex items-center gap-1">
<GitBranch size={12} />
进程树:
</div>
<div className="bg-white p-2 rounded font-mono text-xs overflow-x-auto">
{detail.evidence.processTree.map((line, idx) => (
<div key={idx} className="whitespace-nowrap">
{line}
</div>
))}
</div>
</div>
)}
</div>
)}
</List.Item>
)}
/>
)}
</Panel>
))}
</Collapse>
</Card>
{/* 修复建议 */}
{result.recommendations && result.recommendations.length > 0 && (
<Card
title={<span className="font-semibold">修复建议</span>}
className="rounded-lg border border-gray-200 mt-4 lg:mt-6"
>
<List
dataSource={result.recommendations}
renderItem={(item, index) => (
<List.Item>
<Space align="start">
<span className="font-semibold text-gray-500">{index + 1}.</span>
<span className={
item.startsWith('【紧急】') ? 'text-red-600 font-medium' :
item.startsWith('【警告】') ? 'text-yellow-600' :
'text-gray-700'
}>
{item}
</span>
</Space>
</List.Item>
)}
/>
</Card>
)}
</>
)}
{/* 历史记录 */}
{historyList.length > 0 && (
<Card
title={<span className="font-semibold">审计历史</span>}
className="rounded-lg border border-gray-200 mt-4 lg:mt-6"
>
<List
dataSource={historyList}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
title={
<Space>
<span>
{dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</span>
<Tag color="blue">{item.systemInfo.os}</Tag>
</Space>
}
description={
<Space size="large">
<span>
<CheckCircle size={14} className="inline mr-1 text-green-500" />
通过: {item.passCount}
</span>
<span>
<XCircle size={14} className="inline mr-1 text-red-500" />
失败: {item.failCount}
</span>
<span>
<AlertTriangle size={14} className="inline mr-1 text-yellow-500" />
警告: {item.warnCount}
</span>
</Space>
}
/>
</List.Item>
)}
/>
</Card>
)}
</Space>
);
};
export default AuditResult;
+254
View File
@@ -0,0 +1,254 @@
import {Card, Collapse, List, Space, Tag, Descriptions} from 'antd';
import {
CheckCircle,
XCircle,
AlertTriangle,
MinusCircle,
FileText,
Hash,
GitBranch,
Clock
} from 'lucide-react';
import type {VPSAuditResult} from '../../api/agent';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
dayjs.extend(duration);
const {Panel} = Collapse;
interface AuditResultViewProps {
result: VPSAuditResult;
}
const AuditResultView = ({result}: AuditResultViewProps) => {
const getStatusIcon = (status: string) => {
switch (status) {
case 'pass':
return <CheckCircle size={16} className="text-green-500"/>;
case 'fail':
return <XCircle size={16} className="text-red-500"/>;
case 'warn':
return <AlertTriangle size={16} className="text-yellow-500"/>;
case 'skip':
return <MinusCircle size={16} className="text-gray-400"/>;
default:
return null;
}
};
const getStatusTag = (status: string) => {
const configs = {
pass: {color: 'success', text: '通过'},
fail: {color: 'error', text: '失败'},
warn: {color: 'warning', text: '警告'},
skip: {color: 'default', text: '跳过'},
};
const config = configs[status as keyof typeof configs] || {color: 'default', text: status};
return <Tag color={config.color}>{config.text}</Tag>;
};
const getCategoryName = (category: string) => {
const names: Record<string, string> = {
'non_root_user': '非Root用户',
'ufw_security': 'UFW防火墙',
'ssh_security': 'SSH安全',
'access_control': '访问控制',
'port_security': '端口安全',
'unattended_upgrades': '自动更新',
'fail2ban': 'Fail2ban',
'rootkit_detection': 'Rootkit检测',
'suspicious_processes': '可疑进程检测',
'listening_ports': '端口监听检查',
'cron_jobs': '定时任务检查',
'suspicious_files': '可疑文件检查',
'system_accounts': '系统账户检查',
'network_connections': '网络连接检查',
'file_integrity': '文件完整性检查',
'login_history': '登录历史检查',
'immutable_files': '不可变文件检查',
'suspicious_env_vars': '环境变量检查',
};
return names[category] || category;
};
const formatUptime = (seconds: number) => {
const d = dayjs.duration(seconds, 'seconds');
const days = Math.floor(d.asDays());
const hours = d.hours();
const minutes = d.minutes();
return `${days}天 ${hours}小时 ${minutes}分钟`;
};
return (
<div className="space-y-4">
{/* 系统信息 */}
<Card
title={<span className="font-semibold">系统信息</span>}
variant={'outlined'}
>
<Descriptions column={{xs: 1, sm: 2}} bordered>
<Descriptions.Item label="主机名">
{result.systemInfo.hostname}
</Descriptions.Item>
<Descriptions.Item label="操作系统">
{result.systemInfo.os}
</Descriptions.Item>
<Descriptions.Item label="内核版本">
{result.systemInfo.kernelVersion}
</Descriptions.Item>
<Descriptions.Item label="运行时长">
{formatUptime(result.systemInfo.uptime)}
</Descriptions.Item>
{result.systemInfo.publicIP && (
<Descriptions.Item label="公网IP" span={2}>
{result.systemInfo.publicIP}
</Descriptions.Item>
)}
<Descriptions.Item label="审计时间" span={2}>
{dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')}
<span className="ml-2 text-gray-500">
(耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒)
</span>
</Descriptions.Item>
</Descriptions>
</Card>
{/* 安全检查结果 */}
<Card
title={<span className="font-semibold">安全检查结果</span>}
variant={'outlined'}
>
<Collapse accordion>
{result.securityChecks.map((check, index) => (
<Panel
key={index}
header={
<Space>
{getStatusIcon(check.status)}
<span className="font-medium">
{getCategoryName(check.category)}
</span>
{getStatusTag(check.status)}
<span className="text-gray-500 text-sm">
{check.message}
</span>
</Space>
}
>
{check.details && check.details.length > 0 && (
<List
size="small"
dataSource={check.details}
renderItem={(detail) => (
<List.Item className="flex-col items-start">
<Space className="w-full">
{getStatusIcon(detail.status)}
<span className="font-mono text-sm">
{detail.name}
</span>
{getStatusTag(detail.status)}
<span className="text-gray-600 flex-1">
{detail.message}
</span>
</Space>
{detail.evidence && (
<div className="ml-6 mt-2 p-3 bg-gray-50 rounded text-xs w-full">
<div className="font-semibold mb-2 flex items-center gap-2">
<FileText size={14}/>
证据信息
{detail.evidence.riskLevel && (
<Tag
color={
detail.evidence.riskLevel === 'high' ? 'error' :
detail.evidence.riskLevel === 'medium' ? 'warning' : 'default'
}
className="ml-2"
>
{detail.evidence.riskLevel === 'high' ? '高危' :
detail.evidence.riskLevel === 'medium' ? '中危' : '低危'}
</Tag>
)}
</div>
{detail.evidence.filePath && (
<div className="mb-1">
<span className="font-medium">文件路径: </span>
<code className="bg-white px-1">{detail.evidence.filePath}</code>
</div>
)}
{detail.evidence.fileHash && (
<div className="mb-1 flex items-start gap-1">
<Hash size={12} className="mt-0.5"/>
<span className="font-medium">SHA256: </span>
<code className="bg-white px-1 break-all">{detail.evidence.fileHash}</code>
</div>
)}
{detail.evidence.timestamp && (
<div className="mb-1 flex items-center gap-1">
<Clock size={12}/>
<span className="font-medium">时间: </span>
{dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')}
</div>
)}
{detail.evidence.networkConn && (
<div className="mb-1">
<span className="font-medium">网络连接: </span>
<code className="bg-white px-1">{detail.evidence.networkConn}</code>
</div>
)}
{detail.evidence.processTree && detail.evidence.processTree.length > 0 && (
<div className="mt-2">
<div className="font-medium mb-1 flex items-center gap-1">
<GitBranch size={12}/>
进程树:
</div>
<div className="bg-white p-2 rounded font-mono text-xs overflow-x-auto">
{detail.evidence.processTree.map((line, idx) => (
<div key={idx} className="whitespace-nowrap">
{line}
</div>
))}
</div>
</div>
)}
</div>
)}
</List.Item>
)}
/>
)}
</Panel>
))}
</Collapse>
</Card>
{/* 修复建议 */}
{result.recommendations && result.recommendations.length > 0 && (
<Card
title={<span className="font-semibold">修复建议</span>}
variant={'outlined'}
>
<List
dataSource={result.recommendations}
renderItem={(item, index) => (
<List.Item>
<Space align="start">
<span className="font-semibold text-gray-500">{index + 1}.</span>
<span className={
item.startsWith('【紧急】') ? 'text-red-600 font-medium' :
item.startsWith('【警告】') ? 'text-yellow-600' :
'text-gray-700'
}>
{item}
</span>
</Space>
</List.Item>
)}
/>
</Card>
)}
</div>
);
};
export default AuditResultView;
-107
View File
@@ -97,15 +97,6 @@ const MonitorList = () => {
const monitorSummaries = monitors;
const lastUpdatedDisplay =
dataUpdatedAt && dataUpdatedAt > 0
? new Date(dataUpdatedAt).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
: '尚未刷新';
const renderGridView = () => (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{monitorSummaries.map((stats) => {
@@ -221,100 +212,6 @@ const MonitorList = () => {
const renderListView = () => (
<>
{/* 移动端:使用卡片式布局 */}
<div className="flex flex-col gap-4 lg:hidden">
{monitorSummaries.map((stats) => {
const hasCert = stats.certExpiryDate > 0;
const certExpiringSoon = hasCert && stats.certExpiryDays < 30;
return (
<div
key={stats.id}
role="button"
tabIndex={0}
onClick={() => navigate(`/monitors/${encodeURIComponent(stats.id)}`)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
navigate(`/monitors/${encodeURIComponent(stats.id)}`);
}
}}
className="cursor-pointer rounded-xl border border-slate-200 bg-white p-4 transition hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300"
>
<div className="mb-3 flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-base font-semibold text-slate-900">
{stats.name}
</span>
<StatusBadge status={stats.lastCheckStatus}/>
</div>
<p className="mt-1 text-sm text-slate-500 break-all">
{stats.showTargetPublic ? stats.target : '***'}
</p>
{stats.agentCount > 1 && (
<p className="mt-1 text-xs text-slate-400">
{stats.agentCount} 个探针
</p>
)}
</div>
</div>
<div className="space-y-2">
<div
className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<span className="text-xs font-medium text-slate-600">当前响应</span>
<span
className="text-sm font-bold text-slate-900">{formatTime(stats.currentResponse)}</span>
</div>
<div
className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<span className="text-xs font-medium text-slate-600">24h 平均响应</span>
<span
className="text-sm font-bold text-slate-900">{formatTime(stats.avgResponse24h)}</span>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-slate-600">24h 在线率</span>
<span
className="text-xs font-bold text-slate-900">{formatPercentValue(stats.uptime24h)}%</span>
</div>
<UptimeBar uptime={stats.uptime24h}/>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-slate-600">30d 在线率</span>
<span
className="text-xs font-bold text-slate-900">{formatPercentValue(stats.uptime30d)}%</span>
</div>
<UptimeBar uptime={stats.uptime30d}/>
</div>
{hasCert && (
<div
className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<div className="flex items-center gap-2">
<Shield
className={`h-4 w-4 ${certExpiringSoon ? 'text-yellow-600' : 'text-slate-600'}`}/>
<span className="text-xs font-medium text-slate-600">证书到期</span>
</div>
<div className="text-right">
<div
className={`text-xs font-medium ${certExpiringSoon ? 'text-yellow-700' : 'text-slate-700'}`}>
{formatDate(stats.certExpiryDate)}
</div>
<div
className={`text-xs ${certExpiringSoon ? 'text-yellow-600' : 'text-slate-500'}`}>
剩余 {stats.certExpiryDays} 天
</div>
</div>
</div>
)}
</div>
</div>
);
})}
</div>
{/* 桌面端:使用表格布局 */}
<div className="hidden overflow-hidden rounded-xl border border-slate-200 bg-white lg:block">
<table className="min-w-full divide-y divide-slate-200 text-sm">
@@ -435,8 +332,6 @@ const MonitorList = () => {
return (
<div className="min-h-screen bg-white text-slate-900 flex flex-col">
<PublicHeader
title="服务监控"
lastUpdated={lastUpdatedDisplay}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
@@ -454,8 +349,6 @@ const MonitorList = () => {
return (
<div className="min-h-screen bg-white text-slate-900 flex flex-col">
<PublicHeader
title="服务监控"
lastUpdated={lastUpdatedDisplay}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
+45 -16
View File
@@ -1,7 +1,17 @@
import React, {type ReactNode} from 'react';
import {useEffect, useMemo, useState} from 'react';
import React, {type ReactNode, useEffect, useMemo, useState} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
import {Activity, ArrowLeft, Cpu, HardDrive, Loader2, MemoryStick, Network, Server, Box, Thermometer, Zap} from 'lucide-react';
import {
Activity,
ArrowLeft,
Cpu,
HardDrive,
Loader2,
MemoryStick,
Network,
Server,
Thermometer,
Zap
} from 'lucide-react';
import type {TooltipProps} from 'recharts';
import {
Area,
@@ -15,15 +25,22 @@ import {
XAxis,
YAxis,
} from 'recharts';
import {getAgent, getAgentLatestMetrics, getAgentMetrics, type GetAgentMetricsRequest, getNetworkMetricsByInterface} from '../../api/agent';
import {
getAgent,
getAgentLatestMetrics,
getAgentMetrics,
type GetAgentMetricsRequest,
getNetworkMetricsByInterface,
type NetworkMetricByInterface
} from '../../api/agent';
import type {
Agent,
AggregatedCPUMetric,
AggregatedDiskIOMetric,
AggregatedDiskMetric,
AggregatedGPUMetric,
AggregatedMemoryMetric,
AggregatedNetworkMetric,
AggregatedDiskMetric,
AggregatedDiskIOMetric,
AggregatedGPUMetric,
AggregatedTemperatureMetric,
LatestMetrics
} from '../../types';
@@ -340,7 +357,7 @@ const useAggregatedMetrics = (agentId: string | undefined, range: TimeRange) =>
// 用于获取按网卡分组的网络指标
const useNetworkMetricsByInterface = (agentId: string | undefined, range: TimeRange) => {
const [networkMetrics, setNetworkMetrics] = useState<AggregatedNetworkMetric[]>([]);
const [networkMetrics, setNetworkMetrics] = useState<NetworkMetricByInterface[]>([]);
useEffect(() => {
if (!agentId) {
@@ -577,7 +594,12 @@ const ServerDetail = () => {
// GPU 图表数据(汇总所有GPU的平均利用率)
const gpuChartData = useMemo(() => {
const aggregated: Record<string, { time: string; utilization: number; temperature: number; count: number }> = {};
const aggregated: Record<string, {
time: string;
utilization: number;
temperature: number;
count: number
}> = {};
metricsData.gpu.forEach((item) => {
const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', {
@@ -775,8 +797,10 @@ const ServerDetail = () => {
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<div className="mx-auto flex min-h-screen max-w-7xl flex-col px-4 pb-10 pt-6 sm:px-6 lg:px-8">
<section className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-slate-900 via-slate-800 to-blue-900 p-6 text-white shadow-xl">
<div className="absolute inset-0 opacity-30 [background-image:radial-gradient(circle_at_top,rgba(255,255,255,0.35),transparent_55%)]"/>
<section
className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-slate-900 via-slate-800 to-blue-900 p-6 text-white shadow-xl">
<div
className="absolute inset-0 opacity-30 [background-image:radial-gradient(circle_at_top,rgba(255,255,255,0.35),transparent_55%)]"/>
<div className="relative flex flex-col gap-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-4">
@@ -789,7 +813,8 @@ const ServerDetail = () => {
返回概览
</button>
<div className="flex items-start gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 text-white">
<div
className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 text-white">
<Server className="h-7 w-7"/>
</div>
<div>
@@ -1209,7 +1234,8 @@ const ServerDetail = () => {
>
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-purple-50 text-purple-600">
<span
className="flex h-9 w-9 items-center justify-center rounded-lg bg-purple-50 text-purple-600">
<Zap className="h-4 w-4"/>
</span>
<div>
@@ -1224,7 +1250,8 @@ const ServerDetail = () => {
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-slate-500">温度</span>
<span className="font-medium text-slate-900">{gpu.temperature.toFixed(1)}°C</span>
<span
className="font-medium text-slate-900">{gpu.temperature.toFixed(1)}°C</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">显存</span>
@@ -1234,11 +1261,13 @@ const ServerDetail = () => {
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">功耗</span>
<span className="font-medium text-slate-900">{gpu.powerDraw.toFixed(1)}W</span>
<span
className="font-medium text-slate-900">{gpu.powerDraw.toFixed(1)}W</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500">风扇转速</span>
<span className="font-medium text-slate-900">{gpu.fanSpeed.toFixed(0)}%</span>
<span
className="font-medium text-slate-900">{gpu.fanSpeed.toFixed(0)}%</span>
</div>
</div>
</div>