vue3 拖动弹窗

<script setup lang="ts">
import { useDraggable, useMouseInElement } from '@vueuse/core';
import type { CSSProperties } from 'vue';

interface Props {
    isMask?: boolean
    dialogStyle?: Record<string, any>
    title?: string
    footer?: boolean
    isClose?: boolean
    // 最外层自定义类
    className?: string
    // 默认插入到body下
    appendTo?: HTMLElement | string
    type?: number | string
    submitText: string
}
defineOptions({
    name: 'ProDialog',
});
withDefaults(defineProps<Props>(), {
    isMask: false,
    title: '标题',
    type: 0,
    dialogStyle: () => {
        return {
            width: '300px',
            left: '40%',
            top: '35%',
        };
    },
    footer: true,
    isClose: true,
    appendTo: 'body',
    submitText: '保存',

});
const emit = defineEmits(['submit', 'close', 'cancel', 'resize']);
const containerEleRef = ref<HTMLElement>();
const modalTitleRef = ref<HTMLElement | null>(null);
const { x, y, isDragging } = useDraggable(modalTitleRef);

const resizeTransformX = ref(0); // 弹窗大小变化导致的水平方向上的位置偏移量

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.value) {
        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>(() => {
    // console.log(transformX.value, resizeTransformX.value);
    return {
        transform: `translate(${transformX.value + resizeTransformX.value}px, ${transformY.value}px)`,
    };
});

// ----------拖动缩放-----------
const dragType = ref('none'); // left:左侧拖动 right: 右侧拖动 down: 下方拖动 leftDown: 左下角拖动 rightDown: 右下角拖动 none: 不拖动
// 光标样式
const cursor = computed(() => {
    if (dragType.value === 'left' || dragType.value === 'right') {
        return 'cursor-ew-resize';
    }
    if (dragType.value === 'down') {
        return 'cursor-ns-resize';
    }
    if (dragType.value === 'leftDown') {
        return 'cursor-nesw-resize';
    }
    if (dragType.value === 'rightDown') {
        return 'cursor-nwse-resize';
    }
    return 'cursor-auto';
});

const modalBodyRef = ref<HTMLElement | null>(null);
const { isDragging: modalBodyIsDragging } = useDraggable(modalBodyRef);
const { x: mouseX, y: mouseY, elementPositionX, elementPositionY, elementHeight, elementWidth } = useMouseInElement(modalBodyRef);

const bodyWidth = ref(0);
const bodyHeight = ref(0);
let startBodyWidth = 0; // 拖动开始时modal-body的宽度
let startBodyHeight = 0;// 拖动开始时modal-body的高度
let startMouseX = 0; // 拖动开始时鼠标的x值
let startMouseY = 0;// 拖动开始时鼠标的y值
let isStartDrag = false; // 拖动开始标识
let preResizeTransformX = 0; // 上一次拖动的x轴偏移量

watch(modalBodyIsDragging, () => {
    // 拖动结束
    if (!modalBodyIsDragging.value) {
        isStartDrag = false;
    }
});

watch([mouseX, mouseY], () => {
    const bodyW = elementWidth.value;
    const bodyH = elementHeight.value;
    const bodyX = elementPositionX.value;
    const bodyY = elementPositionY.value;
    const bodyMaxX = bodyX + bodyW;
    const bodyMaxY = bodyY + bodyH;
    // 计算拖动类型
    if (!modalBodyIsDragging.value) {
        if (mouseX.value >= bodyX && mouseX.value <= bodyX + 5 && mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
            dragType.value = 'leftDown';
        }
        else if (mouseX.value <= bodyMaxX && mouseX.value >= bodyMaxX - 5 && mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
            dragType.value = 'rightDown';
        }
        else if (mouseY.value <= bodyMaxY && mouseY.value >= bodyMaxY - 5) {
            dragType.value = 'down';
        }
        else if ((mouseX.value >= bodyX && mouseX.value <= bodyX + 5)) {
            dragType.value = 'left';
        }
        else if (mouseX.value <= bodyMaxX && mouseX.value >= bodyMaxX - 5) {
            dragType.value = 'right';
        }
        else {
            dragType.value = 'none';
        }
    }

    if (modalBodyIsDragging.value && !isStartDrag) {
        // 开始拖动
        isStartDrag = true;
        bodyWidth.value = bodyW;
        bodyHeight.value = bodyH;
        startBodyWidth = bodyWidth.value;
        startBodyHeight = bodyHeight.value;
        startMouseX = mouseX.value;
        startMouseY = mouseY.value;
        preResizeTransformX = resizeTransformX.value;
    }
    else if (modalBodyIsDragging.value && isStartDrag) {
        const diffX = mouseX.value - startMouseX;
        const diffY = mouseY.value - startMouseY;
        if (dragType.value === 'left') {
            bodyWidth.value = startBodyWidth - diffX;
            resizeTransformX.value = preResizeTransformX + diffX;
        }
        else if (dragType.value === 'leftDown') {
            bodyWidth.value = startBodyWidth - diffX;
            bodyHeight.value = startBodyHeight + diffY;
            resizeTransformX.value = preResizeTransformX + diffX;
        }
        else if (dragType.value === 'right') {
            bodyWidth.value = startBodyWidth + diffX;
        }
        else if (dragType.value === 'rightDown') {
            bodyWidth.value = startBodyWidth + diffX;
            bodyHeight.value = startBodyHeight + diffY;
        }
        else if (dragType.value === 'down') {
            bodyHeight.value = startBodyHeight + diffY;
        }
        emit('resize', { w: bodyWidth.value, h: bodyHeight.value });
    }
});

const show = ref<boolean>(false);
function open() {
    show.value = true;
}
function close() {
    show.value = false;
    emit('close');
}
function submit() {
    emit('submit');
}

function reset() {
    transformX.value = 0;
    transformY.value = 0;
}
function cancel() {
    show.value = false;
    emit('cancel');
}

defineExpose({
    open,
    close,
    cancel,
    show,
    reset,
});
</script>

<template>
    <Teleport :to="appendTo">
        <Transition name="modal">
            <div v-if="show" class="w-full h-auto" :class="className">
                <div v-if="isMask" :class="appendTo === 'body' ? 'modal-mask' : ''" />
                <div ref="containerEleRef"
                    :style="appendTo === 'body' ? Object.assign(dialogStyle, transformStyle) : {}"
                    :class="appendTo === 'body' ? 'modal-wrap' : ''">
                    <div class="modal-container" :class="[`modal_box_${type}`]">
                        <div v-if="isClose" class="close" @click="close">
                            <PubSvgIcon name="close_blue" :size="30" class="m-auto" />
                        </div>
                        <div ref="modalTitleRef" class="modal-header bg-$vp-c-bg select-none cursor-move">
                            <slot name="header">
                                {{ title }}
                            </slot>
                        </div>
                        <div ref="modalBodyRef" class="modal-body" :class="[cursor]" :style="{
                            width: bodyWidth > 0 ? `${bodyWidth}px` : 'auto',
                            height: bodyHeight > 0 ? `${bodyHeight}px` : 'auto'
                        }">
                            <slot />
                        </div>
                        <div class="modal-footer">
                            <slot name="footer">
                                <div v-if="footer" class="flex justify-center">
                                    <a-space :size="8">
                                        <a-button @click="cancel">
                                            取消
                                        </a-button>
                                        <a-button type="primary" @click="submit">
                                            {{ submitText }}
                                        </a-button>
                                    </a-space>
                                </div>
                            </slot>
                        </div>
                    </div>
                </div>
            </div>
        </Transition>
    </Teleport>
</template>

<style lang="less" scoped>
.title_pre(@height, @pl, @fz, @color, @url, @t, @r, @b, @l, @t-2, @r-2, @b-2, @l-2) {
    position: relative;
    z-index: 5;
    height:~"@{height}px";
    padding-left: @pl;
    font-family: "hxbzt";
    font-size: @fz;
    line-height:~"@{height}px";
    color: @color;

    span {
        position: relative;
        z-index: 9;
    }

    span.sub-title {
        font-family: "微软雅黑";
        font-size: 12px;
        color: #f7f7f7;
    }

    &:before {
        position: absolute;
        top: 0;
        left: 0;
        z-index: -1;
        width: 100%;
        height: 100%;
        content: "";
        border-width:~"@{t}px"~"@{r}px"~"@{b}px"~"@{l}px";
        border-image: url(@url) @t @r @b @l fill;
        border-image-width:~"@{t-2}px"~"@{r-2}px"~"@{b-2}px"~"@{l-2}px";
    }
}

.modal-mask {
    position: fixed;
    top: 0;
    left: 0;
    // z-index: 9998;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    transition: opacity 0.3s ease;
}

.modal-wrap {
    position: fixed;
    // z-index: 9999;
}

.modal-container {
    &.modal_box_0 {
        padding: 20px;
        color: #fff;
        background: rgba(0, 20, 42, 0.7);
        backdrop-filter: blur(5px);
        border: 1px solid #747b84;
        border-radius: 2.67px;

        .modal-header {
            text-align: center;

            h3 {
                margin-top: 0;
                color: #42b983;
            }
        }

        .modal-body {
            margin: 20px 0;
        }

        .modal-default-button {
            float: right;
        }

        .close {
            position: absolute;
            top: -8px;
            right: -8px;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 24px;
            height: 24px;
            cursor: pointer;
            background: rgba(0, 0, 0, 0.6);
            border-radius: 12px;
            transition: all 0.3s ease;

            &:hover {
                transform: rotate(90deg);
            }
        }
    }

    &.modal_box_1 {
        .border-box(100, 200, 50, 100, "@/assets/images/common/border_box_5.png");

        .modal-header {
            padding-left: 28px;
            line-height: 40px;
            text-align: left;
        }

        .modal-body {
            padding: 32px;
        }

        .close {
            position: absolute;
            top: 10px;
            right: 22px;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 32px;
            height: 32px;
            cursor: pointer;
            background: #113756;
            border-radius: 4px;

            :deep(svg) {
                font-size: 22px !important;
                transition: all 0.5s ease;

                &:hover {
                    transform: rotate(90deg);
                }
            }
        }
    }

    &.modal_box_3 {
        padding: 20px 0;
        color: #fff;
        background: rgba(0, 20, 42, 0.7);
        backdrop-filter: blur(5px);
        border-radius: 2.67px;

        .modal-header {
            text-align: center;

            h3 {
                margin-top: 0;
                color: #42b983;
            }
        }

        .modal-body {
            margin: 0;
        }

        .modal-default-button {
            float: right;
        }

        .close {
            position: absolute;
            top: -8px;
            right: -8px;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 24px;
            height: 24px;
            cursor: pointer;
            background: rgba(0, 0, 0, 0.6);
            border-radius: 12px;
            transition: all 0.3s ease;

            &:hover {
                transform: rotate(90deg);
            }
        }
    }

    &.modal_box_7 {
        padding-top: 15px;
        padding-bottom: 30px;
        padding-left: 16px;
        padding-right: 16px;
        border-width: 100px 100px;
        border-image: url("@/assets/images/common/box_bg_1.png") 100 80 fill;
        border-image-width: 50px 40px;

        &::before {
            content: "";
            position: absolute;
            top: -45px;
            left: -51px;
            z-index: 3;
            width: 289px;
            height: 193px;
            pointer-events: none;
            background: url("@/assets/images/common/height_light.png");
            background-size: 100% 100%;
        }

        .modal-header {
            .title_pre(36, 42px, 18px, "#fff", "@/assets/images/common/border-box0.png", 2, 120, 2, 120, 1, 60, 1, 60);
            font-family: youshe;
            margin-bottom: 15px;
        }

        .close {
            position: absolute;
            top: 10px;
            right: 10px;
            z-index: 100;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: all 0.3s ease;

            &:hover {
                transform: rotate(90deg);
            }
        }
    }
}


.modal-enter-from {
    opacity: 0;
}

.modal-leave-to {
    opacity: 0;
}

.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
    -webkit-transform: scale(1.1);
    transform: scale(1.1);
}

.border-box(@bt, @lr, @bt-2, @lr-2, @url) {
    position: relative;
    z-index: 5;

    &::after {
        position: absolute;
        top: 0;
        left: 0;
        z-index: -1;
        width: 100%;
        height: 100%;
        content: "";
        border-width:~"@{bt}px"~"@{lr}px";
        border-image: url(@url) @bt @lr fill;
        border-image-width:~"@{bt-2}px"~"@{lr-2}px";
    }
}
</style>
<script setup lang="ts">

import { message } from 'ant-design-vue';
import { certTypeOptions, typeList, levelList, districtCodeList } from './enums.ts'
import mapBox from "./mapBox.vue";

const props = withDefaults(defineProps<{
  obj: object // 传递对象
  appendTo: string
  title: string
  showbtm: boolean
}>(), {
  showbtm: () => false,
  obj: () => {
    return {
      top: '14%',
      left: '30%',
      close: true,
    };
  },
  appendTo: 'body',
  title: '应急救援警情接收',
});
const emit = defineEmits(['saveupdata']);

const styleObj = ref(props.obj) as any;
const handdialogRef = ref();
//表单
const formRef = ref()
const eventform = ref({

});
// 打开
function show() {
  handdialogRef.value.open();
}
// 关闭
function close() {
  handdialogRef.value.close();
}
//提交保存
function confirm() {
  formRef.value?.validate().then(() => {
    postEventSave(eventform.value).then((res) => {
      if (res.success) {
        message.success('提交成功');
        emit('saveupdata') //刷新列表
        handdialogRef.value.close();
      } else {
        message.error(res.msg);
      }
    }).catch(() => { });
  });
}
defineExpose({
  show,
  close,
});
</script>

<template>
  <Dialog2 ref="handdialogRef" :footer="showbtm" :title="props.title" type="7" :is-close="styleObj.close"
    :dialog-style="{ width: '48.43rem', height: '50.36rem', zIndex: 1049, top: styleObj.top, left: styleObj.left }"
    :append-to="props.appendTo" submitText="提交" @close="close" :isMask="true">
    <div class="content">
      <a-form ref="formRef" label-align="right" name="dynamic_rule" :model="eventform"
        :label-col="{ style: { width: '6rem' } }">
        <ContentBox title="用户信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="姓名" :colon="false" name="reportName" :rules="[{ required: true, message: '请输入' }]">
                <a-input v-model:value="eventform.reportName" placeholder="请输入" />
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="手机号码" :colon="false" name="reportPhone"
                :rules="[{ required: true, message: '请输入' },{type: 'string', pattern: /^1[3|4|5|6|7|8|9][0-9]{9}$/, message: '请输入正确的手机号', trigger: 'blur'}]">
                <a-input v-model:value="eventform.reportPhone" placeholder="请输入" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="电子邮箱" :colon="false" name="reportEmail"
                :rules="[{ required: true, message: '请输入' },{type: 'string', pattern:  /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, message: '请输入正确的邮箱', trigger: 'blur'}]">
                <a-input v-model:value="eventform.reportEmail" placeholder="请输入" />
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="证件类型" :colon="false" name="reportCertificateType"
                :rules="[{ required: true, message: '请输入' }]">
                <a-select v-model:value="eventform.reportCertificateType" :options="certTypeOptions" :allow-clear="true"
                  placeholder="请选择" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="证件号码" :colon="false" name="reportCertificateNum"
                :rules="[{ required: true, message: '请输入' },{type: 'string', pattern: /^(\d{18}|\d{17}x|\d{17}X)$/, message: '请输入正确的证件号码', trigger: 'blur'}]">
                <a-input v-model:value="eventform.reportCertificateNum" placeholder="请输入" />
              </a-form-item>
            </a-col>
          </a-row>
        </ContentBox>
        <ContentBox title="设备信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="设备机型" :colon="false" name="equipModel" :rules="[{ required: true, message: '请输入' }]">
                <a-input v-model:value="eventform.equipModel" placeholder="请输入" />
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="设备ID/SN" :colon="false" name="equipSn">
                <a-input v-model:value="eventform.equipSn" placeholder="请输入" />
              </a-form-item>
            </a-col>
          </a-row>
        </ContentBox>
        <ContentBox title="事故信息" :type="2" class="flex-auto relative min-h-0 wrap-box">
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="事故类型" :colon="false" name="type" :rules="[{ required: true, message: '请输入' }]">
                <a-select v-model:value="eventform.type" :options="typeList" :allow-clear="true" placeholder="请选择" />
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="风险等级" :colon="false" name="level" :rules="[{ required: true, message: '请输入' }]">
                <a-select v-model:value="eventform.level" :options="levelList" :allow-clear="true" placeholder="请选择" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="24">
              <a-form-item :colon="false" label="事故时间" name="accidentTime"
                :rules="[{ required: true, message: '请选择时间' }]">
                <a-date-picker v-model:value="eventform.accidentTime" show-time value-format="YYYY-MM-DD HH:mm:ss"
                  style="width: 100%;" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="事故地点" :colon="false" name="districtCode"
                :rules="[{ required: true, message: '请输入' }]">
                <a-select v-model:value="eventform.districtCode" :options="districtCodeList" :allow-clear="true"
                  placeholder="请选择" />
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="" :colon="false" name="place" :rules="[{ required: true, message: '请输入' }]">
                <a-input v-model:value="eventform.place" placeholder="请输入详细地址" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="12" class="pr-5">
              <a-form-item label="事故经度" :colon="false" name="longitude">
                <a-input v-model:value="eventform.longitude" placeholder="请输入" />

              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="事故纬度" :colon="false" name="latitude">
                <a-input v-model:value="eventform.latitude" placeholder="请输入" />
              </a-form-item>
            </a-col>
          </a-row>
          <a-row>
            <a-col :span="24">
              <!-- <a-form-item label="事故原因" :colon="false" name="cause" :rules="[{ required: true, message: '请输入' }]">
                <a-textarea v-model:value="eventform.cause" placeholder="请输入(最多输入300字)" :row="6" :maxlength="300" />
              </a-form-item> -->
              <div class="map-box">
                <mapBox />
              </div>
            </a-col>
          </a-row>
          <a-row class="mt-4">
            <a-col :span="24">
              <a-form-item label="事故原因" :colon="false" name="cause" :rules="[{ required: true, message: '请输入' }]">
                <a-textarea v-model:value="eventform.cause" placeholder="请输入(最多输入300字)" :row="6" :maxlength="300" />
              </a-form-item>
            </a-col>
          </a-row>
        </ContentBox>
      </a-form>
      <div class="text-center">
        <a-button class="mr-4 newbtn" @click="close">
          取消
        </a-button>
        <a-button class="newbtn" @click="confirm">
          确定
        </a-button>
      </div>
    </div>
  </Dialog2>
</template>

<style lang="less" scoped>
.content {
  height: 680px;
  overflow-y: auto;
}

.text-center {
  .newbtn {
    width: 94px;
    height: 34px;
    background: url(@/assets/images/common/btn-bg.png)no-repeat center center;
    background-size: 100% 100%;
    font-family: PingFangSC-Regular;
    font-weight: 400;
    font-size: 15px;
    color: #FFFFFF;
    text-align: center;

    &:hover {
      color: #fff;
    }
  }
}

.map-box {
  width: 100%;
  height: 300px;
}
</style>

 

posted @ 2024-12-27 12:27  abcByme  阅读(7)  评论(0编辑  收藏  举报