Skip to content

拖拽排序示例

使用 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) => {}
})

基于 MIT 许可发布