diff --git a/go-gost/gost b/go-gost/gost index 43ac09c..6e5ee99 100755 Binary files a/go-gost/gost and b/go-gost/gost differ diff --git a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java index 43d3244..4b52f9b 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/TunnelServiceImpl.java @@ -676,7 +676,7 @@ public class TunnelServiceImpl extends ServiceImpl impleme * @return TCP端口号 */ private int getOutNodeTcpPort(Long tunnelId) { - List forwards = forwardService.list(new QueryWrapper().eq("tunnel_id", tunnelId)); + List forwards = forwardService.list(new QueryWrapper().eq("tunnel_id", tunnelId).eq("status", TUNNEL_STATUS_ACTIVE)); if (!forwards.isEmpty()) { return forwards.get(0).getOutPort(); } diff --git a/vite-frontend/package.json b/vite-frontend/package.json index 683ff92..ba721a5 100644 --- a/vite-frontend/package.json +++ b/vite-frontend/package.json @@ -10,57 +10,57 @@ "preview": "vite preview" }, "dependencies": { - "@heroui/accordion": "^2.2.20", - "@heroui/alert": "^2.2.22", - "@heroui/avatar": "^2.2.18", - "@heroui/button": "2.2.22", - "@heroui/card": "^2.2.21", - "@heroui/chip": "^2.2.18", + "@heroui/autocomplete": "^2.3.25", + "@heroui/avatar": "^2.2.19", + "@heroui/badge": "^2.2.15", + "@heroui/button": "2.2.23", + "@heroui/card": "^2.2.22", + "@heroui/chip": "^2.2.19", + "@heroui/code": "2.2.17", "@heroui/date-picker": "^2.3.22", - "@heroui/divider": "^2.2.15", - "@heroui/dropdown": "^2.3.22", - "@heroui/input": "2.4.22", - "@heroui/link": "2.2.19", - "@heroui/modal": "^2.2.19", - "@heroui/navbar": "2.2.20", - "@heroui/pagination": "^2.2.20", - "@heroui/progress": "^2.2.18", - "@heroui/radio": "^2.3.21", + "@heroui/dropdown": "2.3.23", + "@heroui/image": "^2.2.15", + "@heroui/input": "2.4.24", + "@heroui/kbd": "2.2.18", + "@heroui/link": "2.2.20", + "@heroui/modal": "^2.2.21", + "@heroui/navbar": "2.2.21", + "@heroui/pagination": "^2.2.21", "@heroui/select": "^2.4.22", - "@heroui/spacer": "^2.2.16", - "@heroui/spinner": "^2.2.19", - "@heroui/switch": "2.2.20", - "@heroui/system": "2.4.18", - "@heroui/table": "^2.2.21", - "@heroui/tabs": "^2.2.19", - "@heroui/theme": "^2.4.19", - "@heroui/tooltip": "^2.2.19", - "@heroui/use-theme": "2.1.9", - "@internationalized/date": "^3.8.2", + "@heroui/snippet": "2.2.24", + "@heroui/spinner": "^2.2.20", + "@heroui/switch": "^2.2.21", + "@heroui/system": "2.4.19", + "@heroui/table": "^2.2.24", + "@heroui/theme": "2.4.19", + "@heroui/use-theme": "2.1.10", + "@nextui-org/system": "^2.4.6", "@react-aria/visually-hidden": "3.8.25", "@react-types/shared": "3.30.0", - "axios": "^1.10.0", + "@tailwindcss/postcss": "4.1.11", + "@tailwindcss/vite": "4.1.11", + "axios": "^1.11.0", "clsx": "2.1.1", - "framer-motion": "11.15.0", + "framer-motion": "11.18.2", "react": "18.3.1", "react-dom": "18.3.1", "react-hot-toast": "^2.5.2", "react-router-dom": "6.23.0", - "tailwind-variants": "0.3.0", - "tailwindcss": "^3.4.0" + "recharts": "^3.1.1", + "sonner": "^2.0.6", + "tailwind-variants": "1.0.0", + "tailwindcss": "4.1.11" }, "devDependencies": { "@eslint/compat": "1.2.8", "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.25.1", - "@types/node": "20.5.7", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "@vitejs/plugin-react": "4.4.1", - "autoprefixer": "10.4.21", "eslint": "9.25.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.31.0", @@ -71,7 +71,7 @@ "eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-unused-imports": "4.1.4", "globals": "16.0.0", - "postcss": "8.5.3", + "postcss": "8.5.6", "prettier": "3.5.3", "typescript": "5.6.3", "vite": "5.2.0", diff --git a/vite-frontend/postcss.config.cjs b/vite-frontend/postcss.config.cjs deleted file mode 100644 index 96bb01e..0000000 --- a/vite-frontend/postcss.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/vite-frontend/postcss.config.js b/vite-frontend/postcss.config.js new file mode 100644 index 0000000..fe1e17e --- /dev/null +++ b/vite-frontend/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; \ No newline at end of file diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index 63bc8ff..bb693c7 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig; // 缓存相关常量 const CACHE_PREFIX = 'vite_config_'; -const VERSION = "1.2.1"; +const VERSION = "1.2.2"; // 缓存策略: // - 长期有效,不设置过期时间 diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 975cfae..f2538ff 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -125,6 +125,24 @@ export default function ForwardPage() { const [addressModalTitle, setAddressModalTitle] = useState(''); const [addressList, setAddressList] = useState([]); + // 导出相关状态 + const [exportModalOpen, setExportModalOpen] = useState(false); + const [exportData, setExportData] = useState(''); + const [exportLoading, setExportLoading] = useState(false); + const [selectedTunnelForExport, setSelectedTunnelForExport] = useState(null); + + // 导入相关状态 + const [importModalOpen, setImportModalOpen] = useState(false); + const [importData, setImportData] = useState(''); + const [importLoading, setImportLoading] = useState(false); + const [selectedTunnelForImport, setSelectedTunnelForImport] = useState(null); + const [importResults, setImportResults] = useState>([]); + // 表单状态 const [form, setForm] = useState({ name: '', @@ -154,8 +172,8 @@ export default function ForwardPage() { }; // 加载所有数据 - const loadData = async () => { - setLoading(true); + const loadData = async (lod = true) => { + setLoading(lod); try { const [forwardsRes, tunnelsRes] = await Promise.all([ getForwardList(), @@ -639,6 +657,190 @@ export default function ForwardPage() { await copyToClipboard(allAddresses, '所有地址'); }; + // 导出转发数据 + const handleExport = () => { + setSelectedTunnelForExport(null); + setExportData(''); + setExportModalOpen(true); + }; + + // 执行导出 + const executeExport = () => { + if (!selectedTunnelForExport) { + toast.error('请选择要导出的隧道'); + return; + } + + setExportLoading(true); + + try { + // 根据当前显示模式获取要导出的转发列表 + let forwardsToExport: Forward[] = []; + + if (viewMode === 'grouped') { + // 分组模式下,获取指定隧道的转发 + const userGroups = groupForwardsByUserAndTunnel(); + forwardsToExport = userGroups.flatMap(userGroup => + userGroup.tunnelGroups + .filter(tunnelGroup => tunnelGroup.tunnelId === selectedTunnelForExport) + .flatMap(tunnelGroup => tunnelGroup.forwards) + ); + } else { + // 直接显示模式下,过滤指定隧道的转发 + forwardsToExport = forwards.filter(forward => forward.tunnelId === selectedTunnelForExport); + } + + if (forwardsToExport.length === 0) { + toast.error('所选隧道没有转发数据'); + setExportLoading(false); + return; + } + + // 格式化导出数据:remoteAddr|name|inPort + const exportLines = forwardsToExport.map(forward => { + return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`; + }); + + const exportText = exportLines.join('\n'); + setExportData(exportText); + } catch (error) { + console.error('导出失败:', error); + toast.error('导出失败'); + } finally { + setExportLoading(false); + } + }; + + // 复制导出数据 + const copyExportData = async () => { + await copyToClipboard(exportData, '转发数据'); + }; + + // 导入转发数据 + const handleImport = () => { + setImportData(''); + setImportResults([]); + setSelectedTunnelForImport(null); + setImportModalOpen(true); + }; + + // 执行导入 + const executeImport = async () => { + if (!importData.trim()) { + toast.error('请输入要导入的数据'); + return; + } + + if (!selectedTunnelForImport) { + toast.error('请选择要导入的隧道'); + return; + } + + setImportLoading(true); + setImportResults([]); // 清空之前的结果 + + try { + const lines = importData.trim().split('\n').filter(line => line.trim()); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const parts = line.split('|'); + + if (parts.length < 2) { + setImportResults(prev => [{ + line, + success: false, + message: '格式错误:需要至少包含目标地址和转发名称' + }, ...prev]); + continue; + } + + const [remoteAddr, name, inPort] = parts; + + if (!remoteAddr.trim() || !name.trim()) { + setImportResults(prev => [{ + line, + success: false, + message: '目标地址和转发名称不能为空' + }, ...prev]); + continue; + } + + // 验证远程地址格式 - 支持单个地址或多个地址用逗号分隔 + const addresses = remoteAddr.trim().split(','); + const addressPattern = /^[^:]+:\d+$/; + const isValidFormat = addresses.every(addr => addressPattern.test(addr.trim())); + + if (!isValidFormat) { + setImportResults(prev => [{ + line, + success: false, + message: '目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔' + }, ...prev]); + continue; + } + + try { + // 处理入口端口 + let portNumber: number | null = null; + if (inPort && inPort.trim()) { + const port = parseInt(inPort.trim()); + if (isNaN(port) || port < 1 || port > 65535) { + setImportResults(prev => [{ + line, + success: false, + message: '入口端口格式错误,应为1-65535之间的数字' + }, ...prev]); + continue; + } + portNumber = port; + } + + // 调用创建转发接口 + const response = await createForward({ + name: name.trim(), + tunnelId: selectedTunnelForImport, // 使用用户选择的隧道 + inPort: portNumber, // 使用指定端口或自动分配 + remoteAddr: remoteAddr.trim(), + strategy: 'fifo' + }); + + if (response.code === 0) { + setImportResults(prev => [{ + line, + success: true, + message: '创建成功', + forwardName: name.trim() + }, ...prev]); + } else { + setImportResults(prev => [{ + line, + success: false, + message: response.msg || '创建失败' + }, ...prev]); + } + } catch (error) { + setImportResults(prev => [{ + line, + success: false, + message: '网络错误,创建失败' + }, ...prev]); + } + } + + + toast.success(`导入执行完成`); + + // 导入完成后刷新转发列表 + await loadData(false); + } catch (error) { + console.error('导入失败:', error); + toast.error('导入过程中发生错误'); + } finally { + setImportLoading(false); + } + }; + // 获取状态显示 const getStatusDisplay = (status: number) => { switch (status) { @@ -714,7 +916,7 @@ export default function ForwardPage() { className={`cursor-pointer px-2 py-1 bg-default-50 dark:bg-default-100/50 rounded border border-default-200 dark:border-default-300 transition-colors duration-200 ${ hasMultipleAddresses(forward.inIp) ? 'hover:bg-default-100 dark:hover:bg-default-200/50' : '' }`} - onClick={() => showAddressModal(forward.inIp, forward.inPort, '入口地址')} + onClick={() => showAddressModal(forward.inIp, forward.inPort, '入口端口')} title={formatInAddress(forward.inIp, forward.inPort)} >
@@ -865,17 +1067,39 @@ export default function ForwardPage() { )} - + + {/* 导出按钮 */} + + + + +
@@ -1154,7 +1378,7 @@ export default function ForwardPage() {
@@ -1177,6 +1401,267 @@ export default function ForwardPage() { + {/* 导出数据模态框 */} + { + setExportModalOpen(false); + setSelectedTunnelForExport(null); + setExportData(''); + }} + size="2xl" + scrollBehavior="outside" + > + + +

导出转发数据

+

+ 格式:目标地址|转发名称|入口端口 +

+
+ +
+ {/* 隧道选择 */} +
+ +
+ + {/* 导出按钮和数据 */} + {exportData && ( +
+ + +
+ )} + + {/* 初始导出按钮 */} + {!exportData && ( +
+ +
+ )} + + {/* 导出数据显示 */} + {exportData && ( +
+