组件文件夹:Toast,Toast内部文件:Toast.vue,index.ts
一、Toast.vue
html
<template>
<!-- 绑定after‑leave事件,触发组件内部方法 -->
<Transition name="toast" @after-leave="handleAfterLeave">
<!-- 这里继续用v-show没问题,但是初始visible必须为false -->
<div v-show="visible" class="momal" :class="typeClass">{{ msg }}</div>
</Transition>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const props = defineProps(['msg', 'type'])
const emit = defineEmits(['after-leave'])
const typeClass = computed(() => {
return {
'momal-success': props.type === 'success',
'momal-error': props.type === 'error',
'momal-warning': props.type === 'warning'
}
})
// 关键:初始为false!不要一开始true
const visible = ref(false)
// 组件挂载完成后,再打开, false → true,触发enter入场动画
onMounted(() => {
visible.value = true
})
const close = () => {
visible.value = false
}
const handleAfterLeave = () => {
emit('after-leave')
}
defineExpose({ close })
</script>
<style>
.toast-enter-from {
opacity: 0;
/* !!! 和momal基础位置保持一致 translate(-50%, -20%),top:20%,垂直偏移是-20%,不要写‑50%,消除位置跳动 */
transform: translate(-50%, -20%) scale(0.5);
}
.toast-enter-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast-enter-to {
opacity: 1;
transform: translate(-50%, -20%) scale(1);
}
.toast-leave-from {
opacity: 1;
transform: translate(-50%, -20%) scale(1);
}
.toast-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast-leave-to {
opacity: 0;
transform: translate(-50%, -20%) scale(0.9);
}
.momal {
position: fixed;
top: 20%;
left: 50%;
transform: translate(-50%, -20%);
background-color: #ccc;
padding: 12px 24px;
max-width: 280px;
border-radius: 6px;
z-index: 9999;
font-size: 14px;
color:#333;
}
.momal-success {
background-color: #afffb3;
}
.momal-error {
background-color: #f56c6c;
color:#fff;
}
.momal-warning {
background-color: #ffedb3;
}
</style>
二、index.ts
ts
import { h, render } from 'vue'
import Toast from './Toast.vue'
// 属性全部设为可选
interface ToastOptions {
msg: string
type?: string
duration?: number
}
export function toast(options: ToastOptions) {
const msg = options.msg ?? ''
const type = options.type ?? ''
const duration = options.duration ?? 2000
// 先定义 container,再创建vnode,回调才能捕获变量
const container = document.createElement('div')
container.style.pointerEvents = 'none'
const vnode = h(Toast, {
msg,
type,
onAfterLeave: () => {
render(null, container)
container.remove()
}
})
render(vnode, container)
document.body.appendChild(container)
setTimeout(() => {
vnode.component?.exposed?.close()
}, duration)
}
三、调用
在想使用的页面中导入组件的函数
ts
import { toast } from '@/components/Toast'
// 直接函数调用
toast({ msg: '保存成功' ,type:'success',duration: 2000})
知识点
h, render虚拟dom加载组件的操作
Transition组件内置的事件和样式
emit('after-leave')方式抛出事件
vnode如何调用emit事件
组件中使用v-show实现隐藏和显示
组件中使用抛出close函数的方式实现visible值的切换
const visible 初始的时候要设置为true,才能实现入场动画
别忘记绑定@after-leave="handleAfterLeave",在动画结束后销毁dom,节约内存,防止内存泄露
补充:该组件未实现队列功能,在并发情况下,可根据实际需求进行完善。