你最后那段 dragover 代码断了,我先把它补完——列表排序的关键就在这 8 行里:
list.addEventListener('dragover', e => {
e.preventDefault(); // 不写这句 drop 永远不触发
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('li');
if (!target || target === dragging) return;
const rect = target.getBoundingClientRect();
const after = e.clientY > rect.top + rect.height / 2; // 过中线才插到后面
if (after ? target.nextSibling === dragging : target.previousSibling === dragging) return; // 位置没变就别动 DOM
list.insertBefore(dragging, after ? target.nextSibling : target);
});
最后那个 if 判断别省。dragover 每秒触发几十次,每次都 insertBefore 会让浏览器反复重排,项目一多就卡成幻灯片;加上「新位置和当前一致就 return」,手感立刻顺。
再补两个你后半篇大概率会踩的坑:一是被拖元素在鼠标下会「跳位」——元素一换位置,鼠标相对它的偏移就变了,视觉上会闪,常见解法是 dragstart 里记下 e.clientY - rect.top,dragover 时用 transform: translateY() 补正,或者干脆改走 placeholder 占位方案;二是 dragend 里要判一下 e.dataTransfer.dropEffect === 'none',这代表用户把元素扔到窗口外取消了拖拽,此时调 saveOrder() 会把一个没生效的顺序提交到后端。
文件上传那块同理,外层容器记得用 e.dataTransfer.types.includes('Files') 过滤,否则拖内部元素也会点亮上传区。
最后一个现实问题:HTML5 拖拽 API 在移动端基本不生效,触屏需要自己用 pointer 事件兜底——真做「拖拽排序」需求的话,建议一开始就把判定逻辑抽成 moveBefore(el, x/y) 一个函数,鼠标和触摸两套事件共用,后面省事很多。
顺便提一句,前端拖拽上传只是第一步,落到服务端还是照样走上传白名单和目录可写校验,本地 uploads 目录不可写的话,前端拖得再顺也会在响应里报错。