| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- import { Node } from 'cc';
- export type RedpointCallback = (count: number, action: string) => void;
- export type RedpointListener = Node | RedpointCallback;
- interface ListenerRecord {
- listener: RedpointListener;
- target?: unknown;
- }
- /**
- * 红点数据及红点间聚合关系管理器。
- *
- * 未分组的红点数量由 update 直接设置;分组后的父红点数量始终等于
- * 所有直接子红点的数量之和,并会沿父级关系自动向上传播。
- */
- export class RedpointManager {
- /** update 写入的原始数量。父红点一旦分组,其显示数量不再使用该值。 */
- private readonly sourceCounts = new Map<string, number>();
- /** 对外可见的最终数量(包含分组汇总结果)。 */
- private readonly counts = new Map<string, number>();
- private readonly children = new Map<string, Set<string>>();
- private readonly parents = new Map<string, Set<string>>();
- private readonly listeners = new Map<string, Set<ListenerRecord>>();
- private readonly refreshQueue: string[] = [];
- private refreshing = false;
- /**
- * 更新某个红点的数量。
- * 负数及非有限数会按 0 处理;父红点的最终数量仍由其子红点决定。
- */
- update(action: string, count: number): void {
- const normalizedCount = Number.isFinite(count) ? Math.max(0, count) : 0;
- this.sourceCounts.set(action, normalizedCount);
- this.refresh(action);
- }
- /** 获取红点当前的最终数量。 */
- getCount(action: string): number {
- return this.counts.get(action) ?? 0;
- }
- /**
- * 绑定红点组关系。重复调用会在已有关系上追加(重复项会被忽略)。
- * main 的数量等于其所有 sub 的数量之和,支持多级分组。
- */
- group(main: string, ...sub: string[]): void {
- let mainChildren = this.children.get(main);
- let changed = false;
- for (const child of sub) {
- if (child === main || this.hasDescendant(child, main)) {
- console.warn(`[RedpointManager] ignored circular group relation: ${main} -> ${child}`);
- continue;
- }
- if (!mainChildren) {
- mainChildren = new Set<string>();
- this.children.set(main, mainChildren);
- }
- if (mainChildren.has(child)) continue;
- mainChildren.add(child);
- let childParents = this.parents.get(child);
- if (!childParents) {
- childParents = new Set<string>();
- this.parents.set(child, childParents);
- }
- childParents.add(main);
- changed = true;
- }
- if (changed) this.refresh(main);
- }
- /**
- * 添加红点监听。
- * - 绑定 Node 时,数量大于 0 显示,等于 0 隐藏。
- * - 绑定回调时,回调参数为 (count, action),并使用 target 作为 this。
- *
- * 监听添加后会立即同步一次当前状态;相同 listener + target 不会重复添加。
- */
- on(action: string, listener: Node, target?: unknown): void;
- on(action: string, listener: RedpointCallback, target?: unknown): void;
- on(action: string, listener: RedpointListener, target?: unknown): void {
- if (!(listener instanceof Node) && typeof listener !== 'function') {
- console.error(`[RedpointManager] ignored invalid listener: ${action}`);
- return;
- }
- if (listener instanceof Node && !listener.isValid) return;
- // Node 未显式传 owner 时以自身作为 target,便于 offTarget(node) 清理。
- const owner = arguments.length >= 3
- ? target
- : (listener instanceof Node ? listener : undefined);
- let bucket = this.listeners.get(action);
- if (!bucket) {
- bucket = new Set<ListenerRecord>();
- this.listeners.set(action, bucket);
- }
- for (const record of bucket) {
- if (record.listener === listener && record.target === owner) return;
- }
- const record: ListenerRecord = { listener, target: owner };
- bucket.add(record);
- this.dispatch(action, record, this.getCount(action));
- }
- /**
- * 移除监听。
- * - off(action):移除该红点的全部监听。
- * - off(action, listener):移除该 listener 在该红点上的全部监听。
- * - off(action, listener, target):只移除完全匹配的监听。
- * - off(action, undefined, target):移除 target 在该红点上的全部监听。
- * - off(target):移除该 target 在所有红点上的全部监听。
- */
- off(target: object): void;
- off(action: string, listener?: RedpointListener, target?: unknown): void;
- off(actionOrTarget: string | object, listener?: RedpointListener, target?: unknown): void {
- if (typeof actionOrTarget !== 'string') {
- this.offTarget(actionOrTarget);
- return;
- }
- const action = actionOrTarget;
- const bucket = this.listeners.get(action);
- if (!bucket) return;
- const filterByListener = listener !== undefined;
- const filterByTarget = arguments.length >= 3;
- if (!filterByListener && !filterByTarget) {
- this.listeners.delete(action);
- return;
- }
- for (const record of bucket) {
- if (filterByListener && record.listener !== listener) continue;
- if (filterByTarget && record.target !== target) continue;
- bucket.delete(record);
- }
- if (bucket.size === 0) this.listeners.delete(action);
- }
- /** 移除 on 时使用同一 target 添加的所有监听(跨所有 action)。 */
- offTarget(target: unknown): void {
- for (const [action, bucket] of this.listeners) {
- for (const record of bucket) {
- if (record.target === target) bucket.delete(record);
- }
- if (bucket.size === 0) this.listeners.delete(action);
- }
- }
- /** 与 Cocos EventTarget 命名保持兼容。 */
- targetOff(target: unknown): void {
- this.offTarget(target);
- }
- /** 清空所有数量、分组关系和监听。 */
- clear(): void {
- this.sourceCounts.clear();
- this.counts.clear();
- this.children.clear();
- this.parents.clear();
- this.listeners.clear();
- this.refreshQueue.length = 0;
- }
- /** 刷新 action 及其所有祖先;所有数量先稳定,再统一派发通知。 */
- private refresh(action: string): void {
- this.refreshQueue.push(action);
- if (this.refreshing) return;
- this.refreshing = true;
- try {
- while (this.refreshQueue.length > 0) {
- this.performRefresh(this.refreshQueue.shift()!);
- }
- } finally {
- this.refreshing = false;
- }
- }
- private performRefresh(action: string): void {
- const affected = this.collectAffected(action);
- const memo = new Map<string, number>();
- const changed: string[] = [];
- for (const current of affected) {
- const nextCount = this.resolveCount(current, memo, new Set<string>());
- const previousCount = this.counts.get(current) ?? 0;
- this.counts.set(current, nextCount);
- if (nextCount !== previousCount) changed.push(current);
- }
- for (const current of changed) {
- this.emit(current, this.counts.get(current) ?? 0);
- }
- }
- /** 按 action -> 直接父级 -> 更高父级的顺序收集,保证通知顺序可预期。 */
- private collectAffected(action: string): string[] {
- const result: string[] = [];
- const visited = new Set<string>();
- const pending = [action];
- while (pending.length > 0) {
- const current = pending.shift()!;
- if (visited.has(current)) continue;
- visited.add(current);
- result.push(current);
- for (const parent of this.parents.get(current) ?? []) pending.push(parent);
- }
- return result;
- }
- private resolveCount(action: string, memo: Map<string, number>, visiting: Set<string>): number {
- const cached = memo.get(action);
- if (cached !== undefined) return cached;
- const actionChildren = this.children.get(action);
- if (!actionChildren || actionChildren.size === 0) {
- const count = this.sourceCounts.get(action) ?? 0;
- memo.set(action, count);
- return count;
- }
- // group 已阻止循环;这里保留保护,避免异常数据导致无限递归。
- if (visiting.has(action)) return 0;
- visiting.add(action);
- let count = 0;
- for (const child of actionChildren) {
- count += this.resolveCount(child, memo, visiting);
- }
- visiting.delete(action);
- memo.set(action, count);
- return count;
- }
- private hasDescendant(action: string, expected: string): boolean {
- const visited = new Set<string>();
- const pending = [action];
- while (pending.length > 0) {
- const current = pending.pop()!;
- if (current === expected) return true;
- if (visited.has(current)) continue;
- visited.add(current);
- for (const child of this.children.get(current) ?? []) pending.push(child);
- }
- return false;
- }
- private emit(action: string, count: number): void {
- const bucket = this.listeners.get(action);
- if (!bucket) return;
- for (const record of [...bucket]) {
- if (!this.dispatch(action, record, count)) bucket.delete(record);
- }
- if (bucket.size === 0) this.listeners.delete(action);
- }
- /** 返回 false 表示监听对象已失效,应自动清理。 */
- private dispatch(action: string, record: ListenerRecord, count: number): boolean {
- if (record.listener instanceof Node) {
- if (!record.listener.isValid) return false;
- record.listener.active = count > 0;
- return true;
- }
- try {
- record.listener.call(record.target, count, action);
- } catch (error) {
- console.error(`[RedpointManager] listener failed: ${action}`, error, record);
- }
- return true;
- }
- }
|