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>({}); // 生成完整的 localStorage 键名 const getStorageKey = useCallback((key: RedDotKey): string => { return `${prefix}${key}`; }, [prefix]); // 从 localStorage 加载红点状态 const loadRedDotState = useCallback(() => { try { const state: Record = {}; // 遍历 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];