Vue3 Composition API 实战:开发一个支持拖拽排序的列表组件

本文将一步步指导你使用 Vue3 的 Composition API 开发一个支持拖拽排序的列表组件。Composition API 提供了更灵活的逻辑复用方式,而拖拽排序功能在任务管理、图片库等场景中非常实用。教程结构清晰,分为六个步骤,每个步骤包含详细解释和代码片段。最终,你将得到一个完整的可运行组件。

步骤 1: 准备工作

确保你的开发环境已安装 Vue3。如果使用 Vue CLI 或 Vite 创建项目,安装依赖:

npm install vue@3

我们将使用原生 HTML5 拖拽 API,无需额外库,保持轻量。

步骤 2: 组件基础结构

创建 Vue 单文件组件(如 DraggableList.vue)。使用 Composition API 的 setup 函数定义响应式数据和基本模板:

<template>
  <ul class="list">
    <li 
      v-for="(item, index) in items" 
      :key="item.id" 
      class="list-item"
      draggable="true"
      @dragstart="handleDragStart(index)"
      @dragover="handleDragOver"
      @drop="handleDrop(index)"
    >
      {{ item.name }}
    </li>
  </ul>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    // 响应式列表数据,初始示例数据
    const items = ref([
      { id: 1, name: '项目 A' },
      { id: 2, name: '项目 B' },
      { id: 3, name: '项目 C' }
    ]);

    // 存储拖拽源索引
    const draggedIndex = ref(null);

    return { items, draggedIndex };
  }
};
</script>

<style scoped>
.list {
  list-style: none;
  padding: 0;
}
.list-item {
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  border: 1px solid #ddd;
  cursor: grab;
}
</style>

  • 关键点
    • 使用 ref 定义响应式数据 itemsdraggedIndex
    • 模板中,v-for 渲染列表项,draggable="true" 启用拖拽。
    • 事件监听:dragstartdragoverdrop
步骤 3: 实现拖拽开始事件

setup 中添加 handleDragStart 方法,存储被拖拽项的索引:

<script>
import { ref } from 'vue';

export default {
  setup() {
    const items = ref([...]); // 同上
    const draggedIndex = ref(null);

    const handleDragStart = (index) => {
      draggedIndex.value = index; // 存储当前拖拽项索引
      event.dataTransfer.setData('text/plain', index); // 可选:传递数据
    };

    return { items, draggedIndex, handleDragStart };
  }
};
</script>

  • 解释handleDragStart 在拖拽开始时触发,记录索引 $i$(其中 $i$ 是当前项位置)。这为后续排序提供基准。
步骤 4: 处理拖拽悬停事件

添加 handleDragOver 方法,允许放置并更新视觉反馈:

<script>
import { ref } from 'vue';

export default {
  setup() {
    // ... 同上

    const handleDragOver = (event) => {
      event.preventDefault(); // 必须阻止默认行为以允许 drop
      event.dataTransfer.dropEffect = 'move'; // 设置拖拽效果为移动
    };

    return { ..., handleDragOver };
  }
};
</script>

  • 解释event.preventDefault() 是关键,否则浏览器不会触发 drop 事件。dropEffect 提供视觉提示(如光标变化)。
步骤 5: 实现放置事件和排序逻辑

setup 中添加 handleDrop 方法,更新列表顺序:

<script>
import { ref } from 'vue';

export default {
  setup() {
    // ... 同上

    const handleDrop = (targetIndex) => {
      event.preventDefault();
      if (draggedIndex.value === null) return;

      const sourceIndex = draggedIndex.value;
      if (sourceIndex === targetIndex) return; // 相同位置不处理

      // 更新列表:移动元素从索引 $i$ 到 $j$
      const newItems = [...items.value];
      const [movedItem] = newItems.splice(sourceIndex, 1); // 移除源项
      newItems.splice(targetIndex, 0, movedItem); // 插入目标位置

      items.value = newItems; // 更新响应式数据
      draggedIndex.value = null; // 重置拖拽索引
    };

    return { ..., handleDrop };
  }
};
</script>

  • 排序逻辑
    • 当拖拽项从索引 $i$ 放置到索引 $j$ 时,列表顺序更新。
    • 使用数组 splice 方法:先移除源项,再插入目标位置。时间复杂度为 $O(n)$,适用于小型列表。
    • 数学表示:如果原列表为 $[a_0, a_1, \dots, a_{i}, \dots, a_{n-1}]$,移动后变为 $[a_0, a_1, \dots, a_{j}, a_{i}, \dots]$(假设 $i < j$)。
步骤 6: 完整组件代码和优化

整合所有部分,并添加简单动画增强用户体验:

<template>
  <ul class="list">
    <li 
      v-for="(item, index) in items" 
      :key="item.id" 
      class="list-item"
      draggable="true"
      @dragstart="handleDragStart(index)"
      @dragover="handleDragOver"
      @drop="handleDrop(index)"
      @dragend="handleDragEnd"
    >
      {{ item.name }}
    </li>
  </ul>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const items = ref([
      { id: 1, name: '项目 A' },
      { id: 2, name: '项目 B' },
      { id: 3, name: '项目 C' }
    ]);
    const draggedIndex = ref(null);

    const handleDragStart = (index) => {
      draggedIndex.value = index;
      event.dataTransfer.effectAllowed = 'move';
    };

    const handleDragOver = (event) => {
      event.preventDefault();
      event.dataTransfer.dropEffect = 'move';
    };

    const handleDrop = (targetIndex) => {
      event.preventDefault();
      if (draggedIndex.value === null) return;
      
      const sourceIndex = draggedIndex.value;
      if (sourceIndex === targetIndex) return;

      const newItems = [...items.value];
      const [movedItem] = newItems.splice(sourceIndex, 1);
      newItems.splice(targetIndex, 0, movedItem);
      items.value = newItems;
    };

    const handleDragEnd = () => {
      draggedIndex.value = null; // 确保重置
    };

    return { items, handleDragStart, handleDragOver, handleDrop, handleDragEnd };
  }
};
</script>

<style scoped>
.list {
  list-style: none;
  padding: 0;
  max-width: 300px;
}
.list-item {
  padding: 12px;
  margin: 8px 0;
  background: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  cursor: grab;
  transition: all 0.3s ease; /* 添加过渡动画 */
}
.list-item:active {
  cursor: grabbing;
}
.list-item.drag-over {
  background: #e3f2fd; /* 悬停时高亮 */
}
</style>

总结

通过本教程,你已实现了一个支持拖拽排序的列表组件:

  • Composition API 优势:逻辑封装在 setup 中,易于复用(如抽离拖拽逻辑为独立函数)。
  • 核心机制:利用 HTML5 拖拽事件和数组操作更新列表顺序。当拖拽项从索引 $i$ 移动到 $j$ 时,列表更新为 $[a_0, \dots, a_{j-1}, a_i, a_j, \dots]$(需处理边界)。
  • 扩展建议
    • 添加动画库(如 Vue Transition)提升交互体验。
    • 支持嵌套列表或跨组件拖拽。
    • 结合 Vuex 或 Pinia 管理全局状态。

在实际项目中测试此组件,确保功能稳定。Composition API 使代码更模块化,便于维护和扩展。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐