排序:

丝滑的Flip动画

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

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

Tip: 本代码的CSS使用Tailwindcss, 如果没安装的可以自行安装这个库,也可以去问GPT,让它帮忙改成普通的CSS版本的代码
- import { CSSProperties, MutableRefObject, ReactNode } from "react"
- /**有孩子的,基础的组件props,包含className style children */
- interface baseChildrenProps {
- /**组件最外层的className */
- className?: string
- /**组件最外层的style */
- style?: CSSProperties
- /**孩子 */
- children?: ReactNode
- }
- /**ItemRender渲染函数的参数 */
- type itemProps
= { - /**当前元素 */
- item: T,
- /**当前索引 */
- index: number,
- /**父元素宽度 */
- width: number
- /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
- DragBox: (props: baseChildrenProps) => ReactNode
- }
- /**拖拽排序组件的props */
- export interface DragSortProps
{ - /**组件最外层的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
) => ReactNode - /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
- afterDrag: (list: T[]) => any
- }
监听所有子元素的拖拽开始、拖拽中、拖拽结束事件,减少绑定事件数量的同时,还能优化代码。
- /**拖拽排序组件 */
- const DragSort = function
({ - list,
- ItemRender,
- afterDrag,
- keyName,
- cols = 1,
- marginX = 0,
- flipWithListChange = true,
- className,
- style,
- }: DragSortProps
) { - 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>
- );
- };
对于这种移动位置的动画,普通的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))
- }
- }
然后在特定的地方插入代码,记录元素位置,做动画,插入了动画之后的代码,见下面的“完整代码”模块
- // type.ts
-
- import { CSSProperties, ReactNode } from "react"
- /**有孩子的,基础的组件props,包含className style children */
- interface baseChildrenProps {
- /**组件最外层的className */
- className?: string
- /**组件最外层的style */
- style?: CSSProperties
- /**孩子 */
- children?: ReactNode
- }
- /**ItemRender渲染函数的参数 */
- type itemProps
= { - /**当前元素 */
- item: T,
- /**当前索引 */
- index: number,
- /**父元素宽度 */
- width: number
- /**可拖拽的盒子,只有在这上面才能拖拽。自由放置位置。提供了一个默认的拖拽图标。可以作为包围盒,将某块内容作为拖拽区域 */
- DragBox: (props: baseChildrenProps) => ReactNode
- }
- /**拖拽排序组件的props */
- export interface DragSortProps
{ - /**组件最外层的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
) => ReactNode - /**拖拽结束事件,返回排序好的新数组,在里面自己调用setList */
- afterDrag: (list: T[]) => any
- }
由于这段背景设置为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;
- }
一个响应式计算宽度的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
, 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
- // 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))
- }
- }
- 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;
- }
- 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
({ - list,
- ItemRender,
- afterDrag,
- keyName,
- cols = 1,
- marginX = 0,
- flipWithListChange = true,
- className,
- style,
- }: DragSortProps
) { - 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;
一开始展示的效果图的实现代码
- "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
- ([]); //当前列表
- 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等,欢迎指出