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()
相关推荐
用户250694921611 小时前
React项目打包为.next文件部署
前端
tryCbest1 小时前
Html设置网站图标
前端·html
半个落月1 小时前
从页面跳转到前端路由:手写一个简单的 HashRouter
前端
做前端的娜娜子1 小时前
移动端上拉加载与下拉刷新实现方案
前端·面试·掘金·金石计划
叱咤月海鱼鱼猫1 小时前
iframe 弹窗取消按钮触发父页面弹窗接口
前端
勾勾圈圈蛋蛋1 小时前
黑马Vue_day12(一):getters,怎么看 Apifox 文档,工程化等工具介绍;
前端
参宿71 小时前
像素vs条数级虚拟列表
前端
paopaokaka_luck2 小时前
基于springboot3+vue3的云南本土影视文旅推荐平台(协同过滤算法、Echarts图形化分析)
前端·spring boot·学习·算法·echarts·mybatis