1. 使用 HTML5
事件 | 触发时机 | 核心任务 |
@dragstart | 开始拖拽时 | 准备数据,贴上标签 |
@dragover | 经过目标上方时 | 必须 preventDefault(),发出“允许放置”的信号 |
@dragleave | 离开目标上方时 | 清理高亮等临时视觉效果 |
@drop | 在目标上松手时 | 接收数据,执行最终逻辑 |
@dragend | 整个拖拽结束时 | 最终清理,重置所有状态 |
<template><div class="sortable-list-container"><h3>待办事项 (原生 HTML5 拖拽排序)</h3><ul class="task-list"><!-- 每个 li 既是可拖拽项,也是其他项的放置目标。因此,它需要同时监听 drag 和 drop 相关的事件。--><li v-for="(task, index) in tasks" :key="task.id" class="task-item":class="{ 'drag-over-highlight': draggingOverIndex === index }" draggable="true" @dragstart="onDragStart(index)"@dragover.prevent="onDragOver(index)" @dragleave="onDragLeave" @drop="onDrop(index)" @dragend="onDragEnd">{{ task.name }}</li></ul><div class="raw-data"><h4>实时数据顺序:</h4><pre>{{ tasks }}</pre></div></div>
</template><script setup>import { ref } from 'vue';// 初始数据const tasks = ref([{ id: 1, name: '学习原生拖拽 API' },{ id: 2, name: '编写 onDragStart' },{ id: 3, name: '处理 dragover.prevent' },{ id: 4, name: '实现 onDrop 逻辑' },]);// --- 状态变量 ---// 记录当前正在被拖拽的元素的索引const draggingIndex = ref(null);// 记录当前鼠标悬停在其上方的放置目标的索引const draggingOverIndex = ref(null);// --- 事件处理函数 ---/*** 拖拽开始时触发* @param {number} index - 被拖拽的 task 的索引*/function onDragStart(index) {draggingIndex.value = index;console.log(`开始拖拽: 索引 ${index} - ${tasks.value[index].name}`);// 注意:原生 API 不像库那样有好看的 ghost 效果,// 浏览器会自动创建一个半透明的快照。}/*** 当拖拽元素经过其他元素上方时持续触发* @param {number} index - 当前经过的元素的索引*/function onDragOver(index) {// event.preventDefault() 已经在模板中通过 .prevent 修饰符处理了// 更新悬停目标的索引,用于高亮显示draggingOverIndex.value = index;}/*** 当拖拽元素离开一个放置目标时触发*/function onDragLeave() {// 清除高亮效果draggingOverIndex.value = null;}/*** 在一个放置目标上松开鼠标时触发* @param {number} dropIndex - 松手时所在的元素的索引*/function onDrop(dropIndex) {console.log(`放置到: 索引 ${dropIndex}`);// --- 核心排序逻辑 ---if (draggingIndex.value === null || draggingIndex.value === dropIndex) {// 如果没有拖拽项或放在了原位,则什么都不做return;}// 1. 从数组中“剪切”出被拖拽的元素const movedItem = tasks.value.splice(draggingIndex.value, 1)[0];// 2. 将被拖拽的元素“粘贴”到新的位置tasks.value.splice(dropIndex, 0, movedItem);console.log('数组已重新排序');}/*** 拖拽结束时(成功或失败)触发*/function onDragEnd() {console.log('拖拽结束');// 清理所有状态变量draggingIndex.value = null;draggingOverIndex.value = null;}
</script><style scoped>.task-list {list-style-type: none;padding: 0;width: 300px;border: 1px solid #ccc;border-radius: 4px;}.task-item {padding: 12px;margin: 5px;background-color: #f0f0f0;border: 1px solid #ddd;cursor: grab;transition: all 0.2s ease;}.task-item:active {cursor: grabbing;}/* 当有元素被拖拽到某个 li 上方时,给这个 li 添加高亮效果 */.drag-over-highlight {background-color: #c8ebfb;border-top: 2px solid #3498db;transform: scale(1.02);}.raw-data {margin-top: 20px;font-family: monospace;}
</style>
2. 使用sortable.js
核心难点:谁来掌握 DOM
- Vue:数据驱动视图,只有 DOM 是唯一主宰。改动 DOM 时会跟据虚拟 DOM 修改真实的 DOM。
- Sortable:是一个 DOM 操作库,直接移动 DOM 节点实现拖拽效果,对 Vue 的虚拟 DOM 一无所知
如果让它们各干各的,就会乱了。当 SortableJS 移动了 DOM 后,Vue 的数据还是旧的。下一次 Vue 因为任何原因需要重新渲染时,它会看着自己的旧数据,会觉得是出错了,然后,它会强制把 DOM 修正回原始的顺序,导致你拖拽的元素“闪”回原位。
直接把 DOM 操作权给 Sortable
思路:在 onMounted
使用 sortableJS 把数据通过 new Sortable
传递进去,调用 onEnd
函数,把被拖动的数据直接删除 const movedItem = tasks.value.splice(oldIndex, 1)[0];
,然后再将它插入到新的地方 tasks.value.splice(newIndex, 0, movedItem);
,在 onUnmounted
中销毁 Sortable
实例防止内存泄漏。
# 下载依赖
npm install sortablejs
<template><div class="sortable-list-container"><h3>待办事项 (由 SortableJS 直接驱动)</h3><p>安装 SortableJS:npm install sortablejs</p><p>如果在使用 TypeScript,最好也安装它的类型定义文件:npm install @types/sortablejs -D</p><!-- 1. 准备一个容器,并用 ref 标记它 --><!-- Vue 会将这个 <ul> 元素赋值给 listRef --><ul ref="listRef" class="task-list"><!-- 2. 正常使用 v-for 渲染列表 --><!-- data-id 属性是可选的,但对于 SortableJS 来说是一个好习惯 --><li v-for="task in tasks" :key="task.id" :data-id="task.id" class="task-item">{{ task.name }}</li></ul><div class="raw-data"><h4>实时数据顺序:</h4><pre>{{ tasks }}</pre></div></div>
</template><script setup>import { ref, onMounted, onUnmounted } from 'vue';// 3. 导入 SortableJSimport Sortable from 'sortablejs';// 初始数据const tasks = ref([{ id: 1, name: '学习 Vue 拖拽' },{ id: 2, name: '编写 Demo' },{ id: 3, name: '提交代码' },{ id: 4, name: '喝杯咖啡' },]);// 创建一个模板引用,用于获取 DOM 元素const listRef = ref(null);// 用于存放 Sortable 实例,方便后续销毁let sortableInstance = null;// 4. 在 onMounted 钩子中初始化 SortableJS// 必须在这里,因为要确保 DOM 元素已经渲染完成onMounted(() => {if (listRef.value) {// 实例化 SortablesortableInstance = new Sortable(listRef.value, {animation: 150, // 拖拽归位时的动画时长,单位 msghostClass: 'ghost', // 拖拽时占位元素的类名// 5. 监听 onEnd 事件,这是“握手”的关键onEnd: (event) => {const { oldIndex, newIndex } = event;console.log(`元素从索引 ${oldIndex} 移动到了 ${newIndex}`);// --- 核心逻辑:更新 Vue 的数据数组 ---// 1. 从数组中移除被拖拽的元素const movedItem = tasks.value.splice(oldIndex, 1)[0];// 2. 将被拖拽的元素插入到新的位置tasks.value.splice(newIndex, 0, movedItem);// ------------------------------------console.log('Vue 的数据数组已同步更新!');},});}});// 6. 在 onUnmounted 钩子中销毁 Sortable 实例// 这是一个好习惯,可以防止内存泄漏onUnmounted(() => {if (sortableInstance) {sortableInstance.destroy();}});
</script><style scoped>.task-list {list-style-type: none;padding: 0;width: 300px;}.task-item {padding: 10px;margin: 5px 0;background-color: #f0f0f0;border: 1px solid #ddd;cursor: move;}/* 拖拽占位符的样式 */.ghost {opacity: 0.5;background: #c8ebfb;}.raw-data {margin-top: 20px;font-family: monospace;}
</style>
3. 使用vue-draggable
这个官网有很多示例,可以参考一下
https://vue-draggable-plus.netlify.app/