- 可拖拽移动到屏幕外(
2.5.4
版本后支持overflow
属性)。 - 可拖拽拉伸改变尺寸大小。
- 遮罩外元素可操作。
- 拖拽流畅不卡顿优化:
4.1. 拖拽时遮罩无事件状态,避免触发弹窗外鼠标事件消耗性能;
4.2. translate 移动,减少重绘;- 4.3 节流/防抖:减少频率 如 30~50ms;
- 4.4 内容虚拟化:如果弹窗内容是长列表,可以虚拟滚动,减少 DOM 数量。
效果如下图:
980h8-n2pwk.gif
code:
App.vue
:
<template>
<el-button plain @click="dialogVisible = !dialogVisible">
Click to open the Dialog
</el-button>
<div>
dragging: {{dragging}}
</div>
<el-dialog
v-model="dialogVisible"
title="Title"
width="500px"
custom-class="my-dialog"
append-to-body
:close-on-click-modal="false"
>
<div
class="content-wrapper"
:class="{ dragging }"
v-drag-resize="{
initHeight: 300,
initWidth: 500,
updateDraggingState
}">
<span>The dialog content</span>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { vDragResize } from './vDragResize';
const dialogVisible = ref(false)
let dragging = ref(false)
const updateDraggingState = async (bool: boolean) => {
dragging.value = bool
console.log("dragging.value: ", dragging.value)
}
</script>
<style >
.el-dialog__header {
border: 1px solid #666;
border-radius: 5px;
}
.el-overlay-dialog:has(.el-dialog.my-dialog){
overflow: hidden;
padding: 0;
}
.el-dialog.my-dialog {
margin: 0; /** 因为是 translate 移动,所以不需 margin */
}
/** 不拖拽时,遮罩外可操作: */
div:has(.el-overlay-dialog .el-dialog.my-dialog .content-wrapper:not(.dragging)) {
pointer-events: none;
background-color: transparent;
}
.el-dialog.my-dialog {
pointer-events: auto;
outline: 1px solid #494949;
}
</style>
vDragResize.ts
:
/**
* ElDialog 拖拽移动和拉伸调整大小
* @example
* v-drag-resize='{initHeight: 500}'
*/
import type { Directive, DirectiveBinding } from 'vue'
interface BindingOption {
initHeight: number // 拖拽拉伸盒子的初始高度
initWidth: number
updateDraggingState?: Function // 更新是否拖拽中状态
}
interface ElType extends HTMLElement {
setCursor: (e: MouseEvent) => void
stretchMousedown: (e: MouseEvent) => void
}
type Points = {
left: number // 盒子距屏幕位移
top: number
width: number // 盒子宽高
height: number
x: number // 鼠标按下坐标
y: number
endX?: number // 鼠标移动坐标
endY?: number
}
/**
* 拉伸触控间距
*/
const OFFSET = 8
/**
* 鼠标悬停样式
*/
const CURSOR_TYPE = {
top: 'n-resize',
bottom: 's-resize',
left: 'w-resize',
right: 'e-resize',
right_top: 'ne-resize',
left_top: 'nw-resize',
left_bottom: 'sw-resize',
right_bottom: 'se-resize',
default: 'default',
// move: 'move'
}
/**
* 判断鼠标悬浮指针方向
* @param points
*/
const checkCursorType = (points: Points) => {
const { left, top, width, height, x, y } = points
const inRight = x > left + width - OFFSET
const inLeft = left + OFFSET > x
const inBottom = y > top + height - OFFSET
const inTop = top + OFFSET > y
let type: keyof typeof CURSOR_TYPE
if (inRight && y <= top + height - OFFSET && top + OFFSET <= y) {
type = 'right'
} else if (inLeft && y <= top + height - OFFSET && top + OFFSET <= y) {
type = 'left'
} else if (inBottom && x <= left + width - OFFSET && left + OFFSET <= x) {
type = 'bottom'
} else if (inTop && x <= left + width - OFFSET && left + OFFSET <= x) {
type = 'top'
} else if (inRight && inBottom) {
type = 'right_bottom'
} else if (inLeft && inBottom) {
type = 'left_bottom'
} else if (inTop && inRight) {
type = 'right_top'
} else if (inTop && inLeft) {
type = 'left_top'
}
return type || 'default'
}
/**
* 设置文本滑选
* @param value
*/
const setUserSelect = (value: 'none' | '') => {
document.body.style.userSelect = value
}
/**
* 判断是否弹窗头部可拖拽区域
* @param points
* @param headerHeight
*/
const checkHeaderDragZoom = (points: Points, headerHeight: number) => {
const { left, top, width, x, y } = points
if (
x > left + OFFSET &&
x < left + width - OFFSET &&
y > top + OFFSET &&
y < top + headerHeight
) {
return true
}
return false
}
/**
* 边界/宽高限制
* @param points
* @param boxEl
*/
const boundaryLimit = (points: Points, boxEl: ElType, translateX = 0, translateY = 0,screenWidth:number, screenHeight:number) => {
const { left, top, width, height, x, y, endX = 0, endY = 0 } = points
const diffX = endX - x
const diffY = endY - y
const type = checkCursorType({ left, top, width, height, x, y })
const directions = type.split('_') // 转换为数组, 方便判断
if (directions.includes('right')) {
if (width + diffX > screenWidth - left) {
boxEl.style.width = screenWidth - left + 'px'
} else {
boxEl.style.width = width + diffX + 'px'
}
}
let translateX_ = translateX
let translateY_ = translateY
if (directions.includes('left')) {
if (width - diffX > width + left) {
boxEl.style.width = width + left + 'px'
translateX_ = 0
} else {
boxEl.style.width = width - diffX + 'px'
translateX_ = left + diffX
}
}
if (directions.includes('top')) {
if (height - diffY > height + top) {
boxEl.style.height = height + top + 'px'
translateY_ = 0
} else {
boxEl.style.height = height - diffY + 'px'
translateY_ = top + diffY
}
}
if (directions.includes('bottom')) {
if (height + diffY > screenHeight - top) {
boxEl.style.height = screenHeight - top + 'px'
} else {
boxEl.style.height = height + diffY + 'px'
}
}
boxEl.style.transform = `translate3d(${translateX_}px, ${translateY_}px, 0)`
}
/**
* 查找弹窗内容最外层,作为拉伸盒子
* @param el
* @returns
*/
const findResizeBox = (el: HTMLElement) => {
if (el.classList.contains('el-dialog')) return el
if (el.parentElement) return findResizeBox(el.parentElement)
}
const getTranslate = (resizeBox: HTMLElement) => {
const rs = getComputedStyle(resizeBox).transform.split(',')
return {
translateX: parseFloat(rs[4]),
translateY: parseFloat(rs[5]),
}
}
const screenWidth = document.body.clientWidth || document.documentElement.clientWidth || 600
const screenHeight = document.body.clientHeight || document.documentElement.clientHeight || 800
export const vDragResize: Directive = {
mounted: (el: HTMLElement, binding: DirectiveBinding<BindingOption>) => {
// 拖拽拉伸大小盒子
const boxEl = findResizeBox(el) as ElType
// 拖拽移动手柄
const headerEl = boxEl?.querySelector('.el-dialog__header') as ElType
if (!headerEl || !boxEl) return
boxEl.style.height = binding.value.initHeight + 'px'
const translateX = (screenWidth - binding.value.initWidth) / 2
const translateY = (screenHeight - binding.value.initHeight) / 2
boxEl.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`
// dragStart
const dragStart = () => {
setUserSelect('none')
binding.value?.updateDraggingState?.(true)
}
// dragEnd
const dragEnd = () => {
setUserSelect('')
binding.value?.updateDraggingState?.(false)
}
// 设置鼠标悬停样式
const setCursor = (e: MouseEvent) => {
const x = e.clientX
const y = e.clientY
const { left, top, width, height } = boxEl.getBoundingClientRect()
const headerHeight = headerEl.clientHeight
if (checkHeaderDragZoom({ left, top, width, height, x, y }, headerHeight)) {
boxEl.style.cursor = 'move'
} else {
let type = checkCursorType({ left, top, width, height, x, y })
boxEl.style.cursor = CURSOR_TYPE[type] || 'default'
}
}
let initX: number
let initY: number
// 拖拽移动-鼠标按下
const dragMousedown = (e: MouseEvent) => {
const { translateX, translateY } = getTranslate(boxEl)
initX = e.clientX - translateX
initY = e.clientY - translateY
// 移动开始
const dragMousemove = function (e: MouseEvent) {
e.preventDefault()
let x = e.clientX - initX
let y = e.clientY - initY
// 边界限制,避免拖拽手柄完全移出屏幕
const minX = -initX
const maxX = screenWidth
const maxY = screenHeight - 40
const minY = 0
if (x < minX) {
x = minX
} else if (x > maxX) {
x = maxX
}
if (y < minY) {
y = minY
} else if (y > maxY) {
y = maxY
}
boxEl.style.transform = `translate3d(${x}px, ${y}px, 0)`
}
// 移动结束
const dragMouseup = function () {
document.removeEventListener('mousemove', dragMousemove)
document.removeEventListener('mouseup', dragMouseup)
dragEnd()
}
document.addEventListener('mousemove', dragMousemove)
document.addEventListener('mouseup', dragMouseup)
}
// 拖拽拉伸-鼠标按下
const stretchMousedown = (e: MouseEvent) => {
const x = e.clientX
const y = e.clientY
const { left, top, width, height } = boxEl.getBoundingClientRect()
const { translateX, translateY } = getTranslate(boxEl)
dragStart()
// header
const headerHeight = headerEl.clientHeight
const headerRange = checkHeaderDragZoom({ left, top, width, height, x, y }, headerHeight)
if (headerRange) {
dragMousedown(e)
return
}
// 拉伸开始
const stretchMousemove = function (e: MouseEvent) {
e.preventDefault()
const endX = e.clientX
const endY = e.clientY
boundaryLimit(
{
left,
top,
width,
height,
x,
y,
endX,
endY,
},
boxEl,
translateX,
translateY,
screenWidth,
screenHeight
)
}
// 拉伸结束
const stretchMouseup = function () {
document.removeEventListener('mousemove', stretchMousemove)
document.removeEventListener('mouseup', stretchMouseup)
dragEnd()
}
document.addEventListener('mousemove', stretchMousemove)
document.addEventListener('mouseup', stretchMouseup)
}
boxEl.addEventListener('mousemove', setCursor)
boxEl.addEventListener('mousedown', stretchMousedown)
boxEl.setCursor = setCursor
boxEl.stretchMousedown = stretchMousedown
},
beforeUnmount: function (el: HTMLElement) {
// 拖拽拉伸大小盒子
const boxEl = findResizeBox(el) as ElType
if (!boxEl) return
const setCursor = boxEl.setCursor
const stretchMousedown = boxEl.stretchMousedown
boxEl.removeEventListener('mousemove', setCursor)
boxEl.removeEventListener('mousemove', stretchMousedown)
// 删除保存在 el 中的函数
boxEl.setCursor = null
boxEl.stretchMousedown = null
},
}