Element Plus s dialog弹出框 设置拖拽属性之后,缩小浏览器窗口, 弹出框位置发生偏移
原因:Element Plus 把拖拽偏移存在 useDraggable 的闭包里,外部无法访问。所以 resize 修正 DOM 的 transform 后,闭包内的旧偏移并不会同步。
解决方法:手动实现dialog内置的拖拽效果
<template> <el-dialog :draggable="false" v-model="visibleDilog" class="tn-dialog" :class="{ 'hide-footer': hideFooter }" top="50px" :fullscreen="fullscreen" :scrollBody="scrollBody" :before-close="beforeClose" :destroy-on-close="destroyOnClose" > <template #header> <div class="header" :class="{ 'is-draggable': isDraggable }" :style="headerStyles" @mousedown="onDragStart"> <div class="left-title"> <slot name="left-title" /> </div> <slot name="title"> <span>{{ title }}</span> </slot> <div class="operation"> <el-space> <slot name="operation"></slot> </el-space> <!-- 默认全屏的情况下 不再显示全屏按钮 --> <svg-icon class="icon" size="14px" v-if="!hideFull" @click="toggleFullscreen" :name="fullscreen ? 'top-exit' : 'top-full'"></svg-icon> </div> </div> </template> <slot /> <template #footer v-if="$slots.footer"> <slot name="footer" /> </template> </el-dialog> </template> <script setup> import { onMounted, onUnmounted } from 'vue'; import { useVModel } from '@vueuse/core'; const props = defineProps({ modelValue: Boolean, title: String, hideFooter: Boolean, // 隐藏底部footer titleAlign: String, // center scrollBody: { type: Boolean, default: true, }, // 只滚动body部分 hideFull: Boolean, // 是否隐藏全屏按钮 fullscreen: Boolean, // 是否默认全屏, isDraggable: { type: Boolean, default: true, }, destroyOnClose: { type: Boolean, default: true, }, }); const emit = defineEmits(['beforeClose', 'update:modelValue']); const visibleDilog = useVModel(props, 'modelValue', emit); const headerStyles = computed(() => { if (props.titleAlign) { return { justifyContent: props.titleAlign, }; } }); const fullscreen = ref(props.fullscreen); const toggleFullscreen = () => { fullscreen.value = !fullscreen.value; }; // 自实现拖拽,替代 Element Plus 内置 draggable。 // 原因:内置 useDraggable 把拖拽偏移存在闭包内,resize 修正 DOM 的 transform 后闭包不同步, // 再次拖拽时弹窗会跳回旧偏移位置、表头被遮挡。改由组件内部 dragOffset 维护偏移, // resize 修正时同步更新,拖拽不再跳变。 const dragOffset = reactive({ x: 0, y: 0 }); const dialogElRef = ref(null); const onDragStart = e => { if (!props.isDraggable) return; // 仅响应左键(避免右键/中键触发) if (e.button !== 0) return; // 阻止 mousedown 默认行为:浏览器默认会进入文本选区模式, // 拖到顶部后由于遮罩层(fixed inset:0)还盖在底部文档上,鼠标拖动时整页文字会被选中 e.preventDefault(); // 通过 header 元素向上找到 el-dialog 根节点(teleport 挂载到 body) const dialogEl = e.currentTarget.closest('.el-dialog'); if (!dialogEl) return; dialogElRef.value = dialogEl; const downX = e.clientX; const downY = e.clientY; const { x: offsetX, y: offsetY } = dragOffset; const rect = dialogEl.getBoundingClientRect(); const targetLeft = rect.left; const targetTop = rect.top; const targetWidth = rect.width; const targetHeight = rect.height; const clientWidth = document.documentElement.clientWidth; const clientHeight = document.documentElement.clientHeight; // 边界限制:拖拽过程中不允许拖出视口(与 Element Plus 内置逻辑保持一致) const minLeft = -targetLeft + offsetX; const minTop = -targetTop + offsetY; const maxLeft = clientWidth - targetLeft - targetWidth + offsetX; const maxTop = clientHeight - targetTop - targetHeight + offsetY; const onMouseMove = ev => { const moveX = Math.min(Math.max(offsetX + ev.clientX - downX, minLeft), maxLeft); const moveY = Math.min(Math.max(offsetY + ev.clientY - downY, minTop), maxTop); dragOffset.x = moveX; dragOffset.y = moveY; dialogEl.style.transform = `translate(${moveX}px, ${moveY}px)`; // 兜底:拖拽过程中清除任何可能已经产生的文本选区(部分浏览器在 preventDefault 之外仍可能选中 iframe 内的内容) const sel = window.getSelection && window.getSelection(); if (sel && sel.rangeCount > 0) sel.removeAllRanges(); }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; // 弹窗每次打开时重置拖拽状态(destroy-on-close 下 DOM 已重建,偏移需归零) watch(visibleDilog, val => { if (val) { dragOffset.x = 0; dragOffset.y = 0; dialogElRef.value = null; } }); const resetDialogPos = () => { // 监听窗口缩放:弹窗被拖动后遇到窗口尺寸变化,修正其位置,确保表头不被遮挡、弹窗自适应视口。 // 拖拽偏移由组件内部 dragOffset 维护,修正后同步更新,下次拖拽不会跳变。 const dialogEl = dialogElRef.value; if (!dialogEl || !dialogEl.isConnected) return; // 全屏模式下弹窗占满视口,无需处理 if (dialogEl.classList.contains('is-fullscreen')) return; const rect = dialogEl.getBoundingClientRect(); // 宽高为 0 说明弹窗未真正显示,跳过 if (rect.width === 0 || rect.height === 0) return; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let offsetX = dragOffset.x; let offsetY = dragOffset.y; let needUpdate = false; // 顶部超出视口(表头被遮挡)—— 优先处理,保证表头可见 if (rect.top < 0) { offsetY = dragOffset.y - rect.top; needUpdate = true; } // 底部超出视口:弹窗整体高于视口时优先保证表头可见(top = 0),否则上移贴底 if (rect.bottom > viewportHeight) { offsetY = rect.height >= viewportHeight ? dragOffset.y - rect.top : dragOffset.y - (rect.bottom - viewportHeight); needUpdate = true; } // 左侧超出视口 if (rect.left < 0) { offsetX = dragOffset.x - rect.left; needUpdate = true; } // 右侧超出视口:弹窗整体宽于视口时优先保证左边可见,否则左移贴右 if (rect.right > viewportWidth) { offsetX = rect.width >= viewportWidth ? dragOffset.x - rect.left : dragOffset.x - (rect.right - viewportWidth); needUpdate = true; } // 仅在位置越界时才修正,避免对正常居中的弹窗造成不必要的 transform 重置 if (needUpdate) { dragOffset.x = offsetX; dragOffset.y = offsetY; dialogEl.style.transform = `translate(${offsetX}px, ${offsetY}px)`; } }; function beforeClose() { emit('beforeClose'); visibleDilog.value = false; } onMounted(() => { window.addEventListener('resize', resetDialogPos); }); onUnmounted(() => { window.removeEventListener('resize', resetDialogPos); }); </script> <style lang="scss" scoped> @media screen and (max-width: 1300px) { :global(.tn-dialog) { margin: 20px auto 0; } :global(.tn-dialog.is-fullscreen) { margin: 0; } } :global(.tn-dialog.is-fullscreen) { height: 100vh; display: flex; flex-direction: column; } :global(.tn-dialog .el-dialog__header) { border-bottom: 1px solid #f0f0f0; padding: 12px 20px; font-weight: bold; margin-right: 0; } :global(.tn-dialog .el-dialog__headerbtn) { top: 3px; width: 40px; height: 44px; margin-right: 4px; } :global(.tn-dialog[scrollBody='true'] .el-dialog__body) { max-height: calc(100vh - 140px); overflow: auto; } :global(.tn-dialog[scrollBody='true'].is-fullscreen .el-dialog__body) { max-height: none; // max-height: calc(100vh - 131px); overflow: auto; } :global(.tn-dialog[scrollBody='true'].hide-footer.is-fullscreen .el-dialog__body) { height: calc(100vh - 72px); box-sizing: border-box; } :global(.tn-dialog[scrollBody='true'].hide-footer.is-fullscreen .el-dialog__footer) { display: none; } :global(.tn-dialog[scrollBody='true'].hide-footer .el-dialog__footer) { display: none; } :global(.tn-dialog[scrollBody='true'] .el-dialog__footer) { position: relative; border-top: 1px solid #f0f0f0; text-align: right; // height: 60px; padding: 12px 20px; z-index: 2; } :global(.tn-dialog[grayBody='true'] .el-dialog__body) { background: #eeeeee; } .tn-dialog { .header { display: flex; align-items: center; position: relative; font-size: 16px; margin-right: 20px; // 仅在可拖拽时显示移动光标,提示用户可拖动 &.is-draggable { cursor: move; } .left-title { position: absolute; left: 5px; z-index: 1; max-width: 44%; overflow: hidden; } .operation { position: absolute; right: 5px; display: flex; align-items: center; justify-content: center; z-index: 1; .icon { cursor: pointer; color: var(--el-color-info); &:hover { color: var(--el-color-primary); } } } } } </style>

浙公网安备 33010602011771号