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

260 lines
8.1 KiB
TypeScript
Raw Normal View History

2025-11-13 08:38:25 +00:00
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];