Skip to content

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

属性类型默认值说明
childrenReactNode-主要消息内容
descriptionReactNode-描述文本(显示在主内容下方)
beforeReactNode-左侧元素(通常是图标)
afterReactNode-右侧元素(通常是按钮)
linkReactElement-链接组件
durationnumber4000自动关闭时间(毫秒)
onClose() => void-关闭回调(必需)
platform'ios' | 'base''base'平台样式
classNamestring-自定义类名
...propsHTMLAttributes<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. 退出动画

关闭流程:

  1. 设置 closing 状态为 true
  2. 添加 translateY(140%) 过渡(320ms)
  3. 等待动画完成后调用 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>
      )}
    </>
  );
}

最佳实践

  1. 单例模式: 同一时间只显示一个 Snackbar

    tsx
    const [currentSnackbar, setCurrentSnackbar] = useState<string | null>(null);
    
    // 关闭当前 Snackbar 再打开新的
    const showSnackbar = (id: string) => {
      setCurrentSnackbar(null);
      setTimeout(() => setCurrentSnackbar(id), 100);
    };
  2. 合理的持续时间:

    • 短消息: 2-3秒
    • 普通消息: 3-4秒
    • 重要消息或带操作: 4-6秒
    • 错误消息: 5-7秒
  3. 可访问性:

    tsx
    <Snackbar
      role="alert"
      aria-live="polite"
      aria-atomic="true"
      onClose={onClose}
    >
      消息内容
    </Snackbar>
  4. 避免干扰:

    • 不要用于关键信息(使用 Dialog 代替)
    • 不要频繁弹出
    • 提供明确的关闭方式
  5. 与 Toast 的区别:

    • Snackbar: 固定在底部,可包含操作按钮
    • Toast: 自动消失,仅用于通知

性能优化

  1. 条件渲染: 使用条件渲染而非 display: none

    tsx
    {open && <Snackbar />}  // ✅ 好
    <Snackbar style={{ display: open ? 'block' : 'none' }} />  // ❌ 不好
  2. 避免重复渲染: 使用状态管理库(Zustand/Jotai)集中管理

  3. 动画性能: 使用 transform 而非 top/bottom 实现动画

浏览器兼容性

  • ✅ 现代浏览器
  • ✅ 移动端 Safari/Chrome
  • ⚠️ 需要 backdrop-filter 支持(毛玻璃效果)
  • ✅ 降级到半透明背景(不支持时)

注意事项

  1. Z-index: Snackbar 自动渲染到 body,具有较高 z-index
  2. 动画时长: 退出动画固定为 320ms,不可配置
  3. 点击关闭: Snackbar 不支持点击关闭,只能等待自动关闭或调用 onClose
  4. 多个实例: 多个 Snackbar 会叠加显示,建议使用状态管理避免

基于 MIT 许可发布