260 lines
8.1 KiB
TypeScript
260 lines
8.1 KiB
TypeScript
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];
|