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>
相关推荐
YaHuiLiang14 分钟前
小微互联网公司与互联网创业公司的技术之殇 - "新"技术的双刃剑
前端·后端·架构
桃园码工18 分钟前
3_TypeScript 运算符 --[深入浅出 TypeScript 测试]
前端·javascript·typescript
low神18 分钟前
Flutter面试题、Dart面试题
前端·javascript·flutter·react native·dart·前端面试题
风月歌26 分钟前
基于Web的足球青训俱乐部管理后台系统的设计与开发源码(springboot+mysql+vue)
java·前端·spring boot·后端·mysql·mybatis·源码
南城巷陌1 小时前
Node.js中使用Joi 和 express-joi-validation进行数据验证和校验
前端·node.js·express·数据校验
还是大剑师兰特1 小时前
Javascript 编写的一个红、黄、绿灯交替变亮
javascript·交通灯·大剑师
嘻嘻哈哈樱桃1 小时前
有效字母异位词力扣--242
java·javascript·leetcode
初晨未凉1 小时前
uniapp更新版本,apk包进度条,wgt包热更新
前端·javascript·uni-app
boy快快长大1 小时前
【CSS】第二天 画盒子、文字控制属性
前端·css
wh_xia_jun1 小时前
uniapp中判断设备类型
前端·javascript·html