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>