From d787e4b07af281708dc1b5f6a2291379181dc787 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 03:14:18 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E9=A1=BA=E6=BB=91=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite-frontend/src/pages/forward.tsx | 61 ++++++++++++++++++----------- vite-frontend/src/pages/node.tsx | 36 +++++++++++------ vite-frontend/src/pages/tunnel.tsx | 36 +++++++++++------ 3 files changed, 88 insertions(+), 45 deletions(-) diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index febf8a1..5492ab4 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { Input } from "@heroui/input"; @@ -15,7 +15,8 @@ import { DndContext, closestCenter, KeyboardSensor, - PointerSensor, + MouseSensor, + TouchSensor, useSensor, useSensors, DragEndEvent, @@ -353,8 +354,6 @@ export default function ForwardPage() { const userMap = new Map(); // 获取排序后的转发列表 - const sortedForwards = getSortedForwards(); - sortedForwards.forEach(forward => { const userKey = forward.userId ? forward.userId.toString() : 'unknown'; const userName = forward.userName || '未知用户'; @@ -855,7 +854,7 @@ export default function ForwardPage() { ); } else { // 直接显示模式下,过滤指定隧道的转发 - forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport); + forwardsToExport = sortedForwards.filter(forward => forward.tunnelId === selectedTunnelForExport); } if (forwardsToExport.length === 0) { @@ -1102,19 +1101,29 @@ export default function ForwardPage() { // 传感器配置 - 使用默认配置避免错误 const sensors = useSensors( - useSensor(PointerSensor), + useSensor(MouseSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 8, + }, + }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // 根据排序顺序获取转发列表 - const getSortedForwards = (): Forward[] => { + const sortedForwards = useMemo((): Forward[] => { // 确保 forwards 数组存在且有效 if (!forwards || forwards.length === 0) { return []; } - + // 在平铺模式下,只显示当前用户的转发 let filteredForwards = forwards; if (viewMode === 'direct') { @@ -1123,43 +1132,48 @@ export default function ForwardPage() { filteredForwards = forwards.filter(forward => forward.userId === currentUserId); } } - + // 确保过滤后的转发列表有效 if (!filteredForwards || filteredForwards.length === 0) { return []; } - + // 优先使用数据库中的 inx 字段进行排序 - const sortedForwards = [...filteredForwards].sort((a, b) => { + const sortedByDb = [...filteredForwards].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; return aInx - bInx; }); - + // 如果数据库中没有排序信息,则使用本地存储的顺序 - if (forwardOrder && forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) { + if (forwardOrder && forwardOrder.length > 0 && sortedByDb.every(f => f.inx === undefined || f.inx === 0)) { const forwardMap = new Map(filteredForwards.map(f => [f.id, f])); const localSortedForwards: Forward[] = []; - + forwardOrder.forEach(id => { const forward = forwardMap.get(id); if (forward) { localSortedForwards.push(forward); } }); - + // 添加不在排序列表中的转发(新添加的) filteredForwards.forEach(forward => { if (!forwardOrder.includes(forward.id)) { localSortedForwards.push(forward); } }); - + return localSortedForwards; } - - return sortedForwards; - }; + + return sortedByDb; + }, [forwards, forwardOrder, viewMode]); + + const sortableForwardIds = useMemo( + () => sortedForwards.map(f => f.id).filter(id => id > 0), + [sortedForwards] + ); // 可拖拽的转发卡片组件 const SortableForwardCard = ({ forward }: { forward: Forward }) => { @@ -1179,8 +1193,9 @@ export default function ForwardPage() { const style = { transform: transform ? CSS.Transform.toString(transform) : undefined, - transition: transition || undefined, + transition: isDragging ? undefined : transition || undefined, opacity: isDragging ? 0.5 : 1, + willChange: 'transform', }; return ( @@ -1529,11 +1544,11 @@ export default function ForwardPage() { onDragStart={() => {}} // 添加空的 onDragStart 处理器 > f.id || 0).filter(id => id > 0)} + items={sortableForwardIds} strategy={rectSortingStrategy} >
- {getSortedForwards().map((forward) => ( + {sortedForwards.map((forward) => ( forward && forward.id ? ( ) : null @@ -2394,4 +2409,4 @@ export default function ForwardPage() {
); -} \ No newline at end of file +} diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index dbe407d..c71d220 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useMemo, useRef } from "react"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { Input } from "@heroui/input"; @@ -16,7 +16,8 @@ import axios from 'axios'; import { DndContext, KeyboardSensor, - PointerSensor, + MouseSensor, + TouchSensor, type DragEndEvent, useSensor, useSensors, @@ -102,8 +103,9 @@ const SortableItem = ({ const style = { transform: transform ? CSS.Transform.toString(transform) : undefined, - transition: transition || undefined, + transition: isDragging ? undefined : transition || undefined, opacity: isDragging ? 0.5 : 1, + willChange: 'transform', }; return ( @@ -806,24 +808,34 @@ export default function NodePage() { // 传感器配置 const sensors = useSensors( - useSensor(PointerSensor), + useSensor(MouseSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 8, + }, + }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // 根据排序顺序获取节点列表 - const getSortedNodes = (): Node[] => { + const sortedNodes = useMemo((): Node[] => { if (!nodeList || nodeList.length === 0) return []; - const sortedNodes = [...nodeList].sort((a, b) => { + const sortedByDb = [...nodeList].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; return aInx - bInx; }); // 如果数据库中没有排序信息,则使用本地存储的顺序 - if (nodeOrder && nodeOrder.length > 0 && sortedNodes.every((n) => n.inx === undefined || n.inx === 0)) { + if (nodeOrder && nodeOrder.length > 0 && sortedByDb.every((n) => n.inx === undefined || n.inx === 0)) { const nodeMap = new Map(nodeList.map((n) => [n.id, n] as const)); const localSorted: Node[] = []; @@ -841,8 +853,10 @@ export default function NodePage() { return localSorted; } - return sortedNodes; - }; + return sortedByDb; + }, [nodeList, nodeOrder]); + + const sortableNodeIds = useMemo(() => sortedNodes.map((n) => n.id), [sortedNodes]); return ( @@ -900,11 +914,11 @@ export default function NodePage() { ) : ( n.id)} + items={sortableNodeIds} strategy={rectSortingStrategy} >
- {getSortedNodes().map((node) => ( + {sortedNodes.map((node) => ( {(listeners) => ( { + const sortedTunnels = useMemo((): Tunnel[] => { if (!tunnels || tunnels.length === 0) return []; - const sortedTunnels = [...tunnels].sort((a, b) => { + const sortedByDb = [...tunnels].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; return aInx - bInx; }); // 如果数据库中没有排序信息,则使用本地存储的顺序 - if (tunnelOrder && tunnelOrder.length > 0 && sortedTunnels.every((t) => t.inx === undefined || t.inx === 0)) { + if (tunnelOrder && tunnelOrder.length > 0 && sortedByDb.every((t) => t.inx === undefined || t.inx === 0)) { const tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const)); const localSorted: Tunnel[] = []; @@ -621,8 +632,10 @@ export default function TunnelPage() { return localSorted; } - return sortedTunnels; - }; + return sortedByDb; + }, [tunnels, tunnelOrder]); + + const sortableTunnelIds = useMemo(() => sortedTunnels.map((t) => t.id), [sortedTunnels]); const SortableItem = ({ id, @@ -642,8 +655,9 @@ export default function TunnelPage() { const style = { transform: transform ? CSS.Transform.toString(transform) : undefined, - transition: transition || undefined, + transition: isDragging ? undefined : transition || undefined, opacity: isDragging ? 0.5 : 1, + willChange: 'transform', }; return ( @@ -690,11 +704,11 @@ export default function TunnelPage() { {tunnels.length > 0 ? ( t.id)} + items={sortableTunnelIds} strategy={rectSortingStrategy} >
- {getSortedTunnels().map((tunnel) => { + {sortedTunnels.map((tunnel) => { const typeDisplay = getTypeDisplay(tunnel.type); return ( From 531ba0bfed01a43d4a2d537ee00ee9678402e148 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 03:29:47 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=E6=96=B0=E5=BB=BA=E9=9A=A7=E9=81=93?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E8=BF=BD=E5=8A=A0=E5=88=B0=E6=9C=AB=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/admin/service/impl/TunnelServiceImpl.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 5f7e60a..5dd6527 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 @@ -117,6 +117,19 @@ public class TunnelServiceImpl extends ServiceImpl impleme long currentTime = System.currentTimeMillis(); tunnel.setCreatedTime(currentTime); tunnel.setUpdatedTime(currentTime); + + // When tunnels are ordered via `inx`, new tunnels should be appended. + // Only apply this when an order already exists (max `inx` > 0) to avoid + // changing behavior for deployments still relying on local ordering. + Tunnel lastByInx = this.getOne(new QueryWrapper() + .select("inx") + .orderByDesc("inx") + .orderByDesc("id") + .last("LIMIT 1")); + Integer maxInx = lastByInx == null ? null : lastByInx.getInx(); + if (maxInx != null && maxInx > 0) { + tunnel.setInx(maxInx + 1); + } if (StringUtils.isEmpty(tunnel.getInIp())){ java.util.LinkedHashSet inIps = new java.util.LinkedHashSet<>(); for (ChainTunnel chainTunnel : tunnelDto.getInNodeId()) { From ac30f0172faaf9d3c1fbd8bee0aa7a8ee031bc8e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Feb 2026 04:50:22 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B3=E9=93=BA?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=94=A8=E6=88=B7=E5=88=87=E6=8D=A2=E4=B8=8E?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8TS=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite-frontend/src/pages/dashboard.tsx | 66 ++++++++++++++++----------- vite-frontend/src/pages/forward.tsx | 9 ++-- vite-frontend/src/types/recharts.d.ts | 11 +++++ 3 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 vite-frontend/src/types/recharts.d.ts diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index 67b8390..6515618 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -741,31 +741,45 @@ export default function DashboardPage() { tickLine={false} axisLine={{ stroke: '#e5e7eb', strokeWidth: 1 }} /> - { - if (value === 0) return '0'; - if (value < 1024) return `${value}B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}K`; - if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)}M`; - return `${(value / (1024 * 1024 * 1024)).toFixed(1)}G`; - }} - /> - { - if (active && payload && payload.length) { - return ( -
-

{`时间: ${label}`}

-

- {`流量: ${formatFlow(payload[0]?.value as number || 0)}`} -

-
- ); - } - return null; + { + const v = typeof value === 'number' ? value : Number(value); + if (!Number.isFinite(v)) return String(value); + if (v === 0) return '0'; + if (v < 1024) return `${v}B`; + if (v < 1024 * 1024) return `${(v / 1024).toFixed(1)}K`; + if (v < 1024 * 1024 * 1024) return `${(v / (1024 * 1024)).toFixed(1)}M`; + return `${(v / (1024 * 1024 * 1024)).toFixed(1)}G`; + }} + /> + ; + label?: string | number; + }) => { + if (active && payload && payload.length) { + const firstValue = payload[0]?.value; + const numericValue = typeof firstValue === 'number' ? firstValue : Number(firstValue); + const flowValue = Number.isFinite(numericValue) ? numericValue : 0; + + return ( +
+

{`时间: ${label ?? ''}`}

+

+ {`流量: ${formatFlow(flowValue)}`} +

+
+ ); + } + return null; }} /> ); -} \ No newline at end of file +} diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 5492ab4..177791c 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -1117,6 +1117,8 @@ export default function ForwardPage() { }) ); + const tokenUserId = JwtUtil.getUserIdFromToken(); + // 根据排序顺序获取转发列表 const sortedForwards = useMemo((): Forward[] => { // 确保 forwards 数组存在且有效 @@ -1127,9 +1129,8 @@ export default function ForwardPage() { // 在平铺模式下,只显示当前用户的转发 let filteredForwards = forwards; if (viewMode === 'direct') { - const currentUserId = JwtUtil.getUserIdFromToken(); - if (currentUserId !== null) { - filteredForwards = forwards.filter(forward => forward.userId === currentUserId); + if (tokenUserId !== null) { + filteredForwards = forwards.filter(forward => forward.userId === tokenUserId); } } @@ -1168,7 +1169,7 @@ export default function ForwardPage() { } return sortedByDb; - }, [forwards, forwardOrder, viewMode]); + }, [forwards, forwardOrder, viewMode, tokenUserId]); const sortableForwardIds = useMemo( () => sortedForwards.map(f => f.id).filter(id => id > 0), diff --git a/vite-frontend/src/types/recharts.d.ts b/vite-frontend/src/types/recharts.d.ts new file mode 100644 index 0000000..95994aa --- /dev/null +++ b/vite-frontend/src/types/recharts.d.ts @@ -0,0 +1,11 @@ +declare module 'recharts' { + import * as React from 'react'; + + export const LineChart: React.ComponentType>; + export const Line: React.ComponentType>; + export const XAxis: React.ComponentType>; + export const YAxis: React.ComponentType>; + export const CartesianGrid: React.ComponentType>; + export const Tooltip: React.ComponentType>; + export const ResponsiveContainer: React.ComponentType>; +}