vue二次封装ant-design-vue中的Modal弹窗组件,实现拖拽,全屏两种功能,原有参数属性不变

'# vue二次封装ant-design-vue中的Modal弹窗组件,实现拖拽,全屏两种功能,原有参数属性不变

一、背景与问题

在实际开发中,Modal弹窗组件是UI交互中最常见的组件之一。ant-design-vue的Modal组件虽然功能强大,但存在两个显著局限:

  1. 缺乏拖拽功能:用户无法自由拖动弹窗位置
  2. 全屏模式限制:仅支持通过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. 推荐方案

  1. 使用Vue3组合式API:便于管理状态和副作用
  2. 分离业务逻辑和UI:保持组件可复用性
  3. 使用TypeScript:增强类型安全
  4. 添加单元测试:使用Jest进行测试
  5. 使用TypeScript类型定义:增强代码可维护性

2. 推荐代码结构

src/
├── components/
│   └── CustomModal/
│       ├── index.vue
│       ├── config.ts
│       └── utils.ts
├── types/
│   └── modal.d.ts
└── main.ts

3. 推荐配置

// 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的项目

不建议在以下场景使用:

  • 简单的表单提交场景
  • 对性能要求极高的场景
  • 需要严格控制样式和布局的场景

通过合理的封装和优化,我们可以实现一个既保持原有功能,又具备扩展性的弹窗组件,满足复杂业务场景的需求。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日