弹窗之封装拖拽与拖动拉伸

拖拽+横向拖动拉伸

设置不打开直播(直播盒设置为自适应盒)不可上下拖动拉伸

上下拖动拉伸+对角拖动拉伸

该应用页面固定使用px单位

调用页面

属性draggable控制拖拽开启与关闭,属性resizable控制拖动拉伸开启与关闭;

javascript 复制代码
<DraggableResizable
  :draggable="true"
  :resizable="true"
  :resize-vertical="liveVisible"
  :min-width="420"
  :min-height="180"
  :max-width-right-gap="10"
  :max-height-bottom-reserve="160"
  :position="position"
  :size="{ width: panelSize.width, height: liveHeight }"
  :drag-handle-ref="dragHandleRef"
  :drag-target-ref="deskRef"
  :resize-target-ref="panelRef"
  @update:position="onPositionUpdate"
  @update:size="onSizeUpdate"
/>

import DraggableResizable from './utility/DraggableResizable.vue'

DraggableResizable.vue

javascript 复制代码
<template>
  <!-- 仅渲染四角四边拖拽手柄,拖拽逻辑通过 ref 绑定到外部元素 -->
  <template v-if="resizable">
    <div
      class="dr-resize-handle corner br"
      @pointerdown.prevent.stop="onResizeDown('br', $event)"
    />
    <div
      class="dr-resize-handle corner bl"
      @pointerdown.prevent.stop="onResizeDown('bl', $event)"
    />
    <div
      class="dr-resize-handle corner tr"
      @pointerdown.prevent.stop="onResizeDown('tr', $event)"
    />
    <div
      class="dr-resize-handle corner tl"
      @pointerdown.prevent.stop="onResizeDown('tl', $event)"
    />
    <div
      class="dr-resize-handle edge right"
      @pointerdown.prevent.stop="onResizeDown('r', $event)"
    />
    <div
      class="dr-resize-handle edge left"
      @pointerdown.prevent.stop="onResizeDown('l', $event)"
    />
    <div
      class="dr-resize-handle edge bottom"
      @pointerdown.prevent.stop="onResizeDown('b', $event)"
    />
    <div
      class="dr-resize-handle edge top"
      @pointerdown.prevent.stop="onResizeDown('t', $event)"
    />
  </template>
</template>

<script setup lang="ts">
import {
  defineEmits,
  defineProps,
  onBeforeUnmount,
  onMounted,
  watch,
  withDefaults,
} from 'vue'

const props = withDefaults(
  defineProps<{
    draggable?: boolean
    resizable?: boolean
    containerSelector?: string
    resizeVertical?: boolean
    minWidth?: number
    minHeight?: number
    maxWidthRightGap?: number
    maxHeightBottomReserve?: number
    position: { x: number; y: number }
    size: { width: number; height?: number }
    dragHandleRef?: HTMLElement | null
    dragTargetRef?: HTMLElement | null
    resizeTargetRef?: HTMLElement | null
  }>(),
  {
    draggable: false,
    resizable: false,
    containerSelector: '.c-map-wrapper',
    resizeVertical: true,
    minWidth: 360,
    minHeight: 160,
    maxWidthRightGap: 20,
    maxHeightBottomReserve: 160,
    dragHandleRef: null,
    dragTargetRef: null,
    resizeTargetRef: null,
  }
)

const emit = defineEmits<{
  'update:position': [payload: { x: number; y: number }]
  'update:size': [payload: { width?: number; height?: number }]
}>()

let containerRect: DOMRect | null = null
let deskRect: DOMRect | null = null
let panelRect: DOMRect | null = null
let dragging = false
let startX = 0
let startY = 0
let originX = 0
let originY = 0

let resizing = false
let resizeStartX = 0
let resizeStartY = 0
let originWidth = 0
let originHeight = 0
let originDeskX = 0
let originDeskY = 0
let resizeDirection: 't' | 'b' | 'l' | 'r' | 'tl' | 'tr' | 'bl' | 'br' | null = null

function getContainer (): HTMLElement | null {
  const ref = props.dragTargetRef
  if (!ref) return null
  return ref.closest(props.containerSelector) as HTMLElement | null
}

function onPointerDown (event: PointerEvent | MouseEvent) {
  if (!props.draggable || !props.dragTargetRef) return
  const target = event.target as HTMLElement
  if (target.closest('button') || target.closest('a')) return

  const container = getContainer()
  if (!container) return

  containerRect = container.getBoundingClientRect()
  deskRect = props.dragTargetRef.getBoundingClientRect()
  dragging = true
  startX = 'clientX' in event ? event.clientX : 0
  startY = 'clientY' in event ? event.clientY : 0
  originX = props.position.x
  originY = props.position.y

  window.addEventListener('pointermove', onPointerMove)
  window.addEventListener('pointerup', onPointerUp)
}

function onPointerMove (event: PointerEvent) {
  if (!dragging || !containerRect || !deskRect) return
  const deltaX = event.clientX - startX
  const deltaY = event.clientY - startY
  let nextX = originX + deltaX
  let nextY = originY + deltaY
  const maxX = containerRect.width - deskRect.width
  const maxY = containerRect.height - deskRect.height
  nextX = Math.min(Math.max(0, nextX), Math.max(0, maxX))
  nextY = Math.min(Math.max(0, nextY), Math.max(0, maxY))
  emit('update:position', { x: nextX, y: nextY })
}

function onPointerUp () {
  dragging = false
  window.removeEventListener('pointermove', onPointerMove)
  window.removeEventListener('pointerup', onPointerUp)
}

function onResizeDown (
  direction: 't' | 'b' | 'l' | 'r' | 'tl' | 'tr' | 'bl' | 'br',
  event: PointerEvent | MouseEvent
) {
  const container = getContainer()
  if (!container || !props.dragTargetRef || !props.resizeTargetRef) return

  const wantVertical = direction.includes('t') || direction.includes('b')
  if (!props.resizeVertical && wantVertical && (direction === 't' || direction === 'b')) return

  containerRect = container.getBoundingClientRect()
  deskRect = props.dragTargetRef.getBoundingClientRect()
  panelRect = props.resizeTargetRef.getBoundingClientRect()
  if (!panelRect || !deskRect) return

  resizing = true
  resizeDirection = direction
  resizeStartX = 'clientX' in event ? event.clientX : 0
  resizeStartY = 'clientY' in event ? event.clientY : 0
  originWidth = props.size.width
  originHeight = props.size.height ?? 0
  originDeskX = props.position.x
  originDeskY = props.position.y

  window.addEventListener('pointermove', onResizeMove)
  window.addEventListener('pointerup', onResizeUp)
}

function onResizeMove (event: PointerEvent) {
  if (!resizing || !containerRect || !panelRect || !resizeDirection) return

  const deltaX = event.clientX - resizeStartX
  const deltaY = event.clientY - resizeStartY

  let nextWidth = originWidth
  let nextHeight = originHeight
  let nextX = originDeskX

  const canResizeH = resizeDirection.includes('l') || resizeDirection.includes('r')
  const canResizeV =
    props.resizeVertical &&
    (resizeDirection.includes('t') || resizeDirection.includes('b'))

  if (canResizeH) {
    if (resizeDirection.includes('r')) nextWidth = originWidth + deltaX
    if (resizeDirection.includes('l')) {
      nextWidth = originWidth - deltaX
      nextX = originDeskX + deltaX
    }
    nextWidth = Math.max(props.minWidth, nextWidth)
    const maxWidth = containerRect.width - nextX - props.maxWidthRightGap
    nextWidth = Math.min(nextWidth, Math.max(props.minWidth, maxWidth))
    if (resizeDirection.includes('l')) {
      nextX = Math.max(
        0,
        Math.min(nextX, originDeskX + originWidth - props.minWidth)
      )
    }
  }

  if (canResizeV) {
    if (resizeDirection.includes('b')) nextHeight = originHeight + deltaY
    if (resizeDirection.includes('t')) nextHeight = originHeight - deltaY
    const available = containerRect.height - originDeskY - props.maxHeightBottomReserve
    const maxHeight = Math.max(props.minHeight, available)
    nextHeight = Math.min(
      Math.max(props.minHeight, nextHeight),
      maxHeight
    )
  }

  emit('update:position', { x: nextX, y: originDeskY })
  emit('update:size', { width: nextWidth, height: nextHeight })
}

function onResizeUp () {
  resizing = false
  resizeDirection = null
  window.removeEventListener('pointermove', onResizeMove)
  window.removeEventListener('pointerup', onResizeUp)
}

function bindDrag () {
  const el = props.dragHandleRef
  if (!el) return
  el.addEventListener('pointerdown', onPointerDown)
}

function unbindDrag () {
  const el = props.dragHandleRef
  if (!el) return
  el.removeEventListener('pointerdown', onPointerDown)
}

onMounted(() => {
  if (props.draggable && props.dragHandleRef) bindDrag()
})

onBeforeUnmount(() => {
  unbindDrag()
  onPointerUp()
  onResizeUp()
})

watch(
  () => [props.draggable, props.dragHandleRef] as const,
  ([draggable, ref]) => {
    unbindDrag()
    if (draggable && ref) bindDrag()
  }
)
</script>

<style scoped lang="scss">
.dr-resize-handle {
  position: absolute;
  background: transparent;
}

.dr-resize-handle.corner {
  width: 10px;
  height: 10px;
}

.dr-resize-handle.edge {
  z-index: 1;
}

.dr-resize-handle.corner.br {
  right: 0;
  bottom: 0;
  cursor: se-resize;
}

.dr-resize-handle.corner.bl {
  left: 0;
  bottom: 0;
  cursor: sw-resize;
}

.dr-resize-handle.corner.tr {
  right: 0;
  top: 0;
  cursor: ne-resize;
}

.dr-resize-handle.corner.tl {
  left: 0;
  top: 0;
  cursor: nw-resize;
}

.dr-resize-handle.edge.right {
  top: 10px;
  bottom: 10px;
  right: 0;
  width: 6px;
  cursor: e-resize;
}

.dr-resize-handle.edge.left {
  top: 10px;
  bottom: 10px;
  left: 0;
  width: 6px;
  cursor: w-resize;
}

.dr-resize-handle.edge.bottom {
  left: 10px;
  right: 10px;
  bottom: 0;
  height: 6px;
  cursor: s-resize;
}

.dr-resize-handle.edge.top {
  left: 10px;
  right: 10px;
  top: 0;
  height: 6px;
  cursor: n-resize;
}
</style>
相关推荐
清汤饺子2 小时前
Spec Kit:让 AI 编程从 Vibe Coding 到 Spec First
前端·javascript·后端
爱学习的小仙女!2 小时前
面试题 前端(二)元素显示模式 块元素行内元素区别
前端·前端面试题
酉鬼女又兒2 小时前
零基础快速入门前端蓝桥杯 Web 备考:AJAX 与 XMLHttpRequest 核心知识点及实战(可用于备赛蓝桥杯Web应用开发)
前端·ajax·职场和发展·蓝桥杯·css3·js
Sammyyyyy2 小时前
Node.js、Bun 与 Deno,2026 年后端运行时选择指南
前端·后端·node.js·servbay
默 语2 小时前
Web Access:一个Skill,拉满Agent联网和浏览器能力
前端·agent·skill
攒了一袋星辰2 小时前
类抖音的高并发评论盖楼系统
服务器·前端·数据库
大胡子大叔2 小时前
React组件化实现程序化视频生成
前端·react.js·音视频
wjcroom3 小时前
融释涡旋理论-对狭义相对论和洛伦兹变换的兼容
开发语言·前端