vue拖拽实现自定义拖拽定义移动端

1,161 阅读1分钟

最近在做一个拖拽实现配置移动端前端模块的东西,看下最终效果把,效果如下

移动端.gif

复盘下流程, 基于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>