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


    一、效果展示

    排序:

    丝滑的Flip动画 

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

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

    二、主要思路

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

    1. 一些ts类型:

    1. import { CSSProperties, MutableRefObject, ReactNode } from "react"
    2. /**有孩子的,基础的组件props,包含className style children */
    3. interface baseChildrenProps {
    4. /**组件最外层的className */
    5. className?: string
    6. /**组件最外层的style */
    7. style?: CSSProperties
    8. /**孩子 */
    9. children?: ReactNode
    10. }
    11. /**ItemRender渲染函数的参数 */
    12. type itemProps = {
    13. /**当前元素 */
    14. item: T,
    15. /**当前索引 */
    16. index: number,
    17. /**父元素宽度 */
    18. width: number
    19. /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
    20. DragBox: (props: baseChildrenProps) => ReactNode
    21. }
    22. /**拖拽排序组件的props */
    23. export interface DragSortProps {
    24. /**组件最外层的className */
    25. className?: string
    26. /**组件最外层的style */
    27. style?: CSSProperties
    28. /**列表,拖拽后会改变里面的顺序 */
    29. list: T[]
    30. /**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
    31. keyName: keyof T
    32. /**一行个数,默认1 */
    33. cols?: number
    34. /**元素间距,单位px,默认0 (因为一行默认1) */
    35. marginX?: number
    36. /**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
    37. flipWithListChange?: boolean
    38. /**每个元素的渲染函数 */
    39. ItemRender: (props: itemProps) => ReactNode
    40. /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
    41. afterDrag: (list: T[]) => any
    42. }

    2. 使用事件委托

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

    1. /**拖拽排序组件 */
    2. const DragSort = function ({
    3. list,
    4. ItemRender,
    5. afterDrag,
    6. keyName,
    7. cols = 1,
    8. marginX = 0,
    9. flipWithListChange = true,
    10. className,
    11. style,
    12. }: DragSortProps) {
    13. const listRef = useRef<HTMLDivElement>(null);
    14. /**记录当前正在拖拽哪个元素 */
    15. const nowDragItem = useRef<HTMLDivElement>();
    16. const itemWidth = useCalculativeWidth(listRef, marginX, cols);//使用计算宽度钩子,计算每个元素的宽度 (代码后面会有)
    17. const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)
    18. /**事件委托- 监听 拖拽开始 事件,添加样式 */
    19. const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
    20. if (!listRef.current) return;
    21. e.stopPropagation(); //阻止冒泡
    22. /**这是当前正在被拖拽的元素 */
    23. const target = e.target as HTMLDivElement;
    24. //设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
    25. setTimeout(() => {
    26. target.classList.add(...movingClass); //设置正被拖动的元素样式
    27. target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
    28. }, 0);
    29. //记录当前拖拽的元素
    30. nowDragItem.current = target;
    31. //设置鼠标样式
    32. e.dataTransfer.effectAllowed = "move";
    33. };
    34. /**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
    35. const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
    36. e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
    37. if (!listRef.current || !nowDragItem.current) return;
    38. /**孩子数组,每次都会获取最新的 */
    39. const children = [...listRef.current.children];
    40. /**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
    41. const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);
    42. //边界判断
    43. if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
    44. // console.log("拖到自身或者拖到外面");
    45. return;
    46. }
    47. //拿到两个元素的索引,用来判断这俩元素应该怎么移动
    48. /**被拖拽元素在孩子数组中的索引 */
    49. const nowDragtItemIndex = children.indexOf(nowDragItem.current);
    50. /**被进入元素在孩子数组中的索引 */
    51. const enterItemIndex = children.indexOf(realTarget);
    52. //当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
    53. if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
    54. console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
    55. return;
    56. }
    57. if (nowDragtItemIndex < enterItemIndex) {
    58. // console.log("向下移动");
    59. listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
    60. } else {
    61. // console.log("向上移动");
    62. listRef.current.insertBefore(nowDragItem.current, realTarget);
    63. }
    64. };
    65. /**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
    66. const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
    67. if (!listRef.current) return;
    68. /**当前正在被拖拽的元素 */
    69. const target = e.target as Element;
    70. target.classList.remove(...movingClass);//删除前面添加的 被拖拽元素的样式,回归原样式
    71. target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass));//删除所有子元素的透明样式
    72. /**拿到当前DOM的id顺序信息 */
    73. const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序
    74. //把列表按照id排序
    75. const newList = [...list].sort(function (a, b) {
    76. const aIndex = ids.indexOf(String(a[keyName]));
    77. const bIndex = ids.indexOf(String(b[keyName]));
    78. if (aIndex === -1 && bIndex === -1) return 0;
    79. else if (aIndex === -1) return 1;
    80. else if (bIndex === -1) return -1;
    81. else return aIndex - bIndex;
    82. });
    83. afterDrag(newList);//触发外界传入的回调函数
    84. setDragOpen(false);//拖拽完成后,再次禁止拖拽
    85. };
    86. /**拖拽按钮组件 */ //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽”
    87. const DragBox = ({ className, style, children }: baseChildrenProps) => {
    88. return (
    89. <div
    90. style={{ ...style }}
    91. className={cn("hover:cursor-grabbing", className)}
    92. onMouseEnter={() => setDragOpen(true)}
    93. onMouseLeave={() => setDragOpen(false)}
    94. >
    95. {children || <DragIcon size={20} color="#666666" />}
    96. div>
    97. );
    98. };
    99. return (
    100. <div
    101. className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
    102. style={style}
    103. ref={listRef}
    104. onDragStart={onDragStart}
    105. onDragEnter={onDragEnter}
    106. onDragOver={(e) => e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上)
    107. onDragEnd={onDragEnd}
    108. >
    109. {list.map((item, index) => {
    110. const key = item[keyName] as string;
    111. return (
    112. <div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
    113. {ItemRender({ item, index, width: itemWidth, DragBox })}
    114. div>
    115. );
    116. })}
    117. div>
    118. );
    119. };

    3. 使用Flip做动画

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

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

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

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

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

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

    1. /**位置的类型 */
    2. interface position {
    3. x: number,
    4. y: number
    5. }
    6. /**Flip动画 */
    7. export class Flip {
    8. /**dom元素 */
    9. private dom: Element
    10. /**原位置 */
    11. private firstPosition: position | null = null
    12. /**动画时间 */
    13. private duration: number
    14. /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    15. static movingClass = "__flipMoving__"
    16. constructor(dom: Element, duration = 500) {
    17. this.dom = dom
    18. this.duration = duration
    19. }
    20. /**获得元素的当前位置信息 */
    21. private getDomPosition(): position {
    22. const rect = this.dom.getBoundingClientRect()
    23. return {
    24. x: rect.left,
    25. y: rect.top
    26. }
    27. }
    28. /**给原始位置赋值 */
    29. recordFirst(firstPosition?: position) {
    30. if (!firstPosition) firstPosition = this.getDomPosition()
    31. this.firstPosition = { ...firstPosition }
    32. }
    33. /**播放动画 */
    34. play(callback?: () => any) {
    35. if (!this.firstPosition) {
    36. console.warn('请先记录原始位置');
    37. return
    38. }
    39. const lastPositon = this.getDomPosition()
    40. const dif: position = {
    41. x: lastPositon.x - this.firstPosition.x,
    42. y: lastPositon.y - this.firstPosition.y,
    43. }
    44. // console.log(this, dif);
    45. if (!dif.x && !dif.y) return
    46. this.dom.classList.add(Flip.movingClass)
    47. this.dom.animate([
    48. { transform: `translate(${-dif.x}px, ${-dif.y}px)` },
    49. { transform: `translate(0px, 0px)` }
    50. ], { duration: this.duration })
    51. setTimeout(() => {
    52. this.dom.classList.remove(Flip.movingClass)
    53. callback?.()
    54. }, this.duration);
    55. }
    56. }
    57. /**Flip多元素同时触发 */
    58. export class FlipList {
    59. /**Flip列表 */
    60. private flips: Flip[]
    61. /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    62. static movingClass = Flip.movingClass
    63. /**Flip多元素同时触发 - 构造函数
    64. * @param domList 要监听的DOM列表
    65. * @param duration 动画时长,默认500ms
    66. */
    67. constructor(domList: Element[], duration?: number) {
    68. this.flips = domList.map((k) => new Flip(k, duration))
    69. }
    70. /**记录全部初始位置 */
    71. recordFirst() {
    72. this.flips.forEach((flip) => flip.recordFirst())
    73. }
    74. /**播放全部动画 */
    75. play(callback?: () => any) {
    76. this.flips.forEach((flip) => flip.play(callback))
    77. }
    78. }

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

    三、完整代码

    1.类型定义

    1. // type.ts
    2. import { CSSProperties, ReactNode } from "react"
    3. /**有孩子的,基础的组件props,包含className style children */
    4. interface baseChildrenProps {
    5. /**组件最外层的className */
    6. className?: string
    7. /**组件最外层的style */
    8. style?: CSSProperties
    9. /**孩子 */
    10. children?: ReactNode
    11. }
    12. /**ItemRender渲染函数的参数 */
    13. type itemProps = {
    14. /**当前元素 */
    15. item: T,
    16. /**当前索引 */
    17. index: number,
    18. /**父元素宽度 */
    19. width: number
    20. /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
    21. DragBox: (props: baseChildrenProps) => ReactNode
    22. }
    23. /**拖拽排序组件的props */
    24. export interface DragSortProps {
    25. /**组件最外层的className */
    26. className?: string
    27. /**组件最外层的style */
    28. style?: CSSProperties
    29. /**列表,拖拽后会改变里面的顺序 */
    30. list: T[]
    31. /**用作唯一key,在list的元素中的属性名,比如id。必须传递 */
    32. keyName: keyof T
    33. /**一行个数,默认1 */
    34. cols?: number
    35. /**元素间距,单位px,默认0 (因为一行默认1) */
    36. marginX?: number
    37. /**当列表长度变化时,是否需要Flip动画,默认开启 (可能有点略微的动画bug) */
    38. flipWithListChange?: boolean
    39. /**每个元素的渲染函数 */
    40. ItemRender: (props: itemProps) => ReactNode
    41. /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
    42. afterDrag: (list: T[]) => any
    43. }

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

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

    1. /* index.module.css */
    2. /*拖拽时,留在原地的元素*/
    3. .background {
    4. background: linear-gradient(
    5. 45deg,
    6. rgba(0, 0, 0, 0.3) 0,
    7. rgba(0, 0, 0, 0.3) 25%,
    8. transparent 25%,
    9. transparent 50%,
    10. rgba(0, 0, 0, 0.3) 50%,
    11. rgba(0, 0, 0, 0.3) 75%,
    12. transparent 75%,
    13. transparent
    14. );
    15. background-size: 20px 20px;
    16. border-radius: 5px;
    17. }

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

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

    1. // hooks/alculativeWidth.ts
    2. import { RefObject, useEffect, useState } from "react";
    3. /**根据父节点的ref和子元素的列数等数据,计算出子元素的宽度。用于响应式布局
    4. * @param fatherRef 父节点的ref
    5. * @param marginX 子元素的水平间距
    6. * @param cols 一行个数 (一行有几列)
    7. * @param callback 根据浏览器宽度自动计算大小后的回调函数,参数是计算好的子元素宽度
    8. * @returns 返回子元素宽度的响应式数据
    9. */
    10. const useCalculativeWidth = (fatherRef: RefObject, marginX: number, cols: number, callback?: (nowWidth: number) => void) => {
    11. const [itemWidth, setItemWidth] = useState(200);
    12. useEffect(() => {
    13. /**计算单个子元素宽度,根据list的宽度计算 */
    14. const countWidth = () => {
    15. const width = fatherRef.current?.offsetWidth;
    16. if (width) {
    17. const _width = (width - marginX * (cols + 1)) / cols;
    18. setItemWidth(_width);
    19. callback && callback(_width)
    20. }
    21. };
    22. countWidth(); //先执行一次,后续再监听绑定
    23. window.addEventListener("resize", countWidth);
    24. return () => window.removeEventListener("resize", countWidth);
    25. }, [fatherRef, marginX, cols]);
    26. return itemWidth
    27. }
    28. export default useCalculativeWidth

    4. Flip动画实现

    1. // lib/common/util/animation.ts
    2. /**位置的类型 */
    3. interface position {
    4. x: number,
    5. y: number
    6. }
    7. /**Flip动画 */
    8. export class Flip {
    9. /**dom元素 */
    10. private dom: Element
    11. /**原位置 */
    12. private firstPosition: position | null = null
    13. /**动画时间 */
    14. private duration: number
    15. /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    16. static movingClass = "__flipMoving__"
    17. constructor(dom: Element, duration = 500) {
    18. this.dom = dom
    19. this.duration = duration
    20. }
    21. /**获得元素的当前位置信息 */
    22. private getDomPosition(): position {
    23. const rect = this.dom.getBoundingClientRect()
    24. return {
    25. x: rect.left,
    26. y: rect.top
    27. }
    28. }
    29. /**给原始位置赋值 */
    30. recordFirst(firstPosition?: position) {
    31. if (!firstPosition) firstPosition = this.getDomPosition()
    32. this.firstPosition = { ...firstPosition }
    33. }
    34. /**播放动画 */
    35. play(callback?: () => any) {
    36. if (!this.firstPosition) {
    37. console.warn('请先记录原始位置');
    38. return
    39. }
    40. const lastPositon = this.getDomPosition()
    41. const dif: position = {
    42. x: lastPositon.x - this.firstPosition.x,
    43. y: lastPositon.y - this.firstPosition.y,
    44. }
    45. // console.log(this, dif);
    46. if (!dif.x && !dif.y) return
    47. this.dom.classList.add(Flip.movingClass)
    48. this.dom.animate([
    49. { transform: `translate(${-dif.x}px, ${-dif.y}px)` },
    50. { transform: `translate(0px, 0px)` }
    51. ], { duration: this.duration })
    52. setTimeout(() => {
    53. this.dom.classList.remove(Flip.movingClass)
    54. callback?.()
    55. }, this.duration);
    56. }
    57. }
    58. /**Flip多元素同时触发 */
    59. export class FlipList {
    60. /**Flip列表 */
    61. private flips: Flip[]
    62. /**正在移动的动画会有一个专属的class类名,可以用于标识 */
    63. static movingClass = Flip.movingClass
    64. /**Flip多元素同时触发 - 构造函数
    65. * @param domList 要监听的DOM列表
    66. * @param duration 动画时长,默认500ms
    67. */
    68. constructor(domList: Element[], duration?: number) {
    69. this.flips = domList.map((k) => new Flip(k, duration))
    70. }
    71. /**记录全部初始位置 */
    72. recordFirst() {
    73. this.flips.forEach((flip) => flip.recordFirst())
    74. }
    75. /**播放全部动画 */
    76. play(callback?: () => any) {
    77. this.flips.forEach((flip) => flip.play(callback))
    78. }
    79. }

    4. 一些工具函数

    1. import { type ClassValue, clsx } from "clsx"
    2. import { twMerge } from "tailwind-merge"
    3. /**Tailwindcss的 合并css类名 函数
    4. * @param inputs 要合并的类名
    5. * @returns
    6. */
    7. export function cn(...inputs: ClassValue[]) {
    8. return twMerge(clsx(inputs))
    9. }
    10. /**查找符合条件的父节点
    11. * @param node 当前节点。如果当前节点就符合条件,就会返回当前节点
    12. * @param target 参数是当前找到的节点,返回一个布尔值,为true代表找到想要的父节点
    13. * @returns 没找到则返回null,找到了返回Element
    14. */
    15. export function findParent(node: Element, target: (nowNode: Element) => boolean) {
    16. while (node && !target(node)) {
    17. if (node.parentElement) {
    18. node = node.parentElement;
    19. } else {
    20. return null;
    21. }
    22. }
    23. return node;
    24. }

    5. 完整组件代码

    1. import { DragEventHandler, useEffect, useRef, useState } from "react";
    2. import { DragSortProps } from "./type";
    3. import useCalculativeWidth from "@/hooks/calculativeWidth";
    4. import { cn, findParent } from "@/lib/util";
    5. import style from "./index.module.css";
    6. import { DragIcon } from "../../UI/MyIcon"; //这个图标可以自己找喜欢的
    7. import { FlipList } from "@/lib/common/util/animation";
    8. /**拖拽时,留在原位置的元素的样式 */
    9. const movingClass = [style.background]; //使用数组是为了方便以后添加其他类名
    10. /**拖拽时,留在原位置的子元素的样式 */
    11. const opacityClass = ["opacity-0"]; //使用数组是为了方便以后添加其他类名
    12. /**拖拽排序组件 */
    13. const DragSort = function ({
    14. list,
    15. ItemRender,
    16. afterDrag,
    17. keyName,
    18. cols = 1,
    19. marginX = 0,
    20. flipWithListChange = true,
    21. className,
    22. style,
    23. }: DragSortProps) {
    24. const listRef = useRef<HTMLDivElement>(null);
    25. /**记录当前正在拖拽哪个元素 */
    26. const nowDragItem = useRef<HTMLDivElement>();
    27. const itemWidth = useCalculativeWidth(listRef, marginX, cols);
    28. /**存储flipList动画实例 */
    29. const flipListRef = useRef<FlipList>();
    30. const [dragOpen, setDragOpen] = useState(false); //是否开启拖拽 (鼠标进入指定区域开启)
    31. /**创建记录新的动画记录,并立即记录当前位置 */
    32. const createNewFlipList = (exceptTarget?: Element) => {
    33. if (!listRef.current) return;
    34. //记录动画
    35. const listenChildren = [...listRef.current.children].filter((k) => k !== exceptTarget); //除了指定元素,其它的都动画
    36. flipListRef.current = new FlipList(listenChildren, 300);
    37. flipListRef.current.recordFirst();
    38. };
    39. //下面这两个是用于,当列表变化时,进行动画
    40. useEffect(() => {
    41. if (!flipWithListChange) return;
    42. createNewFlipList();
    43. }, [list]);
    44. useEffect(() => {
    45. if (!flipWithListChange) return;
    46. createNewFlipList();
    47. return () => {
    48. flipListRef.current?.play(() => flipListRef.current?.recordFirst());
    49. };
    50. }, [list.length]);
    51. /**事件委托- 监听 拖拽开始 事件,添加样式 */
    52. const onDragStart: DragEventHandler<HTMLDivElement> = (e) => {
    53. if (!listRef.current) return;
    54. e.stopPropagation(); //阻止冒泡
    55. /**这是当前正在被拖拽的元素 */
    56. const target = e.target as HTMLDivElement;
    57. //设置被拖拽元素“留在原地”的样式。为了防止设置正在拖拽的元素样式,所以用定时器,宏任务更晚执行
    58. setTimeout(() => {
    59. target.classList.add(...movingClass); //设置正被拖动的元素样式
    60. target.childNodes.forEach((k) => (k as HTMLDivElement).classList?.add(...opacityClass)); //把子元素都设置为透明,避免影响
    61. }, 0);
    62. //记录元素的位置,用于Flip动画
    63. createNewFlipList(target);
    64. //记录当前拖拽的元素
    65. nowDragItem.current = target;
    66. //设置鼠标样式
    67. e.dataTransfer.effectAllowed = "move";
    68. };
    69. /**事件委托- 监听 拖拽进入某个元素 事件,在这里只是DOM变化,数据顺序没有变化 */
    70. const onDragEnter: DragEventHandler<HTMLDivElement> = (e) => {
    71. e.preventDefault(); //阻止默认行为,默认是不允许元素拖动到人家身上的
    72. if (!listRef.current || !nowDragItem.current) return;
    73. /**孩子数组,每次都会获取最新的 */
    74. const children = [...listRef.current.children];
    75. /**真正会被挪动的元素(当前正悬浮在哪个元素上面) */ //找到符合条件的父节点
    76. const realTarget = findParent(e.target as Element, (now) => children.indexOf(now) !== -1);
    77. //边界判断
    78. if (realTarget === listRef.current || realTarget === nowDragItem.current || !realTarget) {
    79. // console.log("拖到自身或者拖到外面");
    80. return;
    81. }
    82. if (realTarget.className.includes(FlipList.movingClass)) {
    83. // console.log("这是正在动画的元素,跳过");
    84. return;
    85. }
    86. //拿到两个元素的索引,用来判断这俩元素应该怎么移动
    87. /**被拖拽元素在孩子数组中的索引 */
    88. const nowDragtItemIndex = children.indexOf(nowDragItem.current);
    89. /**被进入元素在孩子数组中的索引 */
    90. const enterItemIndex = children.indexOf(realTarget);
    91. //当用户选中文字,然后去拖动这个文字时,就会触发 (可以通过禁止选中文字来避免)
    92. if (enterItemIndex === -1 || nowDragtItemIndex === -1) {
    93. console.log("若第二个数为-1,说明拖动的不是元素,而是“文字”", enterItemIndex, nowDragtItemIndex);
    94. return;
    95. }
    96. //Flip动画 - 记录原始位置
    97. flipListRef.current?.recordFirst();
    98. if (nowDragtItemIndex < enterItemIndex) {
    99. // console.log("向下移动");
    100. listRef.current.insertBefore(nowDragItem.current, realTarget.nextElementSibling);
    101. } else {
    102. // console.log("向上移动");
    103. listRef.current.insertBefore(nowDragItem.current, realTarget);
    104. }
    105. //Flip动画 - 播放
    106. flipListRef.current?.play();
    107. };
    108. /**事件委托- 监听 拖拽结束 事件,删除样式,设置当前列表 */
    109. const onDragEnd: DragEventHandler<HTMLDivElement> = (e) => {
    110. if (!listRef.current) return;
    111. /**当前正在被拖拽的元素 */
    112. const target = e.target as Element;
    113. target.classList.remove(...movingClass); //删除前面添加的 被拖拽元素的样式,回归原样式
    114. target.childNodes.forEach((k) => (k as Element).classList?.remove(...opacityClass)); //删除所有子元素的透明样式
    115. /**拿到当前DOM的id顺序信息 */
    116. const ids = [...listRef.current.children].map((k) => String(k.id)); //根据id,判断到时候应该怎么排序
    117. //把列表按照id排序
    118. const newList = [...list].sort(function (a, b) {
    119. const aIndex = ids.indexOf(String(a[keyName]));
    120. const bIndex = ids.indexOf(String(b[keyName]));
    121. if (aIndex === -1 && bIndex === -1) return 0;
    122. else if (aIndex === -1) return 1;
    123. else if (bIndex === -1) return -1;
    124. else return aIndex - bIndex;
    125. });
    126. afterDrag(newList); //触发外界传入的回调函数
    127. setDragOpen(false); //拖拽完成后,再次禁止拖拽
    128. };
    129. /**拖拽按钮组件 */ //只有鼠标悬浮在这上面的时候,才开启拖拽,做到“指定区域拖拽”
    130. const DragBox = ({ className, style, children }: baseChildrenProps) => {
    131. return (
    132. <div
    133. style={{ ...style }}
    134. className={cn("hover:cursor-grabbing", className)}
    135. onMouseEnter={() => setDragOpen(true)}
    136. onMouseLeave={() => setDragOpen(false)}
    137. >
    138. {children || <DragIcon size={20} color="#666666" />}
    139. div>
    140. );
    141. };
    142. return (
    143. <div
    144. className={cn(cols === 1 ? "" : "flex flex-wrap", className)}
    145. style={style}
    146. ref={listRef}
    147. onDragStart={onDragStart}
    148. onDragEnter={onDragEnter}
    149. onDragOver={(e) => e.preventDefault()} //被拖动的对象被拖到其它容器时(因为默认不能拖到其它元素上)
    150. onDragEnd={onDragEnd}
    151. >
    152. {list.map((item, index) => {
    153. const key = item[keyName] as string;
    154. return (
    155. <div id={key} key={key} style={{ width: itemWidth, margin: `4px ${marginX / 2}px` }} draggable={dragOpen} className="my-1">
    156. {ItemRender({ item, index, width: itemWidth, DragBox })}
    157. div>
    158. );
    159. })}
    160. div>
    161. );
    162. };
    163. export default DragSort;

    6. 效果图的测试用例

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

    1. "use client";
    2. import { useState } from "react";
    3. import DragSort from "@/components/base/tool/DragSort";
    4. import { Button, InputNumber } from "antd";
    5. export default function page() {
    6. interface item {
    7. id: number;
    8. }
    9. const [list, setList] = useState([]); //当前列表
    10. const [cols, setCols] = useState(1); //一行个数
    11. /**创建一个新的元素 */
    12. const createNewItem = () => {
    13. setList((old) =>
    14. old.concat([
    15. {
    16. id: Date.now(),
    17. },
    18. ])
    19. );
    20. };
    21. return (
    22. <div className="p-2 bg-[#a18c83] w-screen h-screen overflow-auto">
    23. <Button type="primary" onClick={createNewItem}>
    24. 点我添加
    25. Button>
    26. 一行个数: <InputNumber value={cols} min={1} onChange={(v) => setCols(v!)} />
    27. <DragSort
    28. list={list}
    29. keyName={"id"}
    30. cols={cols}
    31. marginX={10}
    32. afterDrag={(list) => setList(list)}
    33. ItemRender={({ item, index, DragBox }) => {
    34. return (
    35. <div className="flex items-center border rounded-sm p-2 gap-1 bg-white">
    36. <DragBox />
    37. <div>序号:{index},div>
    38. <div>ID:{item.id}div>
    39. {/* <DragBox className="bg-stone-400 text-white p-1">自定义拖拽位置DragBox> */}
    40. div>
    41. );
    42. }}
    43. />
    44. div>
    45. );
    46. }

    四、结语

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

  • 相关阅读:
    深入理解HTTP的基础知识:请求-响应过程解析
    【JAVA面试】JAVA面试指南
    分布式数据库Schema 变更 in F1 & TiDB
    速卖通、阿里国际如何提升店铺流量?如何安全测评?
    MySQL中 any,some,all 的用法
    牛客算法题:B-装进肚子
    inux安装软件命令yum,apt-get
    一段JS去除畅言免费版广告
    Python数据结构(顺序表)
    uni-app 5小时快速入门 12 uni-app生命周期(下)
  • 原文地址:https://blog.csdn.net/m0_64130892/article/details/134230140