---
title: "应用内通知 — zenit Zig UI 文档"
description: "用 Notifier 在窗口内弹出不打断当前任务的通知：成功、错误、进度、撤销、人员消息，堆叠、悬停展开、八个停靠位置都由它管理。"
url: https://zenit.z.express/zh/docs/advanced/notifications
language: zh-CN
alternate_en: https://zenit.z.express/docs/advanced/notifications.md
alternate_es: https://zenit.z.express/es/docs/advanced/notifications.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/notifications.md
alternate_ko: https://zenit.z.express/ko/docs/advanced/notifications.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/notifications.md
alternate_de: https://zenit.z.express/de/docs/advanced/notifications.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 应用内通知

用 Notifier 在窗口内弹出不打断当前任务的通知：成功、错误、进度、撤销、人员消息，堆叠、悬停展开、八个停靠位置都由它管理。

## Notifier 是什么

`ui.widgets.Notifier` 是一个窗口级服务：初始化一次，之后在任何地方调用 `show`。通知卡挂在窗口浮层 portal 的 toast 层（z 2000，高于对话框、低于 tooltip），不参与页面布局；每张卡有自己固定的节点，删除其中一张不会让其他卡片重建或重放入场动画。它取代了早期版本的 ToastManager。

[Video](https://zenit.z.express/media/stories/notification.mp4?v=5f8cadb85b)

真实 Storybook：依次弹出成功、错误、警告、进度、需要操作、人员消息、撤销、静默与汇总，再连发 4 条形成堆叠，悬停展开后关闭中间一条。

每种组件的逐项 API（`Notification` 字段、`NotifierOptions`、全部方法签名）见组件站的 [Notification 页面](https://zenit.z.express/zh/components/notification)。

## 创建 Notifier

在根挂载函数里创建一次，把指针交给需要发通知的代码（例如放进 `cx.bindState` 的应用状态）。Notifier 由传入的 Scope 持有，Scope 释放时一并移除。

`app.zig`

```zig
const notifier = try ui.widgets.Notifier.init(scope, cx, .{
    .position = .bottom_center,
    // Keep the stack clear of a 56px toolbar.
    .content_insets = .{ .top = 56 },
    .max_visible = 4,
});
```

| NotifierOptions 字段 | 作用 |
| --- | --- |
| `position` | 八个停靠位置之一，默认 .bottom\_center；运行时可用 setPosition 切换 |
| `max_visible` | 展开时最多显示的条数（2–6，默认 4） |
| `expand_on_hover` | false 时悬停只暂停计时、不展开 |
| `content_insets` | 相对窗口的内缩（标题栏、工具栏、状态栏），22px 停靠边距从这里起算 |
| `width` | 卡片宽度，默认 392 |
| `strings` | 界面文案（"刚刚"、"撤销"、"回复…"、折叠角标等），做本地化时替换 |
| `listener` | 用户操作与到期事件的回调，也可用 setListener 设置 |

## 弹出一条通知

`show` 接收一个 `ui.widgets.Notification` 值并返回它的 id；字符串会被 Notifier 复制持有，调用方的缓冲区可以立即复用。同屏超过 6 条时，最旧的一条会走正常的退场动画。

`notify.zig`

```zig
// Success: auto-dismisses after 3.6 s.
_ = try notifier.show(.{
    .tone = .success,
    .title = "Saved to iCloud",
    .body = "3 files synced · 2.1 MB",
});

// Error with two actions: errors stay until the user acts.
const failed = try notifier.show(.{
    .tone = .@"error",
    .title = "Upload failed",
    .body = "network timeout · dist/app.zip",
    .actions = &.{
        .{ .label = "Retry", .primary = true, .tag = "retry" },
        .{ .label = "View log", .tag = "log" },
    },
});

// Undo: a 7 s countdown ring and a default "Undo" button.
_ = try notifier.show(.{
    .kind = .undo,
    .title = "Moved 4 items to the Trash",
    .body = "You can undo this for 7 seconds.",
});

// A person's message with an inline reply box.
_ = try notifier.show(.{
    .tone = .violet,
    .avatar = "L",
    .title = "Lin",
    .body = "I finished the table widths in section three.",
    .reply = true,
});
```

> NOTE
> 
> **按钮最多两个。** 按钮位于分隔线下方、等宽排列，使用中性灰底；primary 只是更重的底色和字重，不会上语义色。

## 类型、色调与停留时长

外观由 `kind`（版式）和 `tone`（语义色）两个维度决定；行首图标默认从两者推导，也可以用 `lead` 或 `glyph` 覆盖。

| kind | 版式 |
| --- | --- |
| `.default` | 图标 + 标题 + 正文，可选按钮、回复框、头像 |
| `.progress` | 旋转指示器 + 确定进度条 + n / N 计数，不显示生命条 |
| `.undo` | 倒计时环（中心显示剩余秒数）+ 默认的「撤销」按钮 |

| tone | 用途 | 默认停留 |
| --- | --- | --- |
| `.success` | 操作完成 | 3.6 s |
| `.@"error"` | 失败，需要用户处理 | 常驻 |
| `.warning` | 需要注意但不阻塞 | 5.6 s |
| `.info` | 一般信息（默认） | 4.2 s |
| `.violet` | 人员、邀请等社交类消息 | 4.2 s；带按钮时常驻 |
| `.quiet` | 低打扰的状态提示（小圆点图标） | 2.8 s |

-   **自动常驻：**progress 卡、带回复框的卡、error 色调、带按钮的 violet 卡默认不会自动消失。
    
-   **显式覆盖：**`sticky = true` 强制常驻；`duration_ms` 覆盖默认时长；`life_bar` 控制底部生命条是否显示。
    
-   **undo 的时长**默认 7 秒，倒计时环与计时同步。
    

## 原地转换与进度

`update(id, …)` 改写同一张卡的类型、色调、标题与正文，并重置生命周期；卡片不销毁、不重建，也不重放入场。进度卡用 `setProgress` 逐帧推进，只更新进度层。用同一个 `id` 再次调用 `show` 也会走原地转换。

`upload.zig`

```zig
const id = try notifier.show(.{
    .kind = .progress,
    .title = "Uploading attachment",
    .body = "assets/hero-shot.png",
    .progress = .{ .value = 0, .label = "Uploading · about 5 s", .count = "0 / 5" },
});

// Each tick: only the progress layer is touched, the card isn't rebuilt.
notifier.setProgress(id, 0.6, "Uploading · about 2 s", "3 / 5");

// Done: the same card turns into a success card in place —
// no new entrance, and it now dismisses itself after 3.4 s.
try notifier.update(id, .{
    .tone = .success,
    .title = "Upload complete",
    .body = "assets/hero-shot.png",
    .duration_ms = 3400,
});
```

## 处理用户操作

Notifier 通过一个 `ui.widgets.NotificationListener` 回报事件。`NotificationEvent` 带有卡片 `id`、事件类型 `kind`，以及按钮的 `tag` / `index` 或回复内容 `text`。

| EventKind | 何时触发 |
| --- | --- |
| `.action` | 点击了按钮（用 tag 区分） |
| `.undo` | 点击了撤销；倒计时环已原地换成 ✓ |
| `.reply` | 回复框提交（Enter 或「发送」），text 为内容 |
| `.dismissed` | 用户关闭（✕ 或拖拽甩出） |
| `.expired` | 到期自动收起 |

`uploads.zig`

```zig
const Uploads = struct {
    fn onEvent(ctx: ?*anyopaque, n: *ui.widgets.Notifier, e: ui.widgets.NotificationEvent) void {
        const self: *Uploads = @ptrCast(@alignCast(ctx orelse return));
        switch (e.kind) {
            .action => if (std.mem.eql(u8, e.tag, "retry")) {
                // Turn the sticky error card into a progress card in place.
                n.update(e.id, .{
                    .kind = .progress,
                    .title = "Retrying upload",
                    .progress = .{ .value = 0, .label = "Uploading", .count = "0 / 5" },
                }) catch return;
                self.restart(e.id);
            } else n.dismiss(e.id),
            .reply => self.send(e.text),
            .undo => self.restoreTrash(),
            .dismissed, .expired => {},
        }
    }
    // …restart / send / restoreTrash
};

const uploads = try cx.bindState(Uploads, .{});
notifier.setListener(.{ .context = @ptrCast(uploads), .callback = Uploads.onEvent });
```

> TIP
> 
> **回复发送失败？** 调用 `notifier.replyFailed(id, title, body)` 把同一张卡原地换成失败状态，用户的输入不会丢。

## 堆叠与悬停

多条通知折叠成一叠：最新的一条离锚点最近、z 序最高，后面的每层露边 9px 并缩小 3.8%，最多露出三层，另有一个「还有 N 条 · 悬停展开」角标。指针进入整组外接矩形（外扩 18px）时展开，间距 9px，同时暂停所有计时；移开后折叠并继续计时。

STACK

数值来自 notification/model.zig 的 Spec：露边 9px、每层缩放 3.8%、展开间距 9px。

**全部清除**

角标旁有一个「全部清除」圆钮：第一下展开出文字，第二下才真正清除，移开即收回。代码里用 dismissAll()。

**拖拽甩出**

单张卡可以被拖出去关闭，会产生 dismissed 事件。

**窗口缩放**

窗口尺寸变化时堆叠刚性跟随锚点，不会出现补间拖尾。

## 停靠位置

`setPosition` 只改锚点、增长方向和入场位移方向，卡片不重建；正在退场的卡片沿新方向继续完成退场。

POSITIONS

[Video](https://zenit.z.express/media/stories/notification-positions.mp4?v=2df7ff87b6)

同一组 4 条通知依次停靠到八个位置；整窗画面，因为左侧停靠位贴着窗口左缘。

## 通知中心属于应用

历史记录、勿扰模式、铃铛入口这些「通知中心」能力是业务逻辑，不在 zenit 里：在你调用 show 的地方记录历史、决定是否弹出，再通过 Listener 得知关闭、到期与按钮操作。Storybook 里的「汇总」卡片就是这种模式——勿扰结束后用一条 quiet 通知概括期间收到的内容。

> WARNING
> 
> **无障碍尚未验收。** 和 zenit 其他组件一样，通知卡的 VoiceOver 验收还没有完成；不要把它作为唯一的关键信息通道。
