Taro 封装小程序toast提示组件

tsx 复制代码
import { View, Text } from "@tarojs/components"
import Taro from "@tarojs/taro"
import { useEffect, useRef, useState } from "react"
import "./index.less"

type ToastIcon = "success" | "error" | "loading" | "none"

interface ToastOptions {
  /** 提示内容,无字数限制 */
  title: string
  /** 图标,默认 none */
  icon?: ToastIcon
  /** 显示时长(ms),loading 模式下不生效,默认 1500 */
  duration?: number
  /** 是否显示透明遮罩,防止触摸穿透,默认 false */
  mask?: boolean
}

interface ToastInstance {
  show: (options: ToastOptions | string) => void
  hide: () => void
}

// 当前激活的 Toast 实例(由挂载在页面中的 <Toast /> 注册)
let activeInstance: ToastInstance | null = null

const resolveOptions = (options: ToastOptions | string): Required<ToastOptions> => {
  const opts: ToastOptions = typeof options === "string" ? { title: options } : options
  return {
    title: opts.title ?? "",
    icon: opts.icon ?? "none",
    duration: opts.duration ?? 1500,
    mask: opts.mask ?? false,
  }
}

const show = (options: ToastOptions | string) => {
  if (!activeInstance) {
    // 未挂载 <Toast /> 时,回退到原生 showToast(仍有字数限制)
    const opts = resolveOptions(options)
    Taro.showToast({ title: opts.title, icon: opts.icon as any, duration: opts.duration, mask: opts.mask })
    return
  }
  activeInstance.show(options)
}

const hide = () => {
  activeInstance?.hide()
}

export interface ToastComponent extends React.FC {
  show: (options: ToastOptions | string) => void
  success: (title: string, duration?: number) => void
  error: (title: string, duration?: number) => void
  loading: (title: string, mask?: boolean) => void
  hide: () => void
}

const ToastInner: React.FC = () => {
  const [visible, setVisible] = useState(false)
  const [options, setOptions] = useState<Required<ToastOptions>>({
    title: "",
    icon: "none",
    duration: 1500,
    mask: false,
  })
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const clearTimer = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current)
      timerRef.current = null
    }
  }

  const doShow = (opts: ToastOptions | string) => {
    const finalOpts = resolveOptions(opts)
    clearTimer()
    setOptions(finalOpts)
    setVisible(true)
    // loading 模式不自动消失,需手动调用 hide()
    if (finalOpts.icon !== "loading") {
      timerRef.current = setTimeout(() => {
        setVisible(false)
      }, finalOpts.duration)
    }
  }

  const doHide = () => {
    clearTimer()
    setVisible(false)
  }

  useEffect(() => {
    activeInstance = { show: doShow, hide: doHide }
    return () => {
      clearTimer()
      if (activeInstance && activeInstance.show === doShow) {
        activeInstance = null
      }
    }
  }, [])

  if (!visible) return null

  const { icon, mask } = options
  const hasIcon = icon !== "none"

  return (
    <View className={`ec-toast ${mask ? "ec-toast--mask" : ""}`}>
      <View className="ec-toast__box">
        {hasIcon && (
          <View className="ec-toast__icon">
            {icon === "success" && <View className="ec-toast__success" />}
            {icon === "error" && (
              <View className="ec-toast__error">
                <View className="ec-toast__line ec-toast__line--1" />
                <View className="ec-toast__line ec-toast__line--2" />
              </View>
            )}
            {icon === "loading" && <View className="ec-toast__loading" />}
          </View>
        )}
        {options.title ? <Text className="ec-toast__title">{options.title}</Text> : null}
      </View>
    </View>
  )
}

// 将命令式 API 挂在默认导出的组件上
const ExportedToast = Object.assign(ToastInner, {
  show,
  success: (title: string, duration = 1500) => show({ title, icon: "success", duration }),
  error: (title: string, duration = 1500) => show({ title, icon: "error", duration }),
  loading: (title: string, mask = true) => show({ title, icon: "loading", mask, duration: 0 }),
  hide,
}) as ToastComponent

export default ExportedToast
less 复制代码
.ec-toast {
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  // 默认透明 + 点击穿透,行为贴近微信 showToast(mask:false)
  background: rgba(0, 0, 0, 0);
  pointer-events: none;

  // mask 模式:半透明遮罩 + 阻挡触摸
  &--mask {
    background: rgba(0, 0, 0, 0.35);
    pointer-events: auto;
  }

  &__box {
    min-width: 180rpx;
    max-width: 560rpx;
    padding: 32rpx 40rpx;
    background: rgba(0, 0, 0, 0.8);
    border-radius: 16rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    pointer-events: auto;
  }

  &__icon {
    width: 80rpx;
    height: 80rpx;
    margin-bottom: 16rpx;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  &__title {
    color: #fff;
    font-size: 30rpx;
    line-height: 1.5;
    text-align: center;
    word-break: break-all;
    white-space: pre-wrap;
  }

  // success 图标:圆 + 勾
  &__success {
    width: 80rpx;
    height: 80rpx;
    border-radius: 50%;
    background: #fff;
    position: relative;
    &::after {
      content: "";
      position: absolute;
      left: 26rpx;
      top: 14rpx;
      width: 22rpx;
      height: 40rpx;
      border-right: 4rpx solid #07c160;
      border-bottom: 4rpx solid #07c160;
      transform: rotate(45deg);
    }
  }

  // error 图标:圆 + 叉
  &__error {
    width: 80rpx;
    height: 80rpx;
    border-radius: 50%;
    background: #fff;
    position: relative;
  }
  &__line {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44rpx;
    height: 4rpx;
    background: #fa5151;
    border-radius: 2rpx;
    &--1 {
      transform: translate(-50%, -50%) rotate(45deg);
    }
    &--2 {
      transform: translate(-50%, -50%) rotate(-45deg);
    }
  }

  // loading 图标:旋转圆环
  &__loading {
    width: 64rpx;
    height: 64rpx;
    border: 6rpx solid rgba(255, 255, 255, 0.25);
    border-top-color: #fff;
    border-radius: 50%;
    animation: ec-toast-spin 0.8s linear infinite;
  }
}

@keyframes ec-toast-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
tsx 复制代码
import Toast from "@/components/Toast"

// 页面根挂载一次
<Toast />

// 任意位置命令式调用
Toast.show("请先选择一个SKU,这个提示文案可以很长很长不受字数限制")
Toast.success("提交成功")
Toast.error("网络异常")
Toast.loading("加载中...")
Toast.hide()
相关推荐
AlienZHOU8 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc11 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师12 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen13 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒13 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow14 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81814 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007814 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明14 小时前
第 6 章 组件进阶
前端·vue.js
江华森14 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端