主要介绍实现拖拽时生成辅助线 以及吸附功能。
注意的是 鼠标按下的mousedown 鼠标移动时事件处理 document.onmousemove 和 鼠标抬起document.onmouseup事件处理
生成辅助线和实现吸附功能
当拖拽元素时,通过比较拖拽元素与画布上其他元素的位置,当某个元素与拖拽元素的距离接近时,显示辅助线
拖拽元素在垂直方向上不同对齐方式 5种
底对顶:拖拽元素的底部与对比元素的顶部对齐
底对底:拖拽元素的底部与对比元素的底部对齐
中对中:拖拽元素的中部与对比元素的中部对齐
顶对顶:拖拽元素的顶部与对比元素的顶部对齐
顶对底:拖拽元素的顶部与对比元素的底部对齐
拖拽元素在水平方向上不同对齐方式 5种
右对左:拖拽元素的右侧与对比元素的左侧对齐
右对右:拖拽元素的右侧与对比元素的右侧对齐
中对中:拖拽元素的水平中心与对比元素的水平中心对齐
左对左:拖拽元素的左侧与对比元素的左侧对齐
左对右:拖拽元素的左侧与对比元素的右侧对齐
当前拖拽元素除外,保存其他所有元素的位置信息和判断条件用于生成辅助线。
在拖拽过程中,监听 drag 事件,并将拖拽元素的位置与之前保存的数据进行比较。如果满足生成辅助线的条件,则显示相应的辅助线。
解析:函数中使用了一个 lines 对象来保存辅助线的位置信息。遍历画布上的所有元素(除了当前拖拽元素),并为每个元素计算并保存辅助线的位置。
对于垂直方向(y 轴),生成了五种辅助线的位置信息:
顶对顶:拖拽元素的顶部与对比元素的顶部对齐
顶对底:拖拽元素的顶部与对比元素的底部对齐
中:拖拽元素的中部与对比元素的中部对齐
底对顶:拖拽元素的底部与对比元素的顶部对齐
底对底:拖拽元素的底部与对比元素的底部对齐
对于每种对齐方式,lines 对象保存了两个位置信息:
showTop:用于显示辅助线的位置
top:用于计算对齐位置的参考位置
我这个项目是组件化拖拽需求。全部都是组件形式进行拖拽的。
代码 如下。主要展示辅助线和吸附功能。如果在编辑的时候需要加方格遮罩,就需要加样式
.es-container {
border: 1px solid #ccc;
background:
-webkit-linear-gradient(top, transparent calc(var(--es-grid-height) - 1px), #ccc var(--es-grid-height)),
-webkit-linear-gradient(left, transparent calc(var(--es-grid-width) - 1px), #ccc var(--es-grid-width))
;
background-size: var(--es-grid-width) var(--es-grid-height);
width: 800px;
height: 600px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
我这里去除了,因为不好看。
具体代码如下:
<template>
<div :id="esContainer ? 'es-container' : 'interact-container'" >
<div
v-for="(item, i) in componentsList"
:id="item.id"
:key="item.id"
class="interact"
:class="{ 'is-edit': editState === 'edit' && activeEle.id === item.id }"
:data-x="item.x"
:data-y="item.y"
:data-w="item.w"
:data-h="item.h"
:style="{
transform: `translate(${item.x}px, ${item.y}px)`,
width: `${item.w}px`,
height: `${item.h}px`,
zIndex: item.zIndex,
background: item.color
}"
@mousedown="editState === 'edit' && handleMouseDown(item, i)"
>
<component
:is="item.is"
ref="component"
:echartsId="item.id"
:style="{ background: ( editState === 'edit' ? 'rgba(255,255,255,0.05)' : item.color), overflow: 'hidden'}"
:dataw="item.w"
:datah="item.h"
:dataInfo.sync="item.dataInfo"
:dataTypeList.sync="item.dataTypeList"
:dataTime="dataTime"
></component>
</div>
<div
v-show="markLine.left"
class="es-markline-left"
:style="{ left: markLine.left + 'px' }"
></div>
<div
v-show="markLine.top"
class="es-markline-top"
:style="{ top: markLine.top + 'px' }"
></div>
<slot></slot>
</div>
</template>
<script>
import interact from 'interactjs'
import { mapGetters } from 'vuex'
export default {
name: 'interact',
props: {
modules: {
type: Array,
required: true,
default: () => []
}
},
data () {
return {
dataTime: 0,
inverterMonTimer: null, // 设置刷新时间 2 分钟一次
activeEle: {},
componentsList: [],
esContainer: false, // 背景网格编辑
markLine: {
left: null,
top: null
},
lines: {
x: [],
y: []
},
currentIndex: -1 // 选中的元素下标
}
},
computed: {
...mapGetters(['workFaceInfo', 'editState', 'historyState', 'currentPage'])
},
watch: {
currentPage: {
handler () {
this.activeEle = {}
}
},
editState: {
handler (value) {
if (value === 'save') {
this.esContainer = false
this.savePageJson()
} else if (value === 'exit' || value === '') {
this.esContainer = false
this.componentsList = this.modules.map((v, i) => {
return { ...v, id: i }
})
this.init()
} else {
this.activeEle = {}
this.init()
}
}
},
historyState: {
deep: true,
handler (value) {
if (value) {
this.savePageJson(value)
}
}
},
modules: {
deep: true,
handler (value) {
if (value?.length) {
this.componentsList = value.map((v, i) => {
return { ...v, id: i }
})
} else {
this.componentsList = []
}
this.init()
}
}
},
mounted () {
this.componentsList = this.componentsList.concat(this.modules).map((v, i) => {
return { ...v, id: i }
})
window.$eventBus.$on('dragItem', param => {
const id = window.$dayjs().valueOf()
this.activeEle = window.$utils.cloneDeep(param)
const allZIndex = this.componentsList.map(v => v.zIndex)
const max = Math.max(...allZIndex)
this.componentsList.push({ ...param, id, zIndex: max > 0 ? max : 1 })
this.dragMoveFn()
this.init()
})
window.$eventBus.$on('elementHandler', (type) => {
if (type === 'del') {
this.deleteComponent()
} else {
this.edit()
}
})
this.getInverterMonTimer()
},
beforeDestroy () {
if (this.inverterMonTimer) {
clearInterval(this.inverterMonTimer)
this.inverterMonTimer = null
}
},
methods: {
getInverterMonTimer () {
// 判断定时刷新是否存在,存在先清除
if (this.inverterMonTimer) {
clearInterval(this.inverterMonTimer)
this.inverterMonTimer = null
}
// 实现轮询 两分钟
this.inverterMonTimer = window.setInterval(() => {
this.dataTime = new Date().getTime()
}, 120000)
},
numberFormat: window.$utils.numberFormat,
edit () {
const index = this.componentsList.findIndex(v => v.id === this.activeEle.id)
if (this.componentsList[index]?.isDataInfo) {
this.$message.warning('该组件不存在测点配置,请重新选择')
} else {
this.$refs['component'][index].edit()
}
},
init: window.$utils.debounce(function () {
console.log('初始化========', this.componentsList)
const zoomItem = interact('.interact')
zoomItem.resizable(this.editState === 'edit' && this.resizableFn())
zoomItem.draggable(this.editState === 'edit' && this.dragMoveFn())
}, 200, false),
setPos (data) {
let { target, x, y, w, h } = data
const index = this.componentsList.findIndex(v => v.id === this.activeEle.id)
if (index >= 0) {
x = Math.ceil(x)
y = Math.ceil(y)
// target.style.transform = `translate(${x}px, ${y}px)`
target.setAttribute('data-x', x)
this.componentsList[index].x = x
target.setAttribute('data-y', y)
this.componentsList[index].y = y
if (w && h) {
w = Math.ceil(w)
h = Math.ceil(h)
// target.style.width = w + 'px'
target.setAttribute('data-w', w)
this.componentsList[index].w = w
// target.style.height = h + 'px'
target.setAttribute('data-h', h)
this.componentsList[index].h = h
}
}
},
handleMouseDown (item, i) {
window.$eventBus.$emit('activeElement', item)
this.activeEle = window.$utils.cloneDeep(item)
// 设置辅助线
this.esContainer = true
this.currentIndex = i
let items = item // 选中的组件
// 获取当前页面的所有对比值
this.lines = this.calcLines()
// console.log(this.lines, 'xy数组')
// 鼠标移动时事件处理
document.onmousemove = (event) => {
this.markLine.top = null
this.markLine.left = null
for (let i = 0; i < this.lines.y.length; i++) {
const { top, showTop } = this.lines.y[i]
if (Math.abs(top - items.y) < 5) {
this.markLine.top = showTop
// console.log(this.markLine.top, 'this.markLine.top')
break
}
}
for (let i = 0; i < this.lines.x.length; i++) {
const { left, showLeft } = this.lines.x[i]
if (Math.abs(left - items.x) < 5) {
this.markLine.left = showLeft
// console.log(this.markLine.left, 'this.markLine.left')
break
}
}
}
// 鼠标抬起时结束
document.onmouseup = (event) => {
// 吸附功能
this.adsorb(event, items, this.markLine.left, this.markLine.top)
document.onmousemove = document.onmouseup = null
this.markLine.top = null
this.markLine.left = null
}
},
adsorb (event, items, left, top) {
let DISTANCE = 5 // 距离
const target = event.target
// console.log(event, '选中的组件items', items, '上边线top', top, '左侧边线', left)
if (top && left) { // 同时存在两条边线
// 完成: 左上对右下, 左上对右上, 左上对左上, 左上对左下
if (Math.abs(items.y - top) <= DISTANCE && Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: top })
return
}
// 完成: 右下对右上, 右下对左上, 右下对左下, 右下对右下
else if (Math.abs((items.x + items.w) - left) <= DISTANCE && Math.abs((items.y + items.h) - top) <= DISTANCE) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: (top - (items.y + items.h)) + items.y })
return
}
// 完成: 左下对左上, 左下对右上, 左下对左下, 左下对右下
else if (Math.abs((items.y + items.h) - top) <= DISTANCE && Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: top - items.h })
}
// 完成: 右上对左下, 右上对右上, 右上对左下, 右上对做上
else if (Math.abs((items.x + items.w) - left) <= DISTANCE && Math.abs((items.y) - top <= DISTANCE)) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: top })
} else if (items.x === left && items.y === top) {
this.setPos({ target, x: left, y: top })
}
} else {
if (top) { // 只有top值变化 则 x 赋值 移动的 items.x, Y轴需要计算
console.log('选中的组件items', items, '上边线top', top)
// 上 对 下
if (Math.abs(items.y - top) <= DISTANCE) {
this.setPos({ target, x: items.x, y: top })
return
}
// 下边框 对 上边框
else if (Math.abs((items.y + items.h) - top) <= DISTANCE) {
this.setPos({ target, x: items.x, y: (top - (items.y + items.h)) + items.y })
return
}
}
if (left) { // 只有left值变化 Y轴赋值 items.y, x轴需要计算
console.log('选中的组件items', items, '左侧边线', left)
// 左对左,左对右
if (Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: items.y })
return
}
// 右对左,右对右
else if (Math.abs((items.x + items.w) - left) <= DISTANCE) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: items.y })
return
}
}
}
},
calcLines () {
const lines = { x: [], y: [] } // w:[], h:[]
// 当前选中要拖拽元素大小
const { w, h, x, y } = this.componentsList[this.currentIndex]
// console.log("当前组件宽", w, "当前组件高", h, "当前组件x轴", x, "当前组件y轴", y)
// 循环遍历画布所有元素,将除当前拖拽元素外所有其它元素生成辅助线的位置保存,每个元素x和y都会有5种
this.componentsList.forEach((item, i) => {
if (this.currentIndex === i) { return }
// 非当前元素
const { y: ATop, x: ALeft, w: AWidth, h: AHeight } = item
// console.log(
// '点击时获取非当前组件其他y轴高度', ATop,
// '点击时获取非当前组件其他x轴宽', ALeft,
// '点击时获取非当前组件其他width', AWidth,
// '点击时获取非当前组件其他height', AHeight
// )
lines.x.push({ left: ALeft, showLeft: ALeft })
lines.x.push({ left: ALeft + parseInt(AWidth), showLeft: ALeft + parseInt(AWidth) })
lines.x.push({ left: ALeft + parseInt(AWidth) / 2 - parseInt(w) / 2, showLeft: ALeft + parseInt(AWidth) / 2 })
lines.x.push({ left: ALeft + parseInt(AWidth) - parseInt(w), showLeft: ALeft + parseInt(AWidth) })
lines.x.push({ left: ALeft - parseInt(w), showLeft: ALeft })
lines.y.push({ showTop: ATop, top: ATop })
lines.y.push({ showTop: ATop, top: ATop - parseInt(h) })
lines.y.push({ showTop: ATop + parseInt(AHeight) / 2, top: ATop + parseInt(AHeight) / 2 - parseInt(h) / 2 })
lines.y.push({ showTop: ATop + parseInt(AHeight), top: ATop + parseInt(AHeight) })
lines.y.push({ showTop: ATop + parseInt(AHeight), top: ATop + parseInt(AHeight) - parseInt(h) })
})
return lines
},
// 删除当前元素
deleteComponent () {
this.componentsList = this.componentsList.filter(v => v.id !== this.activeEle.id)
this.activeEle = {}
},
// 拖拽方法
dragMoveFn () {
const self = this
return {
inertia: false,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
}),
interact.modifiers.snap({
targets: [
interact.snappers.grid({ x: 1, y: 1 })
],
range: Infinity,
relativePoints: [{ x: 0, y: 0 }]
})
],
autoScroll: true,
listeners: {
move (event) {
const target = event.target
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
self.setPos({ target, x, y })
}
}
}
},
// 缩放方法
resizableFn () {
const self = this
return {
inertia: false,
edges: {
left: true,
right: true,
bottom: true,
top: true
},
listeners: {
move (event) {
const target = event.target
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.deltaRect.left
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.deltaRect.top
const w = (parseFloat(target.getAttribute('data-w')) || 0) + event.deltaRect.width
const h = (parseFloat(target.getAttribute('data-h')) || 0) + event.deltaRect.height
self.setPos({ target, x, y, w, h })
}
},
modifiers: [
interact.modifiers.restrictEdges({
outer: 'parent'
}),
interact.modifiers.restrictSize({
min: {
width: 30,
height: 30
}
})
]
}
},
savePageJson (data) {
let value = null
if (data) {
this.$store.commit('setHistoryState', '')
value = JSON.parse(data)
} else {
value = window.$utils.cloneDeep(this.componentsList)
}
value.forEach(v => {
delete v.is
})
const params = {
workFaceCode: this.workFaceInfo.workFaceCode,
userName: '',
dataType: this.currentPage.pageId,
value: JSON.stringify(value)
}
window.$axiosPost('savePageConfig', params).then(() => {
this.$message.success('保存成功')
this.$emit('refresh', true)
}).catch(() => {
this.$message.error('保存失败')
})
}
}
}
</script>
<style scoped lang="scss">
#interact-container {
position: relative;
width: 100%;
height: 100%;
}
.interact {
position: absolute;
user-select: none;
touch-action: none;
//transition: background-color 0.3s;
box-sizing: border-box;
border-radius: 4px;
padding: 3px;
& > div {
width: 100%;
height: 100%;
// background: #152536;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 2px;
// position: absolute;
}
.shade-box {
width: auto;
height: 20px;
background: rgba(47, 62, 79, 0.95);
color: yellow;
position: absolute;
left: 50%;
top: -20px;
transform: translateX(-50%);
font-size: 12px;
padding: 5px;
display: flex;
align-items: center;
span + span {
margin-left: 3px;
}
}
&.is-edit {
box-shadow: 0 2px 12px 0 RGBA(120, 206, 233, 0.1);
&:after {
content: "";
width: 100%;
height: 100%;
border-radius: 5px;
position: absolute;
left: 0;
top: 0;
background: linear-gradient(#78cee9, #78cee9) left top,
linear-gradient(#78cee9, #78cee9) left top,
linear-gradient(#78cee9, #78cee9) right top,
linear-gradient(#78cee9, #78cee9) right top,
linear-gradient(#78cee9, #78cee9) right bottom,
linear-gradient(#78cee9, #78cee9) right bottom,
linear-gradient(#78cee9, #78cee9) left bottom,
linear-gradient(#78cee9, #78cee9) left bottom;
background-size: 5px 15px, 15px 5px;
background-repeat: no-repeat;
z-index: 99;
}
.handler-box {
width: auto;
height: 30px;
position: absolute;
top: -25px;
right: 0;
z-index: 100;
//display: flex;
align-items: center;
justify-content: space-around;
border: dashed #1a395b;
border-width: 2px 2px 0 2px;
padding: 3px 5px;
display: none;
i {
font-size: 16px;
cursor: pointer;
margin: 0 5px;
}
}
&:hover .handler-box {
display: flex;
}
}
}
.ele-config {
background: #0a2744;
display: flex;
align-items: center;
justify-content: center;
}
// 辅助线样式
#es-container {
position: relative;
width: 100%;
height: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
[class^="es-markline"] {
position: absolute;
z-index: 9999;
background-color: #3a7afe;
}
.es-markline-left {
height: 100%;
width: 1px;
top: 0;
}
.es-markline-top {
width: 100%;
height: 1px;
left: 0;
}
}
</style>
布局线的代码在这里,可以直接搬去用,记得拿样式
<div
v-show="markLine.left"
class="es-markline-left"
:style="{ left: markLine.left + 'px' }"
></div>
<div
v-show="markLine.top"
class="es-markline-top"
:style="{ top: markLine.top + 'px' }"
></div>
这里是样式。
// 辅助线样式
#es-container {
position: relative;
width: 100%;
height: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
[class^="es-markline"] {
position: absolute;
z-index: 9999;
background-color: #3a7afe;
}
.es-markline-left {
height: 100%;
width: 1px;
top: 0;
}
.es-markline-top {
width: 100%;
height: 1px;
left: 0;
}
}
辅助线和吸附的事件处理代码在这里
handleMouseDown (item, i) {
window.$eventBus.$emit('activeElement', item)
this.activeEle = window.$utils.cloneDeep(item)
// 设置辅助线
this.esContainer = true
this.currentIndex = i
let items = item // 选中的组件
// 获取当前页面的所有对比值
this.lines = this.calcLines()
// console.log(this.lines, 'xy数组')
// 鼠标移动时事件处理
document.onmousemove = (event) => {
this.markLine.top = null
this.markLine.left = null
for (let i = 0; i < this.lines.y.length; i++) {
const { top, showTop } = this.lines.y[i]
if (Math.abs(top - items.y) < 5) {
this.markLine.top = showTop
// console.log(this.markLine.top, 'this.markLine.top')
break
}
}
for (let i = 0; i < this.lines.x.length; i++) {
const { left, showLeft } = this.lines.x[i]
if (Math.abs(left - items.x) < 5) {
this.markLine.left = showLeft
// console.log(this.markLine.left, 'this.markLine.left')
break
}
}
}
// 鼠标抬起时结束
document.onmouseup = (event) => {
// 吸附功能
this.adsorb(event, items, this.markLine.left, this.markLine.top)
document.onmousemove = document.onmouseup = null
this.markLine.top = null
this.markLine.left = null
}
},
adsorb (event, items, left, top) {
let DISTANCE = 5 // 距离
const target = event.target
// console.log(event, '选中的组件items', items, '上边线top', top, '左侧边线', left)
if (top && left) { // 同时存在两条边线
// 完成: 左上对右下, 左上对右上, 左上对左上, 左上对左下
if (Math.abs(items.y - top) <= DISTANCE && Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: top })
return
}
// 完成: 右下对右上, 右下对左上, 右下对左下, 右下对右下
else if (Math.abs((items.x + items.w) - left) <= DISTANCE && Math.abs((items.y + items.h) - top) <= DISTANCE) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: (top - (items.y + items.h)) + items.y })
return
}
// 完成: 左下对左上, 左下对右上, 左下对左下, 左下对右下
else if (Math.abs((items.y + items.h) - top) <= DISTANCE && Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: top - items.h })
}
// 完成: 右上对左下, 右上对右上, 右上对左下, 右上对做上
else if (Math.abs((items.x + items.w) - left) <= DISTANCE && Math.abs((items.y) - top <= DISTANCE)) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: top })
} else if (items.x === left && items.y === top) {
this.setPos({ target, x: left, y: top })
}
} else {
if (top) { // 只有top值变化 则 x 赋值 移动的 items.x, Y轴需要计算
console.log('选中的组件items', items, '上边线top', top)
// 上 对 下
if (Math.abs(items.y - top) <= DISTANCE) {
this.setPos({ target, x: items.x, y: top })
return
}
// 下边框 对 上边框
else if (Math.abs((items.y + items.h) - top) <= DISTANCE) {
this.setPos({ target, x: items.x, y: (top - (items.y + items.h)) + items.y })
return
}
}
if (left) { // 只有left值变化 Y轴赋值 items.y, x轴需要计算
console.log('选中的组件items', items, '左侧边线', left)
// 左对左,左对右
if (Math.abs(items.x - left) <= DISTANCE) {
this.setPos({ target, x: left, y: items.y })
return
}
// 右对左,右对右
else if (Math.abs((items.x + items.w) - left) <= DISTANCE) {
this.setPos({ target, x: (left - (items.x + items.w)) + items.x, y: items.y })
return
}
}
}
},
calcLines () {
const lines = { x: [], y: [] } // w:[], h:[]
// 当前选中要拖拽元素大小
const { w, h, x, y } = this.componentsList[this.currentIndex]
// console.log("当前组件宽", w, "当前组件高", h, "当前组件x轴", x, "当前组件y轴", y)
// 循环遍历画布所有元素,将除当前拖拽元素外所有其它元素生成辅助线的位置保存,每个元素x和y都会有5种
this.componentsList.forEach((item, i) => {
if (this.currentIndex === i) { return }
// 非当前元素
const { y: ATop, x: ALeft, w: AWidth, h: AHeight } = item
// console.log(
// '点击时获取非当前组件其他y轴高度', ATop,
// '点击时获取非当前组件其他x轴宽', ALeft,
// '点击时获取非当前组件其他width', AWidth,
// '点击时获取非当前组件其他height', AHeight
// )
lines.x.push({ left: ALeft, showLeft: ALeft })
lines.x.push({ left: ALeft + parseInt(AWidth), showLeft: ALeft + parseInt(AWidth) })
lines.x.push({ left: ALeft + parseInt(AWidth) / 2 - parseInt(w) / 2, showLeft: ALeft + parseInt(AWidth) / 2 })
lines.x.push({ left: ALeft + parseInt(AWidth) - parseInt(w), showLeft: ALeft + parseInt(AWidth) })
lines.x.push({ left: ALeft - parseInt(w), showLeft: ALeft })
lines.y.push({ showTop: ATop, top: ATop })
lines.y.push({ showTop: ATop, top: ATop - parseInt(h) })
lines.y.push({ showTop: ATop + parseInt(AHeight) / 2, top: ATop + parseInt(AHeight) / 2 - parseInt(h) / 2 })
lines.y.push({ showTop: ATop + parseInt(AHeight), top: ATop + parseInt(AHeight) })
lines.y.push({ showTop: ATop + parseInt(AHeight), top: ATop + parseInt(AHeight) - parseInt(h) })
})
return lines
},
完成