vue二次封装ant-design-vue中的Modal弹窗组件,实现拖拽,全屏两种功能,原有参数属性不变
'# vue二次封装ant-design-vue中的Modal弹窗组件,实现拖拽,全屏两种功能,原有参数属性不变
一、背景与问题
在实际开发中,Modal弹窗组件是UI交互中最常见的组件之一。ant-design-vue的Modal组件虽然功能强大,但存在两个显著局限:
- 缺乏拖拽功能:用户无法自由拖动弹窗位置
- 全屏模式限制:仅支持通过
fullscreen属性开启全屏,但无法动态切换
在某个电商后台管理系统开发中,我们需要实现如下功能:
- 业务员可以拖动弹窗调整位置
- 查看订单详情时可切换全屏模式
- 保持原有
title、width、footer等属性不变
传统做法需要继承Modal组件并手动添加拖拽逻辑,但容易出现以下问题:
- 事件绑定不完整导致拖拽失效
- 全屏切换时样式不统一
- 响应式处理不完善
二、基本原理
1. 拖拽功能实现原理
拖拽功能的核心是处理以下事件:
mousedown → mousemove → mouseup需要:
- 记录初始位置
- 监听鼠标移动事件更新位置
- 防止事件冒泡影响父级元素
2. 全屏功能实现原理
全屏模式需要处理:
- 状态切换逻辑(全屏/普通)
- 响应式布局(不同分辨率适配)
- 退出全屏的恢复机制
三、环境准备
npm install ant-design-vue
npm install @types/ant-design-vue --save-dev开发环境配置:
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"esModuleInterop": true,
"esModuleResolution": "node",
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}四、核心实现
1. 拖拽功能实现
<template>
<a-modal
v-model:visible="visible"
:title="title"
:width="width"
:footer="footer"
:mask-closable="maskClosable"
:keyboard="keyboard"
:destroy-on-close="destroyOnClose"
:transition-name="transitionName"
:centered="centered"
:footer="footer"
:mask-style="maskStyle"
:body-style="bodyStyle"
:get-container="getContainer"
:class="['custom-modal', isFullscreen ? 'fullscreen' : '']"
:style="{ transform: isFullscreen ? 'scale(1.1)' : 'none' }"
>
<template #title>
<div class="modal-header">
<span>{{ title }}</span>
<div class="drag-handle" @mousedown="startDrag"></div>
</div>
</template>
<slot />
</a-modal>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { Modal } from 'ant-design-vue'
const props = defineProps({
visible: Boolean,
title: String,
width: [String, Number],
footer: [Array, Boolean],
maskClosable: Boolean,
keyboard: Boolean,
destroyOnClose: Boolean,
transitionName: String,
centered: Boolean,
maskStyle: Object,
bodyStyle: Object,
getContainer: Function,
draggable: Boolean,
fullscreen: Boolean
})
const emit = defineEmits(['update:visible', 'ok', 'cancel'])
const isFullscreen = ref(false)
const isDragging = ref(false)
const initialX = ref(0)
const initialY = ref(0)
const offsetX = ref(0)
const offsetY = ref(0)
const container = ref(null)
// 拖拽事件处理
const startDrag = (e) => {
if (!props.draggable) return
e.preventDefault()
initialX.value = e.clientX
initialY.value = e.clientY
isDragging.value = true
}
const onDrag = (e) => {
if (!isDragging.value) return
const newX = e.clientX - initialX.value + offsetX.value
const newY = e.clientY - initialY.value + offsetY.value
container.value.style.left = `${newX}px`
container.value.style.top = `${newY}px`
}
const endDrag = () => {
isDragging.value = false
}
// 全屏切换
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
if (isFullscreen.value) {
document.body.classList.add('fullscreen-mode')
} else {
document.body.classList.remove('fullscreen-mode')
}
}
</script>
<style scoped>
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
position: relative;
}
.drag-handle {
width: 10px;
height: 100%;
background: #87d069;
cursor: move;
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
.fullscreen-mode {
padding: 0;
margin: 0;
}
</style>2. 全屏功能实现
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { Modal } from 'ant-design-vue'
const props = defineProps({
fullscreen: Boolean
})
const emit = defineEmits(['update:fullscreen'])
const isFullscreen = ref(false)
const container = ref(null)
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
emit('update:fullscreen', isFullscreen.value)
if (isFullscreen.value) {
document.body.classList.add('fullscreen-mode')
document.documentElement.requestFullscreen()
} else {
document.body.classList.remove('fullscreen-mode')
document.exitFullscreen()
}
}
</script>
<style>
.fullscreen-mode {
padding: 0;
margin: 0;
}
</style>3. 动态样式处理
<script setup>
import { computed } from 'vue'
const props = defineProps({
isFullscreen: Boolean
})
const computedStyle = computed(() => {
return {
transform: props.isFullscreen ? 'scale(1.1)' : 'none',
transition: 'transform 0.3s ease'
}
})
</script>五、完整案例
1. 业务场景:订单详情弹窗
<template>
<div>
<a-button @click="showModal">查看订单</a-button>
<CustomModal
v-model:visible="visible"
:title="title"
:width="800"
:footer="footer"
draggable
fullscreen
@ok="handleOk"
@cancel="handleCancel"
>
<div class="modal-content">
<p>订单号:{{ orderNo }}</p>
<p>商品信息:{{ goods }}</p>
<p>订单状态:{{ status }}</p>
</div>
</CustomModal>
</div>
</template>
<script setup>
import { ref } from 'vue'
import CustomModal from './CustomModal.vue'
const visible = ref(false)
const title = ref('订单详情')
const orderNo = ref('20230901001')
const goods = ref('智能手表')
const status = ref('已发货')
const footer = ref([
<a-button type="primary" @click="handleOk">确认</a-button>,
<a-button @click="handleCancel">取消</a-button>
])
const handleOk = () => {
visible.value = false
alert('确认订单')
}
const handleCancel = () => {
visible.value = false
}
</script>2. 配置文件
// config.ts
export const MODAL_CONFIG = {
defaultWidth: 600,
defaultHeight: 400,
transitionName: 'slide-up',
maskClosable: true,
keyboard: true,
centered: false
}六、源码解析
1. 拖拽事件处理逻辑
// 拖拽事件处理
const startDrag = (e) => {
if (!props.draggable) return
e.preventDefault()
initialX.value = e.clientX
initialY.value = e.clientY
isDragging.value = true
}
const onDrag = (e) => {
if (!isDragging.value) return
const newX = e.clientX - initialX.value + offsetX.value
const newY = e.clientY - initialY.value + offsetY.value
container.value.style.left = `${newX}px`
container.value.style.top = `${newY}px`
}
const endDrag = () => {
isDragging.value = false
}关键点:
- 使用
e.preventDefault()防止默认行为 - 记录初始坐标和偏移量
- 使用
transform实现平滑移动 - 需要处理多屏和窗口大小变化
2. 全屏切换逻辑
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
emit('update:fullscreen', isFullscreen.value)
if (isFullscreen.value) {
document.body.classList.add('fullscreen-mode')
document.documentElement.requestFullscreen()
} else {
document.body.classList.remove('fullscreen-mode')
document.exitFullscreen()
}
}注意:
- 需要处理浏览器兼容性(如
requestFullscreen的兼容性) - 全屏时需要禁用滚动条
- 全屏模式需要特殊样式处理
七、进阶使用
1. 动态调整尺寸
const resizeHandler = () => {
if (isFullscreen.value) {
container.value.style.width = `${window.innerWidth}px`
container.value.style.height = `${window.innerHeight}px`
}
}
onMounted(() => {
window.addEventListener('resize', resizeHandler)
return () => {
window.removeEventListener('resize', resizeHandler)
}
})2. 多级嵌套支持
const getContainer = () => {
return document.body
}3. 与第三方库集成
import { useDrag } from '@vueuse/core'
const { x, y, isDragging } = useDrag(container, {
initialValue: [100, 100],
onStart: () => {
console.log('开始拖拽')
},
onMove: (pos) => {
console.log('拖拽位置', pos)
},
onEnd: () => {
console.log('结束拖拽')
}
})八、性能与工程实践
1. 性能优化策略
| 优化点 | 解决方案 |
|---|---|
| 频繁重绘 | 使用requestAnimationFrame |
| 内存泄漏 | 使用onBeforeUnmount清理事件 |
| 布局抖动 | 使用transform替代position |
| 全屏性能 | 使用requestFullscreen替代CSS全屏 |
2. 异常处理机制
try {
document.documentElement.requestFullscreen()
} catch (e) {
console.error('全屏失败:', e)
}3. 安全防护
// 过滤用户输入内容
const sanitizeContent = (content) => {
return content.replace(/<script\b[^<]*(?:(?!<\/script\b[^<]*<script\b[^<]*)<\/script>/gi, '')
}九、常见问题与踩坑
1. 常见错误
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 拖拽失效 | 未阻止默认事件 | e.preventDefault() |
| 全屏无法退出 | 未处理浏览器兼容性 | 使用document.exitFullscreen() |
| 样式不一致 | 未处理响应式 | 使用@media查询 |
| 跨域问题 | 未处理跨域请求 | 使用CORS配置 |
| 内存泄漏 | 未清理事件监听 | 使用onBeforeUnmount |
2. 典型错误示例
// 错误示例:未处理事件冒泡
const startDrag = (e) => {
initialX.value = e.clientX
initialY.value = e.clientY
isDragging.value = true
}改进方案:
const startDrag = (e) => {
if (!props.draggable) return
e.preventDefault()
initialX.value = e.clientX
initialY.value = e.clientY
isDragging.value = true
}十、最佳实践
1. 推荐方案
- 使用Vue3组合式API:便于管理状态和副作用
- 分离业务逻辑和UI:保持组件可复用性
- 使用TypeScript:增强类型安全
- 添加单元测试:使用Jest进行测试
- 使用TypeScript类型定义:增强代码可维护性
2. 推荐代码结构
src/
├── components/
│ └── CustomModal/
│ ├── index.vue
│ ├── config.ts
│ └── utils.ts
├── types/
│ └── modal.d.ts
└── main.ts3. 推荐配置
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"esModuleInterop": true,
"esModuleResolution": "node",
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
}
}十一、总结
通过二次封装ant-design-vue的Modal组件,我们实现了拖拽和全屏两种功能,同时保持原有参数属性不变。这种封装方案在以下场景中非常有用:
- 需要高度自定义UI交互的场景
- 需要支持多屏显示的业务场景
- 需要统一管理弹窗行为的项目
但需要注意以下限制:
- 可能增加代码复杂度
- 需要处理更多边界情况
- 可能影响原有组件的可维护性
在性能方面,通过使用requestAnimationFrame和transform优化,可以保证流畅的拖拽体验。在安全性方面,需要对用户输入内容进行过滤处理。
建议在以下场景使用本方案:
- 电商后台管理系统
- 复杂的业务系统
- 需要高度自定义UI的项目
不建议在以下场景使用:
- 简单的表单提交场景
- 对性能要求极高的场景
- 需要严格控制样式和布局的场景
通过合理的封装和优化,我们可以实现一个既保持原有功能,又具备扩展性的弹窗组件,满足复杂业务场景的需求。
评论已关闭