Vue 实现可视化拖拽页面编辑器


Posted in Vue.js onFebruary 01, 2021

在线地址 (用梯子会更快些)

可视化页面编辑器,听起来可望不可即是吧,先来张动图观摩观摩一番!

Vue 实现可视化拖拽页面编辑器

实现这功能之前,在网上参考了很多资料,最终一无所获,五花八门的文章,都在述说着曾经的自己!

那么,这时候就需要自己去琢磨了,如何实现?

需要考虑到:

  • 拖拽的实现
  • 数据结构的定义
  • 组件的划分
  • 可维护性、扩展性

对象的引用:在这里是我感觉最酷的技巧了,来一一讲解其中的细节吧!!

拖拽实现

拖拽事件

这里使用 H5的拖拽事件 ,主要用到:

dragstart // 开始拖拽一个元素时触发
draggable // 指定可被拖拽的元素
dragend  // 当拖拽操作结束时触发
dragover // 当拖拽元素在可释放目标上移动时触发
drop  // 当拖拽元素在可释放目标上被释放时触发

我们来看看怎么使用这些事件:

<!-- 拖拽元素列表数据 -->
<script>
// com 为对应的视图组件,在释放区域显示
typeList: {
 banner: {
  name: '轮播图',
  icon: 'el-icon-picture',
  com: Banner
 },
 product: {
  name: '商品',
  icon: 'el-icon-s-goods',
  com: Product
 },
 images: {
  name: '图片',
  icon: 'el-icon-picture',
  com: Images
 },
}
</script>
<!-- 拖拽元素 -->
<ul 
 @dragstart="dragStart"
 @dragend="dragEnd"
>
 <li 
  v-for="(val, key, index) in typeList"
  draggable 
  :data-type="key"
  :key="index + 1"
 >
  <span :class="val.icon"></span>
  <p>{{val.name}}</p>
 </li>
</ul>
<!-- 释放区域 -->
<div 
 class="view-content"
 @drop="drog"
 @dragover="dragOver"
>
</div>

Vue 实现可视化拖拽页面编辑器

拖拽开始

定义一个变量type,用于触发拖拽事件开始的时候,确定当前拖拽元素是哪种类型,比如:产品、广告图...

// 拖拽类型
dragStart(e) {
 this.type = e.target.dataset.type
}

确定类型后,再进入下一步的释放区域

在释放区域中移动

移动的过程中,需要根据鼠标位置实时计算拖拽元素的位置,具体可往下翻预览动图效果!

// 'view-content': 外层盒子的class,直接 push
// 'item': 盒子内部的元素,需计算位置,进行变换操作
dragOver() {
 let className = e.target.className
 let name = className !== 'view-content' ? 'item' : 'view-content'

 // 组件的默认数据
 const defaultData = {
  type: this.type, // 组件类型
  status: 2,   // 默认状态
  data: [],   // 基本数据
  options: {}   // 其他操作
 }

 if (name == 'view-content') {
  //...
 } else if (name == 'item') {
  //...
 }
}

边界处理、角度计算

核心变量:

  • isPush:拖拽元素是否已push到页面数据中
  • index:拖拽元素最终的索引值
  • curIndex:鼠标所在位置的元素的索引值
  • direction:鼠标所在元素的上/下部分

name=='view-content',说明拖拽元素处于外层且空白的可释放区域,如果未添加状态,直接push即可

if (name == 'view-content') {
 if (!this.isPush) {
  this.index = this.view.length
  this.isPush = true
  this.view.push(defaultData)
 }
}

当 name=='item',也就是在已有元素的上方,则需要计算位置,上/下方,添加 or 移动

if (name == 'item') {
 let target = e.target
 let [ y, h, curIndex ] = [ e.offsetY, target.offsetHeight, target.dataset.index ]
 let direction = y < (h / 2) // 计算鼠标处于当前元素的位置,来决定拖拽元素的上/下

 if (!this.isPush) {
  // first
  if (direction) {
   if (curIndex == 0) {
    this.view.unshift(defaultData)
   } else {
    this.view.splice(curIndex, 0, defaultData)
   }
  } else {
   curIndex = +curIndex + 1
   this.view.splice(curIndex, 0, defaultData)
  }
 } else {
  // Moving
  if (direction) {
   var i = curIndex == 0 ? 0 : curIndex - 1
   var result = this.view[i]['status'] == 2
  } else {
   var i = +curIndex + 1
   var result = this.view.length > i && this.view[i]['status'] == 2
  }
  
  // 拖拽元素是否需变换位置
  if (result) return

  const temp = this.view.splice(this.index, 1)
  this.view.splice(curIndex, 0, temp[0])
 }
 this.index = curIndex // 拖拽元素位置
 this.isPush = true  // 进入则push,即true
}
  • first:未 push,则根据当前 index 和 direction 来决定拖拽元素的位置
  • Moving:已 push 且移动状态,根据当前 index 和 direction 来找出对应值的状态,是否为拖拽元素,是则 return,否则变换位置

总结一下:获取当前鼠标所在的元素索引,再计算鼠标是在元素的上半部分还是下半部分,以此推断出拖拽元素的位置!!!

Vue 实现可视化拖拽页面编辑器

小问题:

上面的 name=='item',Event 事件需要阻止默认事件,避免 target 为内层元素,导致无法计算位置,但是只用事件阻止在这里行不通,也不知道为啥,需要把 .item 的所有子元素加上 pointer-events: none 的属性才行!

e.preventDefault()
e.stopPropagation()

.item div{
 pointer-events: none;
}

拖拽结束

即松开鼠标、或离开释放区域,则恢复默认状态。

这里的status有什么作用呢

  1. 上方的计算规则,用于判断元素是否为拖拽元素。
  2. 页面的显示方式,拖拽的时候只显示组件名,释放之后才恢复正常显示内容。
// 结束拖拽
dragEnd(e) {
 this.$delete(this.view[this.index], 'status')
 this.isPush = false
 this.type = null
},
// 已放置到指定位置
drog(e) {
 e.preventDefault()
 e.stopPropagation()
 this.dragEnd()
},

内容块拖拽实现

因时间关系,这里偷懒了,使用了一个较为完美的列表拖拽插件 Vue.Draggable (star 14.2k)

研究了一会儿,其逻辑和上方实现的拖拽有一定联系,具体实现方法大同小异,相信有了上面的实战案例,你也就能做出来了!

要不,你动手试试?

可以根据Vue.Draggable的使用方式,来实现一个拖拽组件,具体会用到(drag、slot、DOM)等操作

(后面有时间,我再回来封装一个)

组件划分

中间视图组件,右边编辑组件,为一套一套的,果然是一套一套的,不愧是有一套一套的!

page=>index 则管理着整个页面的内容

.
├── components
| ├── Edit   ## 右边编辑
| | ├── Info  # 基本信息
| | ├── Image  # 广告图
| | ├── Product # 商品
| | └── Index  # 管理编辑组件的信息
| └── View   ## 中间视图
| | ├── Banner  # 轮播图
| | ├── Images  # 广告图
| | └── Product # 产品列表
└── page
 └── index   ## 主页面

 

想要实现预览页面的效果,直接使用 components=>View 下面的组件即可,与 page=>index 使用方法一致,无需过多修改!

数据结构的定义

实现一个鲜艳且具有扩展性的功能,那么定义一个符合条件的数据结构是必不可少的!与此同时也能决定你的代码质量!

当然,还是得由自身所学和逻辑思维来决定!

这里最为亮眼的处理方式:借用对象的关系,使得组件之间的传值,只需单向传输一次!

view: [
 {
  type: 'info',
  title: '页面标题',
  remarks: '页面备注',
  backgroundColor: 'red'
 },
 {
  type: 'banner',
  data: [
   { url: '1.jpg', name: '轮播图1', link: 'https://轮播图跳转地址.cn' },
   { url: '2.jpg', name: '轮播图2', link: 'https://轮播图跳转地址.cn' }
  ]
 },
 {
  type: 'images',
  data: [
   { url: '1.jpg', name: '广告图1', link: 'https://广告图跳转地址.cn' },
   { url: '2.jpg', name: '广告图2', link: 'https://广告图跳转地址.cn' }
  ]
 },
 {
  type: 'product',
  data: [
   { id: '1', name: '商品1', image: '1.jpg' }, 
   { id: '2', name: '商品2', image: '2.jpg' }
  ],
  options: {
   originalPrice: true, // 划线价
   goodRatio: true,  // 好评率
   volumeStr: false,  // 销量数
  }
 }
]

就是一个数组,数组的item代表着一个模块

  • type:模块类型
  • data:基本信息
  • options:其他操作

....可参考原有组件模块,按照需求去自行扩展等操作

编辑组件的传值

选择视图组件的时候,把view里面指定的item对象作为参数传递给编辑组件!

对象是指向同一个内存地址的,存在着一种引用关系,只需修改一次即可实现多方位的数据更新!

<section class="r">
 <EditForm
  :data="props"
  v-if="isRight"
 ></EditForm>
</section>
<script>
// 切换视图组件
selectType(index) {
 this.isRight = false
 this.props = this.view[index]
 this.$nextTick(() => this.isRight = true)
}
</script>

图片上传

刚好上面有图片上传组件,这里分享一下我的使用技巧!!

使用 Element-ui 自带上传组件的朋友,看过来(敲黑板)

我们先来实现一个简约版的:

<!-- 禁用所有默认方法 -->
<el-upload
 :http-request="upload"
 :show-file-list="false"
 multiple
 action
>
 <img :src="item.url" v-for="(item, index) in list" :key="index">
</el-upload>
<script>
upload(params) {
 const file = params.file;
 const form = new FormData();
 form.append("file", file);
 form.append("clientType", "multipart/form-data");

 const index = this.imageIndex // 编辑图片的索引
 const data = { 
  url: URL.createObjectURL(file), 
  form
 }
 if (index !== null) {
  // this.list => 图片集合
  this.$set(this.list, index, data)
 } else {
  this.list.push(data)
 }
}
</script>
  • 重写上传方法
  • 使用 URL.createObjectURL(file) 创建一个本地预览的地址
  • 把 form 对象存起来,提交时再上传
// 根据上面的代码,使用Promise实现上传功能
const request = []
this.list.forEach(item => {
 request.push(
  new Promise((resolve, reject) => {
   /**
    * 上传接口
    * 替换原 url
    * 删除 form
    */
   imageUpload(item.form).then(res => {
    item.url = res.data.url
    delete item.form
    resolve(res)
   }).catch(err => {
    reject(err)
   })
  })
 )
})
Promise.all(request).then(res => {
 // ... submit ...
})

等到最后一步提交数据的时候,再上传所有的图片,上传完成之后再去调用提交数据的接口!!

在有表单多数据提交的场景下,这才是最正确的做法!

最后总结

其实并不复杂,重点在于数据结构的定型、组件交互的处理、逻辑方式等规划,只要这一步核心的点实现了。

其他的,例如新增组件、新增操作等等扩展性的操作,剩下的问题已不再是问题!

这只能算是一个简版,可按照需求,去优化、去琢磨、去完善,吸收成为自己的知识!

至少我已经满足了工作上的需求了,哇哈哈哈哈哈~~~

更多的细节,欢迎查看源码,Github 地址献上,感谢您的 star,我是不吃茶的李白。

以上就是Vue 实现可视化拖拽页面编辑器的详细内容,更多关于Vue 可视化拖拽页面编辑器的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
详解vue 组件注册
Nov 20 Vue.js
Vue用mixin合并重复代码的实现
Nov 27 Vue.js
vue组件中节流函数的失效的原因和解决方法
Dec 02 Vue.js
vue 基于abstract 路由模式 实现页面内嵌的示例代码
Dec 14 Vue.js
vue实现登录、注册、退出、跳转等功能
Dec 23 Vue.js
vue3.0中友好使用antdv示例详解
Jan 05 Vue.js
vuex的使用和简易实现
Jan 07 Vue.js
开发一个封装iframe的vue组件
Mar 29 Vue.js
Vue中插槽slot的使用方法与应用场景详析
Jun 08 Vue.js
vue-router中hash模式与history模式的区别
Jun 23 Vue.js
SSM VUE Axios详解
Oct 05 Vue.js
Vue.js中v-for指令的用法介绍
Mar 13 Vue.js
vue-video-player 断点续播的实现
Feb 01 #Vue.js
Vite和Vue CLI的优劣
Jan 30 #Vue.js
如何使用RoughViz可视化Vue.js中的草绘图表
Jan 30 #Vue.js
vue监听键盘事件的相关总结
Jan 29 #Vue.js
Vue 实例中使用$refs的注意事项
Jan 29 #Vue.js
vue 项目@change多个参数传值多个事件的操作
Jan 29 #Vue.js
vue 实现click同时传入事件对象和自定义参数
Jan 29 #Vue.js
You might like
php URL跳转代码 减少外链
2011/06/25 PHP
摘自织梦CMS的HTTP文件下载类
2015/08/08 PHP
一个js拖拽的效果类和dom-drag.js浅析
2010/07/17 Javascript
让ie运行js时提示允许阻止内容运行的解决方法
2010/10/24 Javascript
ModelDialog JavaScript模态对话框类代码
2011/04/17 Javascript
jQuery获取浏览器中的分辨率实现代码
2013/04/23 Javascript
JS控制文本框textarea输入字数限制的方法
2013/06/17 Javascript
鼠标移到导航当前位置的LI变色处于选中状态
2013/08/23 Javascript
Seajs的学习笔记
2014/03/04 Javascript
node.js使用nodemailer发送邮件实例
2014/03/10 Javascript
jQuery实现TAB风格的全国省份城市滑动切换效果代码
2015/08/24 Javascript
基于javascript制作微博发布栏效果
2016/04/04 Javascript
URL中“#” “?” &amp;“”号的作用浅析
2017/02/04 Javascript
基于vue组件实现猜数字游戏
2020/05/28 Javascript
jQuery实现表单动态加减、ajax表单提交功能
2018/06/08 jQuery
VUE DOM加载后执行自定义事件的方法
2018/09/07 Javascript
python类定义的讲解
2013/11/01 Python
使用python实现knn算法
2017/12/20 Python
Python中的默认参数实例分析
2018/01/29 Python
Python OpenCV处理图像之图像直方图和反向投影
2018/07/10 Python
解决python os.mkdir创建目录失败的问题
2018/10/16 Python
简单了解Python生成器是什么
2019/07/02 Python
Mac安装python3的方法步骤
2019/08/09 Python
Django为窗体加上防机器人的验证码功能过程解析
2019/08/14 Python
django ajax发送post请求的两种方法
2020/01/05 Python
python里glob模块知识点总结
2021/01/05 Python
python实现简单的学生管理系统
2021/02/22 Python
英国汽车座椅和婴儿车购物网站:Uber Kids
2017/04/19 全球购物
美国娱乐和流行文化商品店:FYE
2017/09/14 全球购物
Exoticca英国:以最优惠的价格提供豪华异国情调旅行
2018/10/18 全球购物
节能环保口号
2014/06/12 职场文书
乡镇党员群众路线教育实践活动对照检查材料思想汇报
2014/10/05 职场文书
2015年幼儿园保育员工作总结
2015/04/23 职场文书
爱的教育读书笔记
2015/06/26 职场文书
Spring boot应用启动后首次访问很慢的解决方案
2021/06/23 Java/Android
本地搭建minio文件服务器(使用bat脚本启动)的方法
2022/07/15 Servers