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/