React实现一个拖拽排序组件 - 支持多行多列、支持TypeScript、支持Flip动画、可自定义拖拽区域

news2025/4/17 5:48:33

一、效果展示

排序:

丝滑的Flip动画 

 自定义列数 (并且宽度会随着屏幕宽度自适应)

自定义拖拽区域:(扩展性高,可以全部可拖拽、自定义拖拽图标)

二、主要思路

Tip: 本代码的CSS使用Tailwindcss, 如果没安装的可以自行安装这个库,也可以去问GPT,让它帮忙改成普通的CSS版本的代码

1. 一些ts类型:

import { CSSProperties, MutableRefObject, ReactNode } from "react"
/**有孩子的,基础的组件props,包含className style children */
interface baseChildrenProps {
    /**组件最外层的className */
    className?: string
    /**组件最外层的style */
    style?: CSSProperties
    /**孩子 */
    children?: ReactNode
}
/**ItemRender渲染函数的参数 */
type itemProps<T> = {
    /**当前元素 */
    item: T,
    /**当前索引 */
    index: number,
    /**父元素宽度 */
    width: number
    /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
    DragBox: (props: baseChildrenProps) => ReactNode
}
/**拖拽排序组件的props */
export interface DragSortProps<T> {
    /**组件最外层的className */
    className?: string
    /**组件最外层的style */
    style?: CSSProperties
    /**列表,拖拽后会改变里面的顺序 */
    list: T[]
    /**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
    keyName: keyof T
    /**一行个数,默认1 */
    cols?: number
    /**元素间距,单位px,默认0 (因为一行默认1) */
    marginX?: number
    /**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
    flipWithListChange?: boolean
    /**每个元素的渲染函数 */
    ItemRender: (props: itemProps<T>) => ReactNode
    /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
    afterDrag: (list: T[]) => any
}

2. 使用事件委托

监听所有子元素的拖拽开始、拖拽中、拖拽结束事件,减少绑定事件数量的同时,还能优化代码。

/**拖拽排序组件 */
const DragSort = function <T>({
  list,
  ItemRender,
  afterDrag,
  keyName,
  cols = 1,
  marginX = 0,
  flipWithListChange = true,
  className,
  style,
}: DragSortProps<T>) {
  const listRef = useRef<HTMLDivElement>(null);
  /**记录当前正在拖拽哪个元素 */
  const nowDragItem = useRef<HTMLDivElement>();
  const itemWidth = useCalculativeWidth(listRef, marginX, cols);//使用计算宽度钩子,计算每个元素的宽度 (代码后面会有)
  const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)


  /**事件委托- 监听 拖拽开始 事件,添加样式 */
  const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
    if (!listRef.current) return;
    e.stopPropagation(); //阻止冒泡

    /**这是当前正在被拖拽的元素 */
    const target = e.target as HTMLDivElement;

    //设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
    setTimeout(() => {
      target.classList.add(...movingClass); //设置正被拖动的元素样式
      target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
    }, 0);

    //记录当前拖拽的元素
    nowDragItem.current = target;

    //设置鼠标样式
    e.dataTransfer.effectAllowed = "move";
  };

  /**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
  const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
    if (!listRef.current || !nowDragItem.current) return;

    /**孩子数组,每次都会获取最新的 */
    const children = [...listRef.current.children];
    /**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
    const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);

    //边界判断
    if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
      // console.log("拖到自身或者拖到外面");
      return;
    }

    //拿到两个元素的索引,用来判断这俩元素应该怎么移动
    /**被拖拽元素在孩子数组中的索引 */
    const nowDragtItemIndex = children.indexOf(nowDragItem.current);
    /**被进入元素在孩子数组中的索引 */
    const enterItemIndex = children.indexOf(realTarget);
 
    //当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
    if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
      console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
      return;
    }

    if (nowDragtItemIndex < enterItemIndex) {
      // console.log("向下移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
    } else {
      // console.log("向上移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget);
    }
  };

  /**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
  const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
    if (!listRef.current) return;
    /**当前正在被拖拽的元素 */
    const target = e.target as Element;
    
    target.classList.remove(...movingClass);//删除前面添加的 被拖拽元素的样式,回归原样式
    target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass));//删除所有子元素的透明样式


    /**拿到当前DOM的id顺序信息 */
    const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序

    //把列表按照id排序
    const newList = [...list].sort(function (a, b) {
      const aIndex = ids.indexOf(String(a[keyName]));
      const bIndex = ids.indexOf(String(b[keyName]));
      if (aIndex === -1 && bIndex === -1) return 0;
      else if (aIndex === -1) return 1;
      else if (bIndex === -1) return -1;
      else return aIndex - bIndex;
    });

    
    afterDrag(newList);//触发外界传入的回调函数

    setDragOpen(false);//拖拽完成后,再次禁止拖拽 
  };

  /**拖拽按钮组件 */  //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽”
  const DragBox = ({ className, style, children }: baseChildrenProps) => {
    return (
      <div
        style={{ ...style }}
        className={cn("hover:cursor-grabbing", className)}
        onMouseEnter={() => setDragOpen(true)}
        onMouseLeave={() => setDragOpen(false)}
      >
        {children || <DragIcon size={20} color="#666666" />}
      </div>
    );
  };

  return (
    <div
      className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
      style={style}
      ref={listRef}
      onDragStart={onDragStart}
      onDragEnter={onDragEnter}
      onDragOver={(e) => e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上)
      onDragEnd={onDragEnd}
    >
      {list.map((item, index) => {
        const key = item[keyName] as string;
        return (
          <div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
            {ItemRender({ item, index, width: itemWidth, DragBox })}
          </div>
        );
      })}
    </div>
  );
};

3. 使用Flip做动画

对于这种移动位置的动画,普通的CSS和JS动画已经无法满足了:

        可以使用Flip动画来做:FLIP是 First、Last、Invert和 Play四个单词首字母的缩写, 意思就是,记录一开始的位置、记录结束的位置、记录位置的变化、让元素开始动画 

        主要的思路为: 记录原位置、记录现位置、记录位移大小,最重要的点来了, 使用CSS的 transform ,让元素在被改动位置的一瞬间, translate 定位到原本的位置上(通过我们前面计算的位移大小), 然后给元素加上 过渡 效果,再让它慢慢回到原位即可。

        代码如下 (没有第三方库,基本都是自己手写实现)

        这里还使用了JS提供的 Web Animations API,具有极高的性能,不阻塞主线程。

        但是由于API没有提供动画完成的回调,故这里使用定时器做回调触发

/**位置的类型 */
interface position {
    x: number,
    y: number
}

/**Flip动画 */
export class Flip {
    /**dom元素 */
    private dom: Element
    /**原位置 */
    private firstPosition: position | null = null
    /**动画时间 */
    private duration: number
    /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    static movingClass = "__flipMoving__"
    constructor(dom: Element, duration = 500) {
        this.dom = dom
        this.duration = duration
    }
    /**获得元素的当前位置信息 */
    private getDomPosition(): position {
        const rect = this.dom.getBoundingClientRect()
        return {
            x: rect.left,
            y: rect.top
        }
    }
    /**给原始位置赋值 */
    recordFirst(firstPosition?: position) {
        if (!firstPosition) firstPosition = this.getDomPosition()
        this.firstPosition = { ...firstPosition }
    }
    /**播放动画 */
    play(callback?: () => any) {
        if (!this.firstPosition) {
            console.warn('请先记录原始位置');
            return
        }
        const lastPositon = this.getDomPosition()
        const dif: position = {
            x: lastPositon.x - this.firstPosition.x,
            y: lastPositon.y - this.firstPosition.y,
        }
        // console.log(this, dif);
        if (!dif.x && !dif.y) return
        this.dom.classList.add(Flip.movingClass)
        this.dom.animate([
            { transform: `translate(${-dif.x}px, ${-dif.y}px)` },
            { transform: `translate(0px, 0px)` }
        ], { duration: this.duration })
        setTimeout(() => {
            this.dom.classList.remove(Flip.movingClass)
            callback?.()
        }, this.duration);
    }
}
/**Flip多元素同时触发 */
export class FlipList {
    /**Flip列表 */
    private flips: Flip[]
    /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    static movingClass = Flip.movingClass
    /**Flip多元素同时触发 - 构造函数
     * @param domList 要监听的DOM列表
     * @param duration 动画时长,默认500ms
     */
    constructor(domList: Element[], duration?: number) {
        this.flips = domList.map((k) => new Flip(k, duration))
    }
    /**记录全部初始位置 */
    recordFirst() {
        this.flips.forEach((flip) => flip.recordFirst())
    }
    /**播放全部动画 */
    play(callback?: () => any) {
        this.flips.forEach((flip) => flip.play(callback))
    }
}

然后在特定的地方插入代码,记录元素位置,做动画,插入了动画之后的代码,见下面的“完整代码”模块

三、完整代码

1.类型定义

// type.ts

import { CSSProperties, ReactNode } from "react"
/**有孩子的,基础的组件props,包含className style children */
interface baseChildrenProps {
    /**组件最外层的className */
    className?: string
    /**组件最外层的style */
    style?: CSSProperties
    /**孩子 */
    children?: ReactNode
}
/**ItemRender渲染函数的参数 */
type itemProps<T> = {
    /**当前元素 */
    item: T,
    /**当前索引 */
    index: number,
    /**父元素宽度 */
    width: number
    /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
    DragBox: (props: baseChildrenProps) => ReactNode
}
/**拖拽排序组件的props */
export interface DragSortProps<T> {
    /**组件最外层的className */
    className?: string
    /**组件最外层的style */
    style?: CSSProperties
    /**列表,拖拽后会改变里面的顺序 */
    list: T[]
    /**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
    keyName: keyof T
    /**一行个数,默认1 */
    cols?: number
    /**元素间距,单位px,默认0 (因为一行默认1) */
    marginX?: number
    /**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
    flipWithListChange?: boolean
    /**每个元素的渲染函数 */
    ItemRender: (props: itemProps<T>) => ReactNode
    /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
    afterDrag: (list: T[]) => any
} 

2. 部分不方便使用Tailwindcss的CSS

由于这段背景设置为tailwindcss过于麻烦,所以单独提取出来

/* index.module.css */


/*拖拽时,留在原地的元素*/
.background {
  background: linear-gradient(
    45deg,
    rgba(0, 0, 0, 0.3) 0,
    rgba(0, 0, 0, 0.3) 25%,
    transparent 25%,
    transparent 50%,
    rgba(0, 0, 0, 0.3) 50%,
    rgba(0, 0, 0, 0.3) 75%,
    transparent 75%,
    transparent
  );
  background-size: 20px 20px;
  border-radius: 5px;
}

3. 计算每个子元素宽度的Hook

一个响应式计算宽度的hook,可以用于列表的多列布局

// hooks/alculativeWidth.ts


import { RefObject, useEffect, useState } from "react";

/**根据父节点的ref和子元素的列数等数据,计算出子元素的宽度。用于响应式布局
 * @param fatherRef 父节点的ref
 * @param marginX 子元素的水平间距
 * @param cols 一行个数 (一行有几列)
 * @param callback 根据浏览器宽度自动计算大小后的回调函数,参数是计算好的子元素宽度
 * @returns 返回子元素宽度的响应式数据
 */
const useCalculativeWidth = (fatherRef: RefObject<HTMLDivElement>, marginX: number, cols: number, callback?: (nowWidth: number) => void) => {
    const [itemWidth, setItemWidth] = useState(200);
    useEffect(() => {
        /**计算单个子元素宽度,根据list的宽度计算 */
        const countWidth = () => {
            const width = fatherRef.current?.offsetWidth;
            if (width) {
                const _width = (width - marginX * (cols + 1)) / cols;
                setItemWidth(_width);
                callback && callback(_width)
            }
        };
        countWidth(); //先执行一次,后续再监听绑定
        window.addEventListener("resize", countWidth);
        return () => window.removeEventListener("resize", countWidth);
    }, [fatherRef, marginX, cols]);
    return itemWidth
}
export default useCalculativeWidth

4. Flip动画实现

// lib/common/util/animation.ts


/**位置的类型 */
interface position {
    x: number,
    y: number
}

/**Flip动画 */
export class Flip {
    /**dom元素 */
    private dom: Element
    /**原位置 */
    private firstPosition: position | null = null
    /**动画时间 */
    private duration: number
    /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    static movingClass = "__flipMoving__"
    constructor(dom: Element, duration = 500) {
        this.dom = dom
        this.duration = duration
    }
    /**获得元素的当前位置信息 */
    private getDomPosition(): position {
        const rect = this.dom.getBoundingClientRect()
        return {
            x: rect.left,
            y: rect.top
        }
    }
    /**给原始位置赋值 */
    recordFirst(firstPosition?: position) {
        if (!firstPosition) firstPosition = this.getDomPosition()
        this.firstPosition = { ...firstPosition }
    }
    /**播放动画 */
    play(callback?: () => any) {
        if (!this.firstPosition) {
            console.warn('请先记录原始位置');
            return
        }
        const lastPositon = this.getDomPosition()
        const dif: position = {
            x: lastPositon.x - this.firstPosition.x,
            y: lastPositon.y - this.firstPosition.y,
        }
        // console.log(this, dif);
        if (!dif.x && !dif.y) return
        this.dom.classList.add(Flip.movingClass)
        this.dom.animate([
            { transform: `translate(${-dif.x}px, ${-dif.y}px)` },
            { transform: `translate(0px, 0px)` }
        ], { duration: this.duration })
        setTimeout(() => {
            this.dom.classList.remove(Flip.movingClass)
            callback?.()
        }, this.duration);
    }
}
/**Flip多元素同时触发 */
export class FlipList {
    /**Flip列表 */
    private flips: Flip[]
    /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    static movingClass = Flip.movingClass
    /**Flip多元素同时触发 - 构造函数
     * @param domList 要监听的DOM列表
     * @param duration 动画时长,默认500ms
     */
    constructor(domList: Element[], duration?: number) {
        this.flips = domList.map((k) => new Flip(k, duration))
    }
    /**记录全部初始位置 */
    recordFirst() {
        this.flips.forEach((flip) => flip.recordFirst())
    }
    /**播放全部动画 */
    play(callback?: () => any) {
        this.flips.forEach((flip) => flip.play(callback))
    }
}

4. 一些工具函数

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

/**Tailwindcss的 合并css类名 函数
 * @param inputs 要合并的类名
 * @returns 
 */
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs))
}



/**查找符合条件的父节点
 * @param node 当前节点。如果当前节点就符合条件,就会返回当前节点
 * @param target 参数是当前找到的节点,返回一个布尔值,为true代表找到想要的父节点
 * @returns 没找到则返回null,找到了返回Element
 */
export function findParent(node: Element, target: (nowNode: Element) => boolean) {
    while (node && !target(node)) {
        if (node.parentElement) {
            node = node.parentElement;
        } else {
            return null;
        }
    }
    return node;
}

5. 完整组件代码

import { DragEventHandler, useEffect,  useRef, useState } from "react";
import { DragSortProps } from "./type";
import useCalculativeWidth from "@/hooks/calculativeWidth"; 
import { cn, findParent } from "@/lib/util"; 
import style from "./index.module.css";
import { DragIcon } from "../../UI/MyIcon"; //这个图标可以自己找喜欢的
import { FlipList } from "@/lib/common/util/animation";

/**拖拽时,留在原位置的元素的样式 */
const movingClass = [style.background]; //使用数组是为了方便以后添加其他类名
/**拖拽时,留在原位置的子元素的样式 */
const opacityClass = ["opacity-0"]; //使用数组是为了方便以后添加其他类名

/**拖拽排序组件 */
const DragSort = function <T>({
  list,
  ItemRender,
  afterDrag,
  keyName,
  cols = 1,
  marginX = 0,
  flipWithListChange = true,
  className,
  style,
}: DragSortProps<T>) {
  const listRef = useRef<HTMLDivElement>(null);
  /**记录当前正在拖拽哪个元素 */
  const nowDragItem = useRef<HTMLDivElement>();
  const itemWidth = useCalculativeWidth(listRef, marginX, cols);
  /**存储flipList动画实例 */
  const flipListRef = useRef<FlipList>();
  const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)

  /**创建记录新的动画记录,并立即记录当前位置 */
  const createNewFlipList = (exceptTarget?: Element) => {
    if (!listRef.current) return;
    //记录动画
    const listenChildren = [...listRef.current.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都动画
    flipListRef.current = new FlipList(listenChildren, 300);
    flipListRef.current.recordFirst();
  };

  //下面这两个是用于,当列表变化时,进行动画
  useEffect(() => {
    if (!flipWithListChange) return;
    createNewFlipList();
  }, [list]);
  useEffect(() => {
    if (!flipWithListChange) return;
    createNewFlipList();
    return () => {
      flipListRef.current?.play(() => flipListRef.current?.recordFirst());
    };
  }, [list.length]);

  /**事件委托- 监听 拖拽开始 事件,添加样式 */
  const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
    if (!listRef.current) return;
    e.stopPropagation(); //阻止冒泡

    /**这是当前正在被拖拽的元素 */
    const target = e.target as HTMLDivElement;

    //设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
    setTimeout(() => {
      target.classList.add(...movingClass); //设置正被拖动的元素样式
      target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
    }, 0);

    //记录元素的位置,用于Flip动画
    createNewFlipList(target);

    //记录当前拖拽的元素
    nowDragItem.current = target;

    //设置鼠标样式
    e.dataTransfer.effectAllowed = "move";
  };

  /**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
  const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
    e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
    if (!listRef.current || !nowDragItem.current) return;

    /**孩子数组,每次都会获取最新的 */
    const children = [...listRef.current.children];
    /**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
    const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);

    //边界判断
    if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
      // console.log("拖到自身或者拖到外面");
      return;
    }
    if (realTarget.className.includes(FlipList.movingClass)) {
      // console.log("这是正在动画的元素,跳过");
      return;
    }

    //拿到两个元素的索引,用来判断这俩元素应该怎么移动
    /**被拖拽元素在孩子数组中的索引 */
    const nowDragtItemIndex = children.indexOf(nowDragItem.current);
    /**被进入元素在孩子数组中的索引 */
    const enterItemIndex = children.indexOf(realTarget);

    //当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
    if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
      console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
      return;
    }

    //Flip动画 - 记录原始位置
    flipListRef.current?.recordFirst();

    if (nowDragtItemIndex < enterItemIndex) {
      // console.log("向下移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
    } else {
      // console.log("向上移动");
      listRef.current.insertBefore(nowDragItem.current, realTarget);
    }

    //Flip动画 - 播放
    flipListRef.current?.play();
  };

  /**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
  const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
    if (!listRef.current) return;
    /**当前正在被拖拽的元素 */
    const target = e.target as Element;

    target.classList.remove(...movingClass); //删除前面添加的 被拖拽元素的样式,回归原样式
    target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //删除所有子元素的透明样式

    /**拿到当前DOM的id顺序信息 */
    const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序

    //把列表按照id排序
    const newList = [...list].sort(function (a, b) {
      const aIndex = ids.indexOf(String(a[keyName]));
      const bIndex = ids.indexOf(String(b[keyName]));
      if (aIndex === -1 && bIndex === -1) return 0;
      else if (aIndex === -1) return 1;
      else if (bIndex === -1) return -1;
      else return aIndex - bIndex;
    });

    afterDrag(newList); //触发外界传入的回调函数

    setDragOpen(false); //拖拽完成后,再次禁止拖拽
  };

  /**拖拽按钮组件 */ //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽”
  const DragBox = ({ className, style, children }: baseChildrenProps) => {
    return (
      <div
        style={{ ...style }}
        className={cn("hover:cursor-grabbing", className)}
        onMouseEnter={() => setDragOpen(true)}
        onMouseLeave={() => setDragOpen(false)}
      >
        {children || <DragIcon size={20} color="#666666" />}
      </div>
    );
  };

  return (
    <div
      className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
      style={style}
      ref={listRef}
      onDragStart={onDragStart}
      onDragEnter={onDragEnter}
      onDragOver={(e) => e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上)
      onDragEnd={onDragEnd}
    >
      {list.map((item, index) => {
        const key = item[keyName] as string;
        return (
          <div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
            {ItemRender({ item, index, width: itemWidth, DragBox })}
          </div>
        );
      })}
    </div>
  );
};
export default DragSort;

6. 效果图的测试用例

一开始展示的效果图的实现代码

"use client";
import { useState } from "react";
import DragSort from "@/components/base/tool/DragSort";
import { Button, InputNumber } from "antd";
export default function page() {
  interface item {
    id: number;
  }
  const [list, setList] = useState<item[]>([]); //当前列表
  const [cols, setCols] = useState(1); //一行个数
  /**创建一个新的元素 */
  const createNewItem = () => {
    setList((old) =>
      old.concat([
        {
          id: Date.now(),
        },
      ])
    );
  };
  return (
    <div className="p-2 bg-[#a18c83] w-screen h-screen overflow-auto">
      <Button type="primary" onClick={createNewItem}>
        点我添加
      </Button>
      一行个数: <InputNumber value={cols} min={1} onChange={(v) => setCols(v!)} />
      <DragSort
        list={list}
        keyName={"id"}
        cols={cols}
        marginX={10}
        afterDrag={(list) => setList(list)}
        ItemRender={({ item, index, DragBox }) => {
          return (
            <div className="flex items-center border rounded-sm p-2 gap-1 bg-white">
              <DragBox />
              <div>序号:{index},</div>
              <div>ID:{item.id}</div>
              {/* <DragBox className="bg-stone-400 text-white p-1">自定义拖拽位置</DragBox> */}
            </div>
          );
        }}
      />
    </div>
  );
}

四、结语

        哪里做的不好、有bug等,欢迎指出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1173637.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu22.04下挂载共享文件夹

1.在自己Windows任意地方建一个文件夹 2.打开虚拟机做如下配置 3.开启虚拟机&#xff0c;打开终端 4.输入&#xff1a;vmware-hgfsclient 看到物理机共享文件夹 5.输入&#xff1a;sudo mkdir /mnt/hgfs 创建虚拟机中的共享文件夹 6.输入&#xff1a;sudo vmhgfs-fuse .h…

Android---App 的安装过程

Android 系统中两个比较重要的服务 ActivityManagerService(AMS) 和 WindowManagerService(WMS)&#xff0c;这篇文章中通过分析 apk 的安装过程&#xff0c;来了解 Android 中另一个比较重要的系统服务 -- PackageManagerService(PMS)。 编译阶段 在分析安装过程之前&#x…

项目实战:service业务逻辑组件引入

1、第一层DAO层 1.1、FruitDao接口 package com.csdn.fruit.dao; import com.csdn.fruit.pojo.Fruit; import java.util.List; //dao &#xff1a;Data Access Object 数据访问对象 //接口设计 public interface FruitDao {void addFruit(Fruit fruit);void delFruit(String fn…

Java--类和对象

目录 面向对象一.类1.类的创建默认初始化2.类的实例化3.注意事项利用类的创建来交换值 二.this1.使用this2.可使用this来调用其他构造方法来简化 三.构造方法3.1概念3.2特性3.3不带参数的构造方法3.4带参数的构造方法当使用自定义的构造方法后&#xff0c;再删除时&#xff0c;…

【Linux系统编程】系统用户和权限的操作

目录 一&#xff0c;Linux的用户 1&#xff0c;用户之间的切换 2&#xff0c;超级用户权限的使用 二&#xff0c;Linux的文件权限 1&#xff0c;文件信息的介绍 2&#xff0c;文件权限的修改 3&#xff0c;用户的修改 3-1&#xff0c;拥有者的更改 3-2&#xff0c;所属…

chinese-stable-diffusion中文场景文生图prompt测评集合

腾讯混元大模型文生图操作指南.dochttps://mp.weixin.qq.com/s/u0AGtpwm_LmgnDY7OQhKGg腾讯混元大模型再进化&#xff0c;文生图能力重磅上线&#xff0c;这里是一手实测腾讯混元的文生图在人像真实感、场景真实感上有比较明显的优势&#xff0c;同时&#xff0c;在中国风景、动…

“一键批量拆分HTML文本,高效整理文件,提升工作效率“

您是否曾经被大量的HTML文本文件困扰&#xff0c;难以找到所需的特定信息&#xff1f;现在&#xff0c;我们向您推荐一款强大的工具&#xff0c;它能够一键拆分HTML文本&#xff0c;让您轻松实现文件整理&#xff0c;提高工作效率&#xff01; 首先&#xff0c;在首助编辑高手…

大航海时代Ⅳ 威力加强版套装 HD Version (WinMac)中文免安装版

《大航海时代》系列的人气SRPG《大航海时代IV》以HD的新面貌再次登场&#xff01;本作品以16世纪的欧洲“大航海时代”为舞台&#xff0c;玩家将以探险家、商人、军人等不同身份与全世界形形色色的人们一起上演出跌宕起伏的海洋冒险。游戏中玩家的目的是在不同的海域中掌握霸权…

使用javafx,结合讯飞ai,搞了个ai聊天系统

第一步&#xff1a;先在讯飞ai那边获取接入的api 点进去&#xff0c;然后出现这个页面&#xff1a; 没有的话&#xff0c;就点击免费试用&#xff0c;有了的话&#xff0c;就点击服务管理&#xff1a; 用v2.0的和用3的都行&#xff0c;不过我推荐用2.0版本 文档位置&#xff1…

1825_ChibiOS的OSLIB中的存储分配器

全部学习汇总&#xff1a; GreyZhang/g_ChibiOS: I found a new RTOS called ChibiOS and it seems interesting! (github.com) 1. 之前有点不是很理解什么是静态OS&#xff0c;从这里基本上得到了这个答案。所谓的静态&#xff0c;其实就是内核之中不会使用Free以及Malloc这样…

ORAM文献笔记

Deterministic, stash-free write-only oram Roche D S, Aviv A, Choi S G, et al. Deterministic, stash-free write-only oram[C]//Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security. ACM, 2017: 507-521. 确定性的、无堆栈的只写O…

【黑马程序员】SSM框架——SpringBoot

文章目录 前言一、SpringBoot 简介1. 入门案例1.1 入门程序① 创建新模块② 选择当前模块需要使用的技术集③ 开发控制类④ 运行自动生成的 Application 类 1.2 创建 SpringBoot 程序的两种方式1.2.1 最简 SpringBoot 程序所包含的基础文件1.2.2 基于 SpringBoot 官网创建项目 …

解析SD-WAN组网方式及应用场景,全面了解典型案例

随着企业业务高速发展&#xff0c;跨区域开展业务首要解决的难题是构建各站点能互联互通的网络&#xff0c;然而目前大多数企业在广域网优化的问题上依旧碰壁&#xff0c;主要原因是企业广域网面临的挑战并不能马上得到解决。 传统网络互联方案无论是IPsec还是专线&#xff0c…

单目结构光三维重建最终公式推导

详细推导&#xff08;建议自己推导一遍&#xff09; 参考&#xff1a;https://blog.csdn.net/u010430651/article/details/104868734?spm1001.2014.3001.5502

【漏洞复现】Apache_Shiro_1.2.4_反序列化漏洞(CVE-2016-4437)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞分析3、漏洞验证 说明内容漏洞编号CVE-2016-4437漏洞名称Apache_Shiro_1.2.4_反序列化漏洞漏洞评级…

【Spring】bean的配置

文章目录 1. 前言2. name3. lazy-init4. init-method5. destroy-method6. factory-method和factory-bean 1. 前言 在之前的文章中.写到过bean的常用配置,当时只是介绍了bean标签中的常用属性配置:class,id和scope这三个属性. 不熟的小伙伴可以看一下这篇文章:【Spring】IOC容器…

类和对象 下

构造函数 初始化列表 构造函数是在构造类的时候给对象赋初值&#xff0c;并不是给类的成员函数初始化&#xff0c;赋值可以赋多次&#xff0c;而初始化只能初始化一次&#xff0c;这里我们引入初始化列表来对成员函数进行初始化 初始化列表&#xff0c;以冒号 &#xff1a;开…

【JVM系列】- 挖掘·JVM堆内存结构

挖掘JVM堆内存结构 文章目录 挖掘JVM堆内存结构堆的核心概念堆的特点 堆的内存结构内存划分新生代/新生区&#xff08;Young Generation&#xff09;老年代&#xff08;Tenured Generation&#xff09;永久代&#xff08;或元数据区&#xff09;&#xff08;PermGen 或 MetaSpa…

学习视频剪辑:巧妙运用中画、底画,制作画中画,提升视频效果

随着数字媒体的普及&#xff0c;视频剪辑已经成为一项重要的技能。在视频剪辑过程中&#xff0c;制作画中画可以显著提升视频效果、信息传达和吸引力。本文讲解云炫AI智剪如何巧妙运用中画、底画批量制作画中画来提升视频剪辑水平&#xff0c;提高剪辑效率。 操作1、先执行云…

两种MySQL OCP认证应该如何选?

很多同学都找姚远老师说要参加MySQL OCP认证培训&#xff0c;但绝大部分同学并不知道MySQL OCP认证有两种&#xff0c;以MySQL 8.0为例。 一种是管理方向&#xff0c;叫&#xff1a;Oracle Certified Professional, MySQL 8.0 Database Administrator&#xff08;我考试的比较…