From 8222b319179d914a934eb90a57460cfc07774452 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 31 Jan 2026 05:52:39 +0000 Subject: [PATCH] feat: enable drag-sort in tunnel management --- vite-frontend/src/pages/tunnel.tsx | 233 ++++++++++++++++++++++++++--- 1 file changed, 213 insertions(+), 20 deletions(-) diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index 12dac79..ced8129 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -10,6 +10,23 @@ import { Divider } from "@heroui/divider"; import { Alert } from "@heroui/alert"; import toast from 'react-hot-toast'; +import { + DndContext, + KeyboardSensor, + PointerSensor, + type DragEndEvent, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + SortableContext, + arrayMove, + rectSortingStrategy, + sortableKeyboardCoordinates, + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + import { createTunnel, @@ -17,7 +34,8 @@ import { updateTunnel, deleteTunnel, getNodeList, - diagnoseTunnel + diagnoseTunnel, + updateTunnelOrder } from "@/api"; interface ChainTunnel { @@ -30,6 +48,7 @@ interface ChainTunnel { interface Tunnel { id: number; + inx?: number; name: string; type: number; // 1: 端口转发, 2: 隧道转发 inNodeId: ChainTunnel[]; // 入口节点列表 @@ -87,6 +106,7 @@ interface DiagnosisResult { export default function TunnelPage() { const [loading, setLoading] = useState(true); const [tunnels, setTunnels] = useState([]); + const [tunnelOrder, setTunnelOrder] = useState([]); const [nodes, setNodes] = useState([]); // 模态框状态 @@ -131,7 +151,45 @@ export default function TunnelPage() { ]); if (tunnelsRes.code === 0) { - setTunnels(tunnelsRes.data || []); + const tunnelsData: Tunnel[] = (tunnelsRes.data || []).map((t: any) => ({ + ...t, + inx: t.inx ?? 0, + })); + setTunnels(tunnelsData); + + // 优先使用数据库中的 inx 字段进行排序,否则回退到本地排序 + const hasDbOrdering = tunnelsData.some((t) => t.inx !== undefined && t.inx !== 0); + if (hasDbOrdering) { + const dbOrder = [...tunnelsData] + .sort((a, b) => (a.inx ?? 0) - (b.inx ?? 0)) + .map((t) => t.id); + setTunnelOrder(dbOrder); + } else { + try { + const stored = localStorage.getItem('tunnel-order'); + if (stored) { + const parsed = JSON.parse(stored); + if (Array.isArray(parsed)) { + const existingIds = new Set(tunnelsData.map((t) => t.id)); + const validOrder = parsed + .map((id: any) => Number(id)) + .filter((id: number) => existingIds.has(id)); + + if (validOrder.length > 0) { + setTunnelOrder(validOrder); + } else { + setTunnelOrder(tunnelsData.map((t) => t.id)); + } + } else { + setTunnelOrder(tunnelsData.map((t) => t.id)); + } + } else { + setTunnelOrder(tunnelsData.map((t) => t.id)); + } + } catch { + setTunnelOrder(tunnelsData.map((t) => t.id)); + } + } } else { toast.error(tunnelsRes.msg || '获取隧道列表失败'); } @@ -481,6 +539,120 @@ export default function TunnelPage() { return { text: '😵 很差', color: 'danger' }; }; + // 处理拖拽结束 + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (!active || !over || active.id === over.id) return; + if (!tunnelOrder || tunnelOrder.length === 0) return; + + const activeId = Number(active.id); + const overId = Number(over.id); + if (isNaN(activeId) || isNaN(overId)) return; + + const oldIndex = tunnelOrder.indexOf(activeId); + const newIndex = tunnelOrder.indexOf(overId); + if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return; + + const newOrder = arrayMove(tunnelOrder, oldIndex, newIndex); + setTunnelOrder(newOrder); + + // 保存到 localStorage + try { + localStorage.setItem('tunnel-order', JSON.stringify(newOrder)); + } catch (error) { + console.warn('无法保存隧道排序到localStorage:', error); + } + + // 持久化到数据库 + try { + const tunnelsToUpdate = newOrder.map((id, index) => ({ id, inx: index })); + const response = await updateTunnelOrder({ tunnels: tunnelsToUpdate }); + if (response.code === 0) { + setTunnels((prev) => + prev.map((tunnel) => { + const updated = tunnelsToUpdate.find((t) => t.id === tunnel.id); + return updated ? { ...tunnel, inx: updated.inx } : tunnel; + }) + ); + } else { + toast.error('保存排序失败:' + (response.msg || '未知错误')); + } + } catch (error) { + console.error('保存隧道排序到数据库失败:', error); + toast.error('保存排序失败,请重试'); + } + }; + + // 传感器配置 + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + // 根据排序顺序获取隧道列表 + const getSortedTunnels = (): Tunnel[] => { + if (!tunnels || tunnels.length === 0) return []; + + const sortedTunnels = [...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)) { + const tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const)); + const localSorted: Tunnel[] = []; + + tunnelOrder.forEach((id) => { + const tunnel = tunnelMap.get(id); + if (tunnel) localSorted.push(tunnel); + }); + + tunnels.forEach((tunnel) => { + if (!tunnelOrder.includes(tunnel.id)) { + localSorted.push(tunnel); + } + }); + + return localSorted; + } + + return sortedTunnels; + }; + + const SortableItem = ({ + id, + children, + }: { + id: number; + children: (listeners: any) => any; + }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: transform ? CSS.Transform.toString(transform) : undefined, + transition: transition || undefined, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+ {children(listeners)} +
+ ); + }; + if (loading) { return ( @@ -516,17 +688,24 @@ export default function TunnelPage() { {/* 隧道卡片网格 */} {tunnels.length > 0 ? ( -
- {tunnels.map((tunnel) => { + + t.id)} + strategy={rectSortingStrategy} + > +
+ {getSortedTunnels().map((tunnel) => { const typeDisplay = getTypeDisplay(tunnel.type); return ( - - -
-
-

{tunnel.name}

-
+ + {(listeners) => ( + + +
+
+

{tunnel.name}

+
{typeDisplay.text} - + +
+
+
+ + + +
-
-
- + - +
{/* 拓扑结构 */}
@@ -654,10 +843,14 @@ export default function TunnelPage() {
- ); - })} -
- ) : ( + )} + + ); + })} +
+ + +) : ( /* 空状态 */ @@ -1629,4 +1822,4 @@ export default function TunnelPage() {
); -} \ No newline at end of file +}