概述
本节通过购物车的删除交互,学习 Vue 3 的 <Teleport> 内置组件,并基于它封装 Modal(模态框)和 Toast(消息提示)两种弹窗组件。涉及 defineModel 双向绑定、<Transition> 过渡动画、watch 自动关闭等核心技术。
1. Teleport 基础
1.1 为什么需要 Teleport
在传统的组件开发中,模态框渲染在当前组件的 DOM 层级内,可能受到父级 overflow: hidden、z-index 等样式影响,导致显示异常。
<Teleport> 允许将组件内容渲染到 DOM 中的任意位置(通常是 <body>),从而脱离当前组件的 DOM 层级约束。
1.2 基本用法
<template>
<button @click="show = !show">删除</button>
<Teleport to="body">
<div v-if="show">Hello World</div>
</Teleport>
</template>
<script setup lang="ts">
const show = ref(false)
</script>
vue
to="body":将内容传送到<body>标签内v-if="show":控制内容的显示与隐藏- 点击按钮时,
<body>末尾会出现对应元素
1.3 Teleport 适用场景
| 场景 | 说明 |
|---|---|
| Modal(模态框) | 危险操作确认(如删除、退出) |
| Toast(消息提示) | 操作结果反馈(如"添加成功") |
| 全屏遮罩 | 不受父级 overflow 影响 |
| 第三方弹窗 | 避免层级嵌套问题 |
2. Modal 组件封装
2.1 组件实现
<!-- components/Modal.vue -->
<script setup lang="ts">
const show = defineModel<boolean>({ default: false })
const emit = defineEmits<{
submit: []
}>()
const onCancel = () => {
show.value = false
}
const onConfirm = () => {
emit('submit')
show.value = false
}
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="show" class="fixed inset-0 z-50">
<!-- 遮罩层 -->
<div class="absolute inset-0 bg-black/30" @click="onCancel" />
<!-- 内容区 -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="bg-white rounded-lg shadow-lg p-6 w-96">
<!-- 默认插槽:自定义内容 -->
<slot />
<!-- 底部按钮 -->
<div class="flex justify-end mt-4 gap-2">
<button class="btn btn-plain" @click="onCancel">取消</button>
<button class="btn btn-primary" @click="onConfirm">确定</button>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
</style>
vue
2.2 关键设计点
defineModel 双向绑定:
const show = defineModel<boolean>({ default: false })
ts
父组件通过 v-model 控制显隐,子组件内部修改 show.value 时自动同步给父组件。
过渡动画:
使用 Vue 内置的 <Transition> 组件,配合 CSS 类名控制淡入淡出:
| CSS 类名 | 触发时机 |
|---|---|
fade-enter-from | 进入动画起始状态 |
fade-enter-active | 进入动画过程 |
fade-leave-to | 离开动画结束状态 |
fade-leave-active | 离开动画过程 |
emit 事件:
点击"确定"时触发 submit 事件,父组件可监听此事件执行具体业务逻辑(如删除购物车条目)。
2.3 使用 Modal
<!-- 在 Cart.vue 中使用 -->
<script setup lang="ts">
const showModal = ref(false)
const onDelete = () => {
showModal.value = true
}
const onConfirmDelete = () => {
// 执行删除逻辑
console.log('已删除')
}
</script>
<template>
<span class="text-red-500 cursor-pointer" @click="onDelete">删除</span>
<Modal v-model="showModal" @submit="onConfirmDelete">
<p class="text-lg">确定要删除这个课程吗?</p>
</Modal>
</template>
vue
3. Toast 组件封装
3.1 组件实现
Toast 组件与 Modal 类似,区别在于它会自动定时消失:
<!-- components/Toast.vue -->
<script setup lang="ts">
const show = defineModel<boolean>({ default: false })
const props = defineProps<{
text?: string
duration?: number
}>()
// 监听 show,自动定时关闭
watch(show, (val) => {
if (val) {
setTimeout(() => {
show.value = false
}, props.duration || 2000)
}
})
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="show" class="fixed top-20 left-1/2 -translate-x-1/2 z-50">
<div class="bg-gray-800 text-white px-6 py-3 rounded-lg shadow-lg">
<slot>{{ text }}</slot>
</div>
</div>
</Transition>
</Teleport>
</template>
vue
3.2 Toast vs Modal 对比
| 特性 | Modal | Toast |
|---|---|---|
| 定位 | 居中全屏遮罩 | 顶部居中浮动 |
| 关闭方式 | 手动点击按钮 | 自动定时消失 |
| 用户交互 | 需要确认/取消 | 仅信息展示 |
| 使用场景 | 危险操作确认 | 操作结果反馈 |
| 默认插槽 | 自定义内容区 | 替代默认文本 |
3.3 使用 Toast
<script setup lang="ts">
const showToast = ref(false)
</script>
<template>
<span @click="showToast = true">添加到购物车</span>
<Toast v-model="showToast" text="已添加到购物车" :duration="2000" />
</template>
vue
4. defineModel 深入理解
4.1 工作原理
defineModel 是 Vue 3.3+ 的编译器宏,本质上等价于以下代码:
// defineModel<boolean>({ default: false }) 等价于:
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
// 并返回一个可写的 ref
const show = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
ts
4.2 local 选项
const activeIndex = defineModel<number>({ default: 0, local: true })
ts
local: true:允许组件内部直接修改值,即使父组件未传递v-model- 不设置
local:如果父组件未绑定v-model,内部修改不会生效
4.3 开启实验特性
在 vite.config.ts 中启用:
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
}
ts
或在 env.d.ts 中声明:
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
ts
5. 组件通信模式总结
| 模式 | 方向 | 适用场景 |
|---|---|---|
defineProps | 父 -> 子 | 传递配置数据 |
defineEmits | 子 -> 父 | 触发事件 |
defineModel | 双向 | 表单控件、显隐控制 |
provide/inject | 跨层级 | 主题、配置、深层传参 |
slot | 父 -> 子(内容) | 自定义内容分发 |
小结
| 要点 | 技术 |
|---|---|
| DOM 传送 | <Teleport to="body"> |
| 双向绑定 | defineModel<boolean>({ default: false }) |
| 过渡动画 | <Transition name="fade"> + CSS |
| 自动关闭 | watch + setTimeout |
| 事件通知 | defineEmits + emit('submit') |
↑