Ant Design Vue 之可定位对话框

前置篇 antdv的对话框在前面已经改成可以全局打开,如此就可以使用右键菜单的方式打开对话框,但是打开的对话框默认都是居中或在头部位置的,我希望的是在右键鼠标点击的位置,所以又研究了下增加了个定位的功能,且不挡住当前元素,参照的也是前篇的实现方式,具体如下:

typescript 复制代码
<script setup lang="ts">
// 此处为前篇的代码,此处不重复贴出。
watch(popoverInfo.value, () => {
  if (popoverInfo.value.visible) {
    setTimeout(() => {
      const { x, y } = calculateElementPosition(
        heightW.value,
        widthW.value,
        heightM.value,
        widthM.value,
        popoverInfo.value.position.x,
        popoverInfo.value.position.y,
        popoverInfo.value.position.width,
        popoverInfo.value.position.height,
      );
        // 计算出希望出现的位置修改transformX、transformY 即可重新定位对话框的位置.
      transformY.value = y - popoverInfo.value.position.y;
      transformX.value = x - popoverInfo.value.position.x;
    }, 50);
  }
});

// 计算位置: 按照 右、左、下、右、居中的顺序计算位置,若前面一个放不下则考虑下一个位置。
// 参数说明:heightW、widthW 当前窗口的大小 heightM、widthM 对话框的大小  x、y 当前鼠标操作的位置  refW、refH 当前元素的大小(对话框会尽量不遮挡此区域)
const calculateElementPosition = (heightW: number, widthW: number, heightM: number, widthM: number, x: number, y: number, refW: number, refH: number) => {
  let finalX = 0;
  let finalY = 0;

  const rightBlance = widthW - x - refW - widthM;
  const leftBlance = x - widthM;
  const topBlance = y - heightM;
  const topCenterBlance = y - heightM / 2;
  const bottomBlance = heightW - y - refH - heightM;

  const canPlaceOnRight = rightBlance >= 0;
  const canPlaceOnLeft = leftBlance >= 0;

  if (canPlaceOnRight) {
    finalX = x + refW + Math.min(rightBlance, 10);
  } else if (canPlaceOnLeft) {
    finalX = x - widthM - Math.min(leftBlance, 10);
  } else {
    finalX = Math.floor((widthW - widthM) / 2);
  }

  if (canPlaceOnRight || canPlaceOnLeft) {
    // 左右可以放下,则计算y坐标位置
    finalY = topCenterBlance >= 0 ? topCenterBlance : 10;

    const bottomBlanceTmp = heightW - finalY - heightM;
    if (bottomBlanceTmp < 0) {
      finalY = finalY + bottomBlanceTmp;
      if (finalY > 20) {
        finalY = finalY - 10;
      } else {
        finalY = finalY / 2;
      }
    }
  } else if (bottomBlance >= 0) {
    // 左右放不下,则先考虑放下方,左右居中
    finalY = y + refH + Math.min(bottomBlance, 10);
  } else if (topBlance >= 0) {
    // 左右放不下,则先考虑放下方,左右居中
    finalY = y - Math.min(topBlance, 10);
  } else {
    //上下都放不下,则左右居中,上下居中
    finalY = Math.floor((heightW - heightM) / 2);
  }

  // 返回最终的坐标位置
  return { x: finalX, y: finalY };
};

onMounted(() => {});
</script>

全量代码:

html 复制代码
<template>
  <a-modal v-model:open="popoverInfo.visible" :wrap-style="{ overflow: 'hidden' }" :style="modalStyle" :mask="false" width="800px" :destroyOnClose="true">
    <slot></slot>
    <template #title>
      <div ref="modalTitleRef" style="width: 100%; cursor: move">{{ props.title }}</div>
    </template>
    <template #modalRender="{ originVNode }">
      <div :style="transformStyle" ref="modalRef">
        <component :is="originVNode" />
      </div>
    </template>
  </a-modal>
</template>
typescript 复制代码
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useMxGraphStore } from "@/stores/mxGraphStore.ts";
import { useDraggable, useElementSize, useWindowSize } from "@vueuse/core";
import { onMounted, ref, watch, watchEffect, computed, CSSProperties, onUpdated } from "vue";

const props = defineProps<{ title: string }>();

const mxGraphStore = useMxGraphStore();

const { popoverInfo } = storeToRefs(mxGraphStore);

const modalTitleRef = ref();

const modalRef = ref();
const { x, y, isDragging } = useDraggable(modalTitleRef);

const { width: widthM, height: heightM } = useElementSize(modalRef);

const modalStyle = computed(() => `top: ${popoverInfo.value.position.y}px;left: ${popoverInfo.value.position.x}px;margin:0px`);

const { width: widthW, height: heightW } = useWindowSize();

const startX = ref<number>(0);
const startY = ref<number>(0);
const startedDrag = ref(false);
const transformX = ref(0);
const transformY = ref(0);
const preTransformX = ref(0);
const preTransformY = ref(0);
const dragRect = ref({ left: 0, right: 0, top: 0, bottom: 0 });
watch([x, y], () => {
  if (!startedDrag.value) {
    startX.value = x.value;
    startY.value = y.value;
    const bodyRect = document.body.getBoundingClientRect();
    const titleRect = modalTitleRef.value.getBoundingClientRect();
    dragRect.value.right = bodyRect.width - titleRect.width;
    dragRect.value.bottom = bodyRect.height - titleRect.height;
    preTransformX.value = transformX.value;
    preTransformY.value = transformY.value;
  }
  startedDrag.value = true;
});
watch(isDragging, () => {
  if (!isDragging) {
    startedDrag.value = false;
  }
});

watchEffect(() => {
  if (startedDrag.value) {
    transformX.value = preTransformX.value + Math.min(Math.max(dragRect.value.left, x.value), dragRect.value.right) - startX.value;
    transformY.value = preTransformY.value + Math.min(Math.max(dragRect.value.top, y.value), dragRect.value.bottom) - startY.value;
  }
});
const transformStyle = computed<CSSProperties>(() => {
  return {
    transform: `translate(${transformX.value}px, ${transformY.value}px)`,
  };
});

watch(popoverInfo.value, () => {
  if (popoverInfo.value.visible) {
    setTimeout(() => {
      const { x, y } = calculateElementPosition(
        heightW.value,
        widthW.value,
        heightM.value,
        widthM.value,
        popoverInfo.value.position.x,
        popoverInfo.value.position.y,
        popoverInfo.value.position.width,
        popoverInfo.value.position.height,
      );
      transformY.value = y - popoverInfo.value.position.y;
      transformX.value = x - popoverInfo.value.position.x;
    }, 50);
  }
});

const calculateElementPosition = (heightW: number, widthW: number, heightM: number, widthM: number, x: number, y: number, refW: number, refH: number) => {
  let finalX = 0;
  let finalY = 0;

  const rightBlance = widthW - x - refW - widthM;
  const leftBlance = x - widthM;
  const topBlance = y - heightM;
  const topCenterBlance = y - heightM / 2;
  const bottomBlance = heightW - y - refH - heightM;

  const canPlaceOnRight = rightBlance >= 0;
  const canPlaceOnLeft = leftBlance >= 0;

  if (canPlaceOnRight) {
    finalX = x + refW + Math.min(rightBlance, 10);
  } else if (canPlaceOnLeft) {
    finalX = x - widthM - Math.min(leftBlance, 10);
  } else {
    finalX = Math.floor((widthW - widthM) / 2);
  }

  if (canPlaceOnRight || canPlaceOnLeft) {
    // 左右可以放下,则计算y坐标位置
    finalY = topCenterBlance >= 0 ? topCenterBlance : 10;

    const bottomBlanceTmp = heightW - finalY - heightM;
    if (bottomBlanceTmp < 0) {
      finalY = finalY + bottomBlanceTmp;
      if (finalY > 20) {
        finalY = finalY - 10;
      } else {
        finalY = finalY / 2;
      }
    }
  } else if (bottomBlance >= 0) {
    // 左右放不下,则先考虑放下方,左右居中
    finalY = y + refH + Math.min(bottomBlance, 10);
  } else if (topBlance >= 0) {
    // 左右放不下,则先考虑放下方,左右居中
    finalY = y - Math.min(topBlance, 10);
  } else {
    //上下都放不下,则左右居中,上下居中
    finalY = Math.floor((heightW - heightM) / 2);
  }

  // 返回最终的坐标位置
  return { x: finalX, y: finalY };
};

onMounted(() => {});
</script>
相关推荐
shawxlee4 分钟前
vue3在public下封装config.js自定义配置动态数据,可在打包后直接修改,方便后端部署及后续维护
前端·javascript·经验分享·vue·团队开发·js·项目优化
用户0595401744631 分钟前
把记忆存储的回归测试从手工换成 Playwright + GitHub Actions,线上缺陷降低 80%
前端·css
触底反弹37 分钟前
🚀 从 DOM0 级到 React 合成事件:前端事件监听的 20 年演进史
前端·react.js·面试
kyriewen41 分钟前
我给前端项目的接口请求套了6层保护——才发现以前一直在裸奔
前端·javascript·面试
颜酱42 分钟前
04 | 召回前置准备:搭好召回所需的四个数据库
前端·人工智能·后端
郝亚军2 小时前
如何安装webstorm、Node.js和vue CLI
前端·javascript·vue.js
IT_陈寒2 小时前
React的useEffect依赖项把我坑惨了
前端·人工智能·后端
东方小月3 小时前
从零开发一个Coding Agent:monorepo项目搭建
前端·后端·node.js
葬送的代码人生3 小时前
别再让 AI 瞎写代码了!Vibe Coding 三步法教你写出靠谱代码
前端·设计模式·架构
Shirley~~3 小时前
Code-Review-Graph:面向 AI 辅助代码审查的结构化上下文引擎
前端·ai编程