№ · 活跃设计 CH.06.29
主页 · 📐 活跃设计 · 红点系统设计规范 v1

红点系统设计规范 v1

📅 2026-06-29 · 📦 17.9 KB · 📐 活跃设计 · 📝 MD 文档 🏷️ Buff🏷️ 路线图🏷️ 设计🏷️ 新手🏷️ 员工🏷️ 规范

文档定位:本规范用于给后续系统/玩法/UI 的规划者作为前置参考—— 任何会"产生新内容""奖励可领""任务可推进""可购买/可使用""有新事件"的设计, 都必须同步考虑是否接入红点系统,以及接入哪个节点、按什么规则触发。

当前状态:核心引擎已实现(src/redDot/),未接入任何业务模块。本文档的后半部分是各模块的接入 checklist,待对应模块开始改造时按表逐项落实。


0. 为什么先做独立引擎

游戏模块数量在增长(邮件 / 任务 / 活动 / 商城 / 员工 / 羁绊 …),每个模块各自实现"小红点"会快速产生以下问题:

  1. 样式不统一:圆点大小、颜色、动画、位置全凭各自发挥。
  2. 状态不同步:父级 Tab 上的红点和子页面内的红点互相不知道,逻辑到处复制。
  3. 触发时机混乱:什么时候 +1、什么时候清零,每个模块各写一套。
  4. 难以调试:出了问题找不到是哪个模块在改红点状态。
  5. 性能问题:每个组件自己 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)