优化拖拽排序顺滑度

This commit is contained in:
root
2026-02-02 03:14:18 +00:00
parent 68e5d0ac0b
commit d787e4b07a
3 changed files with 88 additions and 45 deletions
+29 -14
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { Card, CardBody, CardHeader } from "@heroui/card"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import { Input } from "@heroui/input"; import { Input } from "@heroui/input";
@@ -15,7 +15,8 @@ import {
DndContext, DndContext,
closestCenter, closestCenter,
KeyboardSensor, KeyboardSensor,
PointerSensor, MouseSensor,
TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
DragEndEvent, DragEndEvent,
@@ -353,8 +354,6 @@ export default function ForwardPage() {
const userMap = new Map<string, UserGroup>(); const userMap = new Map<string, UserGroup>();
// 获取排序后的转发列表 // 获取排序后的转发列表
const sortedForwards = getSortedForwards();
sortedForwards.forEach(forward => { sortedForwards.forEach(forward => {
const userKey = forward.userId ? forward.userId.toString() : 'unknown'; const userKey = forward.userId ? forward.userId.toString() : 'unknown';
const userName = forward.userName || '未知用户'; const userName = forward.userName || '未知用户';
@@ -855,7 +854,7 @@ export default function ForwardPage() {
); );
} else { } else {
// 直接显示模式下,过滤指定隧道的转发 // 直接显示模式下,过滤指定隧道的转发
forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport); forwardsToExport = sortedForwards.filter(forward => forward.tunnelId === selectedTunnelForExport);
} }
if (forwardsToExport.length === 0) { if (forwardsToExport.length === 0) {
@@ -1102,14 +1101,24 @@ export default function ForwardPage() {
// 传感器配置 - 使用默认配置避免错误 // 传感器配置 - 使用默认配置避免错误
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(MouseSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 8,
},
}),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) })
); );
// 根据排序顺序获取转发列表 // 根据排序顺序获取转发列表
const getSortedForwards = (): Forward[] => { const sortedForwards = useMemo((): Forward[] => {
// 确保 forwards 数组存在且有效 // 确保 forwards 数组存在且有效
if (!forwards || forwards.length === 0) { if (!forwards || forwards.length === 0) {
return []; return [];
@@ -1130,14 +1139,14 @@ export default function ForwardPage() {
} }
// 优先使用数据库中的 inx 字段进行排序 // 优先使用数据库中的 inx 字段进行排序
const sortedForwards = [...filteredForwards].sort((a, b) => { const sortedByDb = [...filteredForwards].sort((a, b) => {
const aInx = a.inx ?? 0; const aInx = a.inx ?? 0;
const bInx = b.inx ?? 0; const bInx = b.inx ?? 0;
return aInx - bInx; 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 forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
const localSortedForwards: Forward[] = []; const localSortedForwards: Forward[] = [];
@@ -1158,8 +1167,13 @@ export default function ForwardPage() {
return localSortedForwards; 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 }) => { const SortableForwardCard = ({ forward }: { forward: Forward }) => {
@@ -1179,8 +1193,9 @@ export default function ForwardPage() {
const style = { const style = {
transform: transform ? CSS.Transform.toString(transform) : undefined, transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: transition || undefined, transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
willChange: 'transform',
}; };
return ( return (
@@ -1529,11 +1544,11 @@ export default function ForwardPage() {
onDragStart={() => {}} // 添加空的 onDragStart 处理器 onDragStart={() => {}} // 添加空的 onDragStart 处理器
> >
<SortableContext <SortableContext
items={getSortedForwards().map(f => f.id || 0).filter(id => id > 0)} items={sortableForwardIds}
strategy={rectSortingStrategy} strategy={rectSortingStrategy}
> >
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{getSortedForwards().map((forward) => ( {sortedForwards.map((forward) => (
forward && forward.id ? ( forward && forward.id ? (
<SortableForwardCard key={forward.id} forward={forward} /> <SortableForwardCard key={forward.id} forward={forward} />
) : null ) : null
+25 -11
View File
@@ -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 { Card, CardBody, CardHeader } from "@heroui/card";
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import { Input } from "@heroui/input"; import { Input } from "@heroui/input";
@@ -16,7 +16,8 @@ import axios from 'axios';
import { import {
DndContext, DndContext,
KeyboardSensor, KeyboardSensor,
PointerSensor, MouseSensor,
TouchSensor,
type DragEndEvent, type DragEndEvent,
useSensor, useSensor,
useSensors, useSensors,
@@ -102,8 +103,9 @@ const SortableItem = ({
const style = { const style = {
transform: transform ? CSS.Transform.toString(transform) : undefined, transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: transition || undefined, transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
willChange: 'transform',
}; };
return ( return (
@@ -806,24 +808,34 @@ export default function NodePage() {
// 传感器配置 // 传感器配置
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(MouseSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 8,
},
}),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) })
); );
// 根据排序顺序获取节点列表 // 根据排序顺序获取节点列表
const getSortedNodes = (): Node[] => { const sortedNodes = useMemo((): Node[] => {
if (!nodeList || nodeList.length === 0) return []; if (!nodeList || nodeList.length === 0) return [];
const sortedNodes = [...nodeList].sort((a, b) => { const sortedByDb = [...nodeList].sort((a, b) => {
const aInx = a.inx ?? 0; const aInx = a.inx ?? 0;
const bInx = b.inx ?? 0; const bInx = b.inx ?? 0;
return aInx - bInx; 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 nodeMap = new Map(nodeList.map((n) => [n.id, n] as const));
const localSorted: Node[] = []; const localSorted: Node[] = [];
@@ -841,8 +853,10 @@ export default function NodePage() {
return localSorted; return localSorted;
} }
return sortedNodes; return sortedByDb;
}; }, [nodeList, nodeOrder]);
const sortableNodeIds = useMemo(() => sortedNodes.map((n) => n.id), [sortedNodes]);
return ( return (
@@ -900,11 +914,11 @@ export default function NodePage() {
) : ( ) : (
<DndContext sensors={sensors} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext <SortableContext
items={getSortedNodes().map((n) => n.id)} items={sortableNodeIds}
strategy={rectSortingStrategy} strategy={rectSortingStrategy}
> >
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{getSortedNodes().map((node) => ( {sortedNodes.map((node) => (
<SortableItem key={node.id} id={node.id}> <SortableItem key={node.id} id={node.id}>
{(listeners) => ( {(listeners) => (
<Card <Card
+25 -11
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { Card, CardBody, CardHeader } from "@heroui/card"; import { Card, CardBody, CardHeader } from "@heroui/card";
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import { Input, Textarea } from "@heroui/input"; import { Input, Textarea } from "@heroui/input";
@@ -13,7 +13,8 @@ import toast from 'react-hot-toast';
import { import {
DndContext, DndContext,
KeyboardSensor, KeyboardSensor,
PointerSensor, MouseSensor,
TouchSensor,
type DragEndEvent, type DragEndEvent,
useSensor, useSensor,
useSensors, useSensors,
@@ -586,24 +587,34 @@ export default function TunnelPage() {
// 传感器配置 // 传感器配置
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(MouseSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 8,
},
}),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) })
); );
// 根据排序顺序获取隧道列表 // 根据排序顺序获取隧道列表
const getSortedTunnels = (): Tunnel[] => { const sortedTunnels = useMemo((): Tunnel[] => {
if (!tunnels || tunnels.length === 0) return []; if (!tunnels || tunnels.length === 0) return [];
const sortedTunnels = [...tunnels].sort((a, b) => { const sortedByDb = [...tunnels].sort((a, b) => {
const aInx = a.inx ?? 0; const aInx = a.inx ?? 0;
const bInx = b.inx ?? 0; const bInx = b.inx ?? 0;
return aInx - bInx; 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 tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const));
const localSorted: Tunnel[] = []; const localSorted: Tunnel[] = [];
@@ -621,8 +632,10 @@ export default function TunnelPage() {
return localSorted; return localSorted;
} }
return sortedTunnels; return sortedByDb;
}; }, [tunnels, tunnelOrder]);
const sortableTunnelIds = useMemo(() => sortedTunnels.map((t) => t.id), [sortedTunnels]);
const SortableItem = ({ const SortableItem = ({
id, id,
@@ -642,8 +655,9 @@ export default function TunnelPage() {
const style = { const style = {
transform: transform ? CSS.Transform.toString(transform) : undefined, transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: transition || undefined, transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
willChange: 'transform',
}; };
return ( return (
@@ -690,11 +704,11 @@ export default function TunnelPage() {
{tunnels.length > 0 ? ( {tunnels.length > 0 ? (
<DndContext sensors={sensors} onDragEnd={handleDragEnd}> <DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext <SortableContext
items={getSortedTunnels().map((t) => t.id)} items={sortableTunnelIds}
strategy={rectSortingStrategy} strategy={rectSortingStrategy}
> >
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{getSortedTunnels().map((tunnel) => { {sortedTunnels.map((tunnel) => {
const typeDisplay = getTypeDisplay(tunnel.type); const typeDisplay = getTypeDisplay(tunnel.type);
return ( return (