# 可视化拖拽组件
基于 vue
# 业务痛点
- 固定类型的气象数据展示页面重复开发 (活动页面重复开发)
- 需求改变频繁
- 开发时间短
- 开发人力不足
- 开发工作零碎 维护成本高
- 开发流程涉及产品/运营 沟通成本高
解决:
- 组件复用
- 拖拽生成
- 配置修改
类似游戏引擎
# 要点
- 组件化
- 动态组件 根据组件信息生成对应类型组件
- dashboard 画布
- 数组保存每个组件
- 拖拽 缩放 删除 图层 放大缩小 吸附 标线 配置
- 配置表单 JSON Schema
- 组件交互
- eventbus 发布订阅
- 固定类型 固定事件
- 初始化时绑定
- 组件数据
- 配置请求 api
- 页面预览
- 修改页面状态 读取缓存页面数据
- 页面挂载
- 实时渲染快
- 可能污染编辑器页面
- 后台渲染
- 页面分享
- 页面数据保存 得到数据 ID 根据 id 生成分享页面
- 实时数据更新
- websocket 对应 id 请求
# 布局
整体页面布局:
- 工具栏:快捷操作
- 组件列表:可以生成的组件
- 画布:dashboard 用来放置组件
- 属性编辑区域:修改组件属性

思路:
- 用一个数组
componentData
维护画布上的组件数据 - 拖拽组件到画布上,使用
push
将组件的数据加入到componentData
- 使用动态组件和
v-for
来把componentData
中到组件渲染上去
动态组件:
<component
v-for="item in componentData"
:key="item.id"
:is="item.component"
:style="item.style"
:propValue="item.propValue"
/>
2
3
4
5
6
7
componentData
:
{
"component": "v-text", // 组件名称,需要提前注册到 Vue
"label": "文字", // 左侧组件列表中显示的名字
"propValue": "文字", // 组件所使用的值
"icon": "el-icon-edit", // 左侧组件列表中显示的名字
"animations": [], // 动画列表
"events": {}, // 事件列表
"style": {
// 组件样式
"width": 200,
"height": 33,
"fontSize": 14,
"fontWeight": 500,
"lineHeight": "",
"letterSpacing": 0,
"textAlign": "",
"color": ""
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 拖拽
# 从组件列表到画布
思路:
dragstart
开始拖拽将组件信息缓存drop
拖拽结束将缓存信息加入到componentData
触发 drop
事件时,使用 dataTransfer.getData()
接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。
# 组件在画布中移动
首先需要将画布设为相对定位 position: relative
,然后将每个组件设为绝对定位 position: absolute
。除了这一点外,还要通过监听三个事件来进行移动:
mousedown
事件,在组件上按下鼠标时,记录组件当前的位置,即xy
坐标mousemove
事件,每次鼠标移动时,都用当前最新的xy
坐标减去最开始的xy
坐标,从而计算出移动距离,再改变组件位置mouseup
事件,鼠标抬起时结束移动。
# 删除组件,调整图层
由于拖拽组件到画布中是有先后顺序的,所以可以按照数据顺序来分配图层层级。
例如画布新增了五个组件 abcde
,那它们在画布数据中的顺序为 [a, b, c, d, e]
,图层层级和索引一一对应,即它们的 z-index
属性值是 01234
(后来居上):
<div v-for="(item, index) in componentData" :zIndex="index"></div>
改变图层层级,即是改变组件数据在 componentData
数组中的顺序。例如有 [a, b, c]
三个组件,它们的图层层级从低到高顺序为 abc
(索引越大,层级越高)。
删除组件也是把组件信息从组件队列中删除:
componentData.splice(index, 1);
# 放大缩小
选中画布上的组件,组件外圈会出现 8 个小圆点可以拖动进行放大缩小。
思路:
- 每个组件外层包裹一层组件,包裹组件包含 8 个小圆点和插槽,插槽用来放置组件
<!--页面组件列表展示-->
<Shape
v-for="(item, index) in componentData"
:defaultStyle="item.style"
:style="getShapeStyle(item.style, index)"
:key="item.id"
:active="item === curComponent"
:element="item"
:zIndex="index"
>
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</Shape>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
组件内部:
<template>
<div
class="shape"
:class="{ active: this.active }"
@click="selectCurComponent"
@mousedown="handleMouseDown"
@contextmenu="handleContextMenu"
>
<div
class="shape-point"
v-for="(item, index) in active ? pointList : []"
@mousedown="handleMouseDownOnPoint(item)"
:key="index"
:style="getPointStyle(item)"
></div>
<slot></slot>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 点击组件通过样式控制显示小圆点
- 计算每个小圆点的位置(要显示在组件外层,还需要计算小圆点的大小,记小圆点长宽都为
w
):
- 左上
left:0-w top:0-w
- 右上
left:width top:0-w
- 左下
left:0-w top:height
- 右下
left:width top:height
- 中间的点
width/2 height/2
同理计算
- 点击小圆点可以进行缩放操作
- 点击小圆点,记录初始坐标
- 向下拖动就用新的 y 坐标减去初始坐标可以得到移动距离,再把距离加上组件的高度计算得到新的高度(用
movement
鼠标移动距离也可以计算) - 上下只能修改高度 左右只能修改宽度,西北方向特殊处理,需要限制对应圆点的功能
# 撤销 重做
用一个数组来保存编辑器的快照数据。保存快照就是将当前的编辑器数据推入 snapshotData
数组,并增加快照索引 snapshotIndex
。目前以下几个动作会触发保存快照操作:
- 新增组件
- 删除组件
- 改变图层层级
- 拖动组件结束时
// snapshotData: [], // 编辑器快照数据
// snapshotIndex: -1, // 快照索引
undo(state) {
if (state.snapshotIndex >= 0) {
state.snapshotIndex--
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
redo(state) {
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotIndex++
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
setComponentData(state, componentData = []) {
Vue.set(state, 'componentData', componentData)
},
recordSnapshot(state) {
// 添加新的快照
state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
// 在 undo 过程中,添加新的快照时,要将它后面的快照清理掉
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 撤销 就是将快照索引减一,更新索引指向数据给画布
- 撤销后做了新的操作就把新的快照数据加入数组,更新索引
- 重做 就是快照索引加一,更新索引指向数据给画布
# 吸附 对齐
参考 Sketch
墨刀

可以看到,一个组件在画布中可以由 6 条线 (vt / vm / vb | hl / hm / hr)
来表示,组件移动过程中的对齐其实就是组件的 6 条线到其它组件线的集合中寻找临近线,找到后考虑 吸附 + 对齐 的过程。
对齐吸附:横向左移动为例,它的 hl / hm / hr
会不断的去查找与这 3 条线相邻最近的线。
- 当没有找到相邻线时,组件跟随鼠标移动
- 当初次找到时,组件便移动一个较大距离吸附过去
- 当在吸附线上再次移动时,继续查找相邻线,看是否有下一条吸附线
- 如果有,则移动到下一条吸附线上
- 如果没有,则在鼠标移动一定距离后,组件离开
具体(a 是移动组件 b 是固定组件):
- 向下移动 依次会出现下面情况
- a 底部 = b 顶部 显示 水平底部线
- a 顶部 = b 顶部 显示 水平顶部线
- a 中间 = b 中间 显示 水平中线
- a 底部 = b 底部 显示 水平底部线
- a 顶部 = b 底部 显示 水平顶部线
- 向左向右同理
- 向上移动和向下出现顺序相反
上下和左右分别构造两个数组来保存:
- 判断条件 是否满足吸附
- 显示的线的类型
- 吸附后 线的位置
- 吸附后 组件的位置
按照顺序遍历数组,只要满足条件就退出,同时一个方向只会显示一条线
# 属性设置
点击组件显示对应的属性设置,修改属性组件样式应用修改。
思路:
- 点击组件把组件的样式对象绑定到属性设置上
- 利用双向绑定在修改属性的时候修改对应样式
# 预览 保存
预览原理和编辑一样,只是不能编辑,加上一个状态控制是否可以编辑即可
保存就是把画布上的 componentData
存储即可,保存有两种选择:
- 服务器
- 客户端
localStorage
# 绑定事件 动画
设置里面选中然后给组件添加对应的对象,事件和动画都存储到对象数组里,预览的时候绑定
# 数据请求
默认自定义数据可以添加在 componentData
里面的一个属性
远程数据可以配置一个 url
属性利用 websocket
根据组件 id
订阅更新
# 组合 拆分
技术点:
- 选中区域
- 一个加边框
div
来显示 mouseDown
- 画布决定起始位置 设置
showArea = true
- 画布决定起始位置 设置
mouseMove
- 更新区域大小 根据
offet-start
正负情况修改 定位样式
- 更新区域大小 根据
mouseUp
- 判断选中的组件 显示选中区域
- 根据左上角和宽高 是否完全包裹组件的左上角和宽高 符合条件就加入数组
- 遍历数组更新四个顶点 根据顶点构建选中区域样式
- 移除事件绑定 设置
showArea = false
- 判断选中的组件 显示选中区域
- 问题
- 移动的时候 offsetY 有的时候会变成一半,起初以为是冒泡导致,发现阻止冒泡也没效果
- 原因是 mousemove 过程中,触发的事件源元素可能会变成区域元素导致相对位置变化
- 解决:在区域元素样式上添加
pointer-events: none;
使得区域元素永远不会成为鼠标事件的target
- 移动的时候 offsetY 有的时候会变成一半,起初以为是冒泡导致,发现阻止冒泡也没效果
- 一个加边框
- 组合后的移动、
旋转 - 组合后的放大缩小
- 拆分后子组件样式的恢复
组合后,把组件信息从数组中刪除,把选中的组件信息重新生成组合组件,再加入数组 拆分后,把组合组件的每个组件信息重新计算,再添加进组件数组