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

拖拽+横向拖动拉伸

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

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

该应用页面固定使用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>
相关推荐
lichenyang45321 小时前
Docker 学习笔记(一):为什么需要镜像、容器和仓库?
前端
kyriewen1 天前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript
IT_陈寒1 天前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
奇奇怪怪的1 天前
Embedding 模型 10+ 横向评测
前端
陈广亮1 天前
Monorepo 从 0 到 1 实操指南 2026 版:pnpm catalogs + Turborepo 2.x + changesets 全链路
前端
子兮曰1 天前
OpenMontage 深度解剖:你的 AI 编程助手,其实是个视频工作室
前端·后端·ai编程
敲代码的鱼1 天前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
子兮曰1 天前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
Hyyy1 天前
Function Calling / Tool Use的原理和实现模式
前端·llm·ai编程