diff --git a/ruoyi-ui/src/plugins/dialogDrag.js b/ruoyi-ui/src/plugins/dialogDrag.js new file mode 100644 index 0000000..45095a6 --- /dev/null +++ b/ruoyi-ui/src/plugins/dialogDrag.js @@ -0,0 +1,175 @@ +/** + * 全局弹窗可拖动(覆盖项目内所有弹窗) + * 1. 弹窗出现时直接给标题栏绑定拖动(MutationObserver + 延迟扫描) + * 2. document 捕获阶段 mousedown 兜底,确保点击标题栏一定能拖 + * 支持:el-dialog、el-message-box($prompt/confirm/alert)、自定义 .dialog-content + */ +let inited = false + +function startDrag(e, container) { + if (!container) return + e.preventDefault() + e.stopPropagation() + const prevUserSelect = document.body.style.userSelect + document.body.style.userSelect = 'none' + let startX = e.clientX + let startY = e.clientY + const rect = container.getBoundingClientRect() + let left = rect.left + let top = rect.top + container.style.position = 'fixed' + container.style.left = left + 'px' + container.style.top = top + 'px' + container.style.margin = '0' + container.style.transform = 'none' + const onMouseMove = (e2) => { + e2.preventDefault() + const dx = e2.clientX - startX + const dy = e2.clientY - startY + left += dx + top += dy + startX = e2.clientX + startY = e2.clientY + container.style.left = left + 'px' + container.style.top = top + 'px' + } + const onMouseUp = () => { + document.body.style.userSelect = prevUserSelect + document.removeEventListener('mousemove', onMouseMove, true) + document.removeEventListener('mouseup', onMouseUp, true) + } + document.addEventListener('mousemove', onMouseMove, true) + document.addEventListener('mouseup', onMouseUp, true) +} + +/** el-dialog 仅由此处理:document 捕获阶段拦截标题栏 mousedown,避免双重绑定导致乱动/不动 */ +function onDocumentMouseDown(e) { + if (e.button !== 0) return + if (e.target.closest('.el-dialog__headerbtn') || e.target.closest('.el-message-box__headerbtn') || e.target.closest('.close-btn')) return + const elHeader = e.target.closest('.el-dialog__header') + if (elHeader) { + const wrapper = elHeader.closest('.el-dialog__wrapper') + if (wrapper) { + startDrag(e, wrapper) + return + } + } + const msgHeader = e.target.closest('.el-message-box__header') + if (msgHeader) { + const wrapper = msgHeader.closest('.el-message-box__wrapper') + if (wrapper) { + startDrag(e, wrapper) + return + } + } + const customHeader = e.target.closest('.dialog-header') + if (customHeader && !customHeader.closest('.el-dialog__wrapper')) { + const content = customHeader.closest('.dialog-content') + if (content) { + startDrag(e, content) + } + } +} + +function bindMessageBox(wrapper) { + if (wrapper.getAttribute('data-dialog-drag-bound') === '1') return + const header = wrapper.querySelector('.el-message-box__header') + if (!header) return + wrapper.setAttribute('data-dialog-drag-bound', '1') + header.style.cursor = 'move' + header.addEventListener('mousedown', (e) => { + if (e.button !== 0 || e.target.closest('.el-message-box__headerbtn')) return + startDrag(e, wrapper) + }) +} + +function bindCustomDialog(content) { + if (content.getAttribute('data-dialog-drag-bound') === '1') return + const header = content.querySelector('.dialog-header') + if (!header) return + content.setAttribute('data-dialog-drag-bound', '1') + header.style.cursor = 'move' + header.addEventListener('mousedown', (e) => { + if (e.button !== 0 || e.target.closest('.close-btn')) return + startDrag(e, content) + }) +} + +function scanAndBind() { + if (typeof document === 'undefined' || !document.body) return + document.querySelectorAll('.el-message-box__wrapper').forEach(bindMessageBox) + document.querySelectorAll('.dialog-content').forEach((content) => { + if (!content.closest('.el-dialog__wrapper') && content.querySelector('.dialog-header')) { + bindCustomDialog(content) + } + }) +} + +/** 弹窗关闭时清除我们设置的 left/top,避免下次打开从错误位置“瞬移”到中间 */ +function resetClosedDialogPositions() { + if (typeof document === 'undefined' || !document.body) return + document.querySelectorAll('.el-dialog__wrapper').forEach((wrapper) => { + const style = window.getComputedStyle(wrapper) + if (style.display === 'none' || style.visibility === 'hidden') { + wrapper.style.left = '' + wrapper.style.top = '' + wrapper.style.position = '' + wrapper.style.margin = '' + wrapper.style.transform = '' + } + }) + document.querySelectorAll('.el-message-box__wrapper').forEach((wrapper) => { + const style = window.getComputedStyle(wrapper) + if (style.display === 'none' || style.visibility === 'hidden') { + wrapper.style.left = '' + wrapper.style.top = '' + wrapper.style.position = '' + wrapper.style.margin = '' + wrapper.style.transform = '' + } + }) +} + +function initDialogDrag(Vue) { + if (typeof document === 'undefined' || !document.body || inited) return + inited = true + + const style = document.createElement('style') + style.textContent = [ + '.el-dialog__header { cursor: move !important; }', + '.el-dialog__header .el-dialog__headerbtn { cursor: pointer !important; }', + '.el-message-box__header { cursor: move !important; }', + '.el-message-box__header .el-message-box__headerbtn { cursor: pointer !important; }', + '.dialog-header { cursor: move !important; }', + '.dialog-header .close-btn { cursor: pointer !important; }' + ].join(' ') + document.head.appendChild(style) + + scanAndBind() + + document.addEventListener('mousedown', onDocumentMouseDown, true) + + const scheduleScan = () => { + resetClosedDialogPositions() + if (Vue && typeof Vue.nextTick === 'function') Vue.nextTick(scanAndBind) + else scanAndBind() + setTimeout(scanAndBind, 0) + setTimeout(scanAndBind, 50) + setTimeout(scanAndBind, 150) + setTimeout(scanAndBind, 400) + } + const observer = new MutationObserver(scheduleScan) + observer.observe(document.body, { childList: true, subtree: true }) + + if (Vue && typeof Vue.nextTick === 'function') Vue.nextTick(scanAndBind) + setTimeout(scanAndBind, 200) + setTimeout(scanAndBind, 500) + setTimeout(scanAndBind, 1200) +} + +export default { + install(Vue) { + if (typeof document === 'undefined' || !document.body) return + initDialogDrag(Vue) + } +}