最终实现效果如上图所示。

实例地址:http://vue-admin-box.51weblove.com/#/pages/work

接下来就说一说如何实现这个功能吧,需要用的库:sortablejs,这个库可以实现多个列表之间的dom位置互换。

在Vue中,咱们实现的原理如下:

  1. Vue先把数据渲染到页面上
  2. 利用sortablejs操作真实dom,得到列表,sortablejs 核心就是操作真实dom,这时候由于vue本身数据未更新,所以也不会触发dom的重新渲染,这就导致了vue里面的数据与真实dom的数据是对不上的,所以有了第3步
  3. 利用sortablejs的onEnd方法把数据变化映射到真实的vue的数据源list上,实现拖拽时dom变化是对应的

第一步,要实现这么一个列表,根据模块化利用的思想,我们需要把页面切分成三个组件:

  1. index.vue组件,负责整个页面的可拖拽数据的管理,并负责分发至block组件
  2. block.vue组件,负责单个list模块的管理,如:待处理/处理中/待部署等页面的处理
  3. item.vue组件,负责单个可拖动模块的管理,如上述的复制指令、拖拽指令等。

先来看看数据源吧,我就Copy一份简洁的数据放置于此供大家参考。

list: [
  {
    name: '待处理',
    children: [
      {
        id: 1,
        tags: ['新增'],
        name: '拖拽指令',
        options: [
          '类型:页面'
        ]
      },{
        id: 2,
        tags: ['新增'],
        name: '复制指令',
        options: [
          '类型:页面'
        ]
      },{
        id: 3,
        tags: ['新增'],
        name: '防抖指令',
        options: [
          '类型:页面'
        ]
      }]
  },
  {
    name: '处理中',
    children: [{
      id: 16,
      tags: ['新增'],
      name: '页面-工作进度',
      options: [
        '类型:页面'
      ]
    },{
      id: 17,
      tags: ['新增'],
      name: '页面-详情页面',
      options: [
        '类型:页面'
      ]
    }]
  },
  {
    name: '待部署',
    children: []
  },
  {
    name: '测试中',
    children: []
  },
  {
    name: '已完成',
    children: [{
      id: 18,
      tags: ['系统'],
      name: '路由管理router',
      options: [
        '类型:系统'
      ]
    },{
      id: 19,
      tags: ['系统'],
      name: '存储方案store',
      options: [
        '类型:系统'
      ]
    }]
  }
]

index组件简介

我们通过接口获取数据以及渲染多个block组件的任务就放置于index.vue文件中,核心代码如下:

<template>
  <div class="list">
    <Block v-for="(block, key) in list" :key="key" :data="block" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import Block from './block.vue' // block组件的引入
import { getData } from '@/api/work' // 接口函数的引入
export default defineComponent({
  components: {
    Block
  },
  setup() {
    let list = ref([]) // 实现list数据变化的监听
    const getList = () => { // 通过接口拿到list对应的数据
      let params = {}
      getData(params)
      .then(res => {
        list.value = res.data.list
      })
    }
    getList()
    return {
      list
    }
  }
})
</script>

<style lang="scss" scoped>
// 用于处理SCSS的样式代码,使用其他的如less,postcss类的自行处理即可
</style>

Block组件简介

然后,我们就需要对block组件进行处理,使得它里面的item可以在各个组件中拖拽,现在我们先假定item已经写好了,原则上应该是先把所有的样式写好了,再进行block拖拽的实现,为了方便大家理解,我就把整个block组件内的功能先提前大体说一遍。

查看页面,需要显示的信息有,当前block的名称,可拖拽数据的条数,以及具体的item数据。到当前为止,我们就实现了Block页面的基本布局。

下一步,我们就需要实现各个Item可以自由拖拽到各个模块,利用vue的ref属性获取dom给sortable进行绑定,通过 new Sortable(dom.value, { group: ‘shard’, …  } 把所有的列表都绑定到 Sortable的某个叫”shard”的组里面,虽然我们每一个都是new 的Sortable,但是Sortable他本身是存储了所有的实例的,Sortable也能访问到所有的实例,故能实现多个列表之间的拖拽。到现在我们就已经实现了第二步,实现真实Dom之间的相互拖拽换位置

第三步,让真实Dom的变化映射至vue的数据变化上,再由Vue映射至虚拟Dom上,虚拟Dom这时候会去检测真实Dom的状态,以便触发不同的更新,所以我们需要使用item.id来标记每个项的不同,供虚拟Dom来检测变化,所以我们需要使用item.id,而不是v-for产生的key值,因为每个列表的Key值都有0,1,2,只有使用id才能达到此效果,id在所有的data.children属性上都是唯一的,这也是实现映射的前提条件。

数据交互理念

  1. 我们sortable的实例化是在block组件中,但是我们的数据源却是在index组件中,我们进行常规的vue操作是需要进入到index组件中去进行数据的交互处理的,但显然,这样很麻烦,也会产生很多查询位置的开销,所以我们需要另一种思路来实现
  2. 我们每个block组件都会有当前列表的数据源对吧,然后看了下sortable的onEnd函数中,它返回了一个target,和一个to,还有一个pullMode,pullMode用于判断是同列表的位置互换还是两个列表之间的位置互换,target对应的一开始的列表dom对象,to对应拖拽结束后的列表dom对象
  3. 我们把当前的List挂载到dom上,然后通过onEnd函数的dom对象去访问list数据,这样就可以在一个单一的Block组件内,访问到最终的那个Block组件的list,这时候我们就拿到了两个list对象
  4. onEnd函数的Evt参数,里面还包含了,一个oldIndex和一个newIndex,这分别对应拖拽前和拖拽后,对应的真实dom的位置,这时候利用pullMode对应到相应的List里面进行一个数据之间的替换操作即可实现拖拽前后真实dom与vue数据源的数据响应同步操作。

完整代码如下:

<template>
  <div class="block">
    <div class="header">
      <span class="num">{{ data.children.length }}</span>
      <div>{{ data.name }}</div>
    </div>
    <div class="list" ref="dom">
      <Item v-for="item in data.children" :key="item.id" :data="item" />
    </div>
  </div>
</template>

<script lang="ts">
import type { Ref } from 'vue'
import { defineComponent, ref, onMounted } from 'vue'
import Item from './item.vue'
import Sortable, { CustomEvent } from 'sortablejs'
export default defineComponent({
  components: {
    Item
  },
  props: {
    data: {
      type: Object,
      default: () => {
        return {
          name: '',
          children: []
        }
      }
    }
  },
  setup(props) {
    const dom: Ref<HTMLDivElement> = ref(null) as any
    onMounted(() => {
      dom.value.list = props.data
      new Sortable(dom.value, {
        group: 'shared',
        animation: 150,
        ghostClass: 'blue-background-class',
        onEnd: function(evt: CustomEvent) {
          const pullMode = evt.pullMode
          const oldIndex = evt.oldIndex
          const newIndex = evt.newIndex
          
          let oldList = evt.target.list.children
          let newList = evt.to.list.children
          if (pullMode) { // 移动至toList并去除旧数据
            newList.splice(newIndex, 0, oldList[oldIndex])
            oldList.splice(oldIndex, 1)
          } else { // 同List位置修改
            const tem = oldList[oldIndex]
            oldList[oldIndex] = oldList[newIndex]
            oldList[newIndex] = tem
            console.log(oldList[0])
          }
        }
      })
    })
    return {
      dom
    }
  }
})
</script>

<style lang="scss" scoped>
// block块对应的CSS样式
</style>

Item组件简介

其实对于实现拖拽的功能核心,Item组件内部的操作其实并不重要,因为它只是作为一个可拖拽的项来使用的,我们也不需要关心它内部的实现,它内部的功能仅有数据展示一项,对于扩展如编辑,删除这一类操作时,它才需要进行一定的Vue交互处理,但也不会涉及到sortable的处理。

地址

示例地址:

http://vue-admin-box.51weblove.com/#/pages/work

组件完整源码地址(包含在vue-admin-box源码中):

https://github.com/cmdparkour/vue-admin-box/tree/master/src/views/main/pages/work