/** * GlobalSnackbar - 全局 Snackbar 消息提示组件 * * 提供可复用的 Toast 消息提示功能,支持两种使用方式: * 1. 作为受控组件使用:通过 props 控制显示状态 * 2. 通过 useSnackbarState Hook 使用:自动管理状态 * * @module GlobalSnackbar * @version 1.0.0 * * @example * ```tsx * // 方式一:受控组件 * setIsOpen(false)} * severity="success" * /> * * // 方式二:Hook 方式 * const { snackbarProps, showMessage } = useSnackbarState(); * showMessage('Hello!', { severity: 'info' }); * ``` */ import React, { JSX, useState } from 'react'; import { Snackbar, Alert, type SxProps, type Theme, alpha, Portal } from '@mui/material'; /** * Snackbar 消息严重程度类型 * @description 决定 Alert 组件的颜色和图标 * - success: 绿色,成功提示 * - info: 蓝色,信息提示 * - warning: 橙色,警告提示 * - error: 红色,错误提示 */ export type SnackbarSeverity = 'success' | 'info' | 'warning' | 'error'; /** * 重新导出 SnackbarProvider 组件 * @description 提供 Context 方式的全局 Snackbar 功能 */ export { SnackbarProvider } from './SnackbarProvider'; /** * GlobalSnackbar 组件的属性接口 * @interface GlobalSnackbarProps */ export interface GlobalSnackbarProps { /** 消息内容,要显示的提示文本 */ message: string; /** 是否显示 Snackbar */ open: boolean; /** 关闭回调函数 */ onClose: () => void; /** 消息级别,影响颜色和图标样式,默认 'info' */ severity?: SnackbarSeverity; /** 自动隐藏时间(毫秒),设为 0 则不自动关闭,默认 2000 */ autoHideDuration?: number; /** Snackbar 弹出位置,默认 { vertical: 'bottom', horizontal: 'center' } */ anchorOrigin?: { vertical: 'top' | 'bottom'; horizontal: 'left' | 'center' | 'right'; }; /** 是否使用 Alert 组件包裹,false 则使用原生 Snackbar message,默认 true */ showAlert?: boolean; /** 是否隐藏 Alert 图标,默认 false */ hideIcon?: boolean; /** 自定义样式,透传给外层 Snackbar 组件 */ sx?: SxProps; /** 自定义样式,透传给内层 Alert 组件(仅 showAlert=true 时生效) */ alertSx?: SxProps; } /** * showMessage 方法的选项配置 * @interface SnackbarOptions */ export interface SnackbarOptions { /** 消息级别:success | info | warning | error */ severity?: SnackbarSeverity; /** 自动隐藏时间(毫秒),设为 0 则不自动关闭 */ autoHideDuration?: number; /** 是否隐藏 Alert 图标 */ hideIcon?: boolean; /** 是否使用 Alert 组件包裹 */ showAlert?: boolean; } /** * useSnackbarState Hook 的返回值类型 * @interface UseSnackbarStateResult */ export interface UseSnackbarStateResult { /** 传递给 GlobalSnackbar 组件的属性对象 */ snackbarProps: GlobalSnackbarProps; /** 显示消息的方法 */ showMessage: (message: string, options?: SnackbarOptions) => void; /** 关闭消息的方法 */ closeMessage: () => void; } /** * GlobalSnackbar 组件的默认属性配置 * @description 提供类型安全的默认值选择 */ const defaultProps: Required< Pick< GlobalSnackbarProps, 'severity' | 'autoHideDuration' | 'anchorOrigin' | 'showAlert' | 'hideIcon' > > = { severity: 'info', autoHideDuration: 2000, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, showAlert: true, hideIcon: false, }; /** * GlobalSnackbar 组件 * * 全局消息提示的展示组件,支持受控和非受控两种使用模式。 * 使用 MUI Snackbar 和 Alert 组件实现消息提示功能。 * * @param {GlobalSnackbarProps} props - 组件属性 * @returns {JSX.Element} * * @remarks * - 使用 Portal 组件将 Snackbar 渲染到 body 末尾,避免 z-index 问题 * - 默认位置在屏幕底部居中 * - 自动设置高 z-index 确保显示在其他内容之上 * * @example * ```tsx * // 受控模式 * const [open, setOpen] = useState(false); * setOpen(false)} * severity="success" * /> * ``` */ export function GlobalSnackbar({ message, open, onClose, severity = defaultProps.severity, autoHideDuration = defaultProps.autoHideDuration, showAlert = defaultProps.showAlert, hideIcon = defaultProps.hideIcon, }: GlobalSnackbarProps): JSX.Element { /** * 使用 Portal 将 Snackbar 传送到 DOM 顶层 (body 标签下) * * @description * Portal 的优势: * - 避免父容器 overflow、z-index 等样式影响 * - 确保 Snackbar 始终显示在最顶层 * - 避免与其他组件的样式冲突 */ return ( {showAlert ? ( `0 12px 32px ${alpha(theme.palette[severity].main, 0.35)}`, // 图标样式:白色、稍大 '& .MuiAlert-icon': { mr: 0.5, fontSize: '1.1rem', color: '#fff' }, // 消息文字样式:白色、适当内边距 '& .MuiAlert-message': { color: '#fff', padding: '6px 0' }, }} > {message} ) : undefined} ); } /** * useSnackbarState - 消息提示的 Hook 方式 * * 提供状态管理的 Snackbar 功能,自动处理 open、message 等状态。 * 适合在组件内部使用,无需额外的状态管理代码。 * * @param {SnackbarOptions} [initialOptions] - 初始配置选项 * @returns {UseSnackbarStateResult} 包含 snackbarProps 和操作方法的对象 * * @description * - 自动管理 Snackbar 的显示/隐藏状态 * - 支持链式调用 showMessage * - 合并初始选项和调用时选项 * * @example * ```tsx * function MyComponent() { * const { snackbarProps, showMessage, closeMessage } = useSnackbarState({ * severity: 'info', * autoHideDuration: 3000, * }); * * const handleSave = () => { * // 业务逻辑... * showMessage('保存成功!', { severity: 'success' }); * }; * * return ( * <> * * * * ); * } * ``` */ export function useSnackbarState(initialOptions?: SnackbarOptions): UseSnackbarStateResult { // Snackbar 显示状态 const [open, setOpen] = useState(false); // 当前显示的消息内容 const [message, setMessage] = useState(''); // 消息配置选项 const [options, setOptions] = useState(initialOptions || {}); /** * 显示消息 * * @param {string} newMessage - 要显示的消息文本 * @param {SnackbarOptions} [newOptions={}] - 新的配置选项 * * @description * - 合并初始选项和新的调用选项 * - 新选项会覆盖初始选项 */ const showMessage = (newMessage: string, newOptions: SnackbarOptions = {}) => { setMessage(newMessage); setOptions({ ...initialOptions, ...newOptions }); setOpen(true); }; /** * 关闭消息 * * @description * - 直接将 open 状态设置为 false */ const closeMessage = () => { setOpen(false); }; /** * 处理 Snackbar 关闭事件 * * @param {React.SyntheticEvent | Event} [_event] - 关闭事件 * @param {string} [reason] - 关闭原因:timeout | clickaway | escapeKeyDown * * @description * - 忽略 clickaway 原因(用户点击其他区域),防止误关闭 * - 其他情况调用 closeMessage 关闭 */ const handleClose = (_event?: React.SyntheticEvent | Event, reason?: string) => { if (reason === 'clickaway') return; closeMessage(); }; /** * 传递给 GlobalSnackbar 组件的属性 * * @description * - 组合当前状态和选项为完整的组件 props * - onClose 使用 handleClose 包装后的版本 */ const snackbarProps: GlobalSnackbarProps = { message, open, onClose: handleClose, severity: options.severity, autoHideDuration: options.autoHideDuration, hideIcon: options.hideIcon, }; return { snackbarProps, showMessage, closeMessage, }; } /** * GlobalSnackbar 组件的默认导出 * @description 方便使用 `import GlobalSnackbar from './GlobalSnackbar'` 方式导入 */ export default GlobalSnackbar;