红点系统设计规范 v1
文档定位:本规范用于给后续系统/玩法/UI 的规划者作为前置参考—— 任何会"产生新内容""奖励可领""任务可推进""可购买/可使用""有新事件"的设计, 都必须同步考虑是否接入红点系统,以及接入哪个节点、按什么规则触发。
当前状态:核心引擎已实现(
src/redDot/),未接入任何业务模块。本文档的后半部分是各模块的接入 checklist,待对应模块开始改造时按表逐项落实。
0. 为什么先做独立引擎
游戏模块数量在增长(邮件 / 任务 / 活动 / 商城 / 员工 / 羁绊 …),每个模块各自实现"小红点"会快速产生以下问题:
- 样式不统一:圆点大小、颜色、动画、位置全凭各自发挥。
- 状态不同步:父级 Tab 上的红点和子页面内的红点互相不知道,逻辑到处复制。
- 触发时机混乱:什么时候 +1、什么时候清零,每个模块各写一套。
- 难以调试:出了问题找不到是哪个模块在改红点状态。
- 性能问题:每个组件自己
useState监听 store,触发频率不可控。
为此先做了一个与具体业务解耦的红点系统引擎,本文档即其设计契约。
1. 设计原则(不可妥协)
| 原则 | 含义 |
|------|------|
| 单一可信源 | 全游戏只一个 redDotStore,禁止再在模块内做平行 store。 |
| 路径即 ID | 每个红点用字符串路径定位(mail/reward),父子关系由 / 推导。 |
| 模式注册时定 | 节点是 dot / number / badge 在注册表声明,运行期可临时覆盖。 |
| 父子联动 | 子节点可见 → 父节点自动可见(无需手动同步)。 |
| 聚合可选 | 父节点是否聚合子节点数字,由 aggregate: true/false 显式声明。 |
| 订阅发布 | 组件用 useSyncExternalStore 订阅,零外部依赖。 |
| 批处理 | 多次变更必须 batch() 包裹,避免抖动。 |
| 零侵入 | 业务模块不感知组件细节,只调用 redDot.set/add/clear。 |
2. 全局架构
┌─────────────────────────────────────────────────────────────┐
│ 业务模块(mail / quest / event / shop / staff …) │
│ 调用 redDot.set / add / clear / setText │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ redDotStore(订阅发布 + 父子联动 + 派生计算) │
│ - 注册表(meta) 节点元数据(mode / aggregate) │
│ - 运行时状态(state) count / text / forcedVisible … │
│ - 派生缓存(derived) visible / mode / count / text │
└─────────────────────────────────────────────────────────────┘
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ React Hooks │ │ <RedDot> │ │ RedDotPanel │
│ useRedDot … │ │ 展示组件 │ │ 调试面板 │
└──────────────┘ └──────────────┘ └──────────────┘
3. 节点路径命名规则
3.1 格式
{module}/{feature}[/{sub}][/{item}]
- 用
/分隔层级,只允许字母、数字、:、-、_,禁止空格、emoji。 - 全部小写,与代码里 import 的路径常量保持完全一致。
- 叶子节点写具体子项;中间层级一般不主动 set,让系统从子节点联动。
3.2 路径常量集中管理
所有路径常量集中在 src/redDot/paths.ts,禁止在业务代码里散落字符串。
// paths.ts
export const RD_MAIL = {
root: 'mail',
system: 'mail/system',
reward: 'mail/reward',
} as const;
// 业务代码
import { RD_MAIL } from '@/redDot/paths';
redDot.add(RD_MAIL.reward, 1);
3.3 路径层级示例
| 路径 | 含义 | 通常的 mode |
|------|------|-------------|
| mail | 邮件 Tab(聚合子邮件数) | number + aggregate:true |
| mail/reward | 奖励邮件分类 | number |
| mail/reward:{mailId} | 单封奖励邮件 | dot |
| quest | 任务 Tab | number + aggregate:true |
| quest/daily | 每日任务分类 | number |
| staff/employee:{staffId} | 单个员工 | dot |
建议:能用三层就够用,最多四层。再深就重新设计结构。
4. 三种模式与适用场景
| 模式 | 视觉 | 适用场景 | 注意事项 |
|------|------|---------|---------|
| dot | 实心圆点 | "有未读""有新""可推进"等布尔型提示 | 不能表达数量;显示尺寸最小 |
| number | 胶囊数字 | "未读 5 封""3 个任务可领"等计数提示 | >99 显示 99+;父节点可选择聚合 |
| badge | 文字徽章 | "新""HOT""限时"等语义提示 | 文字内容 = state.text ?? meta.defaultText |
4.1 强制覆盖 vs 注册表默认
- 注册表里的
mode是默认值。 - 调用
redDot.setMode(path, mode)可运行期覆盖(用于"模式随时变化"的特殊节点)。 <RedDot mode="badge">可组件级覆盖(用于一次性临时调整,不修改 store)。
5. 聚合策略(aggregate)
父节点的 derived.count 是否包含子节点数:
// number + aggregate=true:父节点 = 自身 count + 所有子节点 count 之和
// 适用:邮件总数、任务总数等需要"汇总数字"的场景
{ path: 'mail', mode: 'number', aggregate: true }
// number + aggregate=false:父节点只看自身 count;子节点可见时父节点用 dot 提示
// 适用:父节点本身是分类标签,子节点是具体内容时
{ path: 'mail/reward', mode: 'number', aggregate: false }
// dot + aggregate=true:父节点本身 count 永远是 0,只是"子有则显"的开关
// 适用:Tab 角标、子模块提示
{ path: 'tournament', mode: 'dot', aggregate: true }
规划时的口诀:父节点是"计数容器"就
number + aggregate:true;父节点是"分类标签"就number + aggregate:false;父节点只用作"开关"就dot + aggregate:true。
6. 触发规则(什么时候 set / add / clear)
6.1 通用规则
| 事件 | 推荐 API |
|------|---------|
| 新内容产生(收到邮件 / 完成任务 / 收到奖励 / 限时活动开始) | redDot.add(path, 1) |
| 数量已计算好(拉取邮件列表后知道未读数) | redDot.set(path, count) |
| 用户已处理(读了邮件 / 领了奖 / 推进了任务) | redDot.add(path, -1) 或 redDot.set(path, 0) 或 redDot.clear(path) |
| 活动强提示(首次进入、限时开启) | redDot.setForcedVisible(path, true) + setText(path, '新') |
| 临时屏蔽(版本灰度、A/B 测试) | redDot.setForcedHidden(path, true) |
| 多个变更联动(一次结算多个任务奖励) | redDot.batch(() => { ... }) |
6.2 强制要求
- 必须在
redDot.batch(() => {...})内执行多个相关变更(如:任务完成时同时更新 quest/daily 和 task count),否则会触发多次重渲染。 - 严禁直接写
redDotStore.states.set(...)——只通过 API 调用。 - 必须使用
paths.ts中的常量,严禁在业务代码里手写字符串路径。
7. 节点清单(v1 默认注册表)
表中所有节点已在
src/redDot/registry.ts中预注册。下面是所有节点一览, 后续系统规划时可对照这张表,看新增的功能是否需要新增节点或复用现有节点。
7.1 顶层模块(侧边栏 Tab)
| 路径 | mode | aggregate | 触发场景 |
|------|------|-----------|---------|
| home | dot | false | 客栈总览(一般无红点) |
| jianghu | dot | true | 江湖见闻有可读内容 |
| tournament | number | true | 武林大会可挑战/可领 |
| auction | number | true | 官府竞标进行中/可领取 |
| innStatus | dot | true | 客栈员工待处理 |
| staff | number | true | 员工福利/培训 |
| bonds | dot | true | 羁绊关系变化 |
| quest | number | true | 任务可领/可推进 |
| mail | number | true | 邮件未读 |
| shop | dot | true | 商城有可购买内容 |
| event | badge | false | 活动入口(badge 文字) |
| achievement | number | true | 成就可领取 |
| baggage | dot | false | 背包有新物品 |
| settings | dot | false | 设置(一般无红点) |
7.2 邮件子节点
| 路径 | mode | 触发场景 |
|------|------|---------|
| mail/system | number | 系统通知邮件 |
| mail/battle | number | 战斗报告 |
| mail/reward | number | 奖励邮件 |
| mail/message | number | 私信 |
7.3 任务子节点
| 路径 | mode | 触发场景 |
|------|------|---------|
| quest/daily | number | 每日任务可领/可推进 |
| quest/main | dot | 主线任务可推进 |
| quest/side | number | 支线任务可领 |
| quest/achievement | number | 成就任务 |
7.4 活动子节点
| 路径 | mode | defaultText | 触发场景 |
|------|------|-------------|---------|
| event/limited | badge | 限时 | 限时活动开启 |
| event/signin | dot | — | 每日签到可领 |
| event/notice | badge | 公告 | 系统公告 |
7.5 商城子节点
| 路径 | mode | defaultText | 触发场景 |
|------|------|-------------|---------|
| shop/daily | badge | 惠 | 每日特惠刷新 |
| shop/monthly | dot | — | 月卡可领 |
| shop/gift | badge | 礼 | 礼包可领 |
7.6 员工子节点
| 路径 | mode | 触发场景 |
|------|------|---------|
| staff/welfare | number | 员工福利可领数 |
| staff/train | dot | 员工培训完成 |
7.7 未来扩展节点(规划中,未注册)
| 路径 | 模式 | 触发场景 | 接入模块 |
|------|------|---------|---------|
| auction/bid:{auctionId} | dot | 单个竞标可出价 | 官府竞标模块 |
| staff/employee:{staffId} | dot | 单个员工有新内容 | 员工模块 |
| bonds/character:{characterId} | dot | 单角色羁绊变化 | 羁绊模块 |
| achievement/category:{categoryId} | number | 单分类成就可领 | 成就模块 |
| event/limited:{eventId} | badge | 单个限时活动 | 活动模块 |
8. 各业务模块接入清单
每一行 = 一个模块的接入任务。所有"未接入"项在对应模块的玩法/UI 改造时必须同步处理。
8.1 邮件模块 ⏳ 未接入
- [ ] 在邮件拉取接口返回后
redDot.set(RD_MAIL.system, unreadCount)等 - [ ] 在收到新邮件事件中
redDot.add(RD_MAIL.system, 1) - [ ] 在已读邮件事件中
redDot.clear(RD_MAIL.system)(或按 id 减少) - [ ] 在侧边栏/导航栏挂
<RedDot path="mail" /> - [ ] 在邮件分类 Tab 挂
<RedDot path="mail/reward" />等
8.2 任务模块 ⏳ 未接入
- [ ] 任务推进:
redDot.add(RD_QUEST.main, 1) - [ ] 任务奖励可领:
redDot.set(RD_QUEST.daily, claimableCount) - [ ] 任务领奖:
redDot.add(RD_QUEST.daily, -1) - [ ] 成就解锁:
redDot.set(RD_QUEST.achievement, claimableCount)
8.3 活动模块 ⏳ 未接入
- [ ] 限时活动开启:
redDot.setText(RD_EVENT.limited, 'HOT')+setForcedVisible - [ ] 限时活动结束:
setForcedVisible(false)+setForcedHidden(true) - [ ] 签到刷新:
redDot.set(RD_EVENT.signin, 1) - [ ] 公告推送:
redDot.setForcedVisible(RD_EVENT.notice, true)
8.4 商城模块 ⏳ 未接入
- [ ] 每日特惠刷新:
redDot.set(RD_SHOP.daily, 1)+setText('惠') - [ ] 月卡可领:
redDot.set(RD_SHOP.monthly, 1) - [ ] 礼包可领:
redDot.setForcedVisible(RD_SHOP.gift, true)+setText('礼')
8.5 员工模块 ⏳ 未接入
- [ ] 福利可领:
redDot.set('staff/welfare', pendingCount) - [ ] 培训完成:
redDot.set('staff/train', finishedCount)
8.6 羁绊模块 ⏳ 未接入
- [ ] 关系值变化(达到新等级):
redDot.add('bonds', 1) - [ ] 单角色新剧情:
redDot.set('bonds/character:{id}', 1)(节点需动态注册)
8.7 官府竞标模块 ⏳ 未接入
- [ ] 新竞标可出价:
redDot.set('auction/bid:{id}', 1)(动态注册节点) - [ ] 竞标奖励可领:
redDot.add('auction', 1)
8.8 武林大会模块 ⏳ 未接入
- [ ] 新比赛可报名:
redDot.add('tournament', 1) - [ ] 比赛奖励可领:
redDot.add('tournament', 1)
8.9 成就模块 ⏳ 未接入
- [ ] 成就达成:
redDot.set('achievement', claimableCount) - [ ] 单分类成就可领:动态注册
achievement/category:{id}节点
8.10 客栈状态模块 ⏳ 未接入
- [ ] 员工待处理事项:
redDot.add('innStatus', 1)
9. 视觉规范
9.1 配色(已与客栈暗色木质调对齐)
| 元素 | 色值 | Tailwind 类 |
|------|------|-------------|
| dot 渐变 | 红 → 深红 | from-red-400 via-red-500 to-red-700 |
| number 渐变 | 红 → 深红 | from-red-400 via-red-500 to-red-700 |
| number 文字 | 白色 | text-white |
| badge 渐变 | 琥珀 → 深琥珀 | from-amber-400 via-amber-500 to-amber-700 |
| 描边(光晕) | 米色 | ring-[#f6e7c8]/90 |
| 阴影 | 红光晕 | shadow-[0_0_8px_rgba(239,68,68,0.6)] |
9.2 尺寸
| size | dot 直径 | number/badge 高 | min-width | 字号 |
|------|---------|----------------|-----------|------|
| sm | 8px | 14px | 14px | 9px |
| md | 10px | 18px | 18px | 11px |
| lg | 12px | 22px | 22px | 13px |
默认使用
md。小尺寸仅在边角空间紧张的场景(如头像角标)使用。
9.3 位置
默认 top-right(右上角)。其他选项:
top-right(右上)top-left(左上)bottom-right(右下)bottom-left(左下)
规划时:Tab 角标用
top-right;头像角标用top-right;按钮内文字提示一般用top-right,避免与文字重叠。
9.4 动画
- 首次出现:建议加
pulse动画 1-2 次,吸引注意。 - 常态显示:不加动画。
- 数量变化:建议配合 motion 库的
layout做数字翻牌动画(后续增强项)。
10. 性能与持久化
10.1 性能约束
- 订阅粒度:UI 组件只订阅自己关心的路径,禁止用
subscribeAll。 - 批处理:一次结算事件(领取多个奖励、刷新多个 Tab)必须
redDot.batch()。 - 派生缓存:
getDerived有缓存,单 path 内调用 O(1),但首次访问某 path 会触发整条祖先链重算。 - 调试面板订阅全局,生产 build 中不应挂载:
{import.meta.env.DEV && <RedDotPanel />}
10.2 持久化(后续接入)
redDotStore.export()序列化运行时状态redDotStore.import()恢复- 接入
gameState的 save/load 后,红点状态一并保存/恢复 - 不持久化注册表(注册表是代码常量)
11. 测试与调试
11.1 开发期调试面板
按 Ctrl+Shift+D 唤出/隐藏。功能:
- 树形展示所有注册节点
- 显示每个节点的 meta(mode、aggregate)和运行时 state(count、forced)
- 按钮:+1 / -1 / 清零 / 强制显示 / 强制隐藏 / 取消注册
- 底部统计:注册节点数 / 当前可见节点数
11.2 单元测试(待补充)
- store 层:set/add/clear 语义、父子联动、聚合计算
- hook 层:订阅正确性、引用稳定性
- 组件层:三种模式渲染、可见性切换
12. 新模块接入 Checklist(模板)
复制以下清单,填到对应模块的设计文档里。每完成一项打勾。
## 红点接入 — {模块名}
### 节点声明
- [ ] 在 `paths.ts` 加节点常量
- [ ] 在 `registry.ts` 登记节点元数据(mode / aggregate)
- [ ] 如有动态节点(按 ID),用 `registerRedDotNode` 在运行时注册
### 触发时机
- [ ] {事件 1} → `redDot.{api}({path}, {value})`
- [ ] {事件 2} → `redDot.{api}({path}, {value})`
- [ ] {事件 N} → `redDot.batch(() => { ... })`
### UI 接入
- [ ] 侧边栏 Tab:`<RedDot path="..." />`
- [ ] 页面内 Tab:`<RedDot path="..." anchor="..." />`
- [ ] 文字提示(可选):`<RedDot path="..." mode="badge" text="..." />`
### 测试
- [ ] 触发事件后红点出现
- [ ] 处理事件后红点消失
- [ ] 聚合计算正确(父节点数字 = 子节点之和)
- [ ] 调试面板里能看到节点状态变化
13. 后续路线图
- [ ] 邮件 / 任务 / 活动 / 商城 / 员工 / 羁绊 / 竞标 / 大会 模块接入
- [ ] 红点状态接入
gameState持久化 - [ ] 引导红点(首次进入强制显示,触发后清除)
- [ ] 数字翻牌动画(motion
layout) - [ ] 红点历史/统计面板(开发期分析用)
- [ ] 服务端推送的红点(如全服公告)合并到
event/notice
14. 相关文件索引
| 文件 | 职责 |
|------|------|
| src/redDot/types.ts | 类型定义 |
| src/redDot/paths.ts | 路径常量 |
| src/redDot/store.ts | 核心 store |
| src/redDot/registry.ts | 节点注册表 |
| src/redDot/hooks.ts | React Hook |
| src/redDot/RedDot.tsx | 展示组件 |
| src/redDot/RedDotPanel.tsx | 调试面板 |
| src/redDot/examples.ts | 演示数据 |
| src/redDot/index.ts | 对外导出 |
| src/redDot/README.md | API 参考手册 |
| src/App.tsx | 启动注册 + 调试面板挂载 |
最后修订:2026-06-29 v1(红点引擎初版 + 各模块接入 checklist)