refactor: 优化探针详情页与流量统计页
- 探针详情: /admin/agents 前缀匹配宽屏布局;状态并入页头,返回列表改为名称旁小字链接,编辑探针移至页头右侧;基本信息/流量/防篡改/SSH 配置去卡片嵌套,内容扁平化 - 流量统计: 重构页面布局,配置与使用情况分栏;流量限额支持 GB/TB 单位切换与自动适配(后端字节存储无需改动);限额输入与单位下拉组合;使用情况改为卡片呈现
This commit is contained in:
1 parent
6c8d2f13d3
commit
7603906c36
7 files changed
+272
-272
No files matched your search
@@ -14,7 +14,7 @@ export interface Action {
|
||||
}
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
actions?: Action[];
|
||||
}
|
||||
|
||||
@@ -11,17 +11,18 @@ import type {User} from '@/types';
|
||||
const AdminLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isWideCompactPage = [
|
||||
'/admin/agents',
|
||||
'/admin/monitors',
|
||||
'/admin/ddns',
|
||||
'/admin/api-keys',
|
||||
'/admin/manage-api-keys',
|
||||
'/admin/alert-records',
|
||||
'/admin/agents-install/one-click',
|
||||
'/admin/agents-install/manual',
|
||||
'/admin/settings',
|
||||
].includes(location.pathname);
|
||||
// /admin/agents 前缀(含探针详情页)使用全宽布局
|
||||
const isWideCompactPage = location.pathname.startsWith('/admin/agents')
|
||||
|| [
|
||||
'/admin/monitors',
|
||||
'/admin/ddns',
|
||||
'/admin/api-keys',
|
||||
'/admin/manage-api-keys',
|
||||
'/admin/alert-records',
|
||||
'/admin/agents-install/one-click',
|
||||
'/admin/agents-install/manual',
|
||||
'/admin/settings',
|
||||
].includes(location.pathname);
|
||||
const [userInfo, setUserInfo] = useState<User | null>(null);
|
||||
const themeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const {appliedTheme, toggleTheme} = useThemeToggle(themeButtonRef);
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import {useState} from 'react';
|
||||
import {App, Button, Card, Descriptions, Space, Spin, Tag} from 'antd';
|
||||
import {Clock, Edit, RefreshCw} from 'lucide-react';
|
||||
import {Descriptions, Space, Spin, Tag} from 'antd';
|
||||
import {Clock} from 'lucide-react';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import {getAgentForAdmin, getAgentLatestMetricsForAdmin, getTags} from '@/api/agent.ts';
|
||||
import AgentEditModal from './AgentEditModal';
|
||||
import {getAgentForAdmin, getAgentLatestMetricsForAdmin} from '@/api/agent.ts';
|
||||
|
||||
interface AgentBasicInfoProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
data: agent,
|
||||
isLoading: agentLoading,
|
||||
refetch: refetchAgent,
|
||||
} = useQuery({
|
||||
queryKey: ['admin', 'agent', agentId],
|
||||
queryFn: async () => {
|
||||
@@ -38,7 +24,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
||||
const {
|
||||
data: latestMetrics,
|
||||
isLoading: metricsLoading,
|
||||
refetch: refetchMetrics,
|
||||
} = useQuery({
|
||||
queryKey: ['admin', 'agent', agentId, 'metrics', 'latest'],
|
||||
queryFn: async () => {
|
||||
@@ -48,11 +33,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
||||
enabled: !!agentId,
|
||||
});
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetchAgent();
|
||||
refetchMetrics();
|
||||
};
|
||||
|
||||
if (agentLoading || metricsLoading) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
@@ -98,28 +78,7 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
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 column={{xs: 1, sm: 2, lg: 3, xl: 4}} bordered>
|
||||
<Descriptions.Item label="探针名称">{agent.name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="主机名">{agent.hostname || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="探针ID">
|
||||
@@ -242,19 +201,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
||||
) : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* 编辑模态框 */}
|
||||
<AgentEditModal
|
||||
open={editModalVisible}
|
||||
agentId={agentId}
|
||||
existingTags={tags}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setEditModalVisible(false);
|
||||
refetchAgent();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import {useEffect, useState} from 'react';
|
||||
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
|
||||
import type {TabsProps} from 'antd';
|
||||
import {Alert, Spin, Tabs, Tag} from 'antd';
|
||||
import {Activity, ArrowLeft, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import {getAgentForAdmin} from '@/api/agent.ts';
|
||||
import {Activity, ArrowLeft, Edit, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react';
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {getAgentForAdmin, getTags} from '@/api/agent.ts';
|
||||
import AgentBasicInfo from './AgentBasicInfo';
|
||||
import AgentAudit from './AgentAudit';
|
||||
import AgentEditModal from './AgentEditModal';
|
||||
import TamperProtection from './TamperProtection';
|
||||
import SSHLoginMonitor from './SSHLoginMonitor';
|
||||
import TrafficStats from './TrafficStats';
|
||||
@@ -16,8 +17,10 @@ import {PagePanel} from '@/components/PagePanel';
|
||||
const AgentDetail = () => {
|
||||
const {id} = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info');
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
|
||||
// 获取探针基本信息(用于显示头部卡片)
|
||||
const {data: agent, isLoading} = useQuery({
|
||||
@@ -30,6 +33,15 @@ const AgentDetail = () => {
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
// 获取标签列表(编辑探针弹窗使用)
|
||||
const {data: tags = []} = useQuery({
|
||||
queryKey: ['admin', 'agents', 'tags'],
|
||||
queryFn: async () => {
|
||||
const response = await getTags();
|
||||
return response.data.tags || [];
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 同步 activeTab 到 URL
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
@@ -136,29 +148,29 @@ const AgentDetail = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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={[{
|
||||
key: 'back',
|
||||
label: '返回列表',
|
||||
icon: <ArrowLeft size={16}/>,
|
||||
onClick: () => navigate('/admin/agents'),
|
||||
key: 'edit',
|
||||
label: '编辑探针',
|
||||
type: 'primary',
|
||||
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 内容 */}
|
||||
<PagePanel>
|
||||
<Tabs
|
||||
@@ -167,6 +179,18 @@ const AgentDetail = () => {
|
||||
items={tabItems}
|
||||
/>
|
||||
</PagePanel>
|
||||
|
||||
{/* 编辑探针模态框 */}
|
||||
<AgentEditModal
|
||||
open={editModalVisible}
|
||||
agentId={id}
|
||||
existingTags={tags}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setEditModalVisible(false);
|
||||
queryClient.invalidateQueries({queryKey: ['admin', 'agent', id]});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import type {SSHLoginConfig as SSHLoginConfigType} from '@/types';
|
||||
@@ -96,14 +96,12 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
||||
}, [config, form]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||
<Terminal size={18}/>
|
||||
<span>SSH 登录监控配置</span>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Save size={16}/>}
|
||||
@@ -112,10 +110,13 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Space direction="vertical" style={{width: '100%'}} size="large">
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<Spin/>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" style={{width: '100%'}} size="large" className="mt-4">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -165,7 +166,8 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {getTamperConfig, updateTamperConfig} from '@/api/tamper';
|
||||
@@ -70,14 +70,12 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
||||
}, [config, form]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||
<Shield size={18}/>
|
||||
<span>防篡改保护配置</span>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Save size={16}/>}
|
||||
@@ -86,10 +84,13 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Space orientation="vertical" style={{width: '100%'}} size="large">
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<Spin/>
|
||||
</div>
|
||||
) : (
|
||||
<Space orientation="vertical" style={{width: '100%'}} size="large" className="mt-4">
|
||||
<Alert
|
||||
title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。"
|
||||
type="info"
|
||||
@@ -180,7 +181,8 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
InputNumber,
|
||||
Progress,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Statistic,
|
||||
Switch,
|
||||
Tag
|
||||
@@ -33,6 +31,22 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
const [form] = Form.useForm();
|
||||
const queryClient = useQueryClient();
|
||||
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>({
|
||||
@@ -61,7 +75,7 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
try {
|
||||
const values = enabled ? await form.validateFields() : form.getFieldsValue();
|
||||
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({
|
||||
enabled: enabled,
|
||||
type: values.trafficType || 'recv',
|
||||
@@ -104,18 +118,24 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
if (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({
|
||||
trafficType: stats.type || 'recv',
|
||||
trafficLimit: stats.limit > 0 ? stats.limit / (1024 * 1024 * 1024) : 0,
|
||||
trafficUsed: stats.used > 0 ? stats.used / (1024 * 1024 * 1024) : 0,
|
||||
trafficLimit: stats.limit > 0 ? stats.limit / divisor : 0,
|
||||
trafficResetDay: stats.resetDay || 0,
|
||||
});
|
||||
} else {
|
||||
setEnabled(false);
|
||||
setLimitUnit('GB');
|
||||
form.setFieldsValue({
|
||||
trafficType: 'recv',
|
||||
trafficLimit: 0,
|
||||
trafficUsed: 0,
|
||||
trafficResetDay: 0,
|
||||
});
|
||||
}
|
||||
@@ -144,43 +164,26 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="large" style={{width: '100%'}}>
|
||||
<Alert
|
||||
title="流量统计功能可以监控探针的流量使用情况。设置流量限额(大于0)将启用告警功能,超过阈值时会发送通知;设置为0则仅统计流量不发送告警。"
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<Activity size={16}/>}
|
||||
/>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:流量配置 */}
|
||||
<Col xs={24} lg={10}>
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity size={18}/>
|
||||
<span>流量统计配置</span>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Save size={16}/>}
|
||||
onClick={handleSave}
|
||||
loading={saveMutation.isPending}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
}
|
||||
loading={isLoading}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:流量配置 */}
|
||||
<Col xs={24} lg={10}>
|
||||
<div className="flex items-center gap-2 mb-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||
<Activity size={18}/>
|
||||
<span>流量统计配置</span>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<Spin/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Form
|
||||
form={form}
|
||||
className="mt-4"
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
trafficType: 'recv',
|
||||
trafficLimit: 0,
|
||||
trafficUsed: 0,
|
||||
trafficResetDay: 0,
|
||||
}}
|
||||
>
|
||||
@@ -216,36 +219,32 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
|
||||
<Form.Item
|
||||
label="流量限额"
|
||||
name="trafficLimit"
|
||||
rules={[
|
||||
{type: 'number', min: 0, message: '流量限额不能小于0'},
|
||||
]}
|
||||
extra="设置流量限额(GB),0表示仅统计不告警"
|
||||
extra={`设置流量限额(${limitUnit}),0表示仅统计不告警`}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
precision={0}
|
||||
placeholder="请输入流量限额(GB)"
|
||||
style={{width: '100%'}}
|
||||
addonAfter="GB"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="已使用流量"
|
||||
name="trafficUsed"
|
||||
extra="当前已使用的流量(只读)"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.1}
|
||||
precision={2}
|
||||
placeholder="已使用流量(GB)"
|
||||
style={{width: '100%'}}
|
||||
addonAfter="GB"
|
||||
disabled
|
||||
/>
|
||||
<Space.Compact style={{width: '100%'}}>
|
||||
<Form.Item
|
||||
name="trafficLimit"
|
||||
noStyle
|
||||
rules={[{type: 'number', min: 0, message: '流量限额不能小于0'}]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={limitUnit === 'TB' ? 0.5 : 1}
|
||||
precision={limitUnit === 'TB' ? 2 : 0}
|
||||
placeholder={`请输入流量限额`}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Select
|
||||
value={limitUnit}
|
||||
onChange={handleUnitChange}
|
||||
options={[
|
||||
{label: 'GB', value: 'GB'},
|
||||
{label: 'TB', value: 'TB'},
|
||||
]}
|
||||
style={{width: 88}}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -268,110 +267,136 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Save size={16}/>}
|
||||
onClick={handleSave}
|
||||
loading={saveMutation.isPending}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* 右侧:流量统计 */}
|
||||
<Col xs={24} lg={14}>
|
||||
{stats && stats.enabled ? (
|
||||
<Card
|
||||
title="流量使用情况"
|
||||
variant="outlined"
|
||||
>
|
||||
<Space orientation="vertical" size="large" style={{width: '100%'}}>
|
||||
{/* 基础统计 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Statistic
|
||||
title="已使用流量"
|
||||
value={formatBytes(stats.used)}
|
||||
styles={{
|
||||
content: {color: stats.limit > 0 ? getProgressColor(usagePercent) : undefined}
|
||||
}}
|
||||
/>
|
||||
{stats.limit > 0 && (
|
||||
<>
|
||||
<Statistic
|
||||
title="流量限额"
|
||||
value={formatBytes(stats.limit)}
|
||||
/>
|
||||
<Statistic
|
||||
title="剩余流量"
|
||||
value={formatBytes(stats.remaining)}
|
||||
styles={{
|
||||
content: {color: stats.remaining > 0 ? '#52c41a' : '#ff4d4f'}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 使用率进度条 - 仅在有限额时显示 */}
|
||||
{stats.limit > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<span>使用率</span>
|
||||
<span className="font-medium">{usagePercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={usagePercent}
|
||||
strokeColor={getProgressColor(usagePercent)}
|
||||
showInfo={false}
|
||||
<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 ? (
|
||||
<div className="mt-4 flex flex-col gap-5">
|
||||
{/* 基础统计 */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<Statistic
|
||||
title="已使用流量"
|
||||
value={formatBytes(stats.used)}
|
||||
styles={{
|
||||
content: {color: stats.limit > 0 ? getProgressColor(usagePercent) : undefined}
|
||||
}}
|
||||
/>
|
||||
{stats.limit > 0 && (
|
||||
<>
|
||||
<Statistic
|
||||
title="流量限额"
|
||||
value={formatBytes(stats.limit)}
|
||||
/>
|
||||
<Statistic
|
||||
title="剩余流量"
|
||||
value={formatBytes(stats.remaining)}
|
||||
styles={{
|
||||
content: {color: stats.remaining > 0 ? '#52c41a' : '#ff4d4f'}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详细信息 */}
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="统计类型">
|
||||
{/* 使用率进度条 - 仅在有限额时显示 */}
|
||||
{stats.limit > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<span>使用率</span>
|
||||
<span className="font-medium">{usagePercent.toFixed(2)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={usagePercent}
|
||||
strokeColor={getProgressColor(usagePercent)}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详细信息 */}
|
||||
<div className="text-[13px]">
|
||||
<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' ? '出站流量 (上传)' : '全部流量 (上传+下载)'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="重置日期">
|
||||
{stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前周期">
|
||||
</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]">
|
||||
{formatCurrentPeriod(stats.periodStart, stats.periodEnd, stats.resetDay)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="距离重置">
|
||||
</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} 号` : '不自动重置'}
|
||||
</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.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'}
|
||||
</Descriptions.Item>
|
||||
{/* 告警状态 - 仅在有限额时显示 */}
|
||||
{stats.limit > 0 && (
|
||||
<Descriptions.Item label="告警状态">
|
||||
<Space>
|
||||
{stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>}
|
||||
{stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>}
|
||||
{stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>}
|
||||
{!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && (
|
||||
<Tag color="green">正常</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</span>
|
||||
</div>
|
||||
{stats.limit > 0 && (
|
||||
<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="flex items-center gap-1">
|
||||
{stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>}
|
||||
{stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>}
|
||||
{stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>}
|
||||
{!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && (
|
||||
<Tag color="green">正常</Tag>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-[#e8ebf0] dark:border-[#272b33]"/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<RotateCcw size={16}/>}
|
||||
onClick={handleResetTraffic}
|
||||
loading={resetMutation.isPending}
|
||||
danger
|
||||
>
|
||||
立即重置流量
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
) : (
|
||||
<Card variant="outlined">
|
||||
<div className="text-center text-gray-400 py-12">
|
||||
<Activity size={48} className="mx-auto mb-4 opacity-50"/>
|
||||
<p>请先启用流量统计功能</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
icon={<RotateCcw size={16}/>}
|
||||
onClick={handleResetTraffic}
|
||||
loading={resetMutation.isPending}
|
||||
danger
|
||||
>
|
||||
立即重置流量
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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]">
|
||||
<Activity size={48} className="mx-auto mb-4 opacity-50"/>
|
||||
<p>请先启用流量统计功能</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user