vue3 实现拉伸拖拽模态框,位置拖拽+盒子大小拉伸

🚀 个人简介:某大型测绘遥感企业资深Webgis开发工程师,软件设计师(中级)、优快云优质创作者
💟 作 者:柳晓黑胡椒❣️
📝 专 栏:vue实践
🌈 若有帮助,还请关注点赞收藏,不行的话我再努努力💪💪💪

需求背景

需要封装一个可通过拖拽实现的伸拖拽模态框

  1. 鼠标拖拽实现位置移动
  2. 鼠标拖拽实现盒子大小拉伸功能
  3. 可以同时存在多个模态框,当前点击显示在最上层

解决思路

1.基本功:使用js盒模型client系列,offset系列api
2.使用 getBoundingClientRect ,获取当前盒模型的位置,再处理逻辑
3.使用 insertBefore方法,当第二个参数为 null,将直接插到子节点末尾

实现效果

组件文档

Attributes

参数说明类型可选值默认值
v-model绑定值boolean
class类目string
minWidth最小宽度number500
minHeight最小宽度number300
biasVal拉伸区域操作paddingnumber15
initialLeft初始定位leftnumber
initialTop初始定位topnumber
draggingTypes允许的拖拽方式string[][“move”, “left-top”, “right-top”, “left-bottom”, “right-bottom”, “left”, “right”, “top”, “bottom”]

Slot

name说明
dragDialog 的内容

Events

事件名称说明回调参数
closedragDialog 关闭的回调

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳晓黑胡椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值