最终实现效果如上图所示。
实例地址:http://vue-admin-box.51weblove.com/#/pages/work
接下来就说一说如何实现这个功能吧,需要用的库:sortablejs,这个库可以实现多个列表之间的dom位置互换。
在Vue中,咱们实现的原理如下:
- Vue先把数据渲染到页面上
- 利用sortablejs操作真实dom,得到列表,sortablejs 核心就是操作真实dom,这时候由于vue本身数据未更新,所以也不会触发dom的重新渲染,这就导致了vue里面的数据与真实dom的数据是对不上的,所以有了第3步
- 利用sortablejs的onEnd方法把数据变化映射到真实的vue的数据源list上,实现拖拽时dom变化是对应的
第一步,要实现这么一个列表,根据模块化利用的思想,我们需要把页面切分成三个组件:
- index.vue组件,负责整个页面的可拖拽数据的管理,并负责分发至block组件
- block.vue组件,负责单个list模块的管理,如:待处理/处理中/待部署等页面的处理
- 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属性上都是唯一的,这也是实现映射的前提条件。
数据交互理念
- 我们sortable的实例化是在block组件中,但是我们的数据源却是在index组件中,我们进行常规的vue操作是需要进入到index组件中去进行数据的交互处理的,但显然,这样很麻烦,也会产生很多查询位置的开销,所以我们需要另一种思路来实现
- 我们每个block组件都会有当前列表的数据源对吧,然后看了下sortable的onEnd函数中,它返回了一个target,和一个to,还有一个pullMode,pullMode用于判断是同列表的位置互换还是两个列表之间的位置互换,target对应的一开始的列表dom对象,to对应拖拽结束后的列表dom对象
- 我们把当前的List挂载到dom上,然后通过onEnd函数的dom对象去访问list数据,这样就可以在一个单一的Block组件内,访问到最终的那个Block组件的list,这时候我们就拿到了两个list对象
- 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
大佬,在vue2中怎么实现该功能,js怎么实现