引言
在前端开发中,实现可交互的组件能够极大地提升用户体验。本文将介绍一个基于 Vue 封装的可缩放卡片组件,从实现思路、代码具体实现以及使用方法等方面进行详细阐述,帮助开发者更好地理解和运用这一组件。项目源码地址:https://gitcode.com/Jiaberrr/vue3-pc-template
实现思路
- 定位与布局 :通过
position: absolute
对卡片进行定位,利用left
、top
、right
、bottom
属性确定其在页面中的位置,同时设置width
和height
来定义卡片的初始大小。 - 缩放控制点 :在卡片的四个角(左上角、右上角、左下角、右下角)添加可交互的缩放控制点,通过监听这些控制点的鼠标事件(
mousedown
、mousemove
、mouseup
)来实现卡片的缩放功能。 - 状态跟踪:使用变量来记录卡片的初始大小、位置以及鼠标的初始位置,在缩放过程中根据鼠标的移动距离计算卡片新的大小和位置。
代码实现
模板部分(template)
html
<template>
<div class="absolute" :id="idName" :style="{width: width,height: height,top: top + 'px',left: left + 'px',right: right + 'px',bottom: bottom + 'px'}">
<slot></slot>
<div class="resize-handle-tl" :class="'resize-handle'+ idName"></div>
<div class="resize-handle-tr" :class="'resize-handle'+ idName"></div>
<div class="resize-handle-bl" :class="'resize-handle'+ idName"></div>
<div class="resize-handle-br" :class="'resize-handle'+ idName"></div>
</div>
</template>
在模板中,外层div
通过id
和style
绑定来设置卡片的位置和大小。slot
用于插入卡片的内容,四个角的div
分别代表缩放控制点,通过动态绑定类名来标识不同的控制点。
script 部分(script setup)
javascript
import { onMounted } from "vue";
const porp = defineProps({
idName: {
Type: String,
required: true
},
width: {
type: [Number, String],
default: "100%", // 默认宽度
},
height: {
type: [Number, String],
default: "100%", // 默认高度
},
top: {
type: Number,
default: null,
},
left: {
type: Number,
default: null,
},
bottom: {
type: Number,
default: null,
},
right: {
type: Number,
default: null,
}
})
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(porp.idName);
resizeHandle = document.querySelectorAll(".resize-handle"+ porp.idName);
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 = porp.left;
let firstTop = porp.top;
let firstBottom = porp.bottom;
let firstRight = porp.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(porp.idName);
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);
};
- 属性定义 :通过
defineProps
定义组件接受的属性,包括idName
(必选,用于唯一标识卡片)、width
、height
、top
、left
、bottom
、right
,并设置了默认值。 - 变量初始化:声明了一系列变量用于跟踪卡片的初始状态和缩放过程中的状态。
- 生命周期钩子 :在
onMounted
钩子函数中,获取卡片元素和缩放控制点元素,并为每个缩放控制点添加mousedown
事件监听器。当鼠标按下时,记录卡片的初始大小和鼠标位置,同时添加mousemove
和mouseup
事件监听器。 - 缩放函数 :
resize
函数根据鼠标移动的距离和缩放控制点的类型来计算并更新卡片的大小和位置。 - 停止缩放函数 :
stopResize
函数在鼠标松开时,移除mousemove
和mouseup
事件监听器,并更新卡片位置的累计偏移量。
样式部分(style scoped)
css
.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;
}
样式部分定义了四个缩放控制点的大小、位置和鼠标悬停时的光标样式。
使用方法
在 Vue 项目中使用该组件,首先确保组件已正确引入和注册。例如,在父组件的模板中:
javascript
<template>
<div id="app">
<ScalableCard
idName="myCard"
width="300px"
height="200px"
top="100"
left="100"
>
<p>这是卡片的内容</p>
</ScalableCard>
</div>
</template>
<script setup>
import ScalableCard from './components/ScalableCard.vue';
</script>
在上述示例中,通过传入idName
、width
、height
、top
、left
等属性来定制卡片的初始状态,并在组件内部插入卡片内容。
总结
通过上述的实现思路、代码实现和使用方法介绍,我们可以看到这个基于 Vue 的可缩放卡片组件为前端开发中实现可交互的卡片功能提供了一个有效的解决方案。你也可以根据实际需求进一步扩展和优化该组件,以满足不同项目的需求。希望本文能对大家有所帮助。