From efa505f3784cc950620e62e8ea9c54d97fe661e6 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 27 Aug 2026 21:24:19 +0800 Subject: [PATCH] feat(web): support batch traffic stats configuration --- web/src/pages/Agents/AgentList.tsx | 23 +- .../pages/Agents/BatchTrafficStatsModal.tsx | 206 ++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 web/src/pages/Agents/BatchTrafficStatsModal.tsx diff --git a/web/src/pages/Agents/AgentList.tsx b/web/src/pages/Agents/AgentList.tsx index e9387a1..068c511 100644 --- a/web/src/pages/Agents/AgentList.tsx +++ b/web/src/pages/Agents/AgentList.tsx @@ -4,7 +4,7 @@ import {Link, useNavigate} from 'react-router-dom'; import type {MenuProps} from 'antd'; import {App, Button, Dropdown, Form, Input, Select, Space, Tag} from 'antd'; import type {ColumnsType} from 'antd/es/table'; -import {Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Play, Plus, PowerOff, RefreshCw, Shield, Tags, Trash2} from 'lucide-react'; +import {Activity, Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Play, Plus, PowerOff, RefreshCw, Shield, Tags, Trash2} from 'lucide-react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import type {DragEndEvent} from '@dnd-kit/core'; import {DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core'; @@ -28,6 +28,7 @@ import BatchTagsModal from './BatchTagsModal'; import BatchTamperProtectionModal from './BatchTamperProtectionModal'; import BatchSSHLoginConfigModal from './BatchSSHLoginConfigModal'; import BatchVisibilityModal from './BatchVisibilityModal'; +import BatchTrafficStatsModal from './BatchTrafficStatsModal'; type SortableRowHook = ReturnType; @@ -103,6 +104,7 @@ const AgentList = () => { const [batchTamperModalVisible, setBatchTamperModalVisible] = useState(false); const [batchSSHModalVisible, setBatchSSHModalVisible] = useState(false); const [batchVisibilityModalVisible, setBatchVisibilityModalVisible] = useState(false); + const [batchTrafficModalVisible, setBatchTrafficModalVisible] = useState(false); const [editingAgentId, setEditingAgentId] = useState(undefined); // 过滤条件 @@ -279,6 +281,14 @@ const AgentList = () => { setBatchVisibilityModalVisible(true); }; + const handleBatchTrafficConfig = () => { + if (selectedRowKeys.length === 0) { + messageApi.warning('请先选择要操作的探针'); + return; + } + setBatchTrafficModalVisible(true); + }; + // 前端过滤数据 const filteredAgents = useMemo(() => { let result = agents || []; @@ -635,6 +645,7 @@ const AgentList = () => { const batchMenuItems: MenuProps['items'] = [ {key: 'tags', icon: , label: '修改标签', onClick: handleBatchTags}, {key: 'visibility', icon: , label: '修改可见性', onClick: handleBatchVisibility}, + {key: 'traffic', icon: , label: '配置流量统计', onClick: handleBatchTrafficConfig}, {key: 'tamper', icon: , label: '配置防篡改保护', onClick: handleBatchTamperConfig}, {key: 'ssh', icon: , label: '配置 SSH 登录监控', onClick: handleBatchSSHConfig}, ]; @@ -816,6 +827,16 @@ const AgentList = () => { setSelectedRowKeys([]); }} /> + + setBatchTrafficModalVisible(false)} + onSuccess={() => { + setBatchTrafficModalVisible(false); + setSelectedRowKeys([]); + }} + /> ); }; diff --git a/web/src/pages/Agents/BatchTrafficStatsModal.tsx b/web/src/pages/Agents/BatchTrafficStatsModal.tsx new file mode 100644 index 0000000..abbea16 --- /dev/null +++ b/web/src/pages/Agents/BatchTrafficStatsModal.tsx @@ -0,0 +1,206 @@ +import {useEffect, useState} from 'react'; +import {App, Form, InputNumber, Modal, Select, Space, Switch} from 'antd'; +import {useMutation, useQueryClient} from '@tanstack/react-query'; +import {updateTrafficConfig} from '@/api/agent'; +import type {UpdateTrafficConfigRequest} from '@/types'; +import {getErrorMessage} from '@/lib/utils'; + +type TrafficLimitUnit = 'GB' | 'TB'; + +interface BatchTrafficStatsModalProps { + open: boolean; + agentIds: string[]; + onCancel: () => void; + onSuccess: () => void; +} + +interface TrafficConfigFormValues { + enabled: boolean; + trafficType: UpdateTrafficConfigRequest['type']; + trafficLimit: number; + trafficResetDay: number; +} + +const BYTES_PER_GB = 1024 * 1024 * 1024; +const BYTES_PER_TB = 1024 * BYTES_PER_GB; + +const BatchTrafficStatsModal = ({open, agentIds, onCancel, onSuccess}: BatchTrafficStatsModalProps) => { + const {message: messageApi} = App.useApp(); + const [form] = Form.useForm(); + const queryClient = useQueryClient(); + const [limitUnit, setLimitUnit] = useState('GB'); + const enabled = Form.useWatch('enabled', form) ?? false; + + useEffect(() => { + if (!open) { + return; + } + form.setFieldsValue({ + enabled: false, + trafficType: 'recv', + trafficLimit: 0, + trafficResetDay: 0, + }); + setLimitUnit('GB'); + }, [open, form]); + + const handleUnitChange = (unit: TrafficLimitUnit) => { + if (unit === limitUnit) { + return; + } + + const currentLimit = form.getFieldValue('trafficLimit') ?? 0; + form.setFieldValue( + 'trafficLimit', + unit === 'TB' ? currentLimit / 1024 : currentLimit * 1024, + ); + setLimitUnit(unit); + }; + + const batchMutation = useMutation({ + mutationFn: async (payload: UpdateTrafficConfigRequest) => { + const results = await Promise.allSettled( + agentIds.map(agentId => updateTrafficConfig(agentId, payload)), + ); + const failedCount = results.filter(result => result.status === 'rejected').length; + return { + total: agentIds.length, + successCount: agentIds.length - failedCount, + failedCount, + }; + }, + onSuccess: (result) => { + if (result.successCount === 0) { + messageApi.error(`所选 ${result.total} 个探针的流量统计配置均失败,请重试`); + return; + } + if (result.failedCount > 0) { + messageApi.warning(`已配置 ${result.successCount} 个探针,失败 ${result.failedCount} 个`); + } else { + messageApi.success(`成功配置 ${result.total} 个探针的流量统计`); + } + queryClient.invalidateQueries({queryKey: ['admin', 'agents']}); + queryClient.invalidateQueries({queryKey: ['trafficStats']}); + onSuccess(); + }, + onError: (error: unknown) => { + messageApi.error(getErrorMessage(error, '批量配置流量统计失败')); + }, + }); + + const handleOk = async () => { + if (agentIds.length === 0) { + messageApi.warning('请先选择要操作的探针'); + return; + } + + try { + const values = await form.validateFields(); + const bytesPerUnit = limitUnit === 'TB' ? BYTES_PER_TB : BYTES_PER_GB; + await batchMutation.mutateAsync({ + enabled: values.enabled, + type: values.trafficType || 'recv', + limit: values.enabled ? Math.round((values.trafficLimit ?? 0) * bytesPerUnit) : 0, + resetDay: values.enabled ? (values.trafficResetDay ?? 0) : 0, + }); + } catch (error: unknown) { + if (typeof error === 'object' && error !== null && 'errorFields' in error) { + return; + } + messageApi.error(getErrorMessage(error, '批量配置流量统计失败')); + } + }; + + return ( + +
+ + + + + {enabled && ( + <> + + ({ + label: `每月 ${index + 1} 号`, + value: index + 1, + })), + ]} + /> + + + )} +
+
+ ); +}; + +export default BatchTrafficStatsModal;