129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
/* ============================================================
|
|
NAV CONFIG — 按设备 MAC 下发的导航入口隐藏配置
|
|
- 连接设备拿到 deviceState.mac 后请求 /audio/getNavConfig
|
|
- localStorage 缓存(30min):首屏/离线即时生效,后台再刷新
|
|
- fail-open:接口失败或无 mac 时隐藏集为空,默认全部显示,
|
|
绝不因后端异常导致导航不可用
|
|
============================================================ */
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { useDevice } from "@/contexts/DeviceContext";
|
|
import { getNavConfig } from "@/lib/luxsinApi";
|
|
|
|
/** nav key ↔ 路由 path 映射;隐藏入口时据此同步拦截路由。 */
|
|
export const NAV_KEY_TO_PATH: Record<string, string> = {
|
|
community: "/community",
|
|
hpEq: "/eq",
|
|
home: "/",
|
|
effects: "/effects",
|
|
myAi: "/ai",
|
|
};
|
|
|
|
const CACHE_PREFIX = "luxsin:navConfig:";
|
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
|
|
|
type NavConfigContextValue = {
|
|
/** 需要隐藏的 nav key 集合 */
|
|
hiddenKeys: Set<string>;
|
|
/** 某个 nav key 是否被隐藏 */
|
|
isNavHidden: (key: string) => boolean;
|
|
/** 某个路由 path 是否因入口隐藏而被拦截 */
|
|
isPathHidden: (path: string) => boolean;
|
|
};
|
|
|
|
const NavConfigContext = createContext<NavConfigContextValue | null>(null);
|
|
|
|
function cacheKey(mac: string): string {
|
|
return CACHE_PREFIX + mac.toUpperCase();
|
|
}
|
|
|
|
function readCache(mac: string): string[] | null {
|
|
try {
|
|
const raw = localStorage.getItem(cacheKey(mac));
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as { at?: number; hidden?: string[] };
|
|
if (!parsed || typeof parsed.at !== "number" || !Array.isArray(parsed.hidden)) {
|
|
return null;
|
|
}
|
|
if (Date.now() - parsed.at > CACHE_TTL_MS) {
|
|
localStorage.removeItem(cacheKey(mac));
|
|
return null;
|
|
}
|
|
return parsed.hidden;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeCache(mac: string, hidden: string[]): void {
|
|
try {
|
|
localStorage.setItem(cacheKey(mac), JSON.stringify({ at: Date.now(), hidden }));
|
|
} catch {
|
|
/* ignore — 隐私模式/配额不足时静默降级 */
|
|
}
|
|
}
|
|
|
|
export function NavConfigProvider({ children }: { children: ReactNode }) {
|
|
const { deviceState } = useDevice();
|
|
const mac = (deviceState?.mac ?? "").trim();
|
|
const [hidden, setHidden] = useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!mac) {
|
|
setHidden([]);
|
|
return;
|
|
}
|
|
// 先用本地缓存立即生效(避免首屏闪烁 / 离线可用),再后台刷新
|
|
const cached = readCache(mac);
|
|
if (cached) setHidden(cached);
|
|
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const res = await getNavConfig(mac);
|
|
const list = Array.isArray(res.data?.hidden) ? res.data!.hidden! : [];
|
|
if (!cancelled) {
|
|
setHidden(list);
|
|
writeCache(mac, list);
|
|
}
|
|
} catch {
|
|
// fail-open:保留缓存或空数组,默认全部显示
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [mac]);
|
|
|
|
const value = useMemo<NavConfigContextValue>(() => {
|
|
const keySet = new Set(hidden);
|
|
const pathSet = new Set(
|
|
hidden.map((k) => NAV_KEY_TO_PATH[k]).filter((p): p is string => !!p),
|
|
);
|
|
return {
|
|
hiddenKeys: keySet,
|
|
isNavHidden: (key: string) => keySet.has(key),
|
|
isPathHidden: (path: string) => pathSet.has(path),
|
|
};
|
|
}, [hidden]);
|
|
|
|
return (
|
|
<NavConfigContext.Provider value={value}>{children}</NavConfigContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useNavConfig(): NavConfigContextValue {
|
|
const ctx = useContext(NavConfigContext);
|
|
if (!ctx) {
|
|
throw new Error("useNavConfig must be used within NavConfigProvider");
|
|
}
|
|
return ctx;
|
|
}
|