拖拽画布来移动画布内部的容器

<template>
  <div
    class="w-full h-full outline-none overflow-hidden relative flex justify-center items-center cursor-grab active:cursor-grabbing"
    tabindex="-1"
    @pointerdown="onPressDown"
  >
    <div ref="containerRef" class="absolute will-change-transform cursor-default">
      <slot />
    </div>
  </div>
</template>

<script setup>
/**
 * 鼠标按下时的位置
 */
const position = {
  x: 0,
  y: 0
}

/**
 * @constant
 * @type {import('vue').Ref<HTMLElement>}
 * @description 用于获取容器的 ref
 */
const containerRef = ref()

function getPositions (ev) {
  if (ev instanceof MouseEvent) {
    return {
      x: ev.clientX,
      y: ev.clientY
    }
  }

  return {
    x: ev.touches[0].clientX,
    y: ev.touches[0].clientY
  }
}

/**
 * 当按下鼠标或手指
 * @param {MouseEvent|TouchEvent} ev 事件对象
 */
function onPressDown (ev) {
  // 如果不是点击的鼠标左键,则不处理
  if (ev instanceof MouseEvent && ev.button !== 0) {
    return
  }

  // 防止在拖动的过程中选中文字
  document.body.classList.add('select-none')

  // 记录按下时的位置
  Object.assign(position, getPositions(ev))

  document.addEventListener('pointerup', onPressUp, { once: true })
  document.addEventListener('pointermove', onPressMove, { passive: true })
}

/**
 * 当按下并移动鼠标或手指
 * @param {MouseEvent|TouchEvent} ev 事件对象
 */
function onPressMove (ev) {
  const { x, y } = getPositions(ev)

  const elStyle = containerRef.value.style

  // 计算鼠标/手指按下到这一次移动的偏移量
  const offsetX = x - position.x
  const offsetY = y - position.y

  // 获取当前的 transform 位置
  const [, currentX = 0, currentY = 0] = elStyle.transform.match(/translate3d\((.+?)px, (.+?)px, 0px\)/) ?? []

  elStyle.transform = `translate3d(${~~currentX + offsetX}px, ${~~currentY + offsetY}px, 0)`

  // 更新上一次移动的位置
  Object.assign(position, { x, y })
}

/**
 * 当松开鼠标或手指
 */
function onPressUp () {
  document.body.classList.remove('select-none')
  document.removeEventListener('pointermove', onPressMove)
}

</script>