拖拽排序示例
使用 SortableJS 实现看板拖拽排序功能。
功能特性
- 🎯 拖拽排序 - 支持列表拖拽排序
- 📋 看板视图 - 支持多列看板拖拽
- 🔧 跨列拖拽 - 支持在不同列之间移动
- 💾 数据同步 - 拖拽后自动同步数据
安装依赖
bash
npm install sortablejs基础用法
vue
<template>
<ul ref="listRef" class="sortable-list">
<li v-for="item in list" :key="item.id" class="sortable-item">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Sortable from 'sortablejs'
const listRef = ref()
const list = ref([
{ id: 1, name: '任务 1' },
{ id: 2, name: '任务 2' },
{ id: 3, name: '任务 3' }
])
onMounted(() => {
Sortable.create(listRef.value, {
animation: 150,
onEnd: (evt) => {
const { oldIndex, newIndex } = evt
// 更新数据顺序
const item = list.value.splice(oldIndex, 1)[0]
list.value.splice(newIndex, 0, item)
}
})
})
</script>
<style scoped>
.sortable-list {
list-style: none;
padding: 0;
}
.sortable-item {
padding: 15px;
margin: 10px 0;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: move;
}
.sortable-item:hover {
background: #e8e8e8;
}
.sortable-ghost {
opacity: 0.5;
background: #c6ebff;
}
.sortable-drag {
opacity: 0.9;
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>完整示例 - 看板
vue
<template>
<div class="page-container">
<div class="toolbar">
<h2 class="page-title">看板示例</h2>
<el-button type="primary" @click="addTask">添加任务</el-button>
</div>
<div class="kanban-board">
<div
v-for="column in columns"
:key="column.id"
class="kanban-column"
>
<div class="column-header">
<h3>{{ column.name }}</h3>
<span class="task-count">{{ column.tasks.length }}</span>
</div>
<div
:ref="el => setColumnRef(el, column.id)"
class="task-list"
:data-column-id="column.id"
>
<div
v-for="task in column.tasks"
:key="task.id"
class="task-card"
:data-task-id="task.id"
>
<div class="task-title">{{ task.title }}</div>
<div class="task-meta">
<el-tag size="small" :type="getPriorityType(task.priority)">
{{ task.priority }}
</el-tag>
<span class="task-date">{{ task.date }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import Sortable from 'sortablejs'
import { ElMessage } from 'element-plus'
const columns = ref([
{
id: 'todo',
name: '待处理',
tasks: [
{ id: '1', title: '设计首页 UI', priority: '高', date: '2024-02-15' },
{ id: '2', title: '编写接口文档', priority: '中', date: '2024-02-16' }
]
},
{
id: 'doing',
name: '进行中',
tasks: [
{ id: '3', title: '开发用户模块', priority: '高', date: '2024-02-14' }
]
},
{
id: 'done',
name: '已完成',
tasks: [
{ id: '4', title: '项目初始化', priority: '低', date: '2024-02-10' }
]
}
])
const columnRefs = ref({})
const setColumnRef = (el, columnId) => {
if (el) {
columnRefs.value[columnId] = el
}
}
const getPriorityType = (priority) => {
const types = { '高': 'danger', '中': 'warning', '低': 'info' }
return types[priority] || 'info'
}
onMounted(() => {
nextTick(() => {
initSortable()
})
})
const initSortable = () => {
columns.value.forEach(column => {
const el = columnRefs.value[column.id]
if (!el) return
Sortable.create(el, {
group: 'kanban', // 允许跨列拖拽
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
delay: 0,
onEnd: (evt) => {
const { from, to, oldIndex, newIndex } = evt
const fromColumnId = from.dataset.columnId
const toColumnId = to.dataset.columnId
// 找到对应的列
const fromColumn = columns.value.find(c => c.id === fromColumnId)
const toColumn = columns.value.find(c => c.id === toColumnId)
if (fromColumn && toColumn) {
// 移动任务
const [task] = fromColumn.tasks.splice(oldIndex, 1)
toColumn.tasks.splice(newIndex, 0, task)
ElMessage.success(`任务移动到: ${toColumn.name}`)
}
}
})
})
}
const addTask = () => {
const newTask = {
id: Date.now().toString(),
title: '新任务 ' + Date.now(),
priority: '中',
date: new Date().toISOString().split('T')[0]
}
columns.value[0].tasks.push(newTask)
ElMessage.success('任务已添加')
}
</script>
<style scoped>
.page-container {
padding: 20px;
height: calc(100vh - 60px);
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.kanban-board {
display: flex;
gap: 20px;
height: calc(100% - 60px);
overflow-x: auto;
}
.kanban-column {
width: 300px;
min-width: 300px;
background: #f5f5f5;
border-radius: 8px;
display: flex;
flex-direction: column;
}
.column-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.column-header h3 {
margin: 0;
font-size: 16px;
}
.task-count {
background: #ddd;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
}
.task-list {
flex: 1;
padding: 10px;
overflow-y: auto;
}
.task-card {
background: #fff;
padding: 15px;
margin-bottom: 10px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: move;
}
.task-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.task-title {
font-weight: 500;
margin-bottom: 10px;
}
.task-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-date {
font-size: 12px;
color: #999;
}
.sortable-ghost {
opacity: 0.5;
background: #c6ebff;
}
.sortable-drag {
opacity: 0.9;
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>配置选项
javascript
Sortable.create(el, {
// 分组,相同 group 的元素可以互相拖拽
group: 'name',
// 动画时间(毫秒)
animation: 150,
// 拖拽时的样式类
ghostClass: 'sortable-ghost',
// 拖拽元素的样式类
dragClass: 'sortable-drag',
// 选择器,指定哪些元素可以拖拽
draggable: '.item',
// 选择器,指定拖拽手柄
handle: '.drag-handle',
// 延迟拖拽(毫秒)
delay: 0,
// 是否禁用
disabled: false,
// 拖拽结束回调
onEnd: (evt) => {
console.log('from:', evt.from)
console.log('to:', evt.to)
console.log('oldIndex:', evt.oldIndex)
console.log('newIndex:', evt.newIndex)
}
})事件
javascript
Sortable.create(el, {
// 元素开始拖拽
onStart: (evt) => {},
// 元素结束拖拽
onEnd: (evt) => {},
// 元素被添加到新列表
onAdd: (evt) => {},
// 元素在新列表中更新位置
onUpdate: (evt) => {},
// 元素被移除
onRemove: (evt) => {},
// 元素在列表中排序变化
onSort: (evt) => {},
// 元素在列表间移动
onMove: (evt, originalEvent) => {}
})