import { useState, useEffect } from "react"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { Input } from "@heroui/input"; import { Select, SelectItem } from "@heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; import { Chip } from "@heroui/chip"; import { Spinner } from "@heroui/spinner"; import { Divider } from "@heroui/divider"; import { Alert } from "@heroui/alert"; import toast from 'react-hot-toast'; import AdminLayout from "@/layouts/admin"; import { createTunnel, getTunnelList, updateTunnel, deleteTunnel, getNodeList, diagnoseTunnel } from "@/api"; interface Tunnel { id: number; name: string; type: number; // 1: 端口转发, 2: 隧道转发 inNodeId: number; outNodeId?: number; inIp: string; outIp?: string; protocol?: string; tcpListenAddr: string; udpListenAddr: string; flow: number; // 1: 单向, 2: 双向 trafficRatio: number; status: number; createdTime: string; } interface Node { id: number; name: string; status: number; // 1: 在线, 0: 离线 } interface TunnelForm { id?: number; name: string; type: number; inNodeId: number | null; outNodeId?: number | null; protocol: string; tcpListenAddr: string; udpListenAddr: string; flow: number; trafficRatio: number; status: number; } interface DiagnosisResult { tunnelName: string; tunnelType: string; timestamp: number; results: Array<{ success: boolean; description: string; nodeName: string; nodeId: string; targetIp: string; message?: string; averageTime?: number; packetLoss?: number; }>; } export default function TunnelPage() { const [loading, setLoading] = useState(true); const [tunnels, setTunnels] = useState([]); const [nodes, setNodes] = useState([]); // 模态框状态 const [modalOpen, setModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false); const [isEdit, setIsEdit] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [diagnosisLoading, setDiagnosisLoading] = useState(false); const [tunnelToDelete, setTunnelToDelete] = useState(null); const [currentDiagnosisTunnel, setCurrentDiagnosisTunnel] = useState(null); const [diagnosisResult, setDiagnosisResult] = useState(null); // 表单状态 const [form, setForm] = useState({ name: '', type: 1, inNodeId: null, outNodeId: null, protocol: 'tls', tcpListenAddr: '0.0.0.0', udpListenAddr: '0.0.0.0', flow: 1, trafficRatio: 1.0, status: 1 }); // 表单验证错误 const [errors, setErrors] = useState<{[key: string]: string}>({}); useEffect(() => { loadData(); }, []); // 加载所有数据 const loadData = async () => { setLoading(true); try { const [tunnelsRes, nodesRes] = await Promise.all([ getTunnelList(), getNodeList() ]); if (tunnelsRes.code === 0) { setTunnels(tunnelsRes.data || []); } else { toast.error(tunnelsRes.msg || '获取隧道列表失败'); } if (nodesRes.code === 0) { setNodes(nodesRes.data || []); } else { console.warn('获取节点列表失败:', nodesRes.msg); } } catch (error) { console.error('加载数据失败:', error); toast.error('加载数据失败'); } finally { setLoading(false); } }; // 表单验证 const validateForm = (): boolean => { const newErrors: {[key: string]: string} = {}; if (!form.name.trim()) { newErrors.name = '请输入隧道名称'; } else if (form.name.length < 2 || form.name.length > 50) { newErrors.name = '隧道名称长度应在2-50个字符之间'; } if (!form.inNodeId) { newErrors.inNodeId = '请选择入口节点'; } if (!form.tcpListenAddr.trim()) { newErrors.tcpListenAddr = '请输入TCP监听地址'; } if (!form.udpListenAddr.trim()) { newErrors.udpListenAddr = '请输入UDP监听地址'; } if (form.trafficRatio < 0.0 || form.trafficRatio > 100.0) { newErrors.trafficRatio = '流量倍率必须在0.0-100.0之间'; } // 隧道转发时的验证 if (form.type === 2) { if (!form.outNodeId) { newErrors.outNodeId = '请选择出口节点'; } else if (form.inNodeId === form.outNodeId) { newErrors.outNodeId = '隧道转发模式下,入口和出口不能是同一个节点'; } if (!form.protocol) { newErrors.protocol = '请选择协议类型'; } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // 新增隧道 const handleAdd = () => { setIsEdit(false); setForm({ name: '', type: 1, inNodeId: null, outNodeId: null, protocol: 'tls', tcpListenAddr: '0.0.0.0', udpListenAddr: '0.0.0.0', flow: 1, trafficRatio: 1.0, status: 1 }); setErrors({}); setModalOpen(true); }; // 编辑隧道 - 只能修改部分字段 const handleEdit = (tunnel: Tunnel) => { setIsEdit(true); setForm({ id: tunnel.id, name: tunnel.name, type: tunnel.type, inNodeId: tunnel.inNodeId, outNodeId: tunnel.outNodeId || null, protocol: tunnel.protocol || 'tls', tcpListenAddr: tunnel.tcpListenAddr || '0.0.0.0', udpListenAddr: tunnel.udpListenAddr || '0.0.0.0', flow: tunnel.flow, trafficRatio: tunnel.trafficRatio, status: tunnel.status }); setErrors({}); setModalOpen(true); }; // 删除隧道 const handleDelete = (tunnel: Tunnel) => { setTunnelToDelete(tunnel); setDeleteModalOpen(true); }; const confirmDelete = async () => { if (!tunnelToDelete) return; setDeleteLoading(true); try { const response = await deleteTunnel(tunnelToDelete.id); if (response.code === 0) { toast.success('删除成功'); setDeleteModalOpen(false); setTunnelToDelete(null); loadData(); } else { toast.error(response.msg || '删除失败'); } } catch (error) { console.error('删除失败:', error); toast.error('删除失败'); } finally { setDeleteLoading(false); } }; // 隧道类型改变时的处理 const handleTypeChange = (type: number) => { setForm(prev => ({ ...prev, type, outNodeId: type === 1 ? null : prev.outNodeId, protocol: type === 1 ? 'tls' : prev.protocol })); }; // 提交表单 const handleSubmit = async () => { if (!validateForm()) return; setSubmitLoading(true); try { const data = { ...form }; const response = isEdit ? await updateTunnel(data) : await createTunnel(data); if (response.code === 0) { toast.success(isEdit ? '更新成功' : '创建成功'); setModalOpen(false); loadData(); } else { toast.error(response.msg || (isEdit ? '更新失败' : '创建失败')); } } catch (error) { console.error('提交失败:', error); toast.error('网络错误,请重试'); } finally { setSubmitLoading(false); } }; // 诊断隧道 const handleDiagnose = async (tunnel: Tunnel) => { setCurrentDiagnosisTunnel(tunnel); setDiagnosisModalOpen(true); setDiagnosisLoading(true); setDiagnosisResult(null); try { const response = await diagnoseTunnel(tunnel.id); if (response.code === 0) { setDiagnosisResult(response.data); } else { toast.error(response.msg || '诊断失败'); setDiagnosisResult({ tunnelName: tunnel.name, tunnelType: tunnel.type === 1 ? '端口转发' : '隧道转发', timestamp: Date.now(), results: [{ success: false, description: '诊断失败', nodeName: '-', nodeId: '-', targetIp: '-', message: response.msg || '诊断过程中发生错误' }] }); } } catch (error) { console.error('诊断失败:', error); toast.error('网络错误,请重试'); setDiagnosisResult({ tunnelName: tunnel.name, tunnelType: tunnel.type === 1 ? '端口转发' : '隧道转发', timestamp: Date.now(), results: [{ success: false, description: '网络错误', nodeName: '-', nodeId: '-', targetIp: '-', message: '无法连接到服务器' }] }); } finally { setDiagnosisLoading(false); } }; // 获取显示的IP(处理多IP) const getDisplayIp = (ipString?: string): string => { if (!ipString) return '-'; const ips = ipString.split(',').map(ip => ip.trim()).filter(ip => ip); if (ips.length === 0) return '-'; if (ips.length === 1) return ips[0]; return `${ips[0]} 等${ips.length}个`; }; // 获取节点名称 const getNodeName = (nodeId?: number): string => { if (!nodeId) return '-'; const node = nodes.find(n => n.id === nodeId); return node ? node.name : `节点${nodeId}`; }; // 获取状态显示 const getStatusDisplay = (status: number) => { switch (status) { case 1: return { text: '启用', color: 'success' }; case 0: return { text: '禁用', color: 'default' }; default: return { text: '未知', color: 'warning' }; } }; // 获取类型显示 const getTypeDisplay = (type: number) => { switch (type) { case 1: return { text: '端口转发', color: 'primary' }; case 2: return { text: '隧道转发', color: 'secondary' }; default: return { text: '未知', color: 'default' }; } }; // 获取流量计算显示 const getFlowDisplay = (flow: number) => { switch (flow) { case 1: return '单向计算'; case 2: return '双向计算'; default: return '未知'; } }; // 格式化时间 const formatTime = (timestamp: string | number): string => { if (!timestamp) return '-'; const date = new Date(timestamp); return date.toLocaleString('zh-CN'); }; // 获取连接质量 const getQualityDisplay = (averageTime?: number, packetLoss?: number) => { if (averageTime === undefined || packetLoss === undefined) return null; if (averageTime < 30 && packetLoss === 0) return { text: '🚀 优秀', color: 'success' }; if (averageTime < 50 && packetLoss === 0) return { text: '✨ 很好', color: 'success' }; if (averageTime < 100 && packetLoss < 1) return { text: '👍 良好', color: 'primary' }; if (averageTime < 150 && packetLoss < 2) return { text: '😐 一般', color: 'warning' }; if (averageTime < 200 && packetLoss < 5) return { text: '😟 较差', color: 'warning' }; return { text: '😵 很差', color: 'danger' }; }; if (loading) { return (
正在加载...
); } return (
{/* 页面头部 */}

隧道管理

{/* 隧道卡片网格 */} {tunnels.length > 0 ? (
{tunnels.map((tunnel) => { const statusDisplay = getStatusDisplay(tunnel.status); const typeDisplay = getTypeDisplay(tunnel.type); return (

{tunnel.name}

{typeDisplay.text} {statusDisplay.text}
{/* 流程展示 */}
入口节点
{getNodeName(tunnel.inNodeId)} {getDisplayIp(tunnel.inIp)}
{tunnel.type === 1 ? '出口节点(同入口)' : '出口节点'}
{tunnel.type === 1 ? getNodeName(tunnel.inNodeId) : getNodeName(tunnel.outNodeId)} {tunnel.type === 1 ? getDisplayIp(tunnel.inIp) : getDisplayIp(tunnel.outIp)}
{/* 配置信息 */}
流量计算
{getFlowDisplay(tunnel.flow)}
倍率
{tunnel.trafficRatio}x
{/* 时间信息 */}
创建时间: {formatTime(tunnel.createdTime)}
); })}
) : ( /* 空状态 */

暂无隧道配置

还没有创建任何隧道配置,点击上方按钮开始创建

)} {/* 新增/编辑模态框 */} {(onClose) => ( <>

{isEdit ? '编辑隧道' : '新增隧道'}

{isEdit ? '修改现有隧道配置的信息' : '创建新的隧道配置'}

setForm(prev => ({ ...prev, name: e.target.value }))} isInvalid={!!errors.name} errorMessage={errors.name} variant="bordered" />
setForm(prev => ({ ...prev, trafficRatio: parseFloat(e.target.value) || 0 }))} isInvalid={!!errors.trafficRatio} errorMessage={errors.trafficRatio} variant="bordered" endContent={
x
} />

入口配置

setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))} isInvalid={!!errors.tcpListenAddr} errorMessage={errors.tcpListenAddr} variant="bordered" startContent={
TCP
} /> setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))} isInvalid={!!errors.udpListenAddr} errorMessage={errors.udpListenAddr} variant="bordered" startContent={
UDP
} />
{/* 隧道转发时显示出口配置 */} {form.type === 2 && ( <>

出口配置

)} {/* 端口转发提示 */} {form.type === 1 && ( )}
)}
{/* 删除确认模态框 */} {(onClose) => ( <>

确认删除

确定要删除隧道 "{tunnelToDelete?.name}" 吗?

此操作不可恢复,请谨慎操作。

)}
{/* 诊断结果模态框 */} {(onClose) => ( <>

隧道诊断结果

{currentDiagnosisTunnel && (
{currentDiagnosisTunnel.name} {currentDiagnosisTunnel.type === 1 ? '端口转发' : '隧道转发'}
)}
{diagnosisLoading ? (
正在诊断...
) : diagnosisResult ? (
{diagnosisResult.results.map((result, index) => { const quality = getQualityDisplay(result.averageTime, result.packetLoss); return (
{result.success ? '✓' : '✗'}

{result.description}

{result.nodeName}

{result.success ? '成功' : '失败'}
{result.success ? (
{result.averageTime?.toFixed(0)}
平均延迟(ms)
{result.packetLoss?.toFixed(1)}
丢包率(%)
{quality && ( <> {quality.text}
连接质量
)}
目标地址: {result.targetIp}
) : (
目标地址: {result.targetIp}
)}
); })}
) : (

暂无诊断数据

)}
{currentDiagnosisTunnel && ( )} )}
); }