vue3 让元素可以拖动指令v-draggable

第一种情况:仅需移动

仅需要使得某个容器可以拖动,仅此而已

定义指令

// v-draggable.ts
type DraggableElement = HTMLElement & {
  _cleanupDrag?: () => void;
};

function setupDraggable(el: DraggableElement) {
  // 清除旧的拖拽监听器(避免重复绑定)
  el._cleanupDrag?.();

  let startX = 0;
  let startY = 0;
  let currentX = 0;
  let currentY = 0;

  // 初始化位置信息
  el.dataset.currentX = String(currentX);
  el.dataset.currentY = String(currentY);

  // 设置样式
  el.style.position = "fixed";
  el.style.cursor = "move";
  el.style.transform = `translate(${currentX}px, ${currentY}px)`;

  // 鼠标按下时,准备拖动
  const onMouseDown = (event: MouseEvent) => {
    event.preventDefault();

    startX = event.clientX - Number(el.dataset.currentX);
    startY = event.clientY - Number(el.dataset.currentY);

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);
  };

  // 拖动过程中
  const onMouseMove = (event: MouseEvent) => {
    const deltaX = event.clientX - startX;
    const deltaY = event.clientY - startY;

    const { width, height } = el.getBoundingClientRect();
    const maxX = window.innerWidth - width;
    const maxY = window.innerHeight - height;

    const newX = Math.max(0, Math.min(deltaX, maxX));
    const newY = Math.max(0, Math.min(deltaY, maxY));

    el.dataset.currentX = String(newX);
    el.dataset.currentY = String(newY);
    el.style.transform = `translate(${newX}px, ${newY}px)`;
  };

  // 拖动结束,移除监听器
  const onMouseUp = () => {
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
  };

  // 绑定初始事件
  el.addEventListener("mousedown", onMouseDown);

  // 提供清理函数
  el._cleanupDrag = () => {
    el.removeEventListener("mousedown", onMouseDown);
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
  };
}

// Vue 指令定义
const vDraggable = {
  mounted(el: DraggableElement) {
    setupDraggable(el);
  },
  unmounted(el: DraggableElement) {
    el._cleanupDrag?.();
  },
};

export default vDraggable;

使用

app.vue中使用

<template>
  <div class="app">
    <div class="card" v-draggable></div>
  </div>
</template>

<script setup lang="ts">
import vDraggable from "../directives/v-draggable";
</script>
<style scoped>
.card {
  width: 100px;
  height: 100px;
  background-color: red;
}
</style>

效果

第二种情况:放大缩小

加上了工具栏,上边有放大缩小的按钮,并且根据点击放大缩小的按钮,指令要监听传入的窗口状态更新大小和位置

定义指令

// v-draggable.ts
type DraggableElement = HTMLElement & {
  _cleanupDrag?: () => void;
};

const minSize = {
  width: 260,
  height: 150,
};

const maxSize = {
  width: 400,
  height: 200,
};

function setupDraggable(el: DraggableElement, params) {
  // 清除旧的拖拽监听器(避免重复绑定)
  el._cleanupDrag?.();

  let startX = 0;
  let startY = 0;
  let currentX = 20;
  let currentY = 20;

  // 计算位置
  if (params.big) {
    // 如果是大就--->居中对齐
    const rect = el.getBoundingClientRect();
    currentX = (window.innerWidth - rect.width) / 2;
    currentY = (window.innerHeight - rect.height) / 2;
  }

  // 初始化位置信息
  el.dataset.currentX = String(currentX);
  el.dataset.currentY = String(currentY);

  // 设置样式
  el.style.width = params.big ? `${maxSize.width}px` : `${minSize.width}px`;
  el.style.height = params.big ? `${maxSize.height}px` : `${minSize.height}px`;
  el.style.position = "fixed";
  el.style.cursor = "move";
  el.style.transform = `translate(${currentX}px, ${currentY}px)`;

  // 鼠标按下时,准备拖动
  const onMouseDown = (event: MouseEvent) => {
    event.preventDefault();

    startX = event.clientX - Number(el.dataset.currentX);
    startY = event.clientY - Number(el.dataset.currentY);

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);
  };

  // 拖动过程中
  const onMouseMove = (event: MouseEvent) => {
    const deltaX = event.clientX - startX;
    const deltaY = event.clientY - startY;

    const { width, height } = el.getBoundingClientRect();
    const maxX = window.innerWidth - width;
    const maxY = window.innerHeight - height;

    const newX = Math.max(0, Math.min(deltaX, maxX));
    const newY = Math.max(0, Math.min(deltaY, maxY));

    el.dataset.currentX = String(newX);
    el.dataset.currentY = String(newY);
    el.style.transform = `translate(${newX}px, ${newY}px)`;
  };

  // 拖动结束,移除监听器
  const onMouseUp = () => {
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
  };

  // 绑定初始事件
  el.addEventListener("mousedown", onMouseDown);

  // 提供清理函数
  el._cleanupDrag = () => {
    el.removeEventListener("mousedown", onMouseDown);
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
  };
}

// Vue 指令定义
const vDraggable = {
  mounted(el: DraggableElement, binding: any) {
    setupDraggable(el, binding.value);
  },
  updated(el: DraggableElement, binding: any) {
    setupDraggable(el, binding.value);
  },
  unmounted(el: DraggableElement) {
    el._cleanupDrag?.();
  },
};

export default vDraggable;

使用

app.vue

<template>
  <div class="app">
    <div class="card" v-draggable="{ big: winBig }">
      <div class="tool">
        <span @click="setWinBig" v-if="winBig">-</span>
        <span @click="setWinBig" v-else>+</span>
      </div>
      <div class="content"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import vDraggable from "../directives/v-draggable";

const winBig = ref(false);
const setWinBig = () => {
  winBig.value = !winBig.value;
  console.log(winBig.value);
};
</script>

<style scoped lang="scss">
.card {
  display: flex;
  flex-direction: column;
  box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
  .tool {
    gap: 20px;
    box-sizing: border-box;
    height: 40px;
    width: 100%;
    background-color: white;
    display: flex;
    align-items: center;
    justify-content: end;
    padding: 10px;
    border-bottom: rgba(0, 0, 0, 0.1) solid 1px;
    >span{
      cursor: pointer;
    }
  }
  .content {
    overflow: auto;
    flex: 1;
  }
}
</style>

效果

第三种情况: iframe

如果在上一种情况的 content 里放入 iframe,你再拖动就会出现粘手现象,这是因为:你的鼠标在拖动中进入了 iframe,浏览器会将事件“转交”给 iframe 的上下文(也就是 iframe 里面的页面),于是外部页面就无法再监听 mousemove 和 mouseup 事件,拖动就中断了。
解决方案:用“透明遮罩层”挡住 iframe

// v-draggable.ts
type DraggableElement = HTMLElement & {
  _cleanupDrag?: () => void;
};

// 如果不是 iframe 则不用遮罩
function addMask() {
  if (document.querySelector(".drag-mask")) return;
  const mask = document.createElement("div");
  mask.style.position = "fixed";
  mask.style.top = "0";
  mask.style.left = "0";
  mask.style.width = "100vw";
  mask.style.height = "100vh";
  mask.style.zIndex = "9999"; // 保证在所有元素之上
  mask.style.cursor = "move";
  mask.style.background = "transparent"; // 保证你还能看见 iframe
  mask.className = "drag-mask";
  document.body.appendChild(mask);
}

function removeMask() {
  const mask = document.querySelector(".drag-mask");
  if (mask) document.body.removeChild(mask);
}

const minSize = {
  width: 260,
  height: 150,
};

const maxSize = {
  width: 400,
  height: 200,
};

function setupDraggable(el: DraggableElement, params) {
  // 清除旧的拖拽监听器(避免重复绑定)
  el._cleanupDrag?.();

  let startX = 0;
  let startY = 0;
  let currentX = 20;
  let currentY = 20;

  // 计算位置
  if (params.big) {
    // 如果是大就--->居中对齐
    const rect = el.getBoundingClientRect();
    currentX = (window.innerWidth - rect.width) / 2;
    currentY = (window.innerHeight - rect.height) / 2;
  }

  // 初始化位置信息
  el.dataset.currentX = String(currentX);
  el.dataset.currentY = String(currentY);

  // 设置样式
  el.style.width = params.big ? `${maxSize.width}px` : `${minSize.width}px`;
  el.style.height = params.big ? `${maxSize.height}px` : `${minSize.height}px`;
  el.style.position = "fixed";
  el.style.cursor = "move";
  el.style.transform = `translate(${currentX}px, ${currentY}px)`;

  // 鼠标按下时,准备拖动
  const onMouseDown = (event: MouseEvent) => {
    event.preventDefault();

    startX = event.clientX - Number(el.dataset.currentX);
    startY = event.clientY - Number(el.dataset.currentY);

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);
  };

  // 拖动过程中
  const onMouseMove = (event: MouseEvent) => {
    const deltaX = event.clientX - startX;
    const deltaY = event.clientY - startY;

    // 如果移动距离超过 3px,添加遮罩
    const movedDistance = Math.sqrt(
      (deltaX - Number(el.dataset.currentX)) ** 2 +
        (deltaY - Number(el.dataset.currentY)) ** 2
    );
    if (movedDistance > 3) {
      addMask();
    }

    const { width, height } = el.getBoundingClientRect();
    const maxX = window.innerWidth - width;
    const maxY = window.innerHeight - height;

    const newX = Math.max(0, Math.min(deltaX, maxX));
    const newY = Math.max(0, Math.min(deltaY, maxY));

    el.dataset.currentX = String(newX);
    el.dataset.currentY = String(newY);
    el.style.transform = `translate(${newX}px, ${newY}px)`;
  };

  // 拖动结束,移除监听器
  const onMouseUp = () => {
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
    removeMask();
  };

  // 绑定初始事件
  el.addEventListener("mousedown", onMouseDown);

  // 提供清理函数
  el._cleanupDrag = () => {
    el.removeEventListener("mousedown", onMouseDown);
    document.removeEventListener("mousemove", onMouseMove);
    document.removeEventListener("mouseup", onMouseUp);
    removeMask();
  };
}

// Vue 指令定义
const vDraggable = {
  mounted(el: DraggableElement, binding: any) {
    setupDraggable(el, binding.value);
  },
  updated(el: DraggableElement, binding: any) {
    setupDraggable(el, binding.value);
  },
  unmounted(el: DraggableElement) {
    el._cleanupDrag?.();
  },
};

export default vDraggable;
// app.vue
<template>
  <div class="app">
    <div class="card" v-draggable="{ big: winBig }">
      <div class="tool">
        <span @click="setWinBig" v-if="winBig">-</span>
        <span @click="setWinBig" v-else>+</span>
      </div>
      <div class="content">
        <iframe
          src="https://www.cnblogs.com"
          width="100%"
          height="100%"
          frameborder="0"
        ></iframe>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import vDraggable from "../directives/v-draggable";

const winBig = ref(false);
const setWinBig = () => {
  winBig.value = !winBig.value;
  console.log(winBig.value);
};
</script>

<style scoped lang="scss">
.card {
  display: flex;
  flex-direction: column;
  box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
  .tool {
    gap: 20px;
    box-sizing: border-box;
    height: 40px;
    width: 100%;
    background-color: white;
    display: flex;
    align-items: center;
    justify-content: end;
    padding: 10px;
    border-bottom: rgba(0, 0, 0, 0.1) solid 1px;
    > span {
      cursor: pointer;
    }
  }
  .content {
    overflow: auto;
    flex: 1;
  }
}
</style>
posted @ 2025-04-18 16:17  丁少华  阅读(1089)  评论(0)    收藏  举报