本文介绍了如何使用 Vue 和 @vueuse/motion 创建一个带有动画的模态框。你可以在 这里 找到最终代码。
首先,让我们来创建一个 Modal,核心部分如下(略去了自定义类的具体内容,可以在最终代码处查看):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from "vue"; const isOpen = ref(false); const openModal = () => { isOpen.value = true; }; const closeModal = () => { isOpen.value = false; }; const handleKeydown = (event: KeyboardEvent) => { if (event.key === "Escape") { closeModal(); } }; onMounted(() => { document.addEventListener("keydown", handleKeydown); }); onUnmounted(() => { document.removeEventListener("keydown", handleKeydown); }); </script>
<template> <button class="btn" @click="openModal">Open</button> <Teleport to="body"> <div v-if="isOpen" class="modal-background" /> <div v-if="isOpen" class="modal-container" @click.self="closeModal"> <div class="modal"> <p class="modal-text">Hello, world!</p> <button class="btn" @click="closeModal">Close</button> </div> </div> </Teleport> </template>
|
几个注意点如下:
- 使用
Teleport
来将最后的模态框 “传送” 到 DOM 中更合理的位置去;
- 拆分了模态框背景(标注为
modal-background
)和模态容器(标注为 modal-container
),这是因为我打算创建的模态框的动画是从下往上上浮显示、同时带有一个透明度的变化,而背景的动画我只希望其带有一个透明度的变化;
- 使用
position: fixed;
和 inset: 0;
来让模态框背景和模态容器正确定位;
- 使用
@click.self
来为模态框外侧区域( modal-container
的区域)添加一个点击事件,此处为关闭模态框;
- 使用
onMounted
和 onUnmounted
来在组件加载和卸载时分别添加和移除一个事件监听器,用于在 Esc 键被按下时关闭模态框;
下面让我们来制作动画。背景动画比较简单,我们直接使用 Vue 提供的 Transition
组件将原来的 modal-background
包裹,并添加相应的 CSS 即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <Transition name="fade"> <div v-if="isOpen" class="modal-background" @click="closeModal" /> </Transition> </template>
<style> .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
|
然后是模态框主体的动画,这里使用了 @vueuse/motion 。首先,添加这个包:
1
| pnpm install @vueuse/motion
|
接着编辑 main.ts
:
1 2 3 4 5 6 7 8
| import { createApp } from "vue"; import { MotionPlugin } from "@vueuse/motion"; import "./style.css"; import App from "./App.vue";
const app = createApp(App); app.use(MotionPlugin); app.mount("#app");
|
OK,可以开始使用了。但是简单地为 modal-container
添加 v-motion
并不足够:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div v-if="isOpen" class="modal-container" @click.self="closeModal" v-motion :initial="{ opacity: 0, y: 100 }" :enter="{ opacity: 1, y: 0 }" :leave="{ opacity: 0, y: 100 }" > <div class="modal"> <p class="modal-text">Hello, world!</p> <button class="btn" @click="closeModal">Close</button> </div> </div> </template>
|
如果是这样,你会发现进入动画非常顺利地播放了,但退出动画完全没有生效!为什么会这样?
原因在于我们是通过 v-if
来控制是否显示这个组件的。在进入时,首先 v-if
后跟的值 isOpen
为真,接着进入动画正常播放;但是在退出时,isOpen
变为假,v-if
检测到后直接将该组件从 DOM 中移除了,退出动画根本没有时间播放!
如何解决呢?这个 Demo 演示了这一点,可以配合 Transition
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <script setup lang="ts"> import { useMotions } from "@vueuse/motion"; const motions = useMotions(); </script>
<template> <Transition :css="false" @leave="(_, done) => motions.modal.leave(done)"> <div v-if="isOpen" class="modal-container" @click.self="closeModal" v-motion="'modal'" :initial="{ opacity: 0, y: 100 }" :enter="{ opacity: 1, y: 0 }" :leave="{ opacity: 0, y: 100 }" > <div class="modal"> <p class="modal-text">Hello, world!</p> <button class="btn" @click="closeModal">Close</button> </div> </div> </Transition> </template>
|
这样退出动画也能顺利播放了。