iter(g320): 补配方 21 示范消息中心的角标与通知流
This commit is contained in:
@@ -467,6 +467,48 @@ card({ title: '开通向导' },
|
||||
- 末步提交前若还有异步规则(如邀请码查重),改用 formItem 的 `validateAsync` 聚合 await,校验期间禁用提交按钮;纯同步表单 `validate()` 已足够。
|
||||
- 提交成功给 `toast.success` 反馈并把 `step` 归零,向导可以立即重开;要保留数据就别归零,改跳一个只读的完成页。
|
||||
|
||||
## 配方 21:消息中心(铃铛角标 + 抽屉通知流)
|
||||
|
||||
```ts
|
||||
const open = signal(false); // 抽屉开关:铃铛与抽屉共用一份
|
||||
const notices = signal([ // 通知数据唯一真相:未读角标、列表、按钮禁用态都从它派生
|
||||
{ title: '审批通过', description: '报销单 RB-2048 已打款', time: Date.now() - 5 * 60_000, read: false },
|
||||
{ title: '库存预警', description: 'SKU-88 低于安全库存线', time: Date.now() - 26 * 3600_000, read: false },
|
||||
]);
|
||||
const unread = computed(() => notices().filter((n) => !n.read).length); // 未读数单一来源,不另立计数变量
|
||||
const markRead = (title) => notices(notices().map((n) => (n.title === title ? { ...n, read: true } : n))); // 点单条置已读:只写数据源
|
||||
const bellSlot = h('span'); // badge 的 count 是静态快照:未读数变化时重挂新实例
|
||||
effect(() => bellSlot.replaceChildren(badge({ count: unread() })));
|
||||
const bell = h('button', { class: 'ua-btn', onclick: () => open(true) }, '🔔 消息', bellSlot); // badge 不是按钮:点进中心的语义与键盘可达性交给外层 button
|
||||
const feed = h('div');
|
||||
effect(() => { // list 同样是静态快照组件:数据一变整个重建
|
||||
feed.replaceChildren(list({
|
||||
header: `未读 ${unread()}`,
|
||||
items: notices().map((n) => ({
|
||||
title: n.title,
|
||||
description: n.description,
|
||||
extra: h('span', { class: 'desc' }, relativeTime({ value: n.time, ariaLabel: '通知时间' }), n.read ? '' : badge({ label: '未读', variant: 'danger' })),
|
||||
})),
|
||||
onItemClick: (item) => markRead(item.title),
|
||||
}));
|
||||
});
|
||||
card({ title: '消息中心' },
|
||||
bell,
|
||||
drawer({ title: '消息中心', open, width: 380 },
|
||||
feed,
|
||||
button({ label: '全部已读', variant: 'ghost', disabled: computed(() => unread() === 0), onClick: () => notices(notices().map((n) => ({ ...n, read: true }))) }),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
要点:
|
||||
- 未读数只有一个来源:`unread` 从 `notices` 派生,铃铛角标、列表头、按钮禁用态都读它——任何一处「自己再记一份未读数」都会在已读回写时漂移。
|
||||
- `list` / `badge` 返回 DOM 而非句柄,是静态快照组件,不自己订信号:接信号统一用「effect + `replaceChildren` 重挂」这一招;句柄式组件(drawer 的 `open`、button 的 `disabled` 收 Signal)才直接传信号。
|
||||
- badge 不是按钮(无 tabindex 也无键盘响应):「点角标进消息中心」把手势交给外层 `button`,角标只负责报数——这正是 badge 手把手第 4 条的落点。
|
||||
- 已读回写只改数据源:`markRead` 与「全部已读」都写 `notices`,角标、列表、按钮态一处写处处更新,不要在回调里手动改 DOM。
|
||||
- `relativeTime` 默认每 30 秒自刷新,「3 分钟前」自己会走字;列表重建后新实例接着算,不用手动更新时间文案。条数极多时把 `list` 换 `virtualList`,并把 `refresh` 调大或传 0。
|
||||
- 通知条目要可操作时在 list 的 `onItemClick` 里跳详情或调 `notification`(右下角)补一条带按钮的强提醒;轻提示走 `toast`,别在同一屏堆两层浮层。
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 状态一律放信号(`signal` / `computed`),组件只读写信号,不持有业务数据副本。
|
||||
|
||||
Reference in New Issue
Block a user