Toast 组件封装文档训练
📋 概述
本文档介绍了一个基于 Vue 3 h
函数实现的 Toast 组件封装,旨在解决 wot-design-uni Toast 组件需要在模板中手动放置 <wd-toast />
的问题,实现在纯 TypeScript 文件中也能使用 Toast 功能。
🎯 代码
type 类型定义
ts
export interface ToastOptions {
show: boolean;
msg?: string;
iconName?: "success" | "error" | "warning" | "info" | "loading";
duration?: number;
position?: "top" | "middle" | "bottom";
cover?: boolean;
}
export interface Toast {
show: (options: ToastOptions) => void;
success: (msg: string, duration?: number) => void;
error: (msg: string, duration?: number) => void;
warning: (msg: string, duration?: number) => void;
info: (msg: string, duration?: number) => void;
loading: (msg: string) => void;
hide: () => void;
destroy: () => void;
}
渲染器
ts
import { h, ref, render, watch, type VNode } from "vue";
import type { ToastOptions } from "./toast-types";
/**
* 基于 h 函数的 Toast 渲染器
* 高度还原 wot-design-uni 的 wd-toast 样式和功能
*/
export class ToastRenderer {
private container: HTMLElement | null = null;
private vnode: VNode | null = null;
private toastState = ref<ToastOptions>({ show: false });
private timer: number | null = null;
constructor() {
this.init();
}
private init() {
// 创建容器
this.container = document.createElement("div");
this.container.id = "toast-container";
this.container.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
`;
document.body.appendChild(this.container);
// 监听状态变化,重新渲染
watch(
() => this.toastState.value,
() => {
this.renderToast();
},
{ deep: true, immediate: true }
);
}
private renderToast() {
const state = this.toastState.value;
if (!state.show) {
// 隐藏 Toast
if (this.vnode && this.container) {
render(null, this.container);
this.vnode = null;
}
return;
}
// 清除之前的定时器
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
// 创建 Toast VNode
this.vnode = this.createToastVNode(state);
if (this.container) {
render(this.vnode, this.container);
}
// 自动隐藏
if (state.duration && state.duration > 0) {
this.timer = setTimeout(() => {
this.hide();
}, state.duration) as any;
}
}
private createToastVNode(state: ToastOptions): VNode {
const { msg, iconName, position = "middle", cover = false } = state;
const children = [];
// 1. 遮罩层(如果需要)
if (cover) {
children.push(this.createOverlayVNode());
}
// 2. Toast 主体
children.push(this.createToastMainVNode(state));
return h("div", { style: { pointerEvents: "none" } }, children);
}
private createOverlayVNode(): VNode {
const overlayStyle = {
position: "fixed" as const,
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "transparent",
pointerEvents: "auto" as const,
zIndex: 99,
};
return h("div", { style: overlayStyle });
}
private createToastMainVNode(state: ToastOptions): VNode {
const { msg, iconName, position = "middle" } = state;
// Toast 容器样式(完全模仿 wd-toast 的 transitionStyle)
const containerStyle = {
zIndex: 100,
position: "fixed" as const,
top: "50%",
left: 0,
width: "100%",
transform: "translate(0, -50%)",
textAlign: "center" as const,
pointerEvents: "none" as const,
};
// 判断 Toast 类型(完全按照 wd-toast 的逻辑)
const hasIcon = iconName && (iconName !== "loading" || msg);
const isLoadingOnly = iconName === "loading" && !msg;
// Toast 内容样式(高度还原 wd-toast 的样式)
const toastStyle = {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "row" as const,
padding: isLoadingOnly ? "15px" : hasIcon ? "12px 16px" : "10px 16px",
backgroundColor: "rgba(0, 0, 0, 0.8)",
color: "#fff",
borderRadius: "8px",
fontSize: "14px",
lineHeight: "20px",
fontWeight: "400",
maxWidth: isLoadingOnly ? "none" : "70%",
wordWrap: "break-word" as const,
boxSizing: "border-box" as const,
minHeight: isLoadingOnly ? "48px" : "auto",
minWidth: isLoadingOnly ? "48px" : "auto",
whiteSpace: "pre-wrap" as const,
textAlign: "left" as const,
// 添加阴影效果
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)",
};
// 创建内容
const children = [];
// 图标
if (iconName) {
children.push(this.createIconVNode(iconName, !!msg));
}
// 文本
if (msg) {
children.push(this.createTextVNode(msg));
}
return h(
"div",
{ style: containerStyle },
h(
"div",
{
style: toastStyle,
},
children
)
);
}
private createIconVNode(iconName: string, hasText: boolean): VNode {
const baseIconStyle = {
display: "inline-block",
flexShrink: 0,
verticalAlign: "middle",
};
// 根据图标类型和是否有文本调整样式
const iconStyle = {
...baseIconStyle,
width: iconName === "loading" && !hasText ? "24px" : "20px",
height: iconName === "loading" && !hasText ? "24px" : "20px",
marginRight: hasText ? "8px" : "0",
};
if (iconName === "loading") {
return this.createLoadingVNode(iconStyle);
}
return this.createSvgIconVNode(iconName, iconStyle);
}
private createLoadingVNode(style: any): VNode {
// 添加 loading 动画的 CSS
this.addLoadingAnimation();
const loadingStyle = {
...style,
border: "2px solid rgba(255, 255, 255, 0.2)",
borderTop: "2px solid #4D80F0", // 使用 wot-design-uni 的主色调
borderRadius: "50%",
animation: "wd-toast-loading 1s linear infinite",
};
return h("div", {
style: loadingStyle,
});
}
private createSvgIconVNode(iconName: string, style: any): VNode {
// 使用与 wot-design-uni 相同的 SVG 图标
const svgMap: Record<string, string> = {
success: this.getSuccessSvg(),
error: this.getErrorSvg(),
warning: this.getWarningSvg(),
info: this.getInfoSvg(),
};
const svgContent = svgMap[iconName] || "";
return h("div", {
style: {
...style,
backgroundImage: `url("data:image/svg+xml;base64,${btoa(svgContent)}")`,
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
},
});
}
private createTextVNode(msg: string): VNode {
return h(
"span",
{
class: "wd-toast__msg",
style: {
wordBreak: "break-all" as const,
},
},
msg
);
}
private addLoadingAnimation() {
const styleId = "wd-toast-loading-animation";
if (document.getElementById(styleId)) return;
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
@keyframes wd-toast-loading {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
// SVG 图标(简化版,与 wot-design-uni 保持一致的视觉效果)
private getSuccessSvg(): string {
return `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="#34D19D"/>
<path d="M6 10l2.5 2.5L14 7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
}
private getErrorSvg(): string {
return `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="#FA4350"/>
<path d="M7 7l6 6M13 7l-6 6" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`;
}
private getWarningSvg(): string {
return `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="#F0883A"/>
<path d="M10 6v4M10 14h.01" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`;
}
private getInfoSvg(): string {
return `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="#909CB7"/>
<path d="M10 10v4M10 6h.01" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`;
}
// 公共方法
show(options: ToastOptions) {
this.toastState.value = {
...options,
show: true,
};
}
hide() {
this.toastState.value = { ...this.toastState.value, show: false };
}
success(msg: string, duration = 2000) {
this.show({
msg,
iconName: "success",
duration,
show: true,
});
}
error(msg: string, duration = 2000) {
this.show({
msg,
iconName: "error",
duration,
show: true,
});
}
warning(msg: string, duration = 2000) {
this.show({
msg,
iconName: "warning",
duration,
show: true,
});
}
info(msg: string, duration = 2000) {
this.show({
msg,
iconName: "info",
duration,
show: true,
});
}
loading(msg: string) {
this.show({
msg,
iconName: "loading",
duration: 0, // 不自动隐藏
show: true,
});
}
destroy() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
this.container = null;
this.vnode = null;
}
}
实例类
ts
import { ToastRenderer } from "./toast-renderer";
import { Toast } from "./toast-types";
/**
* 全局 Toast 实例
* 可在任何 TS 文件中直接使用,无需依赖 Vue 组件
*/
class GlobalToast implements Toast {
private renderer: ToastRenderer | null = null;
private getRenderer(): ToastRenderer {
if (!this.renderer) {
this.renderer = new ToastRenderer();
}
return this.renderer;
}
show(options: any) {
this.getRenderer().show(options);
}
success(msg: string, duration = 2000) {
this.getRenderer().success(msg, duration);
}
error(msg: string, duration = 2000) {
this.getRenderer().error(msg, duration);
}
warning(msg: string, duration = 2000) {
this.getRenderer().warning(msg, duration);
}
info(msg: string, duration = 2000) {
this.getRenderer().info(msg, duration);
}
loading(msg: string) {
this.getRenderer().loading(msg);
}
hide() {
this.getRenderer().hide();
}
destroy() {
if (this.renderer) {
this.renderer.destroy();
this.renderer = null;
}
}
}
// 导出全局实例
export const toast = new GlobalToast();
// 也可以导出类,支持创建多个实例
export { GlobalToast, ToastRenderer };
// 默认导出
export default toast;
使用示例
html
<template>
<view class="toast-test-page">
<view class="section">
<text class="section-title">Toast 测试</text>
<view class="button-group">
<wd-button @click="testSuccess" type="primary">成功提示</wd-button>
<wd-button @click="testError" type="error">错误提示</wd-button>
<wd-button @click="testWarning" type="warning">警告提示</wd-button>
<wd-button @click="testInfo">信息提示</wd-button>
<wd-button @click="testLoading" type="primary">加载提示</wd-button>
<wd-button @click="testLoadingOnly">仅加载图标</wd-button>
<wd-button @click="testLongText">长文本测试</wd-button>
<wd-button @click="testHide" plain>隐藏提示</wd-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import toast from "@/utils/toast";
import { useToast } from "wot-design-uni";
// 自定义 Toast 测试
const testSuccess = () => {
toast.success("操作成功!");
};
const testError = () => {
toast.error("操作失败,请重试");
};
const testWarning = () => {
toast.warning("请注意检查输入内容");
};
const testInfo = () => {
toast.info("这是一条信息提示");
};
const testLoading = () => {
toast.loading("加载中,请稍候...");
// 3秒后自动隐藏
// setTimeout(() => {
// toast.hide()
// toast.success('加载完成')
// }, 3000)
};
const testLoadingOnly = () => {
toast.loading("");
setTimeout(() => {
toast.hide();
}, 2000);
};
const testLongText = () => {
toast.info("这是一个很长很长的文本内容,用来测试 Toast 组件在显示长文本时的表现效果,看看是否能够正确换行和显示");
};
const testHide = () => {
toast.hide();
};
</script>
<style lang="scss" scoped>
.toast-test-page {
padding: 20px;
.section {
margin-bottom: 40px;
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.button-group {
display: flex;
flex-direction: column;
gap: 12px;
}
}
}
</style>