Taro实现自定义showToast
Toast.tsx
import { document, TaroElement } from "@tarojs/runtime"
import { createElement, ReactNode, useEffect, useRef, useState } from "react"
import { mountPortal, unmountPortal } from "../utils"
import "./toast.css"
const toastSelectorSet = new Set<string>()
const DEFAULT_TOAST_ID = "#toast"
interface ToastOptions {
/**
* 提示的内容
*/
title: ReactNode;
/**
* 是否显示透明蒙层
*/
mask?: boolean;
/**
* 提示的延迟时间
*/
duration?: number;
/**
* 结束的回调函数
*/
onClose?: () => void;
[key: string]: any
}
interface ToastProps extends ToastOptions {
open?: boolean
id?: string
}
function Toast(props: ToastProps) {
const { open, id, duration = 1500, onClose, title, mask } = props
const [visible, setVisible] = useState(open)
const timer = useRef<NodeJS.Timeout | null>(null)
const isMountedRef = useRef(true)
useEffect(() => {
if (open) {
setVisible(true)
// 清理之前的定时器
if (timer.current) {
clearTimeout(timer.current)
}
timer.current = setTimeout(() => {
// 检查组件是否仍然挂载
if (isMountedRef.current) {
onClose && onClose()
setVisible(false)
}
}, duration)
}
return () => {
if (timer.current) {
clearTimeout(timer.current)
timer.current = null
}
}
}, [open, duration, onClose])
// 组件卸载时标记状态
useEffect(() => {
return () => {
isMountedRef.current = false
}
}, [])
if (!visible) return null
return (
<view className='toast-container' id={id} style={{ backgroundColor: mask ? 'rgba(0, 0, 0, 0.5)' : 'transparent' }}>
<view className='toast-content'>
{title}
</view>
</view>
)
}
/**
* 显示 Toast
* @param options
* @returns
*/
Toast.showToast = (options: ToastOptions) => {
const { onClose, ...rest } = options || {}
const toastId = DEFAULT_TOAST_ID
const hasExistingToast = toastSelectorSet.has(toastId)
if (hasExistingToast) return toastId
// 添加Toast标识
toastSelectorSet.add(toastId)
try {
const viewDom = document.createElement("view")
// 创建Toast组件实例
const toastElement = createElement(Toast, {
...rest,
open: true,
id: toastId,
onClose: () => {
onClose && onClose()
toastSelectorSet.delete(toastId)
unmountPortal(viewDom)
}
})
// 挂载Toast组件
mountPortal(toastElement, viewDom)
} catch (error) {
console.error('Toast.open执行出错:', error)
return null
}
}
/**
* 主动关闭 Toast
*/
Toast.hideToast = () => {
const toastId = DEFAULT_TOAST_ID
const hasExistingToast = toastSelectorSet.has(toastId)
if (!hasExistingToast) {
return
}
try {
// 查找Toast对应的父级DOM元素
const viewDom = document.getElementById(toastId)?.parentElement
if (viewDom) {
// 从集合中移除Toast标识
toastSelectorSet.delete(toastId)
// 卸载Toast组件
unmountPortal(viewDom)
} else {
console.warn("未找到Toast 父级DOM元素")
}
} catch (error) {
console.error('Toast.hideToast执行出错:', error)
}
}
// 检查 Toast 是否显示中
Toast.isShowToast = () => {
const toastId = DEFAULT_TOAST_ID
return toastSelectorSet.has(toastId)
}
export default Toasttoast.css
.toast-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 24px;
border-radius: 16px;
color: #fff;
font-size: 24px;
max-width: 80%;
text-align: center;
animation: toastFadeIn 0.3s linear;
}
@keyframes toastFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
utils.ts
import { getCurrentPages } from "@tarojs/taro"
import { render, unmountComponentAtNode } from "@tarojs/react"
import { document, TaroNode, type TaroElement } from "@tarojs/runtime"
import { ReactNode } from "react"
const portalViewMap: Map<string, TaroElement> = new Map()
const PORTAL_VIEW_ID = "portal_view"
// 滚动锁定相关变量
let scrollLockCount = 0
let originalBodyStyle = ''
/**
* 禁止背景滚动
*/
export const lockScroll = () => {
scrollLockCount++
if (scrollLockCount === 1) {
// 保存原始样式
const body = document.body
originalBodyStyle = body.style.cssText
// 设置禁止滚动样式
body.style.cssText = `
${originalBodyStyle}
overflow: hidden !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
`
}
}
/**
* 恢复背景滚动
*/
export const unlockScroll = () => {
scrollLockCount = Math.max(0, scrollLockCount - 1)
if (scrollLockCount === 0) {
const body = document.body
body.style.cssText = originalBodyStyle
}
}
export function getPagePath() {
const currentPages = getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const path = currentPage.$taroPath
return path
}
// 获取或创建 Portal 容器
function getPortalContainer(path: string): TaroElement {
if (!portalViewMap.has(path)) {
const portalView = document.createElement("view")
portalView.setAttribute("id", PORTAL_VIEW_ID)
const pageElement = document.getElementById(path)
if (pageElement) {
pageElement.appendChild(portalView)
portalViewMap.set(path, portalView)
} else {
console.error("cannot find page element")
}
}
return portalViewMap.get(path)!
}
// 挂载 Portal 内容
export function mountPortal(children: ReactNode, dom: TaroElement): TaroElement {
const path = getPagePath()
const portalContainer = getPortalContainer(path)
// 直接渲染内容到容器中
render(children, dom)
portalContainer.appendChild(dom)
return portalContainer
}
// 清理 Portal 内容
export function unmountPortal(dom: TaroNode) {
const path = getPagePath()
const pageElement = document.getElementById(path)
unmountComponentAtNode(dom)
if (pageElement) {
dom.parentElement?.removeChild(dom)
} else {
console.error("cannot find page element")
}
}
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果