使用 `<Teleport>` 实现全局模态框(Vue 3)

使用 <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 控制类名,实现平滑打开/关闭。
插槽灵活 提供 headerdefaultfooter 插槽 父组件可完全自定义标题、内容和底部按钮。
多个模态框 可同时使用多个 <GlobalModal> 每个实例独立控制,但注意 z-index 堆叠。