Antd Upload组件实现拖拽排序的方法

需求

一个管理后台项目用到了Antd中的Upload组件,需求是使用picture-card类型,同时支持拖拽排序。

初期思路

因为项目本身就使用了react-sortable-hoc这个拖拽组件库,所以遵循着能用就用的原则,打算使用react-sortable-hoc配合Upload中的itemRender属性来完成这个需求,代码如下

typescript 复制代码
import {SortableContainer, SortableElement, SortEnd} from "react-sortable-hoc";

const SortableItem = SortableElement<any>(({node}) => <div>{node}</div>);  
  
const SortableList = SortableContainer((props) => <div {...props}/>);

const handleItemRender: UploadProps['itemRender'] = (node, file) => {  
    return <SortableItem index={getIndex(file)} node={node}/>  
}

<SortableList   ....>  
    <Upload  
        multiple  
        maxCount={5}  
        name={'file'}  
        listType={"picture-card"}  
        accept=".png, .jpg, .jpeg"   
        itemRender={handleItemRender}   
        {...restProps}>{fileList.length < 5 ? child : undefined}</Upload>  
</SortableList>

看起来一切顺利,原神代码,启动!

可以看到拖拽排序的功能是实现了,但是拖拽的间距检测明显是有问题。

分析问题

首先替换了Upload改用div来试试基本的功能是否正常,代码如下

javascript 复制代码
<SortableList  ...>  
    {[1, 2, 3, 4, 5].map((v, i) => <SortableItem index={i} node={<div .../>)}  
</SortableList>

可以看到效果是非常完美的,那问题只能是出在Upload组件上了,当时做到这一步的时候我认为应该有很多人会碰到这样的问题,打开搜索引擎搜索后发现并没有...,很多人就直接选择不用Upload里面自带的组件了,但是这样相当于要自己写上传进度条 状态上传失败的状态等等等orz,既然没有搜索到解决办法,只能自己动手啦,我不入地狱谁入地狱。

层级问题

首先还是打开F12 简单分析一下可能出现问题的原因,对比了一下Upload和普通的div能看到其实UploaditemRender方法返回的Element不是紧邻着SortableList的,而是还包裹着一层。

如果是层级问题其实我心里大概也有了答案,不过为了确认还是要从源码入手看一看到底是怎么造成这种情况的。

源码分析

kotlin 复制代码
---> SortableContainer

// 按下鼠标时触发的事件
handlePress = async (event) => { 
   ....
   ....
   
   const {index} = node.sortableInfo;  
   const margin = getElementMargin(node);  
   const gridGap = getContainerGridGap(this.container);  
   const containerBoundingRect = this.scrollContainer.getBoundingClientRect();  
   const dimensions = getHelperDimensions({index, node, collection});  

   this.node = node;  
   this.margin = margin;  
   this.gridGap = gridGap;  
   this.width = dimensions.width;  
   this.height = dimensions.height;  
   this.marginOffset = {  
    x: this.margin.left + this.margin.right + this.gridGap.x,  
    y: Math.max(this.margin.top, this.margin.bottom, this.gridGap.y),  
   };  
   this.boundingClientRect = node.getBoundingClientRect();  
   this.containerBoundingRect = containerBoundingRect;  
   this.index = index;  
   this.newIndex = index;
   
   // 👆找到指定的元素并计算padding,margin等位置信息
   
   ...
   ...
   
   this.helper = this.helperContainer.appendChild(cloneNode(node));
   
   // 👆克隆一份鼠标选中的元素
   
   ...
   ...
   
   events.move.forEach((eventName) =>  this.listenerNode.addEventListener(eventName,this.handleSortMove,false));
   
   // 👆给克隆的元素身上绑定拖拽事件

}

OK,经过简单的分析我们大概从源码中看到了如何克隆一个元素以及元素的事件绑定,接着我们应该重点关注的就是克隆元素绑定的handleSortMove事件

ini 复制代码
---> handleSortMove

handleSortMove = (event) => {  
    const {onSortMove} = this.props;  

    // Prevent scrolling on mobile  
    if (typeof event.preventDefault === 'function') {  
        event.preventDefault();  
    }  

    this.updateHelperPosition(event); 
    // 👆克隆元素的拖拽坐标信息
    this.animateNodes(); 
    // 👆其他元素的对齐滚动动画事件
    ...
    ...
};

---> animateNodes

animateNodes() {
   ...
   ...
   
   for (let i = 0, len = nodes.length; i < len; i++) {  
            const {node} = nodes[i];  
            const {index} = node.sortableInfo;  
            const width = node.offsetWidth;  
            const height = node.offsetHeight;  
  
  
            const offset = {  
                height: this.height > height ? height / 2 : this.height / 2,  
                width: this.width > width ? width / 2 : this.width / 2,  
            };  
  
            if (!edgeOffset) {  
                // 👇重点来了,计算元素边距距离
                edgeOffset = getEdgeOffset(node, this.container);  
                nodes[i].edgeOffset = edgeOffset;  
            }  
            
            ...
            ...
            ...
    }

}


---> getEdgeOffset

export function getEdgeOffset(node, parent, offset = {left: 0, top: 0}) {  
        if (!node) {  
            return undefined;  
        }  

        // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested  
        const nodeOffset = {  
            left: offset.left + node.offsetLeft,  
            top: offset.top + node.offsetTop,  
        };  
        
        // 👇如果父元素不是SortContainer会一直递归直到找到SortContainer元素
        if (node.parentNode === parent) {  
            return nodeOffset;  
        }  

        return getEdgeOffset(node.parentNode, parent, nodeOffset);  
}

OOOK!果然问题是出现在了层级结构上面,因为我们现在的UploadItem外面还会被套上一层div所以递归的时候会重复计算offset.left offset.top,这和我们的表现形式也是一致的。

解决问题

上面我们已经分析出了问题原因是因为层级 结构导致的重复计算offset,那么解决问题的答案也很明显了,就是让子元素offset0就可以,也就是按照CSS规则来说

  1. 如果父辈元素中有定位 的元素,那么就返回距离当前元素最近的定位元素边缘的距离
  2. 如果父辈元素中没有定位 元素,那么就返回相对于body左边缘距离。

所以我们给父元素一个相对定位即可

css 复制代码
:global{  
    .ant-upload-list-picture-card-container{  
        position: relative;  
    }  
}

大功告成。

封面

pixiv

相关推荐
islandzzzz1 小时前
(第二篇)HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
前端小盆友2 小时前
从零实现一个GPT 【React + Express】--- 【2】实现对话流和停止生成
前端·gpt·react.js
晴殇i3 小时前
CSS 迎来重大升级:Chrome 137 支持 if () 条件函数,样式逻辑从此更灵活
前端·css·面试
Hilaku3 小时前
2025年,每个前端都应该了解的CSS选择器:`:has()`, `:is()`, `:where()`
前端·css
OLong3 小时前
2025年最强React插件,支持大量快捷操作
前端·react.js·visual studio code
摸鱼仙人~3 小时前
重塑智能体决策路径:深入理解 ReAct 框架
前端·react.js·前端框架
sunbyte3 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | DragNDrop(拖拽占用组件)
前端·javascript·css·vue.js·vue
namehu3 小时前
浏览器中的扫码枪:从需求到踩坑再到优雅解决
前端·react.js
杨进军3 小时前
React 使用 MessageChannel 实现异步更新
react.js
namehu4 小时前
浏览器中的打印魔法:Lodop与系统打印机
前端·react.js