使用 <Teleport> 实现全局模态框(Vue 3)
<Teleport> 是 Vue 3 内置组件,可以将组件模板的一部分 "传送" 到 DOM 中任意位置(如 <body>),从而突破组件层级限制,非常适合实现模态框、全局提示、下拉菜单等需要脱离父容器样式的场景。
下面实现一个 可复用、支持插槽、带有动画 的全局模态框组件。
1. 模态框组件:GlobalModal.vue
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. 父组件使用示例
vue
<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" |
组件内通过 modelValue prop 和 update:modelValue 事件实现。 |
| 遮罩点击关闭 | @click.self="close" |
只有点击遮罩本身(而非其子元素)才触发关闭。 |
| 防止滚动穿透 | document.body.style.overflow = 'hidden' |
模态框打开时禁用 body 滚动,关闭时恢复。 |
| 过渡动画 | 使用 CSS transition + 动态 class | 通过内部 isVisible 控制类名,实现平滑打开/关闭。 |
| 插槽灵活 | 提供 header、default、footer 插槽 |
父组件可完全自定义标题、内容和底部按钮。 |
| 多个模态框 | 可同时使用多个 <GlobalModal> |
每个实例独立控制,但注意 z-index 堆叠。 |