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

拖拽+横向拖动拉伸

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

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

该应用页面固定使用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>
相关推荐
小小小小宇3 分钟前
设计稿转代码:如何将生成代码与内部组件库关联
前端
七牛云行业应用4 分钟前
别每个 AI 工具单独配了!MCP 一次搭建,Claude、Cursor、TRAE 全能用
前端
_xaboy4 分钟前
FormCreate 设计器 v6.3 正式发布:AI 表单助理3.0登场!
前端·vue.js·低代码·开源·表单设计器
胡志辉5 分钟前
邮件中点击“加载图片”,你的IP地址已经被泄漏
前端·后端·安全
openKaka_17 分钟前
reconcileChildren 深入:React 如何根据 ReactElement 构建子 Fiber
前端·javascript·react.js
三翼鸟数字化技术团队32 分钟前
事件循环原来这么简单!
前端
gf132111134 分钟前
python_【更新已发送的消息卡片】
java·前端·python
一点一木43 分钟前
2026 终端 AI 编码 Agent 六大工具深度横评
前端·人工智能·claude
Highcharts.js1 小时前
Highcharts React v5升级三问|最大的升级方向是什么?需要注意什么?有什么优化?
前端·javascript·react.js·前端框架·highcharts·大数据渲染·前端性能
马玉霞1 小时前
vue web端页面组件展示
前端·vue.js