DragResizeZoom.vue
背景:在cesium大屏项目需要拖拽,缩放模块,还需要全屏。
功能如下:
-
拖拽组件
-
拉升组件长宽
-
对角线拉升会比例缩放
-
双击组件则全屏
-
复原长宽和位置
使用案例:
ts
<script setup lang="ts">
import { ref } from "vue";
import DragResizeZoom from "./components/DragResizeZoom.vue";
const dragResizeZoom = ref<InstanceType<typeof DragResizeZoom> | null>(null);
function handleReset() {
dragResizeZoom.value?.resetPosition();
}
</script>
<template>
<div id="app">
<button @click="handleReset">reset</button>
<DragResizeZoom
ref="dragResizeZoom"
:initial-x="100"
:initial-y="100"
:initial-width="400"
:initial-height="300"
xdirection="right"
>
<div class="c1">
<div><h1>这是一个标题</h1></div>
<div>这是一个段落fdsafdsafdsafdsfdsa</div>
</div>
</DragResizeZoom>
</div>
</template>
<style scoped style="less">
#app {
width: 100vw;
height: 100vh;
background-color: #b4b4b4;
}
.c1 {
/* width: 300px;
height: 300px; */
background-color: #fff;
color: pink;
}
</style>
ts
<template>
<div
ref="containerRef"
class="drag-resize-zoom"
:style="containerStyle"
@mousedown="handleMouseDown"
@mouseenter="showHandles = true"
@mouseleave="showHandles = false"
@dblclick="toggleFullscreen"
>
<slot></slot>
<!-- Resize handles -->
<!-- <div class="resize-handle resize-nw" @mousedown="(e: any) => handleResizeMouseDown(e, 'nw')"></div> -->
<!-- <div
class="resize-handle resize-ne"
@mousedown="(e: any) => handleResizeMouseDown(e, 'ne')"
></div> -->
<!-- <div
class="resize-handle resize-sw"
@mousedown="(e: any) => handleResizeMouseDown(e, 'sw')"
></div> -->
<div
class="resize-handle resize-se"
:class="{ 'handle-visible': showHandles }"
@mousedown="(e: any) => handleResizeMouseDown(e, 'se')"
></div>
<!-- <div class="resize-handle resize-n" @mousedown="(e: any) => handleResizeMouseDown(e, 'n')"></div> -->
<div
class="resize-handle resize-s"
:class="{ 'handle-visible': showHandles }"
@mousedown="(e: any) => handleResizeMouseDown(e, 's')"
></div>
<!-- <div
class="resize-handle resize-w"
@mousedown="(e: any) => handleResizeMouseDown(e, 'w')"
></div> -->
<div
class="resize-handle resize-e"
:class="{ 'handle-visible': showHandles }"
@mousedown="(e: any) => handleResizeMouseDown(e, 'e')"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
// 添加props定义
const props = defineProps({
initialX: {
type: Number,
default: 0,
},
initialY: {
type: Number,
default: 0,
},
initialWidth: {
type: Number,
default: 300,
},
initialHeight: {
type: Number,
default: 300,
},
// 添加xdirection属性,支持'left'和'right'值
xdirection: {
type: String,
default: "left",
validator: (value: string) => ["left", "right"].includes(value),
},
});
const containerRef = ref<HTMLElement | null>(null);
// 添加控制句柄显示的状态
const showHandles = ref(false);
// 添加全屏状态
const isFullscreen = ref(false);
// 组件状态
const state = ref({
x: props.initialX, // 使用props.initialX初始化
y: props.initialY, // 使用props.initialY初始化
width: props.initialWidth, // 使用props.initialWidth初始化
height: props.initialHeight, // 使用props.initialHeight初始化
zoom: 1,
isDragging: false,
isResizing: false,
dragStartX: 0,
dragStartY: 0,
startX: 0,
startY: 0,
startWidth: 0,
startHeight: 0,
resizeDirection: "",
// 添加right和bottom属性
right: undefined as number | undefined,
bottom: undefined as number | undefined,
});
// 容器样式
const containerStyle = computed(() => {
// 根据xdirection决定使用left还是right定位
if (props.xdirection === "right") {
return {
position: "absolute" as const,
right: state.value.x + "px",
top: state.value.y + "px",
// 添加bottom,当它有值时使用
...(state.value.bottom !== undefined && {
bottom: state.value.bottom + "px",
}),
width: state.value.width + "px",
height: state.value.height + "px",
transform: `scale(${state.value.zoom})`,
transformOrigin: "top left",
};
} else {
return {
position: "absolute" as const,
left: state.value.x + "px",
top: state.value.y + "px",
// 添加right和bottom,当它们有值时使用
...(state.value.right !== undefined && {
right: state.value.right + "px",
}),
...(state.value.bottom !== undefined && {
bottom: state.value.bottom + "px",
}),
width: state.value.width + "px",
height: state.value.height + "px",
transform: `scale(${state.value.zoom})`,
transformOrigin: "top left",
};
}
});
// 处理鼠标按下事件(拖拽)
const handleMouseDown = (e: MouseEvent) => {
if ((e.target as HTMLElement).classList.contains("resize-handle")) {
return;
}
state.value.isDragging = true;
state.value.dragStartX = e.clientX;
state.value.dragStartY = e.clientY;
state.value.startX = state.value.x;
state.value.startY = state.value.y;
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// 处理鼠标移动事件(拖拽)
const handleMouseMove = (e: MouseEvent) => {
if (state.value.isDragging) {
// 根据xdirection决定如何更新x坐标
if (props.xdirection === "right") {
state.value.x = state.value.startX - (e.clientX - state.value.dragStartX);
} else {
state.value.x = state.value.startX + (e.clientX - state.value.dragStartX);
}
state.value.y = state.value.startY + (e.clientY - state.value.dragStartY);
}
if (state.value.isResizing) {
resizeComponent(e);
}
};
// 处理鼠标释放事件
const handleMouseUp = () => {
state.value.isDragging = false;
state.value.isResizing = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
// 处理调整大小鼠标按下事件
const handleResizeMouseDown = (e: MouseEvent, direction: string) => {
e.stopPropagation();
state.value.isResizing = true;
state.value.resizeDirection = direction;
state.value.startX = e.clientX;
state.value.startY = e.clientY;
state.value.startWidth = state.value.width;
state.value.startHeight = state.value.height;
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// 调整组件大小
const resizeComponent = (e: MouseEvent) => {
const deltaX = e.clientX - state.value.startX;
const deltaY = e.clientY - state.value.startY;
const direction = state.value.resizeDirection;
// 记录调整前的尺寸用于计算缩放
const oldWidth = state.value.width;
const oldHeight = state.value.height;
const oldX = state.value.x;
const oldY = state.value.y;
// 重置right和bottom值
state.value.right = undefined;
state.value.bottom = undefined;
switch (direction) {
case "nw": // 西北角
state.value.width = Math.max(50, state.value.startWidth - deltaX);
state.value.height = Math.max(50, state.value.startHeight - deltaY);
// 修改:根据需求调整left和top定位
if (state.value.width !== oldWidth || state.value.height !== oldHeight) {
if (props.xdirection === "right") {
state.value.x = oldX - (state.value.startWidth - state.value.width);
} else {
state.value.x = e.clientX;
}
state.value.y = oldY + (state.value.startHeight - state.value.height);
}
break;
case "ne": // 东北角
state.value.width = Math.max(50, state.value.startWidth + deltaX);
state.value.height = Math.max(50, state.value.startHeight - deltaY);
break;
case "sw": // 西南角
state.value.width = Math.max(50, state.value.startWidth - deltaX);
state.value.height = Math.max(50, state.value.startHeight + deltaY);
// 使用right定位
if (state.value.width !== oldWidth) {
if (props.xdirection === "right") {
state.value.x = oldX - (state.value.startWidth - state.value.width);
} else {
state.value.right = window.innerWidth - oldX - state.value.startWidth;
state.value.x = oldX + (state.value.startWidth - state.value.width);
}
}
break;
case "se": // 东南角
state.value.width = Math.max(50, state.value.startWidth + deltaX);
state.value.height = Math.max(50, state.value.startHeight + deltaY);
break;
case "n": // 上边
state.value.height = Math.max(50, state.value.startHeight - deltaY);
if (state.value.height !== oldHeight) {
state.value.y = oldY + (state.value.startHeight - state.value.height);
}
break;
case "s": // 下边
state.value.height = Math.max(50, state.value.startHeight + deltaY);
break;
case "w": // 左边
state.value.width = Math.max(50, state.value.startWidth - deltaX);
if (state.value.width !== oldWidth) {
if (props.xdirection === "right") {
state.value.x = oldX - (state.value.startWidth - state.value.width);
} else {
state.value.x = oldX + (state.value.startWidth - state.value.width);
}
}
break;
case "e": // 右边
state.value.width = Math.max(50, state.value.startWidth + deltaX);
break;
}
// 计算缩放比例(仅对角调整时应用缩放)
if (
direction === "se" ||
direction === "ne" ||
direction === "sw" ||
direction === "nw"
) {
const widthRatio = state.value.width / state.value.startWidth;
const heightRatio = state.value.height / state.value.startHeight;
state.value.zoom = Math.min(widthRatio, heightRatio);
}
};
// 添加全屏切换功能
const toggleFullscreen = () => {
if (!containerRef.value) return;
const element = containerRef.value;
if (!isFullscreen.value) {
// 进入全屏
if (element.requestFullscreen) {
element.requestFullscreen();
} else if ((element as any).mozRequestFullScreen) {
// Firefox
(element as any).mozRequestFullScreen();
} else if ((element as any).webkitRequestFullscreen) {
// Chrome, Safari and Opera
(element as any).webkitRequestFullscreen();
} else if ((element as any).msRequestFullscreen) {
// IE/Edge
(element as any).msRequestFullscreen();
}
} else {
// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen();
} else if ((document as any).mozCancelFullScreen) {
// Firefox
(document as any).mozCancelFullScreen();
} else if ((document as any).webkitExitFullscreen) {
// Chrome, Safari and Opera
(document as any).webkitExitFullscreen();
} else if ((document as any).msExitFullscreen) {
// IE/Edge
(document as any).msExitFullscreen();
}
}
isFullscreen.value = !isFullscreen.value;
};
// 监听全屏变化事件
const handleFullscreenChange = () => {
isFullscreen.value = !!(
document.fullscreenElement ||
(document as any).mozFullScreenElement ||
(document as any).webkitFullscreenElement ||
(document as any).msFullscreenElement
);
};
// 添加恢复初始位置的函数
const resetPosition = () => {
state.value.x = props.initialX;
state.value.y = props.initialY;
state.value.width = props.initialWidth;
state.value.height = props.initialHeight;
state.value.zoom = 1;
state.value.right = undefined;
state.value.bottom = undefined;
};
// 暴露函数给父组件使用
defineExpose({
resetPosition,
});
// 清理事件监听器
onBeforeUnmount(() => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("fullscreenchange", handleFullscreenChange);
document.removeEventListener(
"webkitfullscreenchange",
handleFullscreenChange
);
document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
});
// 添加全屏事件监听
onMounted(() => {
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
document.addEventListener("mozfullscreenchange", handleFullscreenChange);
document.addEventListener("MSFullscreenChange", handleFullscreenChange);
});
</script>
<style scoped>
.drag-resize-zoom {
/* margin: 3px; */
position: relative;
cursor: move;
z-index: 9999;
overflow: hidden;
}
.resize-handle {
position: absolute;
/* background: linear-gradient(135deg, #4a90e2, #1a3a6a); */
background-color: #5a9fff;
z-index: 9999;
opacity: 0; /* 默认隐藏 */
transition: opacity 0.2s ease;
}
.handle-visible {
opacity: 1;
}
.resize-handle::before {
content: "";
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
opacity: 0;
}
.resize-handle:hover::before {
opacity: 1;
}
.resize-se {
width: 12px;
height: 12px;
background: transparent;
border: none;
box-shadow: none;
top: auto;
right: 0px;
bottom: 0px;
cursor: se-resize; /* 添加斜对角鼠标样式 */
}
.resize-se::before {
display: none;
}
.resize-se::after {
content: "";
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 12px 12px;
border-color: transparent transparent #4a90e2 transparent;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
/* transition: all 0.2s ease; */
}
.resize-se:hover::after {
border-color: transparent transparent #5aa0ff transparent;
box-shadow: 2px 2px 4px rgba(74, 144, 226, 0.8);
transform: scale(1.1);
}
.resize-s {
/* 下边句柄改为瘦高 */
width: 100%;
height: 3px;
/* border-radius: 1px; */
bottom: 0px;
left: 0%;
margin-left: -8px;
cursor: s-resize;
}
.resize-e {
/* 右边句柄改为瘦高 */
width: 3px;
height: 100%;
/* border-radius: 2px; */
top: 0%;
right: 0px;
margin-top: -8px;
cursor: e-resize;
}
</style>