Ant Design Vue 之可定位对话框

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

<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>

全量代码:

<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>
<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>

### 如何在 Ant Design Vue 中使用对话框组件 #### 导入 Modal 组件 为了能够在项目中使用 `Modal` 对话框组件,需先导入该组件。这可以通过如下代码完成: ```javascript import { Modal } from 'ant-design-vue'; ``` 此操作确保了后续能够调用 `Modal` 提供的各种静态方法来显示不同类型的模态窗口[^1]。 #### 基本用法示例 下面是一个简单的例子,展示了如何创建并展示一个基础的信息提示: ```html <template> <a-button @click="showConfirm">Show Confirm</a-button> </template> <script setup> const showConfirm = () => { Modal.confirm({ title: 'Do you Want to delete these items?', content: 'Some descriptions', okText: 'Yes', cancelText: 'No' }); }; </script> ``` 这段代码实现了点击按钮后出确认删除项目的对话框,其中包含了标题、描述文字以及两个按钮选项:“是”和“否”。这种形式非常适合用来获取用户的简单确认反馈。 #### 自定义对话框行为 除了标准的消息提示外,还可以进一步定制化对话框的行为逻辑。比如设置回调函数处理用户的选择动作;调整默认的按钮文本;甚至完全替换整个内容区域的内容等。具体来说就是利用 `onOk`, `onCancel` 参数传入相应的处理器函数,在这些函数内部编写业务逻辑即可满足特定应用场景下的需求。 #### 实现带拖拽功能的对话框 对于某些特殊情况下需要允许用户移动对话框位置的需求,则可以参考其他开发者分享的技术方案。一种可行的做法是在初始化时给定额外参数使能拖动特性,并监听鼠标事件以更新容器的位置坐标[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蜗牛_snail

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值