import { useState, useEffect, useCallback } from "react"; import { Button } from "@heroui/button"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Tabs, Tab } from "@heroui/tabs"; import { Input } from "@heroui/input"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, } from "@heroui/modal"; import { Select, SelectItem } from "@heroui/select"; import { toast } from "react-hot-toast"; import { getNodeList, createPeerShare, getPeerShareList, deletePeerShare, resetPeerShareFlow, getPeerRemoteUsageList, importRemoteNode, updatePeerShare, } from "@/api"; interface Node { id: number; name: string; isRemote?: number; } interface PeerShare { id: number; name: string; token: string; maxBandwidth: number; currentFlow: number; expiryTime: number; portRangeStart: number; portRangeEnd: number; isActive: number; allowedDomains?: string; allowedIps?: string; usedPorts?: number[]; usedPortDetails?: Array<{ runtimeId: number; port: number; role: string; protocol: string; resourceKey: string; applied: number; updatedTime: number; }>; activeRuntimeNum?: number; } interface RemoteUsageBinding { bindingId: number; tunnelId: number; tunnelName: string; chainType: number; hopInx: number; allocatedPort: number; resourceKey: string; remoteBindingId: string; updatedTime: number; } interface RemoteUsageNode { nodeId: number; nodeName: string; remoteUrl: string; shareId: number; portRangeStart: number; portRangeEnd: number; maxBandwidth: number; currentFlow: number; usedPorts: number[]; bindings: RemoteUsageBinding[]; activeBindingNum: number; syncError?: string; } export default function PanelSharingPage() { const [selectedTab, setSelectedTab] = useState("my-shares"); const [shares, setShares] = useState([]); const [remoteUsageNodes, setRemoteUsageNodes] = useState( [], ); const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(false); const [remoteUsageLoading, setRemoteUsageLoading] = useState(false); // Modals const [createShareOpen, setCreateShareOpen] = useState(false); const [editShareOpen, setEditShareOpen] = useState(false); const [importNodeOpen, setImportNodeOpen] = useState(false); // Forms const [shareForm, setShareForm] = useState({ name: "", nodeId: "", maxBandwidth: 0, expiryDays: 30, portRangeStart: 10000, portRangeEnd: 20000, allowedDomains: "", allowedIps: "", }); const [importForm, setImportForm] = useState({ remoteUrl: "", token: "", }); const [editForm, setEditForm] = useState({ id: 0, name: "", maxBandwidth: 0, expiryTime: 0, portRangeStart: 10000, portRangeEnd: 20000, allowedDomains: "", allowedIps: "", }); const loadShares = useCallback(async () => { setLoading(true); try { const res = await getPeerShareList(); if (res.code === 0) { setShares(res.data || []); } else { toast.error(res.msg || "加载分享列表失败"); } } finally { setLoading(false); } }, []); const loadNodes = useCallback(async () => { try { const res = await getNodeList(); if (res.code === 0) { const localNodes: Node[] = (res.data || []).filter( (node: Node) => (node?.isRemote ?? 0) !== 1, ); setNodes(localNodes); setShareForm((prev) => { if (!prev.nodeId) { return prev; } const hasSelectedNode = localNodes.some( (node: Node) => String(node.id) === prev.nodeId, ); return hasSelectedNode ? prev : { ...prev, nodeId: "" }; }); } } catch { // ignore } }, []); const loadRemoteUsage = useCallback(async () => { setRemoteUsageLoading(true); try { const res = await getPeerRemoteUsageList(); if (res.code === 0) { setRemoteUsageNodes(res.data || []); } else { toast.error(res.msg || "加载远程占用端口失败"); } } finally { setRemoteUsageLoading(false); } }, []); useEffect(() => { if (selectedTab === "my-shares") { loadShares(); loadNodes(); return; } if (selectedTab === "remote-nodes") { loadRemoteUsage(); } }, [selectedTab, loadShares, loadNodes, loadRemoteUsage]); const handleCreateShare = async () => { if (!shareForm.name || !shareForm.nodeId) { toast.error("请填写必要信息"); return; } const nodeId = parseInt(shareForm.nodeId, 10); if (Number.isNaN(nodeId) || !nodes.some((node) => node.id === nodeId)) { toast.error("仅可选择本地节点"); return; } if (shareForm.maxBandwidth < 0) { toast.error("流量上限不能为负数"); return; } try { const expiryTime = Date.now() + shareForm.expiryDays * 24 * 60 * 60 * 1000; const res = await createPeerShare({ name: shareForm.name, nodeId, maxBandwidth: Math.max(0, shareForm.maxBandwidth) * 1024 * 1024 * 1024, expiryTime: shareForm.expiryDays === 0 ? 0 : expiryTime, portRangeStart: shareForm.portRangeStart, portRangeEnd: shareForm.portRangeEnd, allowedDomains: shareForm.allowedDomains, allowedIps: shareForm.allowedIps, }); if (res.code === 0) { toast.success("创建成功"); setCreateShareOpen(false); loadShares(); } else { toast.error(res.msg || "创建失败"); } } catch { toast.error("网络错误"); } }; const handleDeleteShare = async (id: number) => { try { const res = await deletePeerShare(id); if (res.code === 0) { toast.success("删除成功"); loadShares(); } else { toast.error(res.msg || "删除失败"); } } catch { toast.error("网络错误"); } }; const handleResetShareFlow = async (id: number) => { try { const res = await resetPeerShareFlow(id); if (res.code === 0) { toast.success("共享流量已重置"); loadShares(); } else { toast.error(res.msg || "重置流量失败"); } } catch { toast.error("网络错误"); } }; const openEditShare = (share: PeerShare) => { setEditForm({ id: share.id, name: share.name, maxBandwidth: share.maxBandwidth > 0 ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024)) : 0, expiryTime: share.expiryTime, portRangeStart: share.portRangeStart, portRangeEnd: share.portRangeEnd, allowedDomains: share.allowedDomains || "", allowedIps: share.allowedIps || "", }); setEditShareOpen(true); }; const handleEditShare = async () => { if (!editForm.name) { toast.error("名称不能为空"); return; } if (editForm.maxBandwidth < 0) { toast.error("流量上限不能为负数"); return; } try { const res = await updatePeerShare({ id: editForm.id, name: editForm.name, maxBandwidth: Math.max(0, editForm.maxBandwidth) * 1024 * 1024 * 1024, expiryTime: editForm.expiryTime, portRangeStart: editForm.portRangeStart, portRangeEnd: editForm.portRangeEnd, allowedDomains: editForm.allowedDomains, allowedIps: editForm.allowedIps, }); if (res.code === 0) { toast.success("编辑成功"); setEditShareOpen(false); loadShares(); } else { toast.error(res.msg || "编辑失败"); } } catch { toast.error("网络错误"); } }; const handleImportNode = async () => { if (!importForm.remoteUrl || !importForm.token) { toast.error("请填写完整信息"); return; } try { // Automatically add http/https if missing let url = importForm.remoteUrl.trim(); if (!url.startsWith("http")) { url = "http://" + url; } const res = await importRemoteNode({ remoteUrl: url, token: importForm.token.trim(), }); if (res.code === 0) { toast.success("导入成功,请前往节点列表查看"); setImportNodeOpen(false); setImportForm({ remoteUrl: "", token: "" }); loadRemoteUsage(); } else { toast.error(res.msg || "导入失败"); } } catch { toast.error("网络错误"); } }; const copyToken = (token: string) => { navigator.clipboard.writeText(token); toast.success("Token已复制"); }; const formatFlowGB = (bytes: number) => { if (!Number.isFinite(bytes) || bytes <= 0) { return "0 B"; } if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + " MB"; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"; }; const formatChainType = (chainType: number, hopInx: number) => { if (chainType === 2) { return `中继跳点 #${hopInx}`; } if (chainType === 3) { return "出口节点"; } return "未知链路"; }; return (

面板共享 (Panel Peering)

setSelectedTab(k as string)} >
{loading ? (
加载中...
) : shares.length === 0 ? (
暂无分享
) : (
{shares.map((share) => (

{share.name}

端口范围: {share.portRangeStart} -{" "} {share.portRangeEnd}

流量上限:{" "} {share.maxBandwidth > 0 ? formatFlowGB(share.maxBandwidth) : "不限制"}

当前流量: {formatFlowGB(share.currentFlow || 0)}

远程占用端口:{" "} {share.usedPorts && share.usedPorts.length > 0 ? share.usedPorts.join(", ") : "暂无"}

{share.usedPortDetails && share.usedPortDetails.length > 0 && (
{share.usedPortDetails.map((item) => ( {item.port} / {item.role || "reserved"} ))}
)} {share.allowedDomains && (

允许域名: {share.allowedDomains}

)} {share.allowedIps && (

允许API IP: {share.allowedIps}

)}

过期时间:{" "} {share.expiryTime === 0 ? "永久" : new Date(share.expiryTime).toLocaleDateString()}

))}
)}
{remoteUsageLoading ? (
加载中...
) : remoteUsageNodes.length === 0 ? (

暂无远程节点占用记录。

导入远程节点并创建隧道后,这里会显示远端端口占用情况。

) : (
{remoteUsageNodes.map((node) => (

{node.nodeName}

绑定 {node.activeBindingNum || 0}
{node.syncError && (
{node.syncError === "provider_share_deleted" ? "提供方已删除该分享" : node.syncError === "provider_share_disabled" ? "提供方已禁用该分享" : node.syncError === "provider_share_expired" ? "提供方分享已过期" : `远程同步失败: ${node.syncError}`}
)} {node.remoteUrl &&

远程地址: {node.remoteUrl}

}

共享ID: {node.shareId || "-"}

端口范围:{" "} {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}

共享流量:{" "} {node.maxBandwidth > 0 ? `${formatFlowGB(node.currentFlow || 0)} / ${formatFlowGB(node.maxBandwidth)}` : `${formatFlowGB(node.currentFlow || 0)} / 不限制`}

远端占用端口:{" "} {node.usedPorts && node.usedPorts.length > 0 ? node.usedPorts.join(", ") : "暂无"}

{node.bindings && node.bindings.length > 0 && (
{node.bindings.map((binding) => (

隧道{" "} {binding.tunnelName || `#${binding.tunnelId}`} {" · "} 端口 {binding.allocatedPort} {" · "} {formatChainType( binding.chainType, binding.hopInx, )}

))}
)}
))}
)}
{/* Create Share Modal */} setCreateShareOpen(false)}> 创建分享 setShareForm({ ...shareForm, name: e.target.value }) } />
setShareForm({ ...shareForm, portRangeStart: parseInt(e.target.value), }) } /> setShareForm({ ...shareForm, portRangeEnd: parseInt(e.target.value), }) } />
setShareForm({ ...shareForm, expiryDays: parseInt(e.target.value), }) } /> setShareForm({ ...shareForm, maxBandwidth: parseInt(e.target.value, 10) || 0, }) } /> setShareForm({ ...shareForm, allowedDomains: e.target.value }) } /> setShareForm({ ...shareForm, allowedIps: e.target.value }) } />
{/* Edit Share Modal */} setEditShareOpen(false)}> 编辑分享 setEditForm({ ...editForm, name: e.target.value }) } />
setEditForm({ ...editForm, portRangeStart: parseInt(e.target.value) || 0, }) } /> setEditForm({ ...editForm, portRangeEnd: parseInt(e.target.value) || 0, }) } />
setEditForm({ ...editForm, maxBandwidth: parseInt(e.target.value, 10) || 0, }) } /> 0 ? new Date(editForm.expiryTime).toISOString().slice(0, 16) : "" } onChange={(e) => setEditForm({ ...editForm, expiryTime: e.target.value ? new Date(e.target.value).getTime() : 0, }) } /> setEditForm({ ...editForm, allowedDomains: e.target.value }) } /> setEditForm({ ...editForm, allowedIps: e.target.value }) } />
{/* Import Node Modal */} setImportNodeOpen(false)}> 导入远程节点 setImportForm({ ...importForm, remoteUrl: e.target.value }) } /> setImportForm({ ...importForm, token: e.target.value }) } />
); }