最近在做一个拖拽实现配置移动端前端模块的东西,看下最终效果把,效果如下
复盘下流程, 基于vuedraggable插件,市面上的前端拖拽大多都是基于Sortable.js,总结下几个问题
- 拖拽起的样式问题 (拖起来那个浅黑色的样式)
- 拖拽放在目标区域的样式问题 (拖起来放在目标区域的样式)
- 被拖拽区域的的停靠问题 (本场景:被拖拽区域只能移动自身,不能和被拖拽区域其他模块互换位置)
- 目标拖拽区域的动画
- 目标拖拽区域无法停放 (目标拖拽区域高度务必不能为0,建议设置个min-height)
vuedraggable使用文档,各种拖拽的例子Demo, github, 非Vue拖拽文档
第一步: 安装 npm i vuedraggable -s
第二步: 看代码,已经写了注释,主要是move方法和样式,本想专门写个demo,做的匆忙,代码直接供上,仅供参考哈哈哈
<template>
<div class="container-content">
<el-card shadow="nerver"
style="border:0px;">
<el-row>
<!-- 左侧选组件区域 -->
<el-col :span="3">
<el-row v-for="(compontent,i) in components_names"
:key="i">
<el-row :style="{marginBottom:'10px',marginTop:i!=0?'10px':''}">
<span style="font-weight:bold">{{compontent.parentName}}</span>
</el-row>
<div>
<draggable :list="compontent.childrens"
chosenClass="drag-chosen"
:group="{ name: 'people', pull: 'clone', put: false }"
class="parent-flex"
:move="move"
@change="log">
<div v-for="(item,cIndex) in compontent.childrens"
:key="cIndex"
class="layout-flex">
<div class="component-item">
<div>
{{item.components_name}}
</div>
<div :class="['components-icon',item.icon] "></div>
</div>
</div>
</draggable>
</div>
</el-row>
</el-col>
<!-- 中间可选区域 -->
<el-col :span="15"
class="layout-h layout-app">
<div class="layout-app-content">
<div class="layout-app-drag">
<!-- 渲染组件 -->
<!-- 注意,很重要:drag-chosen是放在目标区域的占位样式,此处两个模块需保持一致,确保拖起来和快放下预览的时候的样式一致哈, drag-ghost,拖起来的样式,为了好看,被拖区域没给,拖拽目标区域给了透明度为0,效果就是在从被拖到目标区域还是有样式的,但是目标区域拖动就什么都没有了-->
<draggable v-model="dataForm.list"
animation="300"
chosenClass="drag-chosen"
ghostClass="drag-ghost"
group="people"
:scroll="true"
handle=".mover"
class="drag-out-class"
@add="add"
@end="end"
@choose="choose"
@change="update">
<div v-for="(e,index) in dataForm.list"
:key="index"
class="layout-app-item">
<div class="layout-app-select"
v-if="index==curIndex">
</div>
<div v-if="e.type=='search'"
class="mover">
<templateSearch :searchData="e"></templateSearch>
</div>
<div v-if="e.type=='swiper'"
class="mover">
<templateSwiper :swiperData="e"></templateSwiper>
</div>
<div v-if="e.type=='grid'"
class="mover">
<templateGrid :gridData="e"></templateGrid>
</div>
<div v-if="e.type=='ads'"
class="mover">
<templatePhoto :photoData="e"></templatePhoto>
</div>
<div v-if="e.type=='course'"
class="mover">
<templateCourse :courseData="e"></templateCourse>
</div>
<div v-if="e.type=='live'"
class="mover">
<templateLive :courseData="e"></templateLive>
</div>
<div v-if="e.type=='activity'"
class="mover">
<templateAct :courseData="e"></templateAct>
</div>
<div v-if="e.type=='card'"
class="mover">
<tamplateCard :courseData="e"></tamplateCard>
</div>
</div>
</draggable>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
import draggable from "vuedraggable"; // 文档 https://www.itxst.com/vue-draggable/ufjv2i7j.html
import templateSearch from "./dymic-view/search"
import templateSwiper from "./dymic-view/swiper"
import templateGrid from "./dymic-view/grid"
import templatePhoto from "./dymic-view/photo"
import templateCourse from "./dymic-view/course"
import templateLive from "./dymic-view/live"
import templateAct from "./dymic-view/act"
import tamplateCard from "./dymic-view/card"
import cloneDeep from 'lodash/cloneDeep'
export default {
components: {
draggable,
dialogAds,
templateSearch,
templateSwiper,
templateGrid,
templatePhoto,
templateCourse,
templateLive,
templateAct,
tamplateCard
},
data () {
return {
curIndex: -1, // 默认选中的标的
// 左侧展示的组件
components_names: [
{
parentName: '基础组件',
childrens: [
{
components_name: '轮播图',
type: 'swiper',
icon: 'el-icon-picture-outline',
sub_list: []
},
{
components_name: '导航栏',
type: 'grid',
icon: 'el-icon-menu',
limit_line: 4, // 展示风格 1行几个
sub_list: [
{
url: "",
title: "导航1",
img_cover: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/fa68773abd7e4d11bd3f5c597062408c.png"
},
{
url: "",
title: "导航2",
img_cover: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/d14174ad8c424071a6e9b8168187beec.png"
},
{
url: "",
title: "导航3",
img_cover: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/72af111adafb459c852b8b144716c443.png"
}, {
url: "",
title: "导航4",
img_cover: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/bfcb5b97dee143b6a06fa1d62c16f1fb.png"
}
]
},
{
components_name: '搜索',
type: 'search',
icon: 'el-icon-search',
title: '搜索',
},
{
components_name: '广告位',
type: 'ads',
icon: 'el-icon-collection-tag',
style: '', // 展示风格
sub_list: []
}
]
},
{
parentName: '课程组件',
childrens: [
{
components_name: '直播',
type: 'live',
icon: 'el-icon-video-camera',
sub_list: [], // 列表
title: '直播名',
show_title: true, // 展示标题
check_all: true, // 查看素有
list_style: "list", //list small,,big 列表 小 大
from: 1, // 1/2 auto, hand 自动规则还是手动
},
{
components_name: '课程',
type: 'course',
icon: 'el-icon-data-line',
sub_list: [], // 列表
title: '课程名',
show_title: true, // 展示标题
check_all: true, // 查看素有
list_style: "list", //list small,,big 列表 小 大
from: 1, // 1/2 auto, hand 自动规则还是手动
},
{
components_name: '活动',
type: 'activity',
icon: 'el-icon-guide',
sub_list: [], // 列表
title: '活动名',
show_title: true, // 展示标题
check_all: true, // 查看素有
list_style: "list", //list small,,big 列表 小 大
from: 1, // 1/2 auto, hand 自动规则还是手动
},
]
}, {
parentName: '助学组件',
childrens: [
{
components_name: '打卡',
type: 'card',
icon: 'el-icon-place',
sub_list: [], // 列表
title: '打卡名',
show_title: true, // 展示标题
check_all: true, // 查看素有
list_style: "list", //list small,,big 列表 小 大
from: 1, // 1/2 auto, hand 自动规则还是手动
},
]
},
],
dataForm: {
list: [
],
}
}
},
methods: {
// 拖拽完成
add: function (e) {
const { type, newIndex } = e
if (type == 'add') {
this.curIndex = newIndex
}
},
// 拖拽结束
end: function (e) {
const { type, newIndex } = e
if (type == 'end') {
this.curIndex = newIndex
}
},
// 拖起时刻
choose: function (e) {
const { type, oldIndex } = e
if (type == 'choose') {
this.curIndex = oldIndex
}
},
// 限制拖拽区域,再次仅限制被拖拽区域互换位置
move: function (e) {
// 通过此回调解决拖拽停靠问题
const { to, from, draggedContext, relatedContext } = e
if (to && to.className == 'drag-out-class') {
return true
} else {
if (relatedContext.index == null || relatedContext.index == undefined) {
return true
}
if (draggedContext.index == relatedContext.index) {
return true
} else {
return false
}
}
},
// 数据更新
update: function (e) {
// 这个是我们的业务场景
this.dataForm.list = cloneDeep(this.dataForm.list)
},
},
}
</script>
<style scoped lang="scss">
$-border-color: #409eff;
$-page-height: 710px;
//----- 左侧样式-----
.parent-flex {
display: flex;
flex-wrap: wrap;
.layout-flex {
display: flex;
justify-content: center;
align-items: center;
width: 50%;
}
}
.layout-h {
height: $-page-height;
}
.component-item {
// 此时的component-item给宽度,避免拖拽到目标区域形成flex-warp的bug,拖到目标区域没放下的时刻这个component-item就是layout-flex的字节点,layout-flex是flex布局,所以component-item要给宽度,确保独占一行,不用flex,用float同理
width: 90%;
max-width: 72px;
height: 72px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
cursor: grab;
&:hover {
background-color: $-border-color;
color: #ffffff;
}
}
.components-icon {
font-size: 24px;
}
//----- 右侧样式-----
.layout-app {
background: #edf0f1;
height: $-page-height;
overflow-y: scroll;
display: flex;
justify-content: center;
}
.layout-app-content {
margin-top: 20px;
width: 375px;
}
.layout-app-drag {
width: 375px;
min-height: 603px;
background: #f5f6f9;
box-shadow: 0px 1px 3px 1px #0000000d;
}
.drag-out-class {
min-height: 603px;
.layout-flex {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.drag-chosen {
// border: 1px dashed $-border-color;
position: relative;
z-index: 2;
outline: 1px solid $-border-color;
color: $-border-color;
background: #ffffff;
opacity: 0;
.layout-app-select {
// 默认高亮标的边框,drag-chosen之下,避免两个border重叠
display: none;
}
}
.drag-ghost {
// 拖起的时候阴影直接隐藏
opacity: 1;
}
}
.mover {
// 调高层级,确保拖拽的模块可不被遮住
position: relative;
z-index: 1;
}
// 目标区域默认高亮其中一个选中的模块
.layout-app-select {
position: absolute;
border: 1px solid $-border-color;
width: 100%;
height: 100%;
}
.layout-app-item {
position: relative;
margin-bottom: 10px;
background-color: #ffffff;
cursor: move;
}
</style>