本文是基于Vue3 实现的一个命令式的全局弹窗,命令式就是只需一行代码,执行一个函数就能弹窗,与声明式相比优势还是很多的:减少冗余模板(templete中引入组件)、全局易用(js、模板中都能使用)、动态性强(成功、失败、警告等状态)
命令式全局弹窗的实现
预期:函数调用弹窗
参考ui框架 使用命令式调用,就能弹出弹窗
// 希望只调这么个函数就能提示框
function showToast() {
useToast('这是一个消息', 'success')
}
实现思路:
Toast
组件的封装- 封装
useToast
函数 - 调用
useToast('这是一个消息', 'success')
传入内容和类型
<!-- // src\components\Toast.vue -->
<template>
<Teleport to="body">
<div class="toast" :class="type">
{{ msg }}
</div>
</Teleport>
</template>
<script setup>
defineProps({
msg: {
type: String,
default: ''
},
type: {
type: String,
default: 'info'
}
})
</script>
<style scoped>
.toast {
position: fixed;
top: 20px;
left: 50%;
padding: 12px 24px;
background: #333;
color: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
transform: translateX(-50%);
animation: fadeIn 0.3s ease-out;
}
.toast.success {
background: #4CAF50;
}
.toast.error {
background: #F44336;
}
.toast.warning {
background: #FF9800;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px) translateX(-50%); }
to { opacity: 1; transform: translateY(0) translateX(-50%); }
}
</style>
- 封装
useToast
函数
// src\utils\message.ts
import Toast from "../components/Toast.vue";
import { createApp } from "vue";
export function useToast( msg = '', type='info', delay = 3000) {
const div = document.createElement("div");
const toastId = `toast-${Date.now()}`; // 使用时间戳生成唯一 ID
div.id = toastId;
document.body.appendChild(div);
const close = () => {
app.unmount();
document.body.removeChild(div);
}
const app = createApp(Toast, {type, msg, close});
app.mount('#'+toastId);
setTimeout(() => {
close();
}, delay);
};
- 调用
useToast('这是一个消息', 'success')
传入内容和类型
<template>
<div class="mesage">
<div @click="showToast">显示Toast</div>
</div>
</template>
<script setup lang="ts">
import { useToast } from '../utils/message'
function showToast() {
useToast('这是一个消息', 'success')
}
</script>
我们看一下效果~~~
我们发现已经基本实现了,,但是细心的小伙伴肯定发现了 ==>> 这后面的把前面的覆盖了,人家官网的是往下挤的来着。
重叠提升框的处理
思考:因为我们的提示框是使用的定位,所以需要维护活跃的提示框、计算当前提示框的top、及高度
添加的top计算
- 第一个 top => 20px
- 第二个 top => 上一个的top + 上一个的高度 + 10px 的边距
- 第三个 top => 上一个的top + 上一个的高度 + 10px 的边距
计算当前提示框距离顶部位置
当前项存进数组中activeToasts
// 记录所有活跃toast
const activeToasts = [];
export function useToast( msg = '', type='info', delay = 3000) {
const div = document.createElement("div");
const toastId = `toast-${Date.now()}`; // 使用时间戳生成唯一 ID
div.id = toastId;
// 计算当前提示框距离顶部位置----start-----计算当前提示框距离顶部位置
const calculateTop = () => {
if (activeToasts.length === 0) return 20; // 默认 top:20px
const lastToast = activeToasts[activeToasts.length - 1];
return lastToast.top + lastToast.height + 10; // 前一个 Toast 的 bottom + 10px margin
};
const top = calculateTop();
// 计算当前提示框距离顶部位置----end-----计算当前提示框距离顶部位置
document.body.appendChild(div);
const app = createApp(Toast, {type, msg, close});
app.mount('#'+toastId);
// 当前项存进数组中 ------start-------当前项存进数组中
setTimeout(() => {
const toastEl = document.getElementById(toastId)?.querySelector('.toast');
if (toastEl) {
const height = toastEl.offsetHeight;
// id top height 存储在 activeToasts 数组中
activeToasts.push({ id: toastId, top, height });
toastEl.style.top = `${top}px`;
}
}, 0);
// 当前项存进数组中 ------end-------当前项存进数组中
setTimeout(close, delay);
};
移除时的top计算
移除项的下面的所有项 都需要减去 移除项的高度
// 移除当前 Toast,并更新后续 Toast 的位置
const index = activeToasts.findIndex(t => t.id === toastId);
if (index !== -1) {
const removedHeight = activeToasts[index].height;
activeToasts.splice(index, 1);
// 后续 Toast 的 top 要减去被删除的高度
for (let i = index; i < activeToasts.length; i++) {
activeToasts[i].top -= removedHeight + 10;
const toastEl = document.getElementById(activeToasts[i].id)?.querySelector('.toast');
if (toastEl) {
toastEl.style.top = `${activeToasts[i].top}px`;
}
}
}
最终效果
完整代码
Toast组件
<template>
<div class="toast" :id="toast + '-item'" :class="type">
{{ msg }}
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
msg: {
type: String,
default: ''
},
type: {
type: String,
default: 'info'
},
toastId: {
type: String,
default: ''
}
})
</script>
<style scoped>
.toast {
position: fixed;
top: 20px;
left: 50%;
padding: 12px 24px;
background: #333;
color: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
transform: translateX(-50%);
animation: fadeIn 0.3s ease-out;
}
.toast.success {
background: #4CAF50;
}
.toast.error {
background: #F44336;
}
.toast.warning {
background: #FF9800;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px) translateX(-50%); }
to { opacity: 1; transform: translateY(0) translateX(-50%); }
}
</style>
useToast
// utils/message.js
import Toast from "../components/Toast.vue";
import { createApp } from "vue";
// 记录所有活跃toast
const activeToasts = [];
export function useToast( msg = '', type='info', delay = 3000) {
const div = document.createElement("div");
const toastId = `toast-${Date.now()}`; // 使用时间戳生成唯一 ID
div.id = toastId;
// 计算当前提示框距离顶部位置----start-----计算当前提示框距离顶部位置
const calculateTop = () => {
if (activeToasts.length === 0) return 20; // 默认 top:20px
const lastToast = activeToasts[activeToasts.length - 1];
return lastToast.top + lastToast.height + 10; // 前一个 Toast 的 bottom + 10px margin
};
const top = calculateTop();
// 计算当前提示框距离顶部位置----end-----计算当前提示框距离顶部位置
document.body.appendChild(div);
const close = () => {
app.unmount();
document.body.removeChild(div);
// 移除当前 Toast,并更新后续 Toast 的位置
const index = activeToasts.findIndex(t => t.id === toastId);
if (index !== -1) {
const removedHeight = activeToasts[index].height;
activeToasts.splice(index, 1);
// 后续 Toast 的 top 要减去被删除的高度
for (let i = index; i < activeToasts.length; i++) {
activeToasts[i].top -= removedHeight + 10;
const toastEl = document.getElementById(activeToasts[i].id)?.querySelector('.toast');
if (toastEl) {
toastEl.style.top = `${activeToasts[i].top}px`;
}
}
}
}
const app = createApp(Toast, {type, msg, close});
app.mount('#'+toastId);
// 当前项存进数组中 ------start-------当前项存进数组中
setTimeout(() => {
const toastEl = document.getElementById(toastId)?.querySelector('.toast');
if (toastEl) {
const height = toastEl.offsetHeight;
// id top height 存储在 activeToasts 数组中
activeToasts.push({ id: toastId, top, height });
toastEl.style.top = `${top}px`;
}
}, 0);
// 当前项存进数组中 ------end-------当前项存进数组中
setTimeout(close, delay);
};
使用
<template>
<div class="mesage">
<div @click="showToast">显示Toast</div>
<div @click="useToast('这是一个警告消息' + Date.now(), 'warning')">warning</div>
<div @click="useToast('这是一个错误消息' + Date.now(), 'error')">error</div>
</div>
</template>
<script setup lang="ts">
import { useToast } from '../utils/message'
function showToast() {
useToast('这是一个消息' + Date.now(), 'success')
}
</script>
总结
- 每个Toast id应唯一,且不能被覆盖
- 需考虑存在多个提示框的情况
- 存在多个提示框需动态计算其位置(添加和删除)
- 可以把useToast 挂载在实例的原型上,就省去了引入的步骤
结语:
如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻
因为收藏===会了
如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾