Snackbar 消息通知
移动端消息通知组件,从屏幕底部弹出,自动消失。
导入
ts
import { Snackbar } from '@xcloud/ui-mobile';示例
基础用法
tsx
import { useState } from 'react';
import { Snackbar } from '@xcloud/ui-mobile';
function Example() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>显示通知</button>
{open && (
<Snackbar onClose={() => setOpen(false)} duration={3000}>
消息已发送
</Snackbar>
)}
</>
);
}带图标
tsx
{open && (
<Snackbar
onClose={() => setOpen(false)}
before={<CheckIcon />}
>
操作成功
</Snackbar>
)}带操作按钮
tsx
{open && (
<Snackbar
onClose={() => setOpen(false)}
after={
<Snackbar.Button onClick={handleUndo}>
撤销
</Snackbar.Button>
}
>
文件已删除
</Snackbar>
)}长文本
tsx
{open && (
<Snackbar
onClose={() => setOpen(false)}
description="此操作无法撤销,请确认继续"
>
确认删除这个文件吗?
</Snackbar>
)}平台样式
tsx
{/* Android 样式 - 圆角10px */}
<Snackbar platform="base" onClose={onClose}>
Android 风格
</Snackbar>
{/* iOS 样式 - 圆角14px,更大底部间距 */}
<Snackbar platform="ios" onClose={onClose}>
iOS 风格
</Snackbar>API
Snackbar
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| children | ReactNode | - | 主要消息内容 |
| description | ReactNode | - | 描述文本(显示在主内容下方) |
| before | ReactNode | - | 左侧元素(通常是图标) |
| after | ReactNode | - | 右侧元素(通常是按钮) |
| link | ReactElement | - | 链接组件 |
| duration | number | 4000 | 自动关闭时间(毫秒) |
| onClose | () => void | - | 关闭回调(必需) |
| platform | 'ios' | 'base' | 'base' | 平台样式 |
| className | string | - | 自定义类名 |
| ...props | HTMLAttributes<HTMLDivElement> | - | 其他 div 属性 |
Snackbar.Button
操作按钮组件,已预设样式:
tsx
<Snackbar.Button onClick={handleClick}>
操作
</Snackbar.Button>技术实现
1. Portal 渲染
使用 RootRenderer 将 Snackbar 渲染到 document.body,确保:
- 不受父容器
overflow: hidden影响 - 始终显示在最上层
- 独立于组件树层级
2. 自动关闭
使用 useTimeout hook 实现自动关闭:
tsx
const closeTimeout = useTimeout(close, duration);
useEffect(() => closeTimeout.set(), [closeTimeout]);3. 退出动画
关闭流程:
- 设置
closing状态为true - 添加
translateY(140%)过渡(320ms) - 等待动画完成后调用
onClose
4. Telegram 主题
使用 CSS 变量适配 Telegram 主题:
--tgui--surface_dark: 背景颜色--tgui--toast_accent_color: 强调色(图标/按钮)
使用场景
1. 操作反馈
tsx
function DeleteButton({ onDelete }) {
const [showSnackbar, setShowSnackbar] = useState(false);
const handleDelete = async () => {
await onDelete();
setShowSnackbar(true);
};
return (
<>
<button onClick={handleDelete}>删除</button>
{showSnackbar && (
<Snackbar
onClose={() => setShowSnackbar(false)}
after={
<Snackbar.Button onClick={handleUndo}>撤销</Snackbar.Button>
}
>
已删除
</Snackbar>
)}
</>
);
}2. 网络状态
tsx
function NetworkStatus() {
const isOnline = useNetworkStatus();
return (
<>
{!isOnline && (
<Snackbar
onClose={() => {}}
duration={Infinity}
before={<WifiOffIcon />}
>
网络连接已断开
</Snackbar>
)}
</>
);
}3. 表单提交
tsx
function Form() {
const [submitStatus, setSubmitStatus] = useState<'success' | 'error' | null>(null);
const handleSubmit = async (data) => {
try {
await api.submit(data);
setSubmitStatus('success');
} catch (error) {
setSubmitStatus('error');
}
};
return (
<>
<form onSubmit={handleSubmit}>...</form>
{submitStatus === 'success' && (
<Snackbar
onClose={() => setSubmitStatus(null)}
before={<CheckIcon />}
>
提交成功
</Snackbar>
)}
{submitStatus === 'error' && (
<Snackbar
onClose={() => setSubmitStatus(null)}
before={<ErrorIcon />}
duration={5000}
>
提交失败,请重试
</Snackbar>
)}
</>
);
}最佳实践
单例模式: 同一时间只显示一个 Snackbar
tsxconst [currentSnackbar, setCurrentSnackbar] = useState<string | null>(null); // 关闭当前 Snackbar 再打开新的 const showSnackbar = (id: string) => { setCurrentSnackbar(null); setTimeout(() => setCurrentSnackbar(id), 100); };合理的持续时间:
- 短消息: 2-3秒
- 普通消息: 3-4秒
- 重要消息或带操作: 4-6秒
- 错误消息: 5-7秒
可访问性:
tsx<Snackbar role="alert" aria-live="polite" aria-atomic="true" onClose={onClose} > 消息内容 </Snackbar>避免干扰:
- 不要用于关键信息(使用 Dialog 代替)
- 不要频繁弹出
- 提供明确的关闭方式
与 Toast 的区别:
- Snackbar: 固定在底部,可包含操作按钮
- Toast: 自动消失,仅用于通知
性能优化
条件渲染: 使用条件渲染而非
display: nonetsx{open && <Snackbar />} // ✅ 好 <Snackbar style={{ display: open ? 'block' : 'none' }} /> // ❌ 不好避免重复渲染: 使用状态管理库(Zustand/Jotai)集中管理
动画性能: 使用
transform而非top/bottom实现动画
浏览器兼容性
- ✅ 现代浏览器
- ✅ 移动端 Safari/Chrome
- ⚠️ 需要
backdrop-filter支持(毛玻璃效果) - ✅ 降级到半透明背景(不支持时)
注意事项
- Z-index: Snackbar 自动渲染到 body,具有较高 z-index
- 动画时长: 退出动画固定为 320ms,不可配置
- 点击关闭: Snackbar 不支持点击关闭,只能等待自动关闭或调用
onClose - 多个实例: 多个 Snackbar 会叠加显示,建议使用状态管理避免