使用<Teleport>实现全局模态框(Vue 3)
<Teleport>是 Vue 3 内置组件,可以将组件模板的一部分“传送”到 DOM 中任意位置(如<body>),从而突破组件层级限制,非常适合实现模态框、全局提示、下拉菜单等需要脱离父容器样式的场景。
下面实现一个可复用、支持插槽、带有动画的全局模态框组件。
1. 模态框组件:GlobalModal.vue
<template> <!-- 使用 Teleport 将模态框传送到 body --> <Teleport to="body"> <!-- 遮罩层(点击关闭) --> <div v-if="modelValue" class="modal-overlay" @click.self="close"> <!-- 内容容器 --> <div class="modal-container" :class="{ 'modal-enter': isVisible }"> <div class="modal-content"> <!-- 关闭按钮 --> <button class="modal-close" @click="close">✕</button> <!-- 标题插槽 --> <div class="modal-header"> <slot name="header"> <h3>{{ title }}</h3> </slot> </div> <!-- 默认插槽:主体内容 --> <div class="modal-body"> <slot /> </div> <!-- 底部操作插槽 --> <div class="modal-footer" v-if="$slots.footer"> <slot name="footer" /> </div> </div> </div> </div> </Teleport> </template> <script setup> import { watch, nextTick, ref } from 'vue' const props = defineProps({ modelValue: { type: Boolean, default: false }, title: { type: String, default: '提示' }, // 点击遮罩是否关闭 closeOnClickOverlay: { type: Boolean, default: true }, // 是否显示动画(简单过渡) animated: { type: Boolean, default: true } }) const emit = defineEmits(['update:modelValue', 'close', 'open']) // 内部控制动画显示 const isVisible = ref(false) // 监听 modelValue 变化,控制打开/关闭动画 watch( () => props.modelValue, async (newVal) => { if (newVal) { // 打开时,先让元素显示,再触发动画 await nextTick() isVisible.value = true emit('open') // 禁止 body 滚动(避免穿透) document.body.style.overflow = 'hidden' } else { // 关闭动画 isVisible.value = false document.body.style.overflow = '' emit('close') } }, { immediate: true } ) const close = () => { if (props.closeOnClickOverlay) { emit('update:modelValue', false) } } </script> <style scoped> /* 遮罩 */ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999; animation: fadeIn 0.3s ease; } /* 内容容器(带动画) */ .modal-container { background: white; border-radius: 12px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); transform: scale(0.8); opacity: 0; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease; } .modal-container.modal-enter { transform: scale(1); opacity: 1; } /* 内容内边距 */ .modal-content { padding: 24px; position: relative; } /* 关闭按钮 */ .modal-close { position: absolute; top: 12px; right: 16px; background: none; border: none; font-size: 22px; line-height: 1; cursor: pointer; color: #999; transition: color 0.2s; } .modal-close:hover { color: #333; } /* 标题 */ .modal-header { margin-bottom: 16px; padding-right: 30px; } .modal-header h3 { margin: 0; font-size: 18px; } /* 主体 */ .modal-body { margin-bottom: 20px; } /* 底部 */ .modal-footer { display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #eee; padding-top: 16px; } /* 进入动画(遮罩淡入) */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } </style>2. 父组件使用示例
<template> <div> <button @click="openModal">打开模态框</button> <!-- 使用模态框,通过 v-model 控制显示 --> <GlobalModal v-model="showModal" title="温馨提示"> <!-- 默认插槽:主体内容 --> <p>这是模态框的内容区域,可以放置任何 Vue 组件或 HTML。</p> <input v-model="inputValue" placeholder="输入一些内容..." /> <p>输入的值:{{ inputValue }}</p> <!-- 底部插槽:自定义按钮 --> <template #footer> <button class="btn-primary" @click="confirm">确认</button> <button class="btn-secondary" @click="showModal = false">取消</button> </template> </GlobalModal> </div> </template> <script setup> import { ref } from 'vue' import GlobalModal from './GlobalModal.vue' const showModal = ref(false) const inputValue = ref('') const openModal = () => { showModal.value = true } const confirm = () => { alert(`确认操作,输入内容:${inputValue.value}`) showModal.value = false } </script> <style> .btn-primary { background: #42b883; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .btn-secondary { background: #eee; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } </style>3. 关键技术与注意事项
| 技术点 | 实现方式 | 说明 |
|---|---|---|
| 传送目标 | <Teleport to="body"> | 模态框 DOM 将被挂载到<body>下,脱离父组件样式和定位限制。 |
| 双向绑定 | v-model="showModal" | 组件内通过modelValueprop 和update:modelValue事件实现。 |
| 遮罩点击关闭 | @click.self="close" | 只有点击遮罩本身(而非其子元素)才触发关闭。 |
| 防止滚动穿透 | document.body.style.overflow = 'hidden' | 模态框打开时禁用 body 滚动,关闭时恢复。 |
| 过渡动画 | 使用 CSS transition + 动态 class | 通过内部isVisible控制类名,实现平滑打开/关闭。 |
| 插槽灵活 | 提供header、default、footer插槽 | 父组件可完全自定义标题、内容和底部按钮。 |
| 多个模态框 | 可同时使用多个<GlobalModal> | 每个实例独立控制,但注意 z-index 堆叠。 |