crush-level-web/src/hooks/useRedDot.ts

260 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
/**
* 红点管理 Hook
* 功能:
* 1. 管理各种功能的红点显示状态
* 2. 使用 localStorage 持久化存储
* 3. 默认显示红点查看后消失localStorage 不存在记录时显示红点)
*/
type RedDotKey = string;
interface UseRedDotOptions {
/**
* localStorage 的前缀,用于避免键名冲突
*/
prefix?: string;
}
interface UseRedDotReturn {
/**
* 检查指定功能是否显示红点
*/
hasRedDot: (key: RedDotKey) => boolean;
/**
* 标记某个功能为已查看(消除红点)
*/
markAsViewed: (key: RedDotKey) => void;
/**
* 显示红点(设置新功能时使用)
*/
showRedDot: (key: RedDotKey) => void;
/**
* 批量标记多个功能为已查看
*/
markMultipleAsViewed: (keys: RedDotKey[]) => void;
/**
* 清除所有红点
*/
clearAllRedDots: () => void;
/**
* 获取所有有红点的功能键
*/
getAllRedDotKeys: () => RedDotKey[];
}
const DEFAULT_PREFIX = 'red_dot_';
export const useRedDot = (options: UseRedDotOptions = {}): UseRedDotReturn => {
const { prefix = DEFAULT_PREFIX } = options;
// 状态:存储当前所有红点的状态
const [redDotState, setRedDotState] = useState<Record<RedDotKey, boolean>>({});
// 生成完整的 localStorage 键名
const getStorageKey = useCallback((key: RedDotKey): string => {
return `${prefix}${key}`;
}, [prefix]);
// 从 localStorage 加载红点状态
const loadRedDotState = useCallback(() => {
try {
const state: Record<RedDotKey, boolean> = {};
// 遍历 localStorage 中所有以 prefix 开头的键
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
const redDotKey = key.replace(prefix, '');
const value = localStorage.getItem(key);
// 如果存在记录且值为 'viewed',则不显示红点(已查看)
state[redDotKey] = value !== 'viewed';
}
}
setRedDotState(state);
} catch (error) {
console.warn('Failed to load red dot state from localStorage:', error);
}
}, [prefix]);
// 保存红点状态到 localStorage
const saveRedDotState = useCallback((key: RedDotKey, viewed: boolean) => {
try {
const storageKey = getStorageKey(key);
if (viewed) {
// 标记为已查看
localStorage.setItem(storageKey, 'viewed');
} else {
// 移除记录,恢复默认显示红点状态
localStorage.removeItem(storageKey);
}
// 触发自定义事件,通知同页面内的其他组件实例
const event = new CustomEvent('redDotStateChange', {
detail: { key, viewed, prefix }
});
window.dispatchEvent(event);
} catch (error) {
console.warn('Failed to save red dot state to localStorage:', error);
}
}, [getStorageKey, prefix]);
// 初始化时加载状态
useEffect(() => {
loadRedDotState();
}, [loadRedDotState]);
// 监听状态变化,同步不同组件实例的状态
useEffect(() => {
// 处理同页面内的自定义事件
const handleRedDotStateChange = (e: CustomEvent) => {
const { key, viewed, prefix: eventPrefix } = e.detail;
// 只处理与当前前缀相关的变化
if (eventPrefix === prefix) {
setRedDotState(prev => {
const newState = { ...prev };
if (viewed) {
// 标记为已查看
newState[key] = false;
} else {
// 恢复默认显示红点
delete newState[key];
}
return newState;
});
}
};
// 处理跨标签页的 localStorage 变化
const handleStorageChange = (e: StorageEvent) => {
// 只处理与当前前缀相关的变化
if (e.key && e.key.startsWith(prefix)) {
const redDotKey = e.key.replace(prefix, '');
setRedDotState(prev => {
const newState = { ...prev };
if (e.newValue === 'viewed') {
// 标记为已查看
newState[redDotKey] = false;
} else if (e.newValue === null) {
// 移除了 localStorage 记录,恢复默认显示红点
delete newState[redDotKey];
}
return newState;
});
}
};
// 添加事件监听器
window.addEventListener('redDotStateChange', handleRedDotStateChange as EventListener);
window.addEventListener('storage', handleStorageChange);
// 清理函数
return () => {
window.removeEventListener('redDotStateChange', handleRedDotStateChange as EventListener);
window.removeEventListener('storage', handleStorageChange);
};
}, [prefix]);
// 检查是否显示红点
const hasRedDot = useCallback((key: RedDotKey): boolean => {
// 如果 redDotState 中没有这个 key说明 localStorage 中也没有记录,应该显示红点
// 如果 redDotState 中有这个 key 且为 true说明 localStorage 中没有 'viewed' 记录,应该显示红点
// 如果 redDotState 中有这个 key 且为 false说明 localStorage 中有 'viewed' 记录,不显示红点
return redDotState[key] !== false;
}, [redDotState]);
// 标记为已查看(消除红点)
const markAsViewed = useCallback((key: RedDotKey) => {
setRedDotState(prev => ({
...prev,
[key]: false // 设置为 false 表示已查看,不显示红点
}));
saveRedDotState(key, true); // true 表示已查看,在 localStorage 中存储 'viewed'
}, [saveRedDotState]);
// 显示红点(重置为未查看状态)
const showRedDot = useCallback((key: RedDotKey) => {
setRedDotState(prev => {
const newState = { ...prev };
delete newState[key]; // 删除状态,恢复默认显示红点
return newState;
});
saveRedDotState(key, false); // false 表示移除 localStorage 记录,恢复默认显示红点
}, [saveRedDotState]);
// 批量标记为已查看
const markMultipleAsViewed = useCallback((keys: RedDotKey[]) => {
setRedDotState(prev => {
const newState = { ...prev };
keys.forEach(key => {
newState[key] = false; // 设置为 false 表示已查看
saveRedDotState(key, true); // 在 localStorage 中标记为已查看
});
return newState;
});
}, [saveRedDotState]);
// 清除所有红点(重置为默认显示状态)
const clearAllRedDots = useCallback(() => {
try {
// 清除 localStorage 中的所有红点数据
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
// 清除状态,恢复默认显示红点
setRedDotState({});
} catch (error) {
console.warn('Failed to clear red dots from localStorage:', error);
}
}, [prefix]);
// 获取所有有红点的功能键
const getAllRedDotKeys = useCallback((): RedDotKey[] => {
// 返回所有应该显示红点的键
// 包括1. redDotState 中不存在的键(默认显示)
// 包括2. redDotState 中存在且值为 true 的键
// 由于我们现在用 hasRedDot 来判断,这里简化逻辑
const allPossibleKeys = Object.keys(RED_DOT_KEYS).map(key => RED_DOT_KEYS[key as keyof typeof RED_DOT_KEYS]);
return allPossibleKeys.filter(key => hasRedDot(key));
}, [hasRedDot]);
return {
hasRedDot,
markAsViewed,
showRedDot,
markMultipleAsViewed,
clearAllRedDots,
getAllRedDotKeys,
};
};
// 预定义的红点类型常量,方便使用
export const RED_DOT_KEYS = {
CHAT_BUBBLE: 'chat_bubble',
CHAT_BACKGROUND: 'chat_background',
CHAT_MODEL: 'chat_model',
PROFILE_SETTING: 'profile_setting',
// 可以根据需要添加更多
} as const;
export type RedDotKeyType = typeof RED_DOT_KEYS[keyof typeof RED_DOT_KEYS];