refactor: 优化探针详情页与流量统计页

- 探针详情: /admin/agents 前缀匹配宽屏布局;状态并入页头,返回列表改为名称旁小字链接,编辑探针移至页头右侧;基本信息/流量/防篡改/SSH 配置去卡片嵌套,内容扁平化
- 流量统计: 重构页面布局,配置与使用情况分栏;流量限额支持 GB/TB 单位切换与自动适配(后端字节存储无需改动);限额输入与单位下拉组合;使用情况改为卡片呈现
This commit is contained in:
dushixiang committed 2026-08-23 20:32:56 +08:00
1 parent 6c8d2f13d3
commit 7603906c36
7 files changed
+189 -189

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ export interface Action {
} }
interface PageHeaderProps { interface PageHeaderProps {
title: string; title: React.ReactNode;
extra?: React.ReactNode; extra?: React.ReactNode;
actions?: Action[]; actions?: Action[];
} }
+3 -2
View File
@@ -11,8 +11,9 @@ import type {User} from '@/types';
const AdminLayout = () => { const AdminLayout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const isWideCompactPage = [ // /admin/agents 前缀(含探针详情页)使用全宽布局
'/admin/agents', const isWideCompactPage = location.pathname.startsWith('/admin/agents')
|| [
'/admin/monitors', '/admin/monitors',
'/admin/ddns', '/admin/ddns',
'/admin/api-keys', '/admin/api-keys',
+4 -58
View File
@@ -1,31 +1,17 @@
import {useState} from 'react'; import {Descriptions, Space, Spin, Tag} from 'antd';
import {App, Button, Card, Descriptions, Space, Spin, Tag} from 'antd'; import {Clock} from 'lucide-react';
import {Clock, Edit, RefreshCw} from 'lucide-react';
import {useQuery} from '@tanstack/react-query'; import {useQuery} from '@tanstack/react-query';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import {getAgentForAdmin, getAgentLatestMetricsForAdmin, getTags} from '@/api/agent.ts'; import {getAgentForAdmin, getAgentLatestMetricsForAdmin} from '@/api/agent.ts';
import AgentEditModal from './AgentEditModal';
interface AgentBasicInfoProps { interface AgentBasicInfoProps {
agentId: string; agentId: string;
} }
const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => { const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
const {message: messageApi} = App.useApp();
const [editModalVisible, setEditModalVisible] = useState(false);
const {data: tags = []} = useQuery({
queryKey: ['admin', 'agents', 'tags'],
queryFn: async () => {
const response = await getTags();
return response.data.tags || [];
},
});
const { const {
data: agent, data: agent,
isLoading: agentLoading, isLoading: agentLoading,
refetch: refetchAgent,
} = useQuery({ } = useQuery({
queryKey: ['admin', 'agent', agentId], queryKey: ['admin', 'agent', agentId],
queryFn: async () => { queryFn: async () => {
@@ -38,7 +24,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
const { const {
data: latestMetrics, data: latestMetrics,
isLoading: metricsLoading, isLoading: metricsLoading,
refetch: refetchMetrics,
} = useQuery({ } = useQuery({
queryKey: ['admin', 'agent', agentId, 'metrics', 'latest'], queryKey: ['admin', 'agent', agentId, 'metrics', 'latest'],
queryFn: async () => { queryFn: async () => {
@@ -48,11 +33,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
enabled: !!agentId, enabled: !!agentId,
}); });
const handleRefresh = () => {
refetchAgent();
refetchMetrics();
};
if (agentLoading || metricsLoading) { if (agentLoading || metricsLoading) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
@@ -98,28 +78,7 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
return ( return (
<div> <div>
<Card <Descriptions column={{xs: 1, sm: 2, lg: 3, xl: 4}} bordered>
title="探针详细信息"
variant="outlined"
extra={
<Space>
<Button
icon={<RefreshCw size={16}/>}
onClick={handleRefresh}
>
刷新
</Button>
<Button
type="primary"
icon={<Edit size={16}/>}
onClick={() => setEditModalVisible(true)}
>
编辑探针
</Button>
</Space>
}
>
<Descriptions column={{xs: 1, sm: 2, lg: 3}} bordered>
<Descriptions.Item label="探针名称">{agent.name || '-'}</Descriptions.Item> <Descriptions.Item label="探针名称">{agent.name || '-'}</Descriptions.Item>
<Descriptions.Item label="主机名">{agent.hostname || '-'}</Descriptions.Item> <Descriptions.Item label="主机名">{agent.hostname || '-'}</Descriptions.Item>
<Descriptions.Item label="探针ID"> <Descriptions.Item label="探针ID">
@@ -242,19 +201,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
) : '-'} ) : '-'}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Card>
{/* 编辑模态框 */}
<AgentEditModal
open={editModalVisible}
agentId={agentId}
existingTags={tags}
onCancel={() => setEditModalVisible(false)}
onSuccess={() => {
setEditModalVisible(false);
refetchAgent();
}}
/>
</div> </div>
); );
}; };
+46 -22
View File
@@ -2,11 +2,12 @@ import {useEffect, useState} from 'react';
import {useNavigate, useParams, useSearchParams} from 'react-router-dom'; import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
import type {TabsProps} from 'antd'; import type {TabsProps} from 'antd';
import {Alert, Spin, Tabs, Tag} from 'antd'; import {Alert, Spin, Tabs, Tag} from 'antd';
import {Activity, ArrowLeft, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react'; import {Activity, ArrowLeft, Edit, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react';
import {useQuery} from '@tanstack/react-query'; import {useQuery, useQueryClient} from '@tanstack/react-query';
import {getAgentForAdmin} from '@/api/agent.ts'; import {getAgentForAdmin, getTags} from '@/api/agent.ts';
import AgentBasicInfo from './AgentBasicInfo'; import AgentBasicInfo from './AgentBasicInfo';
import AgentAudit from './AgentAudit'; import AgentAudit from './AgentAudit';
import AgentEditModal from './AgentEditModal';
import TamperProtection from './TamperProtection'; import TamperProtection from './TamperProtection';
import SSHLoginMonitor from './SSHLoginMonitor'; import SSHLoginMonitor from './SSHLoginMonitor';
import TrafficStats from './TrafficStats'; import TrafficStats from './TrafficStats';
@@ -16,8 +17,10 @@ import {PagePanel} from '@/components/PagePanel';
const AgentDetail = () => { const AgentDetail = () => {
const {id} = useParams<{ id: string }>(); const {id} = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info'); const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info');
const [editModalVisible, setEditModalVisible] = useState(false);
// 获取探针基本信息(用于显示头部卡片) // 获取探针基本信息(用于显示头部卡片)
const {data: agent, isLoading} = useQuery({ const {data: agent, isLoading} = useQuery({
@@ -30,6 +33,15 @@ const AgentDetail = () => {
enabled: !!id, enabled: !!id,
}); });
// 获取标签列表(编辑探针弹窗使用)
const {data: tags = []} = useQuery({
queryKey: ['admin', 'agents', 'tags'],
queryFn: async () => {
const response = await getTags();
return response.data.tags || [];
},
});
useEffect(() => { useEffect(() => {
// 同步 activeTab 到 URL // 同步 activeTab 到 URL
const nextParams = new URLSearchParams(searchParams); const nextParams = new URLSearchParams(searchParams);
@@ -136,29 +148,29 @@ const AgentDetail = () => {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<PageHeader <PageHeader
title={agent.name || agent.hostname} title={
<span className="flex flex-wrap items-center gap-2.5">
{agent.name || agent.hostname}
<button
type="button"
onClick={() => navigate('/admin/agents')}
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-xs font-normal text-[#646a73] transition-colors hover:text-[#1677ff] dark:text-[#9ba1ab] dark:hover:text-[#75adff]"
>
<ArrowLeft size={12}/>
<span>返回列表</span>
</button>
</span>
}
extra={agent.status === 1 ? <Tag color="success">在线</Tag> : <Tag color="error">离线</Tag>}
actions={[{ actions={[{
key: 'back', key: 'edit',
label: '返回列表', label: '编辑探针',
icon: <ArrowLeft size={16}/>, type: 'primary',
onClick: () => navigate('/admin/agents'), icon: <Edit size={16}/>,
onClick: () => setEditModalVisible(true),
}]} }]}
/> />
<PagePanel>
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400">
<Activity size={21}/>
</div>
<div>
<div className="text-sm font-medium text-gray-800 dark:text-gray-100">当前连接状态</div>
<div className="mt-1">
{agent.status === 1 ? <Tag color="success">在线</Tag> : <Tag color="error">离线</Tag>}
</div>
</div>
</div>
</PagePanel>
{/* Tabs 内容 */} {/* Tabs 内容 */}
<PagePanel> <PagePanel>
<Tabs <Tabs
@@ -167,6 +179,18 @@ const AgentDetail = () => {
items={tabItems} items={tabItems}
/> />
</PagePanel> </PagePanel>
{/* 编辑探针模态框 */}
<AgentEditModal
open={editModalVisible}
agentId={id}
existingTags={tags}
onCancel={() => setEditModalVisible(false)}
onSuccess={() => {
setEditModalVisible(false);
queryClient.invalidateQueries({queryKey: ['admin', 'agent', id]});
}}
/>
</div> </div>
); );
}; };
+13 -11
View File
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import {Alert, App, Button, Card, Form, Input, Space, Switch} from 'antd'; import {Alert, App, Button, Form, Input, Space, Spin, Switch} from 'antd';
import {Save, Terminal} from 'lucide-react'; import {Save, Terminal} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import type {SSHLoginConfig as SSHLoginConfigType} from '@/types'; import type {SSHLoginConfig as SSHLoginConfigType} from '@/types';
@@ -96,14 +96,12 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
}, [config, form]); }, [config, form]);
return ( return (
<Card <div>
title={ <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
<Terminal size={18}/> <Terminal size={18}/>
<span>SSH 登录监控配置</span> <span>SSH 登录监控配置</span>
</div> </div>
}
extra={
<Button <Button
type="primary" type="primary"
icon={<Save size={16}/>} icon={<Save size={16}/>}
@@ -112,10 +110,13 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
> >
保存配置 保存配置
</Button> </Button>
} </div>
loading={isLoading} {isLoading ? (
> <div className="text-center py-12">
<Space direction="vertical" style={{width: '100%'}} size="large"> <Spin/>
</div>
) : (
<Space direction="vertical" style={{width: '100%'}} size="large" className="mt-4">
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
@@ -165,7 +166,8 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
/> />
)} )}
</Space> </Space>
</Card> )}
</div>
); );
}; };
+13 -11
View File
@@ -1,5 +1,5 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {Alert, App, Button, Card, Form, Input, List, Space, Switch} from 'antd'; import {Alert, App, Button, Form, Input, List, Space, Spin, Switch} from 'antd';
import {Plus, Save, Shield, Trash2} from 'lucide-react'; import {Plus, Save, Shield, Trash2} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {getTamperConfig, updateTamperConfig} from '@/api/tamper'; import {getTamperConfig, updateTamperConfig} from '@/api/tamper';
@@ -70,14 +70,12 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
}, [config, form]); }, [config, form]);
return ( return (
<Card <div>
title={ <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
<Shield size={18}/> <Shield size={18}/>
<span>防篡改保护配置</span> <span>防篡改保护配置</span>
</div> </div>
}
extra={
<Button <Button
type="primary" type="primary"
icon={<Save size={16}/>} icon={<Save size={16}/>}
@@ -86,10 +84,13 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
> >
保存配置 保存配置
</Button> </Button>
} </div>
loading={isLoading} {isLoading ? (
> <div className="text-center py-12">
<Space orientation="vertical" style={{width: '100%'}} size="large"> <Spin/>
</div>
) : (
<Space orientation="vertical" style={{width: '100%'}} size="large" className="mt-4">
<Alert <Alert
title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。" title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。"
type="info" type="info"
@@ -180,7 +181,8 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
/> />
)} )}
</Space> </Space>
</Card> )}
</div>
); );
}; };
+109 -84
View File
@@ -1,17 +1,15 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import { import {
Alert,
App, App,
Button, Button,
Card,
Col, Col,
Descriptions,
Form, Form,
InputNumber, InputNumber,
Progress, Progress,
Row, Row,
Select, Select,
Space, Space,
Spin,
Statistic, Statistic,
Switch, Switch,
Tag Tag
@@ -33,6 +31,22 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [limitUnit, setLimitUnit] = useState<'GB' | 'TB'>('GB');
const bytesPerUnit = limitUnit === 'TB' ? 1024 * 1024 * 1024 * 1024 : 1024 * 1024 * 1024;
// 切换单位时把已填写的数值换算到新单位
const handleUnitChange = (unit: 'GB' | 'TB') => {
if (unit === limitUnit) {
return;
}
const factor = unit === 'TB' ? 1024 : 1 / 1024;
const values = form.getFieldsValue();
form.setFieldsValue({
trafficLimit: (values.trafficLimit || 0) * factor,
});
setLimitUnit(unit);
};
// 获取流量统计 // 获取流量统计
const {data: stats, isLoading} = useQuery<TrafficStatsType>({ const {data: stats, isLoading} = useQuery<TrafficStatsType>({
@@ -61,7 +75,7 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
try { try {
const values = enabled ? await form.validateFields() : form.getFieldsValue(); const values = enabled ? await form.validateFields() : form.getFieldsValue();
const limitValue = typeof values.trafficLimit === 'number' ? values.trafficLimit : 0; const limitValue = typeof values.trafficLimit === 'number' ? values.trafficLimit : 0;
const limitBytes = enabled ? limitValue * 1024 * 1024 * 1024 : 0; const limitBytes = enabled ? limitValue * bytesPerUnit : 0;
saveMutation.mutate({ saveMutation.mutate({
enabled: enabled, enabled: enabled,
type: values.trafficType || 'recv', type: values.trafficType || 'recv',
@@ -104,18 +118,24 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
if (stats) { if (stats) {
// 从服务器返回的 enabled 字段读取启用状态 // 从服务器返回的 enabled 字段读取启用状态
setEnabled(stats.enabled); setEnabled(stats.enabled);
// 限额或已用量达到 1TB 时自动使用 TB 单位展示
const unit: 'GB' | 'TB' = (stats.limit > 0 && stats.limit >= 1024 * 1024 * 1024 * 1024)
|| (stats.used >= 1024 * 1024 * 1024 * 1024)
? 'TB'
: 'GB';
setLimitUnit(unit);
const divisor = unit === 'TB' ? 1024 * 1024 * 1024 * 1024 : 1024 * 1024 * 1024;
form.setFieldsValue({ form.setFieldsValue({
trafficType: stats.type || 'recv', trafficType: stats.type || 'recv',
trafficLimit: stats.limit > 0 ? stats.limit / (1024 * 1024 * 1024) : 0, trafficLimit: stats.limit > 0 ? stats.limit / divisor : 0,
trafficUsed: stats.used > 0 ? stats.used / (1024 * 1024 * 1024) : 0,
trafficResetDay: stats.resetDay || 0, trafficResetDay: stats.resetDay || 0,
}); });
} else { } else {
setEnabled(false); setEnabled(false);
setLimitUnit('GB');
form.setFieldsValue({ form.setFieldsValue({
trafficType: 'recv', trafficType: 'recv',
trafficLimit: 0, trafficLimit: 0,
trafficUsed: 0,
trafficResetDay: 0, trafficResetDay: 0,
}); });
} }
@@ -144,43 +164,26 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
}; };
return ( return (
<Space orientation="vertical" size="large" style={{width: '100%'}}>
<Alert
title="流量统计功能可以监控探针的流量使用情况。设置流量限额(大于0)将启用告警功能,超过阈值时会发送通知;设置为0则仅统计流量不发送告警。"
type="info"
showIcon
icon={<Activity size={16}/>}
/>
<Row gutter={16}> <Row gutter={16}>
{/* 左侧:流量配置 */} {/* 左侧:流量配置 */}
<Col xs={24} lg={10}> <Col xs={24} lg={10}>
<Card <div className="flex items-center gap-2 mb-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
title={
<div className="flex items-center gap-2">
<Activity size={18}/> <Activity size={18}/>
<span>流量统计配置</span> <span>流量统计配置</span>
</div> </div>
} {isLoading ? (
extra={ <div className="text-center py-12">
<Button <Spin/>
type="primary" </div>
icon={<Save size={16}/>} ) : (
onClick={handleSave} <>
loading={saveMutation.isPending}
>
保存配置
</Button>
}
loading={isLoading}
>
<Form <Form
form={form} form={form}
className="mt-4"
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
trafficType: 'recv', trafficType: 'recv',
trafficLimit: 0, trafficLimit: 0,
trafficUsed: 0,
trafficResetDay: 0, trafficResetDay: 0,
}} }}
> >
@@ -216,36 +219,32 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
<Form.Item <Form.Item
label="流量限额" label="流量限额"
extra={`设置流量限额(${limitUnit}),0表示仅统计不告警`}
>
<Space.Compact style={{width: '100%'}}>
<Form.Item
name="trafficLimit" name="trafficLimit"
rules={[ noStyle
{type: 'number', min: 0, message: '流量限额不能小于0'}, rules={[{type: 'number', min: 0, message: '流量限额不能小于0'}]}
]}
extra="设置流量限额(GB),0表示仅统计不告警"
> >
<InputNumber <InputNumber
min={0} min={0}
step={1} step={limitUnit === 'TB' ? 0.5 : 1}
precision={0} precision={limitUnit === 'TB' ? 2 : 0}
placeholder="请输入流量限额(GB)" placeholder={`请输入流量限额`}
style={{width: '100%'}} style={{width: '100%'}}
addonAfter="GB"
/> />
</Form.Item> </Form.Item>
<Select
<Form.Item value={limitUnit}
label="已使用流量" onChange={handleUnitChange}
name="trafficUsed" options={[
extra="当前已使用的流量(只读)" {label: 'GB', value: 'GB'},
> {label: 'TB', value: 'TB'},
<InputNumber ]}
min={0} style={{width: 88}}
step={0.1}
precision={2}
placeholder="已使用流量(GB)"
style={{width: '100%'}}
addonAfter="GB"
disabled
/> />
</Space.Compact>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -268,19 +267,32 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
</> </>
)} )}
</Form> </Form>
</Card>
<div className="mt-4">
<Button
type="primary"
icon={<Save size={16}/>}
onClick={handleSave}
loading={saveMutation.isPending}
>
保存配置
</Button>
</div>
</>
)}
</Col> </Col>
{/* 右侧:流量统计 */} {/* 右侧:流量统计 */}
<Col xs={24} lg={14}> <Col xs={24} lg={12}>
<div className={'max-w-[400px]'}>
<div className="rounded-md border border-[#e8ebf0] bg-white p-5 dark:border-[#272b33] dark:bg-[#1c2028]">
<div className="text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
流量使用情况
</div>
{stats && stats.enabled ? ( {stats && stats.enabled ? (
<Card <div className="mt-4 flex flex-col gap-5">
title="流量使用情况"
variant="outlined"
>
<Space orientation="vertical" size="large" style={{width: '100%'}}>
{/* 基础统计 */} {/* 基础统计 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Statistic <Statistic
title="已使用流量" title="已使用流量"
value={formatBytes(stats.used)} value={formatBytes(stats.used)}
@@ -321,36 +333,50 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
)} )}
{/* 详细信息 */} {/* 详细信息 */}
<Descriptions bordered column={1} size="small"> <div className="text-[13px]">
<Descriptions.Item label="统计类型"> <div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
<span className="text-[#646a73] dark:text-[#9ba1ab]">统计类型</span>
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
{stats.type === 'recv' ? '进站流量 (下载)' : stats.type === 'send' ? '出站流量 (上传)' : '全部流量 (上传+下载)'} {stats.type === 'recv' ? '进站流量 (下载)' : stats.type === 'send' ? '出站流量 (上传)' : '全部流量 (上传+下载)'}
</Descriptions.Item> </span>
<Descriptions.Item label="重置日期"> </div>
{stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'} <div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
</Descriptions.Item> <span className="text-[#646a73] dark:text-[#9ba1ab]">当前周期</span>
<Descriptions.Item label="当前周期"> <span className="text-[#1f2329] dark:text-[#e6e8ec]">
{formatCurrentPeriod(stats.periodStart, stats.periodEnd, stats.resetDay)} {formatCurrentPeriod(stats.periodStart, stats.periodEnd, stats.resetDay)}
</Descriptions.Item> </span>
<Descriptions.Item label="距离重置"> </div>
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
<span className="text-[#646a73] dark:text-[#9ba1ab]">重置日期</span>
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
{stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'}
</span>
</div>
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
<span className="text-[#646a73] dark:text-[#9ba1ab]">距离重置</span>
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
{stats.resetDay <= 0 {stats.resetDay <= 0
? '不自动重置' ? '不自动重置'
: stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'} : stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'}
</Descriptions.Item> </span>
{/* 告警状态 - 仅在有限额时显示 */} </div>
{stats.limit > 0 && ( {stats.limit > 0 && (
<Descriptions.Item label="告警状态"> <div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
<Space> <span className="text-[#646a73] dark:text-[#9ba1ab]">告警状态</span>
<span className="flex items-center gap-1">
{stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>} {stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>}
{stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>} {stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>}
{stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>} {stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>}
{!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && ( {!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && (
<Tag color="green">正常</Tag> <Tag color="green">正常</Tag>
)} )}
</Space> </span>
</Descriptions.Item> </div>
)} )}
</Descriptions> <div className="border-t border-[#e8ebf0] dark:border-[#272b33]"/>
</div>
<div className="flex justify-end">
<Button <Button
icon={<RotateCcw size={16}/>} icon={<RotateCcw size={16}/>}
onClick={handleResetTraffic} onClick={handleResetTraffic}
@@ -359,19 +385,18 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
> >
立即重置流量 立即重置流量
</Button> </Button>
</Space> </div>
</Card> </div>
) : ( ) : (
<Card variant="outlined"> <div className="mt-4 rounded-lg border border-dashed border-[#d9dee7] py-10 text-center text-sm text-[#98a0ab] dark:border-[#30343d] dark:text-[#7d8590]">
<div className="text-center text-gray-400 py-12">
<Activity size={48} className="mx-auto mb-4 opacity-50"/> <Activity size={48} className="mx-auto mb-4 opacity-50"/>
<p>请先启用流量统计功能</p> <p>请先启用流量统计功能</p>
</div> </div>
</Card>
)} )}
</div>
</div>
</Col> </Col>
</Row> </Row>
</Space>
); );
}; };