饭团追书免费版
67.97MB · 2025-11-15
在管理后台系统中,表格数据的排序功能是一个常见的需求。本文介绍如何使用 Vue3、Element Plus 和 SortableJS 实现一个完整的表格拖拽排序功能,支持:
先看实现效果:
用户拖拽表格行
↓
SortableJS 监听拖拽事件
↓
触发 onEnd 回调
↓
更新 Vue 响应式数据
↓
表格自动重新渲染
npm install sortablejs
# 或
pnpm add sortablejs
import { ref, nextTick, watch, onMounted } from "vue";
import Sortable from "sortablejs";
import { Operation } from "@element-plus/icons-vue";//图标
interface TypeItem {
id: string;
name: string;
enabled: boolean;
sortOrder: number;
}
const typeData = ref<TypeItem[]>([
{ id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
{ id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
// ... 更多数据
]);
<template>
<el-table ref="typeTableRef" :data="filteredTypeData" stripe row-key="id">
<!-- 排序列:显示拖拽图标 -->
<el-table-column label="排序" width="131">
<template #default>
<el-icon class="drag-handle">
<Operation />
</el-icon>
</template>
</el-table-column>
<!-- 其他列 -->
<el-table-column prop="name" label="名称" />
<el-table-column prop="enabled" label="启用/禁用">
<template #default="{ row }">
<el-switch v-model="row.enabled" />
</template>
</el-table-column>
</el-table>
</template>
// 表格引用
const typeTableRef = ref<InstanceType<typeof ElTable>>();
// Sortable 实例(用于后续销毁)
let sortableInstance: Sortable | null = null;
/**
* 初始化拖拽排序功能
*/
const initSortable = () => {
// 1. 销毁旧实例,避免重复创建
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
// 2. 等待 DOM 更新完成
nextTick(() => {
// 3. 获取表格的 tbody 元素
const tbody = typeTableRef.value?.$el?.querySelector(
".el-table__body-wrapper tbody"
);
if (!tbody) return;
// 4. 创建 Sortable 实例
sortableInstance = Sortable.create(tbody, {
// 指定拖拽手柄(只能通过拖拽图标来拖拽)
handle: ".drag-handle",
// 动画时长(毫秒)
animation: 300,
// 拖拽结束回调
onEnd: ({ newIndex, oldIndex }) => {
// 5. 更新数据顺序
if (
newIndex !== undefined &&
oldIndex !== undefined &&
filterStatus.value === "all" // 只在"全部"状态下允许排序
) {
// 获取被移动的项
const movedItem = typeData.value[oldIndex];
// 从原位置删除
typeData.value.splice(oldIndex, 1);
// 插入到新位置
typeData.value.splice(newIndex, 0, movedItem);
// 更新排序字段
typeData.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
}
}
});
});
};
/**
* 监听标签页切换,初始化拖拽
*/
const watchActiveTab = () => {
if (activeTab.value === "type") {
// 延迟初始化,确保表格已完全渲染
setTimeout(() => {
initSortable();
}, 300);
}
};
// 组件挂载时初始化
onMounted(() => {
watchActiveTab();
});
// 监听标签页切换
watch(activeTab, () => {
watchActiveTab();
});
// 监听过滤器变化,重新初始化拖拽
watch(filterStatus, () => {
if (activeTab.value === "type") {
setTimeout(() => {
initSortable();
}, 100);
}
});
/* 拖拽手柄样式 */
.drag-handle {
color: #909399;
cursor: move;
font-size: 18px;
transition: color 0.3s;
}
.drag-handle:hover {
color: #1890ff;
}
/* 表格样式 */
.type-table {
margin-top: 0;
}
:deep(.type-table .el-table__header-wrapper) {
background-color: #f9fafc;
}
:deep(.type-table .el-table th) {
background-color: #f9fafc;
font-size: 14px;
font-weight: 500;
color: #33425cfa;
font-family: PingFang SC;
border-bottom: 1px solid #dcdfe6;
}
问题:如果不管理 Sortable 实例,切换标签页或过滤器时会创建多个实例,导致拖拽行为异常。
解决:使用变量保存实例引用,在创建新实例前先销毁旧实例。
let sortableInstance: Sortable | null = null;
const initSortable = () => {
// 先销毁旧实例
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
// 再创建新实例
// ...
};
问题:如果直接获取 DOM,可能表格还未渲染完成,导致获取失败。
解决:使用 nextTick 等待 Vue 完成 DOM 更新,或使用 setTimeout 延迟执行。
nextTick(() => {
const tbody = typeTableRef.value?.$el?.querySelector(
".el-table__body-wrapper tbody"
);
// ...
});
问题:如果不指定拖拽手柄,整行都可以拖拽,可能与其他交互冲突(如点击编辑按钮)。
解决:使用 handle 选项指定只有拖拽图标可以触发拖拽。
Sortable.create(tbody, {
handle: ".drag-handle", // 只允许通过 .drag-handle 元素拖拽
// ...
});
问题:直接操作 DOM 顺序不会更新 Vue 的响应式数据。
解决:在 onEnd 回调中手动更新数据数组的顺序。
onEnd: ({ newIndex, oldIndex }) => {
const movedItem = typeData.value[oldIndex];
typeData.value.splice(oldIndex, 1);
typeData.value.splice(newIndex, 0, movedItem);
// 更新排序字段
typeData.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
}
问题:当表格数据被过滤后,拖拽的索引可能不准确。
解决:只在"全部"状态下允许排序,或根据过滤后的数据计算正确的索引。
onEnd: ({ newIndex, oldIndex }) => {
if (filterStatus.value === "all") {
// 只在全部状态下允许排序
// ...
}
}
A: 检查是否正确更新了响应式数据。SortableJS 只负责 DOM 操作,不会自动更新 Vue 数据。
A: 需要在标签页切换时重新初始化 Sortable 实例,因为 DOM 已经重新渲染。
A: 使用 handle 选项指定拖拽手柄元素。
A: 调整 animation 参数的值,通常 200-300ms 效果较好。
A: 在 onEnd 回调中,将更新后的数据发送到后端 API。
onEnd: ({ newIndex, oldIndex }) => {
// 更新本地数据
// ...
// 保存到后端
saveSortOrder(typeData.value.map(item => ({
id: item.id,
sortOrder: item.sortOrder
})));
}
<template>
<div class="type-setting">
<!-- 过滤器 -->
<div class="filter-actions">
<el-button
:type="filterStatus === 'all' ? 'primary' : ''"
@click="filterStatus = 'all'"
>
全部
</el-button>
<el-button
:type="filterStatus === 'enabled' ? 'primary' : ''"
@click="filterStatus = 'enabled'"
>
启用
</el-button>
</div>
<!-- 表格 -->
<el-table
ref="typeTableRef"
:data="filteredTypeData"
stripe
row-key="id"
>
<el-table-column label="排序" width="131">
<template #default>
<el-icon class="drag-handle">
<Operation />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" />
<el-table-column prop="enabled" label="启用/禁用">
<template #default="{ row }">
<el-switch v-model="row.enabled" />
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
编辑
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch, onMounted } from "vue";
import { ElTable } from "element-plus";
import Sortable from "sortablejs";
import { Operation } from "@element-plus/icons-vue";
interface TypeItem {
id: string;
name: string;
enabled: boolean;
sortOrder: number;
}
const typeData = ref<TypeItem[]>([
{ id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
{ id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
{ id: "3", name: "楼宇性质3", enabled: false, sortOrder: 3 },
]);
const filterStatus = ref<"all" | "enabled" | "disabled">("all");
const typeTableRef = ref<InstanceType<typeof ElTable>>();
let sortableInstance: Sortable | null = null;
const filteredTypeData = computed(() => {
if (filterStatus.value === "all") return typeData.value;
if (filterStatus.value === "enabled") {
return typeData.value.filter(item => item.enabled);
}
return typeData.value.filter(item => !item.enabled);
});
const initSortable = () => {
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
nextTick(() => {
const tbody = typeTableRef.value?.$el?.querySelector(
".el-table__body-wrapper tbody"
);
if (!tbody) return;
sortableInstance = Sortable.create(tbody, {
handle: ".drag-handle",
animation: 300,
onEnd: ({ newIndex, oldIndex }) => {
if (
newIndex !== undefined &&
oldIndex !== undefined &&
filterStatus.value === "all"
) {
const movedItem = typeData.value[oldIndex];
typeData.value.splice(oldIndex, 1);
typeData.value.splice(newIndex, 0, movedItem);
typeData.value.forEach((item, index) => {
item.sortOrder = index + 1;
});
}
}
});
});
};
onMounted(() => {
setTimeout(() => initSortable(), 300);
});
watch(filterStatus, () => {
setTimeout(() => initSortable(), 100);
});
</script>
<style scoped>
.drag-handle {
color: #909399;
cursor: move;
font-size: 18px;
}
.drag-handle:hover {
color: #1890ff;
}
</style>
通过本文的介绍,我们实现了一个完整的表格拖拽排序功能。关键点包括:
这个方案可以轻松应用到其他需要拖拽排序的场景,如菜单管理、分类排序等。希望本文对您有所帮助!