最近项目中需要用到很多的可以收缩的弹窗,于是使用vue3做了一个通用组件,给大家分享一下,效果如下图,附源码。
实现思路:
1、弹窗上加4个角,用来拖拽缩放。
<div class="resize-handle-tl resize-handle"></div>
<div class="resize-handle-tr resize-handle"></div>
<div class="resize-handle-bl resize-handle"></div>
<div class="resize-handle-br resize-handle"></div>
2、设置4个角的样式以及绝对定位。
.resize-handle-br {
width: 10px;
height: 10px;
position: absolute;
bottom: 0;
right: 0;
cursor: se-resize;
}
.resize-handle-bl {
width: 10px;
height: 10px;
position: absolute;
bottom: 0;
left: 0;
cursor: sw-resize;
}
.resize-handle-tl {
width: 10px;
height: 10px;
position: absolute;
top: 0;
left: 0;
cursor: nw-resize;
}
.resize-handle-tr {
width: 10px;
height: 10px;
position: absolute;
top: 0;
right: 0;
cursor: ne-resize;
}
3、监听4个角的鼠标拖拽事件实现缩放。
onMounted(() => {
resizableBox = document.getElementById("resizable-box");
resizeHandle = document.querySelectorAll(".resize-handle");
resizeHandle.forEach((handle) => {
handle.addEventListener("mousedown", function (e) {
e.preventDefault();
originalWidth = parseFloat(getComputedStyle(resizableBox).width);
originalHeight = parseFloat(getComputedStyle(resizableBox).height);
originalMouseX = e.clientX;
originalMouseY = e.clientY;
resizeType = this.className;
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResize);
});
});
});
4、缩放核心代码:
let firstLeft = props.left; // 初始位置
let firstTop = props.top; // 初始位置
let firstBottom = props.bottom; // 初始位置
let firstRight = props.right // 初始位置
let lastTop = 0; // 上次移动距离
let lastLeft = 0; // 上次移动距离
let lastBottom = 0; // 上次移动距离
let lastRight = 0; // 上次移动距离
const resize = (e) => {
const deltaX = e.clientX - originalMouseX;
const deltaY = e.clientY - originalMouseY;
resizableBox = document.getElementById("resizable-box");
if (resizeType.includes("resize-handle-tl")) {
if (resizableBox.style.left) {
resizableBox.style.left = `${
originalX + deltaX + lastLeft + firstLeft
}px`;
resizableBox.style.top = `${originalY + deltaY + lastTop + firstTop}px`;
}
resizableBox.style.width = `${originalWidth - deltaX}px`;
resizableBox.style.height = `${originalHeight - deltaY}px`;
} else if (resizeType.includes("resize-handle-tr")) {
if(resizableBox.style.top) {
resizableBox.style.top = `${originalY + deltaY + firstTop + lastTop}px`;
}else {
resizableBox.style.right = `${ originalX - deltaX + firstRight -lastRight}px`;
}
resizableBox.style.width = `${originalWidth + deltaX}px`;
resizableBox.style.height = `${originalHeight - deltaY}px`;
} else if (resizeType.includes("resize-handle-bl")) {
if( resizableBox.style.left) {
resizableBox.style.left = `${originalX + deltaX + firstLeft + lastLeft}px`;
}else {
resizableBox.style.bottom = `${originalY - deltaY + firstBottom - lastBottom}px`;
}
resizableBox.style.width = `${originalWidth - deltaX}px`;
resizableBox.style.height = `${originalHeight + deltaY}px`;
} else if (resizeType.includes("resize-handle-br")) {
if(resizableBox.style.right) {
resizableBox.style.right = `${ originalX - deltaX + firstRight -lastRight}px`;
resizableBox.style.bottom = `${originalY - deltaY + firstBottom - lastBottom}px`;
}
resizableBox.style.width = `${originalWidth + deltaX}px`;
resizableBox.style.height = `${originalHeight + deltaY}px`;
}
};
const stopResize = (e) => {
if(e.target.classList.contains('resize-handle-tl')) {
lastTop += e.pageY - originalMouseY;
lastLeft += e.pageX - originalMouseX;
}else if(e.target.classList.contains('resize-handle-tr')) {
lastTop += e.pageY - originalMouseY;
lastRight += e.pageX - originalMouseX;
}else if(e.target.classList.contains('resize-handle-bl')) {
lastLeft += e.pageX - originalMouseX;
lastBottom += e.pageY - originalMouseY
}else if(e.target.classList.contains('resize-handle-br')) {
lastBottom += e.pageY - originalMouseY
lastRight += e.pageX - originalMouseX;
}
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResize);
};
完整代码如下:
components代码(pop-up.vue)
<template>
<div class="pop-modal" @click.stop="closePop">
<div
id="resizable-box"
class="popup-content"
:style="{
width: width,
height: height,
top: top + 'px',
left: left + 'px',
right: right + 'px',
bottom: bottom + 'px',
}"
@click.stop="stopClose"
>
<div class="popup-container overflow-y-auto">
<div class="popup-title ALIMAMA_SHUHEITI_BOLD relative flex align-center justif-between">
<span>{{ title }}</span>
<popUpIconVue class="r-0 t-0 h-p-40 w-20 mr-5"></popUpIconVue>
</div>
<el-table
:data="tableData"
stripe
style="width: 100%; overflow-y: auto"
>
<el-table-column prop="target" label="指标" />
<el-table-column prop="unit" label="单位" width="120px" />
<el-table-column prop="value" :label="yearLabel" />
<el-table-column prop="speed" label="增长速度%" width="120px" />
</el-table>
</div>
<div class="resize-handle-tl resize-handle"></div>
<div class="resize-handle-tr resize-handle"></div>
<div class="resize-handle-bl resize-handle"></div>
<div class="resize-handle-br resize-handle"></div>
</div>
</div>
</template>
<script setup>
import { defineProps, onMounted, defineEmits } from "vue";
import popUpIconVue from "../icons/popUpIcon.vue";
const emit = defineEmits(["close", "change"]);
const props = defineProps({
width: {
type: [Number, String],
default: "38vw", // 默认宽度
},
height: {
type: [Number, String],
default: "41vh", // 默认高度
},
top: {
type: Number,
default: null,
},
left: {
type: Number,
default: null,
},
bottom: {
type: Number,
default: null,
},
right: {
type: Number,
default: null,
},
title: {
type: String,
default: "",
},
tableData: {
type: Array,
default: [],
},
yearLabel: {
type: String,
default: "2023年",
}
});
let originalWidth = 0;
let originalHeight = 0;
let originalX = 0;
let originalY = 0;
let originalMouseX = 0;
let originalMouseY = 0;
let resizableBox = null;
let resizeHandle = [];
let resizeType = "";
onMounted(() => {
resizableBox = document.getElementById("resizable-box");
resizeHandle = document.querySelectorAll(".resize-handle");
resizeHandle.forEach((handle) => {
handle.addEventListener("mousedown", function (e) {
e.preventDefault();
originalWidth = parseFloat(getComputedStyle(resizableBox).width);
originalHeight = parseFloat(getComputedStyle(resizableBox).height);
originalMouseX = e.clientX;
originalMouseY = e.clientY;
resizeType = this.className;
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResize);
});
});
});
let firstLeft = props.left;
let firstTop = props.top;
let firstBottom = props.bottom;
let firstRight = props.right
let lastTop = 0;
let lastLeft = 0;
let lastBottom = 0;
let lastRight = 0;
const resize = (e) => {
const deltaX = e.clientX - originalMouseX;
const deltaY = e.clientY - originalMouseY;
resizableBox = document.getElementById("resizable-box");
if (resizeType.includes("resize-handle-tl")) {
if (resizableBox.style.left) {
resizableBox.style.left = `${
originalX + deltaX + lastLeft + firstLeft
}px`;
resizableBox.style.top = `${originalY + deltaY + lastTop + firstTop}px`;
}
resizableBox.style.width = `${originalWidth - deltaX}px`;
resizableBox.style.height = `${originalHeight - deltaY}px`;
} else if (resizeType.includes("resize-handle-tr")) {
if(resizableBox.style.top) {
resizableBox.style.top = `${originalY + deltaY + firstTop + lastTop}px`;
}else {
resizableBox.style.right = `${ originalX - deltaX + firstRight -lastRight}px`;
}
resizableBox.style.width = `${originalWidth + deltaX}px`;
resizableBox.style.height = `${originalHeight - deltaY}px`;
} else if (resizeType.includes("resize-handle-bl")) {
if( resizableBox.style.left) {
resizableBox.style.left = `${originalX + deltaX + firstLeft + lastLeft}px`;
}else {
resizableBox.style.bottom = `${originalY - deltaY + firstBottom - lastBottom}px`;
}
resizableBox.style.width = `${originalWidth - deltaX}px`;
resizableBox.style.height = `${originalHeight + deltaY}px`;
} else if (resizeType.includes("resize-handle-br")) {
if(resizableBox.style.right) {
resizableBox.style.right = `${ originalX - deltaX + firstRight -lastRight}px`;
resizableBox.style.bottom = `${originalY - deltaY + firstBottom - lastBottom}px`;
}
resizableBox.style.width = `${originalWidth + deltaX}px`;
resizableBox.style.height = `${originalHeight + deltaY}px`;
}
};
const stopResize = (e) => {
if(e.target.classList.contains('resize-handle-tl')) {
lastTop += e.pageY - originalMouseY;
lastLeft += e.pageX - originalMouseX;
}else if(e.target.classList.contains('resize-handle-tr')) {
lastTop += e.pageY - originalMouseY;
lastRight += e.pageX - originalMouseX;
}else if(e.target.classList.contains('resize-handle-bl')) {
lastLeft += e.pageX - originalMouseX;
lastBottom += e.pageY - originalMouseY
}else if(e.target.classList.contains('resize-handle-br')) {
lastBottom += e.pageY - originalMouseY
lastRight += e.pageX - originalMouseX;
}
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResize);
};
const closePop = () => {
emit('close');
};
const stopClose = () => {};
</script>
<style lang="scss" scoped>
.pop-modal {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 99999;
}
.popup-content {
background: rgb(161, 177, 255);
padding: 1rem;
position: absolute;
z-index: 1;
}
.popup-container {
background: #fff;
width: 100%;
height: 100%;
padding: 0.5rem;
}
.popup-title {
background:linear-gradient(rgb(31,97,255),rgb(53,79,255), rgb(75,66,255),rgb(80,63,255));
min-height: 9%;
width: 100%;
color: #fff;
padding: 0 0 0 1rem;
display: flex;
align-items: center;
}
.resize-handle-br {
width: 10px;
height: 10px;
// background-color: #333;
position: absolute;
bottom: 0;
right: 0;
cursor: se-resize;
}
.resize-handle-bl {
width: 10px;
height: 10px;
// background-color: #333;
position: absolute;
bottom: 0;
left: 0;
cursor: sw-resize;
}
.resize-handle-tl {
width: 10px;
height: 10px;
// background-color: #333;
position: absolute;
top: 0;
left: 0;
cursor: nw-resize;
}
.resize-handle-tr {
width: 10px;
height: 10px;
// background-color: #333;
position: absolute;
top: 0;
right: 0;
cursor: ne-resize;
}
</style>
<style>
.popup-content .el-table th.el-table__cell {
background-color: rgb(236, 236, 255) !important;
color: #000;
font-family: "MiSans-Bold";
font-size: 1.33rem;
}
.popup-content .el-table__body-wrapper .el-table__row .el-table__cell {
background: #fff !important;
}
.popup-content .el-table__body-wrapper .el-table__row--striped .el-table__cell {
background: rgb(236, 236, 255) !important;
}
.popup-content .el-table td.el-table__cell {
border-bottom: none !important;
}
.popup-content .el-table th.el-table__cell.is-leaf {
border-bottom: none !important;
}
.popup-content .el-table__inner-wrapper:before {
background-color: #fff !important;
}
</style>
父组件中调用:
<popUp
v-if="popShow"
:title="popTitle"
:tableData="grossData"
:top="top"
:left="left"
:right="right"
:bottom="bottom"
:yearLabel="yearLabel"
@close="closePop"
></popUp>