Vue3实现拖拽排序

作者:用户971417181427日期:2025/11/14

Vue3 + Element Plus + SortableJS 实现表格拖拽排序功能

📋 目录

  • 功能概述
  • 技术栈
  • 实现思路
  • 代码实现
  • 核心要点
  • 常见问题
  • 总结

功能概述

在管理后台系统中,表格数据的排序功能是一个常见的需求。本文介绍如何使用 Vue3、Element Plus 和 SortableJS 实现一个完整的表格拖拽排序功能,支持:

  • ✅ 通过拖拽图标对表格行进行排序
  • ✅ 实时更新数据顺序
  • ✅ 支持数据过滤后的排序
  • ✅ 切换标签页时自动初始化
  • ✅ 优雅的动画效果

先看实现效果:在这里插入图片描述

技术栈

  • Vue 3 - 渐进式 JavaScript 框架
  • Element Plus - Vue 3 组件库
  • SortableJS - 轻量级拖拽排序库
  • TypeScript - 类型安全的 JavaScript 超集

实现思路

1. 整体架构

1用户拖拽表格行
23SortableJS 监听拖拽事件
45触发 onEnd 回调
67更新 Vue 响应式数据
89表格自动重新渲染
10

2. 关键步骤

  1. 安装依赖:引入 SortableJS 库
  2. 获取 DOM:获取表格 tbody 元素
  3. 初始化 Sortable:创建拖拽实例
  4. 处理回调:在拖拽结束时更新数据
  5. 生命周期管理:在适当时机初始化和销毁实例

代码实现

1. 安装依赖

1npm install sortablejs
2# 或
3pnpm add sortablejs
4

2. 导入必要的模块

1import { ref, nextTick, watch, onMounted } from "vue";
2import Sortable from "sortablejs";
3import { Operation } from "@element-plus/icons-vue";//图标
4

3. 定义数据结构

1interface TypeItem {
2  id: string;
3  name: string;
4  enabled: boolean;
5  sortOrder: number;
6}
7
8const typeData = ref<TypeItem[]>([
9  { id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
10  { id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
11  // ... 更多数据
12]);
13

4. 模板结构

1<template>
2  <el-table ref="typeTableRef" :data="filteredTypeData" stripe row-key="id">
3    <!-- 排序列:显示拖拽图标 -->
4    <el-table-column label="排序" width="131">
5      <template #default>
6        <el-icon class="drag-handle">
7          <Operation />
8        </el-icon>
9      </template>
10    </el-table-column>
11    
12    <!-- 其他列 -->
13    <el-table-column prop="name" label="名称" />
14    <el-table-column prop="enabled" label="启用/禁用">
15      <template #default="{ row }">
16        <el-switch v-model="row.enabled" />
17      </template>
18    </el-table-column>
19  </el-table>
20</template>
21

5. 核心实现代码

1// 表格引用
2const typeTableRef = ref<InstanceType<typeof ElTable>>();
3
4// Sortable 实例(用于后续销毁)
5let sortableInstance: Sortable | null = null;
6
7/**
8 * 初始化拖拽排序功能
9 */
10const initSortable = () => {
11  // 1. 销毁旧实例,避免重复创建
12  if (sortableInstance) {
13    sortableInstance.destroy();
14    sortableInstance = null;
15  }
16
17  // 2. 等待 DOM 更新完成
18  nextTick(() => {
19    // 3. 获取表格的 tbody 元素
20    const tbody = typeTableRef.value?.$el?.querySelector(
21      ".el-table__body-wrapper tbody"
22    );
23    
24    if (!tbody) return;
25
26    // 4. 创建 Sortable 实例
27    sortableInstance = Sortable.create(tbody, {
28      // 指定拖拽手柄(只能通过拖拽图标来拖拽)
29      handle: ".drag-handle",
30      
31      // 动画时长(毫秒)
32      animation: 300,
33      
34      // 拖拽结束回调
35      onEnd: ({ newIndex, oldIndex }) => {
36        // 5. 更新数据顺序
37        if (
38          newIndex !== undefined &&
39          oldIndex !== undefined &&
40          filterStatus.value === "all" // 只在"全部"状态下允许排序
41        ) {
42          // 获取被移动的项
43          const movedItem = typeData.value[oldIndex];
44          
45          // 从原位置删除
46          typeData.value.splice(oldIndex, 1);
47          
48          // 插入到新位置
49          typeData.value.splice(newIndex, 0, movedItem);
50          
51          // 更新排序字段
52          typeData.value.forEach((item, index) => {
53            item.sortOrder = index + 1;
54          });
55        }
56      }
57    });
58  });
59};
60

6. 生命周期管理

1/**
2 * 监听标签页切换,初始化拖拽
3 */
4const watchActiveTab = () => {
5  if (activeTab.value === "type") {
6    // 延迟初始化,确保表格已完全渲染
7    setTimeout(() => {
8      initSortable();
9    }, 300);
10  }
11};
12
13// 组件挂载时初始化
14onMounted(() => {
15  watchActiveTab();
16});
17
18// 监听标签页切换
19watch(activeTab, () => {
20  watchActiveTab();
21});
22
23// 监听过滤器变化,重新初始化拖拽
24watch(filterStatus, () => {
25  if (activeTab.value === "type") {
26    setTimeout(() => {
27      initSortable();
28    }, 100);
29  }
30});
31

7. 样式定义

1/* 拖拽手柄样式 */
2.drag-handle {
3  color: #909399;
4  cursor: move;
5  font-size: 18px;
6  transition: color 0.3s;
7}
8
9.drag-handle:hover {
10  color: #1890ff;
11}
12
13/* 表格样式 */
14.type-table {
15  margin-top: 0;
16}
17
18:deep(.type-table .el-table__header-wrapper) {
19  background-color: #f9fafc;
20}
21
22:deep(.type-table .el-table th) {
23  background-color: #f9fafc;
24  font-size: 14px;
25  font-weight: 500;
26  color: #33425cfa;
27  font-family: PingFang SC;
28  border-bottom: 1px solid #dcdfe6;
29}
30

核心要点

1. 实例管理

问题:如果不管理 Sortable 实例,切换标签页或过滤器时会创建多个实例,导致拖拽行为异常。

解决:使用变量保存实例引用,在创建新实例前先销毁旧实例。

1let sortableInstance: Sortable | null = null;
2
3const initSortable = () => {
4  // 先销毁旧实例
5  if (sortableInstance) {
6    sortableInstance.destroy();
7    sortableInstance = null;
8  }
9  // 再创建新实例
10  // ...
11};
12

2. DOM 获取时机

问题:如果直接获取 DOM,可能表格还未渲染完成,导致获取失败。

解决:使用 nextTick 等待 Vue 完成 DOM 更新,或使用 setTimeout 延迟执行。

1nextTick(() => {
2  const tbody = typeTableRef.value?.$el?.querySelector(
3    ".el-table__body-wrapper tbody"
4  );
5  // ...
6});
7

3. 拖拽手柄

问题:如果不指定拖拽手柄,整行都可以拖拽,可能与其他交互冲突(如点击编辑按钮)。

解决:使用 handle 选项指定只有拖拽图标可以触发拖拽。

1Sortable.create(tbody, {
2  handle: ".drag-handle", // 只允许通过 .drag-handle 元素拖拽
3  // ...
4});
5

4. 数据更新策略

问题:直接操作 DOM 顺序不会更新 Vue 的响应式数据。

解决:在 onEnd 回调中手动更新数据数组的顺序。

1onEnd: ({ newIndex, oldIndex }) => {
2  const movedItem = typeData.value[oldIndex];
3  typeData.value.splice(oldIndex, 1);
4  typeData.value.splice(newIndex, 0, movedItem);
5  // 更新排序字段
6  typeData.value.forEach((item, index) => {
7    item.sortOrder = index + 1;
8  });
9}
10

5. 过滤状态处理

问题:当表格数据被过滤后,拖拽的索引可能不准确。

解决:只在"全部"状态下允许排序,或根据过滤后的数据计算正确的索引。

1onEnd: ({ newIndex, oldIndex }) => {
2  if (filterStatus.value === "all") {
3    // 只在全部状态下允许排序
4    // ...
5  }
6}
7

常见问题

Q1: 拖拽后数据没有更新?

A: 检查是否正确更新了响应式数据。SortableJS 只负责 DOM 操作,不会自动更新 Vue 数据。

Q2: 切换标签页后拖拽失效?

A: 需要在标签页切换时重新初始化 Sortable 实例,因为 DOM 已经重新渲染。

Q3: 拖拽时整行都可以拖,如何限制?

A: 使用 handle 选项指定拖拽手柄元素。

Q4: 拖拽动画不流畅?

A: 调整 animation 参数的值,通常 200-300ms 效果较好。

Q5: 如何保存排序结果?

A: 在 onEnd 回调中,将更新后的数据发送到后端 API。

1onEnd: ({ newIndex, oldIndex }) => {
2  // 更新本地数据
3  // ...
4  
5  // 保存到后端
6  saveSortOrder(typeData.value.map(item => ({
7    id: item.id,
8    sortOrder: item.sortOrder
9  })));
10}
11

完整示例代码

1<template>
2  <div class="type-setting">
3    <!-- 过滤器 -->
4    <div class="filter-actions">
5      <el-button
6        :type="filterStatus === 'all' ? 'primary' : ''"
7        @click="filterStatus = 'all'"
8      >
9        全部
10      </el-button>
11      <el-button
12        :type="filterStatus === 'enabled' ? 'primary' : ''"
13        @click="filterStatus = 'enabled'"
14      >
15        启用
16      </el-button>
17    </div>
18
19    <!-- 表格 -->
20    <el-table
21      ref="typeTableRef"
22      :data="filteredTypeData"
23      stripe
24      row-key="id"
25    >
26      <el-table-column label="排序" width="131">
27        <template #default>
28          <el-icon class="drag-handle">
29            <Operation />
30          </el-icon>
31        </template>
32      </el-table-column>
33      <el-table-column prop="name" label="名称" />
34      <el-table-column prop="enabled" label="启用/禁用">
35        <template #default="{ row }">
36          <el-switch v-model="row.enabled" />
37        </template>
38      </el-table-column>
39      <el-table-column label="操作">
40        <template #default="{ row }">
41          <el-button type="primary" link @click="handleEdit(row)">
42            编辑
43          </el-button>
44        </template>
45      </el-table-column>
46    </el-table>
47  </div>
48</template>
49
50<script setup lang="ts">
51import { ref, nextTick, watch, onMounted } from "vue";
52import { ElTable } from "element-plus";
53import Sortable from "sortablejs";
54import { Operation } from "@element-plus/icons-vue";
55
56interface TypeItem {
57  id: string;
58  name: string;
59  enabled: boolean;
60  sortOrder: number;
61}
62
63const typeData = ref<TypeItem[]>([
64  { id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
65  { id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
66  { id: "3", name: "楼宇性质3", enabled: false, sortOrder: 3 },
67]);
68
69const filterStatus = ref<"all" | "enabled" | "disabled">("all");
70const typeTableRef = ref<InstanceType<typeof ElTable>>();
71let sortableInstance: Sortable | null = null;
72
73const filteredTypeData = computed(() => {
74  if (filterStatus.value === "all") return typeData.value;
75  if (filterStatus.value === "enabled") {
76    return typeData.value.filter(item => item.enabled);
77  }
78  return typeData.value.filter(item => !item.enabled);
79});
80
81const initSortable = () => {
82  if (sortableInstance) {
83    sortableInstance.destroy();
84    sortableInstance = null;
85  }
86
87  nextTick(() => {
88    const tbody = typeTableRef.value?.$el?.querySelector(
89      ".el-table__body-wrapper tbody"
90    );
91    if (!tbody) return;
92
93    sortableInstance = Sortable.create(tbody, {
94      handle: ".drag-handle",
95      animation: 300,
96      onEnd: ({ newIndex, oldIndex }) => {
97        if (
98          newIndex !== undefined &&
99          oldIndex !== undefined &&
100          filterStatus.value === "all"
101        ) {
102          const movedItem = typeData.value[oldIndex];
103          typeData.value.splice(oldIndex, 1);
104          typeData.value.splice(newIndex, 0, movedItem);
105          typeData.value.forEach((item, index) => {
106            item.sortOrder = index + 1;
107          });
108        }
109      }
110    });
111  });
112};
113
114onMounted(() => {
115  setTimeout(() => initSortable(), 300);
116});
117
118watch(filterStatus, () => {
119  setTimeout(() => initSortable(), 100);
120});
121</script>
122
123<style scoped>
124.drag-handle {
125  color: #909399;
126  cursor: move;
127  font-size: 18px;
128}
129
130.drag-handle:hover {
131  color: #1890ff;
132}
133</style>
134

总结

通过本文的介绍,我们实现了一个完整的表格拖拽排序功能。关键点包括:

  1. 正确的实例管理:避免重复创建和内存泄漏
  2. 合适的初始化时机:确保 DOM 已完全渲染
  3. 数据同步更新:手动更新 Vue 响应式数据
  4. 良好的用户体验:指定拖拽手柄,添加动画效果
  5. 完善的错误处理:处理边界情况

这个方案可以轻松应用到其他需要拖拽排序的场景,如菜单管理、分类排序等。希望本文对您有所帮助!



Vue3实现拖拽排序》 是转载文章,点击查看原文


相关推荐


Python 的内置函数 id
IMPYLH2025/11/13

Python 内建函数列表 > Python 的内置函数 id Python 的内置函数 id() 是一个非常有用的工具函数,它返回一个对象的"身份标识",这个标识实际上是该对象在内存中的地址(以整数形式表示)。以下是关于 id() 函数的详细说明: 基本用法 x = 42 print(id(x)) # 输出一个整数,代表变量x所引用的对象的内存地址 特性说明 每个对象在内存中都有唯一的id这个id在对象的生命周期内保持不变不同对象可能有相同的id(如果前一个对象已被销毁)


FastAPI × SQLAlchemy 2.0 Async:从“能跑”到“可压测”的完整工程实践
Java私教2025/11/11

一句话总结 用 SQLAlchemy 2.0 AsyncIO 模式,把 FastAPI 的并发优势兑现成 真正的数据库吞吐;再叠上连接池、事务、迁移、测试四件套,直接上线不踩坑。 1. 为什么要“异步 ORM”? 场景同步 SQLAlchemy异步 SQLAlchemy100 个并发上传开 100 线程 → 100 个连接 → DB 被打爆单线程 20 连接即可跑满 CPU请求等待 I/O线程上下文切换 8 ms协程切换 0.3 ms代码风格


删一个却少俩:Antd Tag 多节点同时消失的原因
顺凡2025/11/9

删一个却少俩:Antd Tag 多节点同时消失的原因 需求 一个表单的小需求,能填写多个福利,最多十个,福利名称允许重复,和官方的动态添加和删除示例交互一模一样,只是官方示例不支持 tag 内容重复,使用的 tag 内容作为 key 我复制丢给 AI,下掉去重,限制个数,好!满足需求了,key 值怎么办不能用重复的,拼个索引吧,最后主要代码如下, 反问一下:你觉得这会有什么问题,能达到删一个少俩的效果吗🤔??? 问题 大家应该都知道用 index 作为 key,会有一些问题,对于我这个需


程序员副业 | 2025年10月复盘
嘟嘟MD2025/11/7

本文首发于公众号:嘟爷创业日记 。 我已经坚持日更600天+,欢迎过来追剧~ 大家好,我是嘟嘟MD,一个10年程序员,现在离职创业,有700天了,我每个月都会写一篇总结复盘,让大家可以近距离看看一个离职程序员都在干什么,今天这篇是九月份的总结,大概2000字,略长,有空的可以翻翻,希望对大家有一丢丢的借鉴作用! 一、月度大事 10月结束了,一直拖到现在才有空汇总下10月份的进度,整体来说对外的合作少了,组织内的事情多了。 1:公众号运营+B站视频运营 公众号和B站视频运营还是我的最高优先级


在 Vue3 项目中使用 el-tree
代码工人笔记2025/11/3

在 Vue3 项目中使用 el-tree 文章目录 一、基础用法1. 引入组件 二、常用功能与配置1. 节点选择(复选框 / 单选)2. 展开 / 折叠控制3. 自定义节点内容4. 搜索过滤节点5. 获取选中节点 三、动态加载节点 一、基础用法 1. 引入组件 <template> <el-tree :data="treeData" :props="defaultProps" @node-click="handleNode


SQL之表的查改(上)
啊吧怪不啊吧2025/10/31

目录 1. Retrieve 1.2 Select 1.2.1全列插入 1.2.2 use 1.2.3 指定列查询 1.2.4 select+固定值 1.2.5 列值修改查询 1.2.6 别名 1.2.7 查询结果去重 在前面的文章中我们链接了表的增删操作,今天我们来聊一下表的查找与修改操作。 1. Retrieve 首先我们要了解到Retrieve不是某一个具体的指令,它描述的是 “从数据库表中获取数据” 这一行为本身,而实现这种行为的具体技


Swift 并发编程新选择:Mutex 保护可变状态实战解析
unravel20252025/10/29

前言 Swift 5.5 带来 async/await 与 Actor 后,「用 Actor 包一层」几乎成了默认答案。 但在日常开发里,我们经常会遇到两种尴尬: 只想保护一个计数器、缓存或 token,却不得不把整段逻辑都改成异步; 把对象放到 @MainActor 后,发现后台线程也要用,结果到处是 await。 Apple 在 Swift 5.9 前后把 Mutex 正式搬进标准库(通过 Synchronization 模块),给“同步但不想异步”的场景提供了第三条路。 Mutex 是


F032 材料科学文献知识图谱可视化分析系统(四种知识图谱可视化布局) | vue + flask + echarts + d3.js 实现
B站麦麦大数据2025/10/26

文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站,有好处! 编号: F032 视频 neo4j 文献知识图谱可视化分析系统 | vue + flask + echarts + d3.js 实现 1 系统简介 系统简介:本系统是一个基于Vue+Flask构建的材料科学文献知识图谱可视化分析系统,其核心功能围绕文献数据的抓取、分析、可视化和用户管理展开。主要包括:主页模块,用于展示最新文献卡片,方便用户快速了解最新动态;文献搜索功能,支持用户通过关键词或其他条


【DeepSeek新开源】DeepSeek-OCR如何用“视觉压缩”革新长文本处理
kakaZhui2025/10/23

最近DeepSeek团队刚放出DeepSeek-OCR项目,不再将其视为一个简单的OCR(光学字符识别)工具,而是将其作为一个开创性的实验平台,旨在探索和验证一个激进的理念:我们能否利用视觉模态作为一种超高效的文本信息压缩媒介? 即,将长篇的数字文本“渲染”成一张图像,再用一个强大的视觉语言模型(VLM)从这张图像中“读”出原文。 接下来我们一起看下DeepSeek-OCR从“视觉压缩”的核心哲学,到其创新的DeepEncoder架构和多分辨率支持,再到其庞大的数据工程和训练管线。 1. 引


想偷卷?但微信不支持md文档?这个软件助你!
前端AC2025/10/22

📝 Markdown 查看器 - 现代化的文档预览工具 一个基于 React 19 + TypeScript 构建的现代化 Markdown 文档查看器,支持实时预览、语法高亮、数学公式渲染等功能。 在微信或浏览器上打开此编辑器,上传你的md文档可以上课偷偷看自己写的博客哈哈,这个是我解决微信这个没有md预览的痛点,自己用ai搞了一个小工具出来,效果还不错,还有图片可以借助图床工具:图床 - 简单、快速、免费的图床把自己图片上传到这里,就不会导致路径问题了。 项目概述 项目背景 在日常开发

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0