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()) { 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 febf8a1..177791c 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,64 +1101,80 @@ 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 tokenUserId = JwtUtil.getUserIdFromToken(); + // 根据排序顺序获取转发列表 - const getSortedForwards = (): Forward[] => { + const sortedForwards = useMemo((): Forward[] => { // 确保 forwards 数组存在且有效 if (!forwards || forwards.length === 0) { return []; } - + // 在平铺模式下,只显示当前用户的转发 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); } } - + // 确保过滤后的转发列表有效 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, tokenUserId]); + + const sortableForwardIds = useMemo( + () => sortedForwards.map(f => f.id).filter(id => id > 0), + [sortedForwards] + ); // 可拖拽的转发卡片组件 const SortableForwardCard = ({ forward }: { forward: Forward }) => { @@ -1179,8 +1194,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 +1545,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 +2410,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 ( 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>; +}