Canvas 小结
1. 第三方手势库 AlloyFinger.js 在 Vue 中的使用
参考简书文章 如何在移动端实现手势缩放(缩放图片,div等)
1.1 安装三个库
npm i -D alloyfinger
npm i -D css3transform
还有一个 To.js 可以点击链接下载后放到 assets 目录下,然后打开文件,在最后一行加入 module.exports = To;
即可使用。
1.2 在 Vue 中引入它们
1.2.1 在 main.js 中引入 AlloyFinger
import AlloyFinger from 'alloyfinger'
import AlloyFingerPlugin from 'alloyfinger/vue/alloy_finger_vue'
Vue.use(AlloyFingerPlugin, {
AlloyFinger
})
1.2.2 在自定义的组件中引入 css3transform 和 To
<script>
import To from "@/assets/to.js"; //我将下载好的 to.js 放置到了 assets 目录下
import Transform from "css3transform/dist/css3transform.js";
</script>
1.3 在组件中使用 AlloyFinger
//下列手势事件可以按需选用
<div id="canvasBox"
v-finger:tap="tap"
v-finger:multipoint-start="multipointStart"
v-finger:long-tap="longTap"
v-finger:swipe="swipe"
v-finger:pinch="pinch"
v-finger:rotate="rotate"
v-finger:press-move="pressMove"
v-finger:multipoint-end="multipointEnd"
v-finger:double-tap="doubleTap"
v-finger:single-tap="singleTap"
v-finger:touch-start="touchStart"
v-finger:touch-move="touchMove"
v-finger:touch-end="touchEnd"
v-finger:touch-cancel="touchCancel"
>
//the element that you want to bind event
<canvas
ref="mapCanvas"
:width="canvasWidth"
:height="canvasHeight"
></canvas>
</div>
<script>
import To from "@/assets/to.js";
import Transform from "css3transform/dist/css3transform.js";
export default {
data() {
return {
canvas: null,
context: null,
canvasWidth: 100,
canvasHeight: 100,
};
},
methods: {
ease(x) {
return Math.sqrt(1 - Math.pow(x - 1, 2));
},
multipointStart: function () {
To.stopAll();
this.imgScale = this.canvas.scaleX;
},
pinch: function (evt) {
this.canvas.scaleX = this.canvas.scaleY = this.imgScale * evt.zoom;
},
rotate: function (evt) {
this.canvas.rotateZ += evt.angle;
},
pressMove: function (evt) {
this.canvas.translateX += evt.deltaX;
this.canvas.translateY += evt.deltaY;
evt.preventDefault();
},
multipointEnd: function () {
// 使用To.js来管理js开启的动画
To.stopAll();
// 最小缩放到0.8倍
if (this.canvas.scaleX < 0.8) {
new To(this.canvas, "scaleX", 0.8, 500, this.ease);
new To(this.canvas, "scaleY", 0.8, 500, this.ease);
}
// 最大2倍
if (this.canvas.scaleX > 2) {
new To(this.canvas, "scaleX", 2, 500, this.ease);
new To(this.canvas, "scaleY", 2, 500, this.ease);
}
// 取旋转角度
var rotation = this.canvas.rotateZ % 360;
if (rotation < 0) rotation = 360 + rotation;
this.canvas.rotateZ = rotation;
// 角度回弹
if (rotation > 0 && rotation < 45) {
new To(this.canvas, "rotateZ", 0, 500, this.ease);
} else if (rotation >= 315) {
new To(this.canvas, "rotateZ", 360, 500, this.ease);
} else if (rotation >= 45 && rotation < 135) {
new To(this.canvas, "rotateZ", 90, 500, this.ease);
} else if (rotation >= 135 && rotation < 225) {
new To(this.canvas, "rotateZ", 180, 500, this.ease);
} else if (rotation >= 225 && rotation < 315) {
new To(this.canvas, "rotateZ", 270, 500, this.ease);
}
},
tap: function() {
console.log('onTap'); },
longTap: function() {
console.log('onLongTap'); },
swipe: function(evt) {
console.log("swipe" + evt.direction);
console.log('onSwipe');
},
doubleTap: function() {
console.log('onDoubleTap'); },
singleTap: function () {
console.log('onSingleTap'); },
touchStart: function() {
console.log('onTouchStart'); },
touchMove: function() {
console.log('onTouchMove'); },
touchEnd: function() {
console.log('onTouchEnd'); },
touchCancel: function() {
console.log('onTouchCancel'); }
},
}
</script>
2. 移动端实现在屏幕上选点并在 canvas 画布上画出的方法
可以在屏幕中心固定一个点,然后通过移动画布的操作实现在 canvas 画布上的特定位置画点。
点可以通过 <div>标签
和 绝对定位
来实现
<div class="center-point" ref="point"></div>
<style lang="css" scoped>
.center-point {
width: 4px;
height: 4px;
border-radius: 2px;
background-color: red;
flex: none;
z-index: 100;
position: absolute;
left: calc(50% - 2px);
top: calc(50% - 2px);
}
</style>
<script>
export default {
methods: {
addPoint() {
//这个方法返回一个矩形对象,包含四个属性:left、top、right 和 bottom。分别表示元素各边与页面上边和左边的距离
let pointBox = this.point.getBoundingClientRect();
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2); // 2 是中心点的半径,加上后才是中心点的圆点坐标
this.context.fillStyle = "black";
this.context.beginPath();
this.context.arc(newPoint.x, newPoint.y, 1.5, 0, 2 * Math.PI);
this.context.fill();
},
//获取屏幕上的点在 canvas 中的相对坐标
windowToCanvas(x, y) {
let box = this.canvas.getBoundingClientRect();
return {
x : x - box.left;
y : y - box.top;
}
},
}
}
</script>
3. 关于 canvas 放缩时 canvas 视口上的点相对于原始图片坐标的计算
Alloy Finger.js
是针对 canvas 的视口进行缩放的。 canvas 视口缩放时,即 <canvas>
标签元素的大小发生改变,而 canvas 的画布(可绘画区域)大小不变。
假设初始情况下未通过 css 给 canvas 设置高度和宽度(也不建议这么做,建议使用 <canvas>
标签的 width
和 height
属性设置宽高),那么 canvas 的视口大小和画布大小一致。
当 canvas 的视口不等于 canvas 的画布大小时,浏览器会将画布上画好的内容映射到视口上,填满视口。
canvas 的视口缩放不影响 canvas 的画布坐标,只是将画布内容的映射在屏幕上显示了出来,造成了视觉上的放缩效果。
【canvas 放缩时的画点操作以及点坐标的计算】在进行缩放操作后,在放缩后的 canvas 视口上手动选择点,然后根据屏幕计算出该点相对于 canvas 视口的坐标,然后根据放缩比,计算出该点相对于 canvas 画布的坐标,再调用 canvas 的 api 在画布上画出来,然后更新快照,从而在放缩后的 canvas 视口上显示出来。
【总结】:先获取 canvas 放缩完成后的放缩比,然后再计算相对坐标后除以放缩比即可。
【踩坑】:监听 canvas 放缩的 pinch 事件和监听多指开始操作的 multipointStart 事件代码如下所示,this.canvasScale
即为 canvas 放缩完成后的放缩比,它的值与 evt.zoom
有关 。但是 evt.zoom
是本次放缩操作与上次放缩操作的放缩比,因此在进行多次放缩操作(比如:先放大再缩小)后,evt.zoom
只以最后一次操作为准,而坐标计算则是在所有放缩操作完成后进行计算,所以直接使用 this.canvasScale
会在进行多次放缩操作后造成误差。
pinch: function (evt) {
this.canvas.scaleX = this.canvas.scaleY = this.canvasScale * evt.zoom;
},
multipointStart: function () {
To.stopAll();
this.canvasScale = this.canvas.scaleX;
}
【解决】:通过放缩后 canvas 容器的宽度与 canvas 初始宽度的比值获取最终的放缩比。
//这个方法返回一个矩形对象,包含四个属性:left、top、right 和 bottom。分别表示元素各边与页面上边和左边的距离
let box = this.canvas.getBoundingClientRect();
let finalScale = box.width / this.canvas.width;
canvas 无论经历多少次放缩操作,其最终的容器的宽度是一定的,该值与 canvas 原始宽度的比值恰好是最终的放缩比。
因此计算相对原始 canvas 画布坐标的函数可以这样写,分析见下图
windowToCanvas(x, y) {
let box = this.canvas.getBoundingClientRect();
let finalScale = box.width / this.canvas.width;
return {
x : (x - box.left) / finalScale;
y : (y - box.top) / finalScale;
}
}
【注】:下图中 canvasWidth 为 canvas 的原始宽度。
对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点,因此计算相对于原始图片坐标的函数应为
// canvas 处于初始状态下下列各值的计算情况
let imageScale = this.canvas.height / this.img.height;
//imgX 和 imgY 是图片在 canvas 上放置的位置(图像相对于 canvas 的左边距和上边距)
this.imgX = this.canvas.width / 2 - (this.img.width * this.imgScale) / 2;
this.imgY = this.canvas.height / 2 - (this.img.height * this.imgScale) / 2;
// canvas 经过放缩、旋转后屏幕中心点的计算情况
let pointBox = this.centerPoint.getBoundingClientRect();
// 2 为屏幕中心点的半径
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2);
//对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点
//相对于原始图片的x:
let originImageX = (newPoint.x - this.imgX) / this.imgScale
//相对于原始图片的Y:
let originImageY = (this.img.height * this.imgScale - (newPoint.y - this.imgY)) / this.imgScale
计算示意图如下所示
4. 关于 canvas 旋转时 canvas 视口上的点相对于原始图片坐标的计算
【总结】:无论 canvas 如何旋转,其内部坐标系的原点都是初始位置时位于左上角的点。对于 canvas 中的图片而言,其坐标系的原点是初始位置时位于左下角的点。
4.1 旋转角为 0° 和 360° 时的情况
旋转角为 0° 和 360° 时,相当于没有发生旋转,canvas 视口上的点相对于原始图片坐标的计算和上述 canvas 放缩时 canvas 视口上的点相对于原始图片坐标的计算 方式一致。
4.2 旋转角为 90° 时的情况
【注】:canvasWidth 为 canvas 的原始宽度,缩放比和相对坐标的计算有所不同。
对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点,因此计算相对于原始图片坐标的函数应为
// canvas 处于初始状态下下列各值的计算情况
let imageScale = this.canvas.height / this.img.height;
this.imgX = this.canvas.width / 2 - (this.img.width * this.imgScale) / 2;
this.imgY = this.canvas.height / 2 - (this.img.height * this.imgScale) / 2;
// canvas 经过放缩、旋转后中心点的计算情况
let pointBox = this.centerPoint.getBoundingClientRect();
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2);
//对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点
//相对于原始图片的x:
let originImageX = (newPoint.x - this.imgX) / this.imgScale
//相对于原始图片的Y:
let originImageY = (this.img.height * this.imgScale - (newPoint.y - this.imgY)) / this.imgScale
4.3 旋转角为 180° 时的情况
【注】:canvasWidth 为 canvas 的原始宽度。
缩放比 scale = box.width / canvasWidth
相对画布坐标 (box.width-(x-box.left), box.height-(y-.box.top))
相对原始图片坐标
((box.width-(x-box.left))/scale, (box.height-(y-.box.top))/scale)
对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点,因此计算相对于原始图片坐标的函数应为
// canvas 处于初始状态下下列各值的计算情况
let imageScale = this.canvas.height / this.img.height;
this.imgX = this.canvas.width / 2 - (this.img.width * this.imgScale) / 2;
this.imgY = this.canvas.height / 2 - (this.img.height * this.imgScale) / 2;
// canvas 经过放缩、旋转后中心点的计算情况
let pointBox = this.centerPoint.getBoundingClientRect();
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2);
//对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点
//相对于原始图片的x:
let originImageX = (newPoint.x - this.imgX) / this.imgScale
//相对于原始图片的Y:
let originImageY = (this.img.height * this.imgScale - (newPoint.y - this.imgY)) / this.imgScale
4.4 旋转角为 270° 时的情况
【注】:canvasWidth 为 canvas 的原始宽度。
缩放比 scale = box.height / canvasWidth
相对画布坐标 (box.height-(y-.box.top), x-box.left)
相对原始图片坐标 ((box.height-(y-.box.top))/scale, (x-box.left)/scale)
对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点,因此计算相对于原始图片坐标的函数应为
// canvas 处于初始状态下下列各值的计算情况
let imageScale = this.canvas.height / this.img.height;
this.imgX = this.canvas.width / 2 - (this.img.width * this.imgScale) / 2;
this.imgY = this.canvas.height / 2 - (this.img.height * this.imgScale) / 2;
// canvas 经过放缩、旋转后中心点的计算情况
let pointBox = this.centerPoint.getBoundingClientRect();
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2);
//对于 canvas 中的图片而言,其坐标系的原点一直都是初始位置时位于左下角的点
//相对于原始图片的x:
let originImageX = (newPoint.x - this.imgX) / this.imgScale
//相对于原始图片的Y:
let originImageY = (this.img.height * this.imgScale - (newPoint.y - this.imgY)) / this.imgScale
4.5 核心代码
windowToCanvas(x, y) {
//这个方法返回一个矩形对象,包含四个属性:left、top、right 和 bottom。分别表示元素各边与页面上边和左边的距离
let box = this.canvas.getBoundingClientRect();
let trueScale;
if (this.canvasRotate === 90 || this.canvasRotate === 270) {
trueScale = box.height / this.canvasWidth;
} else {
trueScale = box.width / this.canvasWidth;
}
//以 canvas 未旋转时左上角为基准的(x,y)坐标
let left_top_x = x - box.left;
let left_top_y = y - box.top;
let result;
switch (this.canvasRotate) {
case 0:
result = {
x: left_top_x, y: left_top_y };
break;
case 90:
result = {
x: left_top_y, y: box.width - left_top_x };
break;
case 180:
result = {
x: box.width - left_top_x, y: box.height - left_top_y };
break;
case 270:
result = {
x: box.height - left_top_y, y: left_top_x };
break;
}
result.x /= trueScale;
result.y /= trueScale;
return result;
}
5. 撤销 canvas 中已画图形 (点、直线等) 的实现
使用 canvas 的 context 中的 getImageData()
和 putImageData()
方法可以分别获取或保存每次操作后 canvas 的快照,因此可以在 data() 函数中定义一个名称为 canvasDatas 的列表来模拟一个栈,里面存放的是每次绘画后 canvas 的快照。在进行撤销操作时,可以先将 canvasDatas 中位于栈顶的元素弹出,然后获取新的栈顶元素,将其 put 到画布上。
//向栈中添加当前绘制完成的 canvas 快照
this.canvasDatas.push(this.context.getImageData(0, 0,this.canvasWidth,this.canvasHeight));
//撤销操作,canvas 上展示的是上一次的操作快照
this.canvasDatas.pop();
let nowImageData = this.canvasDatas[this.canvasDatas.length - 1];
this.context.putImageData(nowImageData, 0, 0);
6. 完整代码
<!--
* @Author: Shimmer
* @Description: 侧边菜单和画布组件
-->
<template>
<el-container>
<transition name="slide">
<el-aside v-if="!isFold" width="210px">
<slot name="aside"></slot>
</el-aside>
</transition>
<el-main>
<el-button
style="z-index: 100"
:icon="iconFold"
class="btn-fold"
@click="isFold = !isFold"
></el-button>
<div class="btns">
<el-button type="primary" @click="addPoint" icon="el-icon-plus"
>添加点</el-button
>
<el-button type="primary" @click="removePoint" icon="el-icon-minus"
>删除点</el-button
>
<el-button-group>
<el-button @click="exit" style="width: 50%; padding: 12px 0"
>退出</el-button
>
<el-button
type="primary"
@click="savePath"
icon="el-icon-save"
style="width: 50%; padding: 12px 0"
>保存</el-button
>
</el-button-group>
</div>
<div
id="canvasBox"
style="flex: 1; z-index: 0"
v-finger:multipoint-start="multipointStart"
v-finger:pinch="pinch"
v-finger:rotate="rotate"
v-finger:press-move="pressMove"
v-finger:multipoint-end="multipointEnd"
>
<div class="center-point" ref="point"></div>
<canvas
ref="mapCanvas"
:width="canvasWidth"
:height="canvasHeight"
tabindex="0"
style="display: inline-block"
></canvas>
</div>
</el-main>
</el-container>
</template>
<script>
import To from "@/assets/to.js";
import Transform from "css3transform/dist/css3transform.js";
export default {
name: "SideMenuAndCanvas",
data() {
return {
isFold: false,
centerPoint: null,
canvas: null,
context: null,
img: null,
imgX: 0,
imgY: 0,
imgScale: 1,
canvasWidth: 100,
canvasHeight: 100,
unSavedPoints: [],
canvasDatas: [],
paths: {
},
};
},
computed: {
iconFold() {
return this.isFold ? "el-icon-s-unfold" : "el-icon-s-fold";
},
},
methods: {
exit() {
this.unSavedPoints.length = 0;
this.canvasDatas.length = 1;
let nowImageData = this.canvasDatas[this.canvasDatas.length - 1];
this.context.putImageData(nowImageData, 0, 0);
},
savePath() {
},
addPoint() {
let pointBox = this.point.getBoundingClientRect();
let newPoint = this.windowToCanvas(pointBox.left + 2, pointBox.top + 2);
this.unSavedPoints.push(newPoint);
this.context.fillStyle = "black";
this.context.beginPath();
this.context.arc(newPoint.x, newPoint.y, 1.5, 0, 2 * Math.PI);
this.context.fill();
let len = this.unSavedPoints.length;
if (len >= 2) {
this.drawLine(this.unSavedPoints[len - 2], this.unSavedPoints[len - 1]);
}
this.canvasDatas.push(
this.context.getImageData(0, 0, this.canvasWidth, this.canvasHeight)
);
},
removePoint() {
if (this.unSavedPoints.length > 0) {
this.unSavedPoints.pop();
this.canvasDatas.pop();
let nowImageData = this.canvasDatas[this.canvasDatas.length - 1];
this.context.putImageData(nowImageData, 0, 0);
} else {
this.$message.warning("您还没有添加任何点,请先添加点!");
}
},
drawLine(startPoint, endPoint) {
this.context.moveTo(startPoint.x, startPoint.y);
this.context.lineTo(endPoint.x, endPoint.y);
this.context.stroke();
},
windowToCanvas(x, y) {
//这个方法返回一个矩形对象,包含四个属性:left、top、right 和 bottom。分别表示元素各边与页面上边和左边的距离
let box = this.canvas.getBoundingClientRect();
let relativeX = x - box.left;
let relativeY = y - box.top;
if (this.canvas.rotateZ >= 0 && this.canvas.rotateZ < 45) {
let trueScale = box.width / this.canvas.width;
return {
x: relativeX / trueScale,
y: relativeY / trueScale,
};
} else if (this.canvas.rotateZ >= 315) {
let trueScale = box.width / this.canvas.width;
return {
x: relativeX / trueScale,
y: relativeY / trueScale,
};
} else if (this.canvas.rotateZ >= 45 && this.canvas.rotateZ < 135) {
let trueScale = box.height / this.canvas.width;
return {
x: relativeY / trueScale,
y: (box.width - relativeX) / trueScale,
};
} else if (this.canvas.rotateZ >= 135 && this.canvas.rotateZ < 225) {
let trueScale = box.width / this.canvas.width;
return {
x: (box.width - relativeX) / trueScale,
y: (box.height - relativeY) / trueScale,
};
} else if (this.canvas.rotateZ >= 225 && this.canvas.rotateZ < 315) {
let trueScale = box.height / this.canvas.width;
return {
x: (box.height - relativeY) / trueScale,
y: relativeX / trueScale,
};
}
},
init() {
let box = document.getElementById("canvasBox");
let that = this;
this.canvasWidth = box.clientWidth;
this.canvasHeight = box.clientHeight;
window.addEventListener("resize", () => {
that.canvasWidth = box.clientWidth;
that.canvasHeight = box.clientHeight;
setTimeout(() => {
that.context.putImageData(
that.canvasDatas[this.canvasDatas.length - 1],
0,
0
);
}, 1000);
});
this.point = this.$refs.point;
this.canvas = this.$refs.mapCanvas; //画布对象
this.context = this.canvas.getContext("2d"); //画布显示二维图片
this.loadImg();
},
imgIsLoad() {
this.drawImage();
this.canvasDatas.push(
this.context.getImageData(0, 0, this.canvasWidth, this.canvasHeight)
);
},
loadImg() {
this.img = new Image();
this.img.src = require("../assets/zex1.png");
this.img.onload = this.imgIsLoad;
},
drawImage() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.imgScale = this.canvas.height / this.img.height;
this.imgX = this.canvas.width / 2 - (this.img.width * this.imgScale) / 2;
this.imgY =
this.canvas.height / 2 - (this.img.height * this.imgScale) / 2;
this.context.drawImage(
this.img, //规定要使用的图像、画布或视频
this.imgX, //在画布上放置图像的 x y 坐标位置
this.imgY,
this.img.width * this.imgScale, //要使用的图像的宽度、高度
this.img.height * this.imgScale
);
},
ease(x) {
return Math.sqrt(1 - Math.pow(x - 1, 2));
},
multipointStart: function () {
To.stopAll();
this.imgScale = this.canvas.scaleX;
},
pinch: function (evt) {
this.canvas.scaleX = this.canvas.scaleY = this.imgScale * evt.zoom;
},
rotate: function (evt) {
this.canvas.rotateZ += evt.angle;
},
pressMove: function (evt) {
this.canvas.translateX += evt.deltaX;
this.canvas.translateY += evt.deltaY;
evt.preventDefault();
},
multipointEnd: function () {
// 使用To.js来管理js开启的动画
To.stopAll();
// 最小缩放到0.8倍
if (this.canvas.scaleX < 0.8) {
new To(this.canvas, "scaleX", 0.8, 500, this.ease);
new To(this.canvas, "scaleY", 0.8, 500, this.ease);
}
// 最大2倍
if (this.canvas.scaleX > 2) {
new To(this.canvas, "scaleX", 2, 500, this.ease);
new To(this.canvas, "scaleY", 2, 500, this.ease);
}
// 取旋转角度
var rotation = this.canvas.rotateZ % 360;
if (rotation < 0) rotation = 360 + rotation;
this.canvas.rotateZ = rotation;
// 角度回弹
if (rotation > 0 && rotation < 45) {
new To(this.canvas, "rotateZ", 0, 500, this.ease);
} else if (rotation >= 315) {
new To(this.canvas, "rotateZ", 360, 500, this.ease);
} else if (rotation >= 45 && rotation < 135) {
new To(this.canvas, "rotateZ", 90, 500, this.ease);
} else if (rotation >= 135 && rotation < 225) {
new To(this.canvas, "rotateZ", 180, 500, this.ease);
} else if (rotation >= 225 && rotation < 315) {
new To(this.canvas, "rotateZ", 270, 500, this.ease);
}
},
},
mounted() {
this.init();
Transform(this.canvas);
},
};
</script>
<style lang="css" scoped>
#canvasBox {
display: flex;
height: 100%;
}
.el-container {
height: 100%;
}
.el-aside {
background-color: #545c64;
color: #333;
text-align: center;
}
.el-main {
background-color: #e9eef3;
color: #333;
padding: 0;
display: flex;
position: relative;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter,
.slide-leave-to {
width: 0 !important;
}
.btn-fold {
width: 60px;
height: 50px;
float: left;
}
.center-point {
width: 4px;
height: 4px;
border-radius: 2px;
background-color: red;
flex: none;
z-index: 100;
position: absolute;
left: calc(50% - 2px);
top: calc(50% - 2px);
}
.btns {
position: absolute;
left: 5%;
bottom: 5%;
display: flex;
flex-direction: column;
z-index: 100;
width: 100px;
}
.el-button {
margin-top: 5px !important;
margin-left: 0 !important;
}
</style>