一、datav-vue开源项目简介
该项目是一款数据可视化应用搭建工具,是仿照的阿里云datav项目,使用vue技术栈进行实现的。
因为本人工作中需要开发的可视化拖拽项目也是仿照的阿里云datav项目,但是阿里云项目并未开源,故想要通过阅读该项目的项目实现思路,以应用到后续工作中的可视化拖拽项目的开发中。
在本篇博客中,本人主要是对一些重难点技术如拖拽等的实现进行了分析研究。
二、datav-vue项目地址
三、datav-vue项目阅读总结
3.1画布样式布局分析
画布中除了柱状图,通用标题等组件库中的组件,也有类型是组的组件,当我们选中一个或多个组件进行成组操作的时候,会创建一个类型为组的组件,将选中的组件作为children包裹起来。
那么,在画布中,组组件和组件库组件的位置是如何定位的?
首先,画布是相对定位,画布中的每个组件都是绝对定位,绝对定位的top和left都是0,然后通过transform的translate样式属性最终确定元素的位置。每个组件都有x和y属性,这两个属性就对应着transform的translateX和translateY的值。
const transformStyle = computed(() => {
// 每个组件都有x和y属性,这两个属性就对应着transform的translateX和translateY的值。
const { x, y, w, h } = props.com.attr
return {
top: 0,
left: 0,
width: `${w}px`,
height: `${h}px`,
transform: `translate(${x}px, ${y}px)`,
}
})
我们考虑成组的情况下,组组件和其内部的组件的布局是什么样的呢?
成组的话,会创建一个组组件,这个组组件的x属性取选中的组件们的x的最小值,y属性取选中的组件们的y属性的最小值,然后其内部的组件的x属性和y属性都要减去组组件的x和y属性值。因为组组件是绝对定位,其内部组件也是绝对定位,且top和left都是0,那么也就是说内部组件在不考虑transform属性的情况下,它们是相对于组组件定位的,初始位置是在组组件的左上角,成组前,内部组件如果是相对于画布定位的,那么x属性和y属性是在画布左上角的基础上做的translate的偏移,成组后,x属性和y属性要改为在组组件左上角的基础上做translate的偏移,所以内部组件的x属性和y属性要减去组组件的x和y属性值,以得到相对于组组件的位置偏移值。
如下是成组操作代码详情:
createGroup() {
const scoms = this.selectedComs
const sids = scoms.map(m => m.id)
let top = Infinity, left = Infinity
let right = -Infinity, bottom = -Infinity
scoms.forEach(({ attr }) => {
// 最小的y
top = Math.min(attr.y, top)
// 最小的x
left = Math.min(attr.x, left)
// 最大的x
right = Math.max(attr.x + attr.w, right)
// 最大的y
bottom = Math.max(attr.y + attr.h, bottom)
})
// 创建groupCom 组组件
const gcom = new DatavGroup({
x: left,
y: top,
w: right - left,
h: bottom - top,
})
// gcom的父组件是原所选组件的父组件
gcom.parentId = scoms[0].parentId
// gcom的子组件就是当前选中的组件。
gcom.children.push(...scoms)
// 改变gcom的子组件的位置偏移属性x和y为相对于gcom的位置偏移
gcom.children.forEach(com => {
com.parentId = gcom.id
com.attr.x -= gcom.attr.x
com.attr.y -= gcom.attr.y
gcom.config.push(createGroupConfig(com))
})
// 如果所选成组的组件原先有父组件,那么将gcom插入到这个父组件中
if (gcom.parentId) {
const oldGroup = findCom(this.coms, gcom.parentId) as DatavGroup
oldGroup.children = oldGroup.children.filter(m => !sids.includes(m.id))
oldGroup.config = oldGroup.config.filter(m => !sids.includes(m.transform3d.id))
oldGroup.children.push(gcom)
oldGroup.config.push(createGroupConfig(gcom))
} else {
// 如果所选成组的组件原先没有父组件,那么图层组件变成只剩下没有选中的那些,另外再加上一个gcom即可,并选中gcom
this.coms = this.coms.filter(m => !m.selected)
this.add(gcom).then(() => {
this.select(gcom.id)
})
}
},
3.2 拖拽功能分析
3.2.1 画布中成组组件和非成组组件的拖拽分析
我们将组件分为组组件和非组组件。
我们通过监听onmousedown事件和onmousemove事件来实现画布中组件的拖拽。
onmousedown:当按下任意鼠标按键时触发。
onmousemove:在元素内当鼠标指针移动时持续触发。
另外,这两个事件的事件对象的clientX 表示鼠标指针位于浏览器页面当前窗口可视区的水平坐标(X轴坐标),事件对象的clientY表示鼠标指针位于浏览器页面当前窗口可视区的水平坐标(Y轴坐标)
当我们选中画布中的任意组件进行拖拽的时候,我们通过onmousedown的事件对象的clientX和clientY,拿到拖拽开始的时候鼠标的位置信息。我们通过onmousemove的事件对象的clientX和clientY,拿到实时拖拽的时候鼠标的位置信息。
我们将拖拽实时的位置数值减去拖拽开始时候的位置数值,就得到了被选中的组件应该偏移的位置数值x1和y1,因为我们每个组件有x属性和y属性,作为各个组件的transform的translateX和translateY的属性值,那么将x属性加上x1,y属性加上y1,就实现了组件的拖拽。
那么如果被选中进行拖拽的组件是组组件内部的组件的话,除了被拖拽组件本身的位置的变化,其父组件,也就是组组件的位置也应当随之变化,那么它应当如何变化呢?
首先父组件的位置变化是在拖拽完成的时候才去变化的。所以我们在mouseup事件回调中去调整父组件的位置。
通过还原被拖拽组件以及其他组内组件在未成组的时候的位置,来重新去计算父组件的大小和位置信息,最后再恢复组内组件的位置。代码详情如下:
resizeParents(parentComs: DatavComponent | DatavComponent[]) {
const resizeParent = (com: DatavComponent) => {
let top = Infinity, left = Infinity
let right = -Infinity, bottom = -Infinity
com.children.forEach(({ attr }) => {
// 先还原在父容器里的位置,然后计算边界
attr.x += com.attr.x
attr.y += com.attr.y
top = Math.min(attr.y, top)
left = Math.min(attr.x, left)
right = Math.max(attr.x + attr.w, right)
bottom = Math.max(attr.y + attr.h, bottom)
})
com.attr.x = left
com.attr.y = top
com.attr.w = right - left
com.attr.h = bottom - top
com.children.forEach(({ attr }) => {
attr.x -= left
attr.y -= top
})
}
if (Array.isArray(parentComs)) {
parentComs.forEach(com => {
resizeParent(com)
})
} else {
resizeParent(parentComs)
}
},
3.3画布缩放功能分析
如下图所示,我们页面上是有slider可以缩放我们的画布大小的,或者页面设置调整页面宽度和高度的时候,也会自动调整画布缩放比例。当然如果我们不主动去调节画布缩放比例,页面初始加载或者页面resize事件触发的时候,页面也是会自动的去调节画布到一个合适的比例的,那么这块的功能是如何实现的呢?
3.3.1调整画布缩放比例的算法
1.计算页面水平方向和垂直方向上除了画布之外的内容占据的宽度和高度。
这个页面水平方向上已占据的宽度和高度是通过getPanelOffset计算返回的x和y值
getPanelOffset(state) {
let x = 0
let y = 41
// 41是头部标题栏的高度
let left = 60
let top = 100
if (state.layer.show) {
// 200是图层的宽度
x += 200
left += 200
}
// 276+48 = 324 组件库的展示
if (state.components.show) {
x += 324
left += 324
} else {
x += 45
left += 45
}
// 参考线,滤镜配置工具箱的展示
if (state.toolbox.show) {
y += 40
top += 40
}
// 右边的页面设置的展示
if (state.config.show) {
x += 332
}
return {
x,
y,
left,
top,
}
},
2.用户主动调节缩放比例的算法
参数解析:
scale是用户设置的缩放比例,
offsetX和offsetY是上一步中计算出来的页面水平方向和垂直方向上除了画布意外的内容占据的宽度和高度
async setCanvasScale(scale: number, offsetX: number, offsetY: number) {
// 减去滚动条 4px
// offsetX和offsetY是其他内容占用的宽和高
// 第一步:计算出页面剩余多少空间给画布去展示
let width = document.documentElement.clientWidth - offsetX - 4
let height = document.documentElement.clientHeight - offsetY - 4
// 第二步:控制缩放比例在10-200之间
const deltaS = Math.min(Math.max(scale, 10), 200) / 100
// 从scale和10里取最大的,然后和200比较取最小的,也就是说缩放比例在10-200之间。
// 方便计算滚动条 和 标尺
// 第三步:计算内里画布的宽和高
const deltaW = this.pageConfig.width * deltaS
const deltaH = this.pageConfig.height * deltaS
// 内里画布的宽和高是deltaW和deltaH
// (内里蓝色背景的画布宽高基数是1920和1080,deltaS是缩放的比例)
// 第四步:当页面所剩余的空间不足够内里画布展示的空间的时候,我们计算外画布的宽和高,如果足够,则直接取页面剩余空间为外画布的宽高
// 同时,这里小编有疑问了,在足够的情况下,直接取页面剩余空间为外画布的宽高,但是如果页面剩余空间很接近内里画布展示空间,而外画布里除了内里画布需要展示,还有比例尺20px展示,还要内里画布和外画布的间距60px需要展示,还有底部的缩放比例调整工具要展示,这些内容占据的宽高都没有考虑到外画布的宽高中,导致结果是在这种情况下,外画布放不下里边的内容,结果出现滚动条。当然这个结果是没问题的,只是小编不大理解这个计算方式。
// 小编对于不足够的情况也是有疑问的,为什么宽度上加400,高度上加390呢,数字计算的这样精确,应该不是随意加的数字,但是小编也想不到这么这两个数字的计算方法。不过这两个数字是大于外画布中除了内里画布内容,其他内容占据的宽和高的,所以也没有问题。
if (width < deltaW) {
width = deltaW + 400
}
if (height < deltaH) {
height = deltaH + 390
}
// canvas的宽高是screen-shot,screen-wp的宽高,也就是外画布的宽高
this.canvas = { scale: deltaS, width, height }
},
3.页面自适应调整缩放比例的算法
async autoCanvasScale(offset: () => { x: number; y: number; }) {
const resize = debounce(() => {
const { x, y } = offset()
const width = document.documentElement.clientWidth - x
const height = document.documentElement.clientHeight - y
// 第一步:根据剩余的空间除以1920内里画布宽度和1080px的内里画布高度,看哪个更小,以更小的缩放比例作为最后的内里画布的缩放比例。
// 小编这里有疑问:这里的180px和200px是什么,可能是外画布和内里画布的差距吧
const a = (width - 180) / this.pageConfig.width
const b = (height - 200) / this.pageConfig.height
// 第二步:取更小的缩放比例为最终的缩放比例
const scale = parseFloat((a > b ? b : a).toFixed(6)) * 100
// 第三步:调用setCanvasScale方法(实际上为2中用户主动调节缩放比例的算法方法)
this.setCanvasScale(scale, x, y)
}, 200)
window.onresize = resize
resize()
},