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 19 Vue.js
vuex的数据渲染与修改浅析
Nov 26 Vue.js
Vue通过阿里云oss的url连接直接下载文件并修改文件名的方法
Dec 25 Vue.js
vue实现一个获取按键展示快捷键效果的Input组件
Jan 13 Vue.js
vue实现轮播图帧率播放
Jan 26 Vue.js
vue3.0 项目搭建和使用流程
Mar 04 Vue.js
开发一个封装iframe的vue组件
Mar 29 Vue.js
详解vue中v-for的key唯一性
May 15 Vue.js
vue Element-ui表格实现树形结构表格
Jun 07 Vue.js
Vue过滤器(filter)实现及应用场景详解
Jun 15 Vue.js
Vue3如何理解ref toRef和toRefs的区别
Feb 18 Vue.js
vue cli4中mockjs在dev环境和build环境的配置详情
Apr 06 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框架Symfony2经典入门教程
2014/07/08 PHP
PHP 获取ip地址代码汇总
2015/07/05 PHP
不错的JS中变量相关的细节分析
2007/08/13 Javascript
DOM和XMLHttpRequest对象的属性和方法整理
2012/01/04 Javascript
JavaScript自动设置IFrame高度的小例子
2013/06/08 Javascript
下拉列表select 由左边框移动到右边示例
2013/12/04 Javascript
javascript实现浏览器窗口传递参数的方法
2014/09/03 Javascript
AngularJS自动表单验证
2016/02/01 Javascript
JS中创建函数的三种方式及区别
2016/03/13 Javascript
通用无限极下拉菜单的实现代码
2016/05/31 Javascript
JS库之Waypoints的用法详解
2017/09/13 Javascript
Vue实现todolist删除功能
2018/06/26 Javascript
vue-router 起步步骤详解
2019/03/26 Javascript
VUE单页面切换动画代码(全网最好的切换效果)
2019/10/31 Javascript
vue单文件组件无法获取$refs的问题
2020/06/24 Javascript
Vue-cli assets SubDirectory及PublicPath区别详解
2020/08/18 Javascript
[54:58]完美世界DOTA2联赛PWL S2 LBZS vs Rebirth 第一场 11.25
2020/11/25 DOTA
详解Python的单元测试
2015/04/28 Python
python数字图像处理之高级滤波代码详解
2017/11/23 Python
Django admin美化插件suit使用示例
2017/12/12 Python
Python实现的各种常见分布算法示例
2018/12/13 Python
在linux下实现 python 监控usb设备信号
2019/07/03 Python
python支付宝支付示例详解
2019/08/22 Python
python实现身份证实名认证的方法实例
2019/11/08 Python
python中图像通道分离与合并实例
2020/01/17 Python
Python如何使用turtle库绘制图形
2020/02/26 Python
python 如何把docker-compose.yaml导入到数据库相关条目里
2021/01/15 Python
CSS3中的5个有趣的新技术
2009/04/02 HTML / CSS
白色公司:The White Company
2017/10/11 全球购物
上学迟到的检讨书
2014/01/11 职场文书
高一生物教学反思
2014/01/17 职场文书
《蚂蚁和蝈蝈》教学反思
2014/02/24 职场文书
村党建工作汇报材料
2014/11/02 职场文书
2014年超市工作总结
2014/11/19 职场文书
2014年仓库管理工作总结
2014/12/17 职场文书
庆祝教师节主题班会
2015/08/17 职场文书