📒 背景
最近项目中需要制作一个图层拖拽到组的交互(如下图展示),今天分享一下这个组件功能。希望能抛砖引玉,给大家带来启发。
🔍需求功能
- 1.新建组,再新建图层,在组下显示;
- 2.新建组没选中,则图层和组在同级;
- 3.图层拖拽可以在一级也可在组下面;
- 4.组拖拽只能在一级,不能叠加到组内;
- 5.组删除,该组下图层全部删除;
- 6.组点击眼睛,该组下图层全部显示,点击闭眼,该组下图层全部不显示。
👣设计开发
先说一下我的开发环境版本:
node: v11.3.0
npm: 6.4.1
vue:2.5.11
vuedraggable:2.24.0
如果不是以上版本也没关系,今日分享的思路,相信你可以自己造出来~
安装
npm i -S vuedraggable
引入
import draggable from 'vuedraggable'
注册
components:{
draggable
},
使用帮助
group: "name", // or { name: "...", pull: [true, false, clone], put: [true, false, array] } name相同的组可以互相拖动
sort: true, // 内部排序列表
delay: 0, // 以毫秒为单位定义排序何时开始。
touchStartThreshold: 0, // px,在取消延迟拖动事件之前,点应该移动多少像素?
disabled: false, // 如果设置为真,则禁用sortable。
store: null, // @see Store
animation: 150, // ms, 动画速度运动项目排序时,' 0 ' -没有动画。
handle: ".my-handle", // 在列表项中拖动句柄选择器。
filter: ".ignore-elements", // 不导致拖拽的选择器(字符串或函数)
preventOnFilter: true, // 调用“event.preventDefault()”时触发“filter”
draggable: ".item", // 指定元素中的哪些项应该是可拖动的。
ghostClass: "sortable-ghost", // 设置拖动元素的class的占位符的类名。
chosenClass: "sortable-chosen", // 设置被选中的元素的class
dragClass: "sortable-drag", //拖动元素的class。
dataIdAttr: 'data-id',
forceFallback: false, // 忽略HTML5的DnD行为,并强制退出。
fallbackClass: "sortable-fallback", // 使用forceFallback时克隆的DOM元素的类名。
fallbackOnBody: false, // 将克隆的DOM元素添加到文档的主体中。(默认放在被拖动元素的同级)
fallbackTolerance: 0, // 用像素指定鼠标在被视为拖拽之前应该移动的距离。
scroll: true, // or HTMLElement
scrollFn: function(offsetX, offsetY, originalEvent, touchEvt, hoverTargetEl) { ... }, // if you have custom scrollbar scrollFn may be used for autoscrolling
scrollSensitivity: 30, // px, how near the mouse must be to an edge to start scrolling.
scrollSpeed: 10, // px
我的使用如下:封装成了组件,也可以嵌套循环的方式压缩一下哈。这边主要是组和图层的逻辑有点不一样,就懒得嵌套了。
<draggable group="A" handle=".mover" ghost-class="ghost" chosen-class="chosenClass" drag-class="dragClass" :list="realList" :value="value" :move="dragMove" @start="dragStart" @end="dragEnd" @change="dragChange" @input="saveSort">
<!--// eslint-disable-next-line vue/valid-v-for, vue/no-parsing-error-->
<div v-for="item,i in realList" :key="item.id" class="groupBig" :class="{'hover': curId === item.id}">
<div :class="item.isGroup ? 'groupOne':'childOne'">
<div class="groupLeft">
<div class="groupLeftIcon">
<i class="zhiconfont zhiconfont-xiangxia" :class="{'show':item.isClose}" v-show="item.isGroup && item.links.length>0" @click="changeGroupShow(item,i)"></i>
</div>
<div class="groupName mover" @click="selectOne(item)">
<i v-if="item.isGroup===0" class="zknicon" :class="item.icon"></i>
<b v-if="!item.isEdit" :title="item.name">{{ item.name || "组" }}</b>
<input type="text" v-if="item.isEdit" v-model="item.name" size="mini" />
</div>
</div>
<!--图层默认显示操作,组不显示(选中并展开才可操作组)-->
<div class="groupOperation" v-show="((curId === item.id && !item.isClose) || !item.isGroup) && !item.isEdit ">
<span v-if="item.isGroup === 1">
<i class="zhiconfont zhiconfont-yanjing1" v-if="computedVisible(item) && item.links && item.links.length>0" @click="conVisible(item,i,false)"> </i>
<i class="zhiconfont zhiconfont-yanjing" v-if="!computedVisible(item) && item.links && item.links.length>0" @click="conVisible(item,i,true)"> </i>
</span>
<span v-if="item.isGroup === 0">
<i class="zhiconfont zhiconfont-yanjing1" v-if="!item.visible" @click="item.visible = !item.visible"> </i>
<i class="zhiconfont zhiconfont-yanjing" v-if="item.visible" @click="item.visible = !item.visible"> </i>
</span>
<i @click="editGroupSite(item,i)" class="zhiconfont zhiconfont-bianji"></i>
<i @click="deleteGroupSite(item,i)" class="zhiconfont zhiconfont-shanchu btngry"></i>
</div>
<div class="groupOperation smalbg" v-if="item.isEdit">
<i class="zhiconfont zhiconfont-baocun1" @click="saveName(item,i)"> </i>
</div>
</div>
<!--<el-collapse-transition>-->
<!--图层不允许加叶子节点,折叠时也不允许拖拽-->
<draggable group="A" handle=".mover" ghost-class="ghost" chosen-class="chosenClass" drag-class="dragClass" :list="item.links" :disabled="childrenDisabled" v-if="!item.isClose && item.links" :move="dragMove" @start="dragStart" @end="dragEnd" @change="dragChange" @input="saveSort">
<div class="childBig" v-for="itm,n in item.links" :key="itm.id" :class="{'hover': curId === itm.id}">
<div class="groupChildren">
<div class="childName mover" @click="selectOne(itm)">
<i v-if="itm.isGroup===0" class="zhiconfont" :class="itm.icon"></i>
<b v-if="!itm.isEdit" :title="itm.name">{{ itm.name }}</b>
<input type="text" v-if="itm.isEdit" v-model="itm.name" size="mini" />
</div>
<div class="childOperation morebg" v-show="!itm.isEdit">
<i class="zhiconfont zhiconfont-yanjing1" v-if="!itm.visible" @click="itm.visible = !itm.visible"> </i>
<i class="zhiconfont zhiconfont-yanjing" v-if="itm.visible" @click="itm.visible = !itm.visible"> </i>
<i @click="editSonSite(itm)" class="zhiconfont zhiconfont-bianji"></i>
<i @click="deleteSonSite(itm, n, i)" class="zhiconfont zhiconfont-shanchu btngry"></i>
</div>
<div class="childOperation" v-if="itm.isEdit">
<i class="zhiconfont zhiconfont-baocun1" @click="saveName(itm,i)"> </i>
</div>
</div>
</div>
</draggable>
<!--</el-collapse-transition>-->
</div>
</draggable>
本组件设计了2个入参:一个组件返回值或初始化值,一个是图层和组的id
props: {
value: {
required: false,
type: Array,
default: null
},
initId: {
required: false,
type: Number,
default: 1
}
},
具体业务分析(本质就是二叉树)
新增组
// 新增组站点
addGroupSite () {
this.realList.unshift({
name: `G_${this.groupId}`,
id: this.groupId,
visible: true,
isGroup: 1,
isClose: 0,
isEdit: 0,
links: []
})
this.curId = this.groupId
this.curIsGroup = 1
this.groupId++
},
新增叶子
addSite () {
let newItem = {
name: `T_${this.groupId}`,
id: this.groupId,
icon: 'zhiconfont-iconfontwenzi',
isEdit: 0,
visible: true,
isGroup: 0
}
if (this.curIsGroup === 0) {
// 如果当前选择是图层
let cur = this.realList.find(group => {
if (group.isGroup === 1 && group.links.length > 0) {
let res = group.links.find(one => one.id === this.curId)
return res
}
})
if (cur) {
// 如果当前选择图层在组里
cur.isClose = 0 // 添加到组组先展开
cur.links.unshift(newItem)
} else {
// 如果当前选择图层在根目录
this.realList.unshift(newItem)
}
} else if (this.curIsGroup === 1) {
// 如果当前选择是组
let curIndex = this.realList.findIndex(one => one.id === this.curId)
this.$set(this.realList[curIndex], 'isClose', 0) // 添加到组组先展开
this.realList[curIndex].links.unshift(newItem)
} else if (this.curIsGroup === undefined || this.curIsGroup === null) {
// 当前没有选择,则加在根目录
this.realList.unshift(newItem)
}
this.curId = this.groupId
this.curIsGroup = 0
this.groupId++
},
拖拽开始和结束需要特别排除一下组,组不允许拖拽进入组,需求不允许深层嵌套。
// 拖拽开始
dragStart (evt) {
this.curId = null // 拖拽开始选中取消
this.curIsGroup = null // 拖拽开始选中取消
if (evt.item._underlying_vm_.isGroup === 1) {
// 如果isGroup是1则是组,不能拖入组里
this.childrenDisabled = true
}
},
// 拖拽结束
dragEnd (evt) {
// console.log('拖拽结束', evt)
this.curId = evt.item._underlying_vm_.id
this.curIsGroup = evt.item._underlying_vm_.isGroup
// 防止是组禁止了拖拽
this.childrenDisabled = false
this.saveSort() // 提交一下排列
},
更多逻辑代码欢迎体验组件--mycomponentsvue
npm install mycomponentsvue@0.0.13
import mycomponents from 'mycomponentsvue'
Vue.use(mycomponents)
<dragGroup v-model="data"></dragGroup> // data空数组初始化
本组件只用于学习交流哈!所以名字起的比较随意!~
⛳参考学习
如果你还不会vuedraggable的使用,推荐先学习它的配置。
参考vuedraggable官方例子 https://sortablejs.github.io/Vue.Draggable/#/nested-with-vmodel
🚀写在最后
如果本文中有bug、逻辑错误,或者您有更好的优化方案欢迎评论联系我哦!~关注我持续分享日常工作中的组件设计和学习分享,一起进步加油!