🚀 个人简介:某大型测绘遥感企业资深Webgis开发工程师,软件设计师(中级)、优快云优质创作者
💟 作 者:柳晓黑胡椒❣️
📝 专 栏:vue实践
🌈 若有帮助,还请关注 ➕ 点赞➕收藏,不行的话我再努努力💪💪💪
需求背景
需要封装一个可通过拖拽实现的伸拖拽模态框
- 鼠标拖拽实现位置移动
- 鼠标拖拽实现盒子大小拉伸功能
- 可以同时存在多个模态框,当前点击显示在最上层
解决思路
1.基本功:使用js盒模型
的client
系列,offset
系列api
2.使用 getBoundingClientRect
,获取当前盒模型的位置,再处理逻辑
3.使用 insertBefore
方法,当第二个参数为 null,将直接插到子节点末尾
实现效果
组件文档
Attributes
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
v-model | 绑定值 | boolean | — | — |
class | 类目 | string | — | — |
minWidth | 最小宽度 | number | — | 500 |
minHeight | 最小宽度 | number | — | 300 |
biasVal | 拉伸区域操作padding | number | — | 15 |
initialLeft | 初始定位left | number | — | — |
initialTop | 初始定位top | number | — | — |
draggingTypes | 允许的拖拽方式 | string[] | [“move”, “left-top”, “right-top”, “left-bottom”, “right-bottom”, “left”, “right”, “top”, “bottom”] | — |
Slot
name | 说明 |
---|---|
— | dragDialog 的内容 |
Events
事件名称 | 说明 | 回调参数 |
---|---|---|
close | dragDialog 关闭的回调 | — |
index.vue
/**
* @author: liuk
* @date: 2024-12-05
* @describe:拉伸拖拽模态框,位置拖拽+盒子大小拉伸
*/
<template>
<teleport to="body">
<div v-show="visible" :class="'dragDialog-container ' + props.class " ref="dragDialogRef"
:style="{
cursor: cursorMap[draggingType],
width:`${width?width:props.minWidth}px`,
height:`${height?height:props.minHeight}px`,
left, top
}"
@mousedown.self="mousedownFn" @mousemove="mousemoveFn" @mouseup="mouseupFn">
<slot v-if="visible" name="default"></slot>
<i class="close-btn" @click="emits('close')"></i>
<div class="resize-br"></div>
</div>
</teleport>
</template>
<script lang="ts" setup>
import {reactive, ref, toRefs, computed, withDefaults, watch} from "vue";
import {useglobalStore} from "@/store/modules/global"
// Props
interface Props {
modelValue: boolean //必传
class?:string // 类目
minWidth?: number // 最小宽度
minHeight?: number // 最小高度
biasVal?: number // 拉伸区域操作padding
initialLeft?: number // 初始定位left
initialTop?: number // 初始定位top
draggingTypes?: string[] // 允许的拖拽方式 ["move", "left-top", "right-top", "left-bottom", "right-bottom", "left", "right", "top", "bottom"]
}
const props = withDefaults(defineProps<Props>(), {
minWidth: 500,
minHeight: 300,
biasVal: 15,
draggingTypes: ["move", "right-bottom", "right", "bottom"]
})
// Emits
const emits = defineEmits(['close', 'update:modelValue'])
// Refs
const dragDialogRef = ref(null)
const globalStore = useglobalStore()
const visible = computed({
get() {
return props.modelValue;
},
set(val) {
emits('update:modelValue', val);
},
})
const model = reactive({
draggingType: "default",
offsetX: 0,
offsetY: 0,
width: 0,
height: 0,
left: '',
top: '',
})
const {draggingType, width, height, left, top} = toRefs(model)
watch(() => visible.value, () => {
if (visible.value) {
globalStore.setDragDialogNum(globalStore.dragDialogNum + 1)
model.top = props.initialTop ? `${props.initialTop}px` : `calc(50% - ${props.minHeight / 2 - globalStore.dragDialogNum * 30}px)`
model.left = props.initialLeft ? `${props.initialLeft}px` : `calc(50% - ${props.minWidth / 2 - globalStore.dragDialogNum * 30}px)`
} else {
globalStore.dragDialogNum && globalStore.setDragDialogNum(globalStore.dragDialogNum - 1)
}
})
const raiseToTop = () => {
const parentEle = dragDialogRef.value.parentElement
parentEle.insertBefore(dragDialogRef.value, null)
}
const mousedownFn = (event) => {
event.preventDefault()
// top:元素上边到视窗上边的距离;right:元素右边到视窗左边的距离;bottom:元素下边到视窗上边的距离;left:元素左边到视窗左边的距离;width:是元素自身的宽height是元素自身的高
const {left, right, top, bottom} = dragDialogRef.value.getBoundingClientRect()
const {clientX, clientY} = event
// 点击后,让当前弹框处于最上层
raiseToTop()
// 根据位置判断 拖拽方式
switch (true) {
case clientX - left <= props.biasVal && clientY - top <= props.biasVal:
model.draggingType = "left-top"
break
case right - clientX <= props.biasVal && clientY - top <= props.biasVal:
model.draggingType = "right-top"
break
case clientX - left <= props.biasVal && bottom - clientY <= props.biasVal:
model.draggingType = "left-bottom"
break
case right - clientX <= props.biasVal && bottom - clientY <= props.biasVal:
model.draggingType = "right-bottom"
break
case clientX - left <= props.biasVal:
model.draggingType = "left"
break
case right - clientX <= props.biasVal:
model.draggingType = "right"
break
case clientY - top <= props.biasVal:
model.draggingType = "top"
break
case bottom - clientY <= props.biasVal:
model.draggingType = "bottom"
break
default:
model.draggingType = "move"
}
if (!props.draggingTypes.includes(model.draggingType)) model.draggingType = ""
// 拖拽 -- 算出鼠标垫到元素上边和左边的距离
model.offsetX = clientX - dragDialogRef.value.offsetLeft
model.offsetY = clientY - dragDialogRef.value.offsetTop
}
const mousemoveFn = (event) => {
const {clientX, clientY} = event
const {left, right, top, bottom, width, height} = dragDialogRef.value.getBoundingClientRect()
const parentEle = dragDialogRef.value.parentElement
let topVal, leftVal
switch (model.draggingType) {
case "move":
let x = clientX - model.offsetX;
let y = clientY - model.offsetY;
if (x >= 0 && x <= parentEle.offsetWidth - dragDialogRef.value.offsetWidth) model.left = x + 'px';
if (y >= 0 && y <= parentEle.offsetHeight - dragDialogRef.value.offsetHeight) model.top = y + 'px';
break
case "top":
topVal = top - clientY + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
model.top = top - topVal + 'px'
break
case "bottom":
topVal = clientY - bottom + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
break
case "left":
leftVal = left - clientX + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
model.left = left - leftVal + 'px'
break
case "right":
leftVal = clientX - right + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
break
case "left-top":
leftVal = left - clientX + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
model.left = left - leftVal + 'px'
topVal = top - clientY + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
model.top = top - topVal + 'px'
break
case "right-bottom":
leftVal = clientX - right + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
topVal = clientY - bottom + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
break
case "right-top":
leftVal = clientX - right + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
topVal = top - clientY + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
model.top = top - topVal + 'px'
break
case "left-bottom":
leftVal = left - clientX + props.biasVal + 10
if (width + leftVal < props.minWidth) return
model.width = width + leftVal
model.left = left - leftVal + 'px'
topVal = clientY - bottom + props.biasVal + 10
if (height + topVal < props.minHeight) return
model.height = height + topVal
break
}
}
const mouseupFn = () => {
model.draggingType = "default"
}
const cursorMap = {
"default": "default",
"move": "move",
"top": "n-resize",
"bottom": "s-resize",
"left": "w-resize",
"right": "e-resize",
"left-top": "nw-resize",
"right-bottom": "se-resize",
"right-top": "ne-resize",
"left-bottom": "sw-resize",
}
defineExpose({raiseToTop})
</script>
<style lang="scss" scoped>
.dragDialog-container {
position: absolute;
padding: 0 15px 15px 15px;
box-sizing: border-box;
z-index: 9;
background: url("@/assets/images/drag-doalog-bg.png") no-repeat center/103% 104%;
.close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 30px;
height: 30px;
background: url("@/assets/images/drag-doalog-close.png") no-repeat center/cover;
}
.resize-br {
position: absolute;
right: 2px;
bottom: 2px;
width: 40px;
height: 40px;
pointer-events: none;
background-image: url("@/assets/images/resize.svg");
}
}
</style>