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

拖拽+横向拖动拉伸

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

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

该应用页面固定使用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>
相关推荐
不会聊天真君6477 分钟前
JavaScript基础语法(Web前端开发笔记第三期)
前端·javascript·笔记
IT_陈寒13 分钟前
SpringBoot自动配置这破玩意儿又坑我一次
前端·人工智能·后端
妖精的羽翼37 分钟前
前端(Vue)→ 全栈 + AI 应用开发
前端
码路飞1 小时前
玩了一圈 AI 编程工具,Background Agent 才是让我真正震撼的东西
前端·javascript
UCloud_TShare1 小时前
优刻得发布云搜索服务CSS:面向AI时代的企业级搜索基础设施
前端·css·人工智能
木斯佳1 小时前
前端八股文面经大全:字节暑期前端一面(2026-04-21)·面经深度解析
前端·面试·校招·面经·实习
Jolyne_2 小时前
前端从0开始的LangChain学习(一)
前端·langchain
掘金一周2 小时前
掘友们,一人说一个你买过夯到爆的东西 | 沸点周刊 4.23
前端·人工智能·后端
Developer_Niuge2 小时前
告别翻不动的 1000+ 书签:开源 Chrome / Edge 浏览器书签管理插件 Smart Bookmark 0.2 发布
前端·后端