项目中做看板重构时遇到的开发需求,不能使用组件,乍一看感觉很头大,但实际上手做出来后还是小有成就的。
浏览图:性能探究(一):使用对象代替数组匹配数据,从而省去遍历操作
小demo演示——拖拽移动:
滚轮缩放:
直接进入正题:
先创建一个简单的vue demo项目
<template>
<div class="drag">
<div class="back_box">
这是一个背景
<div class="drag_box">这是一个蓝色可拖拽元素</div>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {};
},
mounted() {
console.log(this.$el);
},
};
</script>
<style scoped>
.back_box {
background: #ccc;
width: 50vw;
height: 50vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -30%);
}
.drag_box {
width: 100px;
height: 100px;
background: skyblue;
user-select: none; /* 不可选中,为了拖拽时不让文字高亮 */
}
</style>
1、实现拖拽移动
首先我们给需要实现功能的元素加一个draggable="true"
让元素能够被拖拽,属性介绍:
回到页面发现我们的元素已经可以拖拽了(因为截图截不进鼠标,所以手绘了个鼠标的图案上去)
但拖拽完放开鼠标后,元素还是在原来的位置,根本没发生任何变化OTL
别急,让我们慢慢捣鼓它
如何实现拖拽功能?我想到的思路是:
①. 把元素设为绝对定位;
②. 鼠标最初摁下左键时,记录该点的坐标位置;
③. 鼠标拖拽移动完松开左键时,再次记录该点坐标位置;
④. 将两个位置的横纵值相减,即可获得拖拽的偏移量,也就是移动距离。
⑤. 最后动态设置元素绝对定位后的top和left值,实现元素移动。
于是我们来一步步实现思路:先来认识两个搭配draggable
属性一起使用的事件——ondragstart
和ondragend
,它们的定义分别为:
①. ondragstart 事件在用户开始拖动元素或选择的文本时触发
②. ondragend 事件在用户完成元素或首选文本的拖动时触发。
于是我们给元素加上两个事件和方法并劫持$event:
我们再回到页面中对元素进行两次拖拽,看看控制台打印了什么:
两次事件里未改变的值肯定不是我们需要的,所以我们就研究那些值产生改变的属性就ok了
首先我们排除掉screenX
和screenY
,因为浏览器框框也有宽高。
其次是offsetX
和offsetY
,用这两个值也是可以的,但若向左或向上拖拽,就会产生负数的值,某些地方用起来会很麻烦,增加不必要的逻辑。
所以最符合我们开发该功能要用到的属性就是剩下的clientX
和clientY
了。
我们先在data
里定义几个需要用到的变量:
HTML中也加一丢丢东西(给父元素加ref
以取到父元素宽高,给拖拽元素加动态的style
控制移动):
methods
里的方法以及页面初始化时需要在mounted
里执行的逻辑:
※ 在此额外补充一下initHeight
的算法备注:正常而言,this.initHeight = this.initWidth * (1080 / 1920);这样写就ok了(以1920*1080分辨率的屏幕为参考计算自适应的高),但有些项目的看板(比如我手头上这个项目)父元素是会带顶部标题栏和左右带默认padding的,如下:
这时我们就需要稍微计算一下全屏时父元素占位的宽高比例,比如像我这样的看板粗略测出title栏大概是12vh,padding大概是左右各1vh,于是就用这个算式代替:this.initHeight = this.initWidth * ((1080 * 0.88) / (1920 - 1080 * 0.02))。
回到正题:css中给拖拽元素加上position: absolute
开启绝对定位(为方便我在一开始就给父元素加了fixed定位,实际项目中也要记得给对应的父元素加定位属性噢~),以防万一再顺手加个z-index(可省略)
功能思路的每一步骤都实现好了,那么切回页面来见证奇迹的一刻——
OHHHHHHHHHHHHHHHHHHHHHHHHHH!~~
到此为止的源码:
<template>
<div class="drag">
<div class="back_box" ref="back_box">
这是一个背景
<div
class="drag_box"
draggable="true"
@dragstart="dragstart($event)"
@dragend="dragend($event)"
:style="`left:${elLeft}px;top:${elTop}px`"
>
这是一个蓝色可拖拽元素
</div>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
initWidth: 0, // 父元素的宽-自适应值
initHeight: 0, // 父元素的高-自适应值
startclientX: 0, // 元素拖拽前距离浏览器的X轴位置
startclientY: 0, //元素拖拽前距离浏览器的Y轴位置
elLeft: 0, // 元素的左偏移量
elTop: 0, // 元素的右偏移量
};
},
methods: {
// 页面初始化
initBodySize() {
this.initWidth = this.$refs.back_box.clientWidth; // 拿到父元素宽
// this.initHeight = this.initWidth * (1080 / 1920);
this.initHeight = this.initWidth * ((1080 * 0.88) / (1920 - 1080 * 0.02)); // 根据宽计算高实现自适应
},
// 拖拽开始事件
dragstart(e) {
console.log(e);
this.startclientX = e.clientX; // 记录拖拽元素初始位置
this.startclientY = e.clientY;
},
// 拖拽完成事件
dragend(e) {
console.log(e);
let x = e.clientX - this.startclientX; // 计算偏移量
let y = e.clientY - this.startclientY;
this.elLeft += x; // 实现拖拽元素随偏移量移动
this.elTop += y;
},
},
mounted() {
// console.log(this.$el);
this.initBodySize();
},
};
</script>
<style scoped>
.back_box {
background: #ccc;
width: 50vw;
height: 50vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -30%);
}
.drag_box {
width: 100px;
height: 100px;
background: skyblue;
position: absolute;
z-index: 10;
user-select: none; /* 不可选中,为了拖拽时不让文字高亮 */
}
</style>
2、实现滚轮缩放
接着我们来实现鼠标停留在元素上时,通过滚轮来实现缩小放大的功能
我的思路:
①. 通过滚轮事件得到判断滚轮是否在向上或向下滚动;
②. 向上滚动时放大元素,向下滚动时缩小元素,即按缩放量乘以元素的宽高,等比例缩放;
③. 使缩放元素内的子元素跟随父元素缩放比例一同缩放。
老样子,先来了解鼠标滚轮事件onmousewheel
……噢不对,是onwheel
:
打印一下($event可省略):
于是我们发现滚轮向上滚动时wheelDelta>0
,向下滚动时wheelDelta<0
,以此做为元素缩放的判断依据。
缩放元素的宽高是动态的,于是我们给元素的动态style
加上width
和height
,并定义两个动态宽高的变量,以及缩放比例zoom
elWidth
和elHeight
需要在mounted
里初始化,计算方式为:父元素宽(高)的自适应值
* ( 元素宽(高)
/ 父元素宽(高)
) ,我们在此前已经拿到initWidth
和initHeight
了,我设的元素为宽高100px的正方形,而父元素为宽50vw,高50vh的长方形,于是得到算式:
接下来写滚轮事件逻辑,按常规来说,我们不能允许元素无止尽地缩放下去,所以当缩放比例达到上下临界值时,需要加个判断,我设它为不能放大超过3倍且不能缩小低于0.5倍。然后将宽高乘以缩放比例,就能得到滚轮事件执行后的新宽高了:
让我们来看下效果~
看起来挺nice,但不难发现还有些问题,那就是缩放元素内的子元素/文字/背景图片并没有随着父元素一同缩放。那我们就再坚持一会会,完成这最后的功能~
我们先把这段“这是一个蓝色可拖拽元素”放入一个标签中,毕竟没有外层标签就不好控制样式
给该标签绝对定位,并加上宽高100px,因为定位后元素就脱离文档流了,必须给元素加个宽高以免文字不换行等内容溢出行为。
再加一个transform-origin
,该属性为元素transform的基点,也可用作于缩放基点,X轴与Y轴的值都设为0,因为我们滚轮缩放实质是改变宽高,在改变时基点为元素的坐标(0,0)
由于子元素是随缩放比例动态缩放,所以其他的大小值可以不必写成动态,而在css里写死,如font-size、line-height等等:
既然提到了缩放,接下来的操作就很明显了,在标签中加入动态的transform: scale实现元素缩放,并且设变量meter_zoom控制缩放倍数。顺带加一个元素定位的位置,该值也需要动态变化,即(位移量
* 父元素动态宽高
) / 父元素原宽高
:
(在案例中我设该子元素向右位移0px,向下位移25px)
子元素的缩放比例计算方法则为:父元素动态宽高
/ 父元素原宽高
:
滚轮缩放事件里也要同步添加:
如此一来就算是全部大功告成啦~
收工!
demo全部代码:
<template>
<div class="drag">
<div class="back_box" ref="back_box">
这是一个背景
<div
class="drag_box"
draggable="true"
@dragstart="dragstart"
@dragend="dragend"
@wheel="handleWeel"
:style="`left:${elLeft}px;top:${elTop}px;width:${elWidth}px;height:${elHeight}px;`"
>
<div
class="text"
:style="`left:${(0 * elWidth) / 100}px;top:${
(25 * elHeight) / 100
}px;-webkit-transform: scale(${meter_zoom} )`"
>
这是一个蓝色可拖拽元素
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
initWidth: 0, // 父元素的宽-自适应值
initHeight: 0, // 父元素的高-自适应值
startclientX: 0, // 元素拖拽前距离浏览器的X轴位置
startclientY: 0, //元素拖拽前距离浏览器的Y轴位置
elLeft: 0, // 元素的左偏移量
elTop: 0, // 元素的右偏移量
zoom: 1, // 缩放比例
elWidth: 0, // 元素宽
elHeight: 0, // 元素高
meter_zoom: 0, // 子元素缩放比例
};
},
methods: {
// 页面初始化
initBodySize() {
this.initWidth = this.$refs.back_box.clientWidth; // 拿到父元素宽
// this.initHeight = this.initWidth * (1080 / 1920);
this.initHeight = this.initWidth * ((1080 * 0.88) / (1920 - 1080 * 0.02)); // 根据宽计算高实现自适应
this.elWidth = this.initWidth * (100 / (1920 / 2));
this.elHeight = this.initHeight * (100 / (1080 / 2));
this.meter_zoom = this.elWidth / 100; // 计算子元素缩放比例
},
// 拖拽开始事件
dragstart(e) {
console.log(e);
this.startclientX = e.clientX; // 记录拖拽元素初始位置
this.startclientY = e.clientY;
},
// 拖拽完成事件
dragend(e) {
console.log(e);
let x = e.clientX - this.startclientX; // 计算偏移量
let y = e.clientY - this.startclientY;
this.elLeft += x; // 实现拖拽元素随偏移量移动
this.elTop += y;
},
// 滚轮放大缩小事件
handleWeel(e) {
console.log(e);
if (e.wheelDelta < 0) {
this.zoom -= 0.05;
} else {
this.zoom += 0.05;
}
if (this.zoom >= 3) {
this.zoom = 3;
return;
}
if (this.zoom <= 0.5) {
this.zoom = 0.5;
return;
}
this.elWidth = this.initWidth * (100 / (1920 / 2)) * this.zoom;
this.elHeight = this.initHeight * (100 / (1080 / 2)) * this.zoom;
this.meter_zoom = this.elWidth / 100;
},
},
mounted() {
// console.log(this.$el);
this.initBodySize();
},
};
</script>
<style scoped>
.back_box {
background: #ccc;
width: 50vw;
height: 50vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -30%);
}
.drag_box {
width: 100px;
height: 100px;
background: skyblue;
position: absolute;
z-index: 10;
user-select: none; /* 不可选中,为了拖拽时不让文字高亮 */
}
.text {
position: absolute;
width: 100px;
height: 100px;
transform-origin: 0 0; /* 用作缩放基点 */
font-size: 16px;
}
</style>
喜欢的话请关注+点赞噢~
您的支持就是我的更新动力~~THX