根据 mac 地址屏蔽导航栏入口
This commit is contained in:
+28
-7
@@ -20,6 +20,7 @@ import SelectPage, { consumeSelectReturnScroll } from "./pages/SelectPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
import ChangelogPage from "./pages/ChangelogPage";
|
||||
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
||||
import { NavConfigProvider, useNavConfig } from "./contexts/NavConfigContext";
|
||||
import DesktopAIShell from "./components/DesktopAIShell";
|
||||
import { appShellColumnClass, appShellOuterClass } from "./lib/appShell";
|
||||
|
||||
@@ -63,6 +64,23 @@ function SelectScrollRestore() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* NavRouteGuard — 当某个导航入口被 MAC 隐藏配置屏蔽时,
|
||||
* 拦截对应路由(如直接访问 /ai),重定向到 /404。
|
||||
* 首页 "/" 永不拦截,避免锁死。
|
||||
*/
|
||||
function NavRouteGuard() {
|
||||
const [location, setLocation] = useLocation();
|
||||
const { isPathHidden } = useNavConfig();
|
||||
useEffect(() => {
|
||||
if (location === "/") return;
|
||||
if (isPathHidden(location)) {
|
||||
setLocation("/404", { replace: true });
|
||||
}
|
||||
}, [location, isPathHidden, setLocation]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function AppRouter() {
|
||||
return (
|
||||
<Switch>
|
||||
@@ -94,14 +112,17 @@ function App() {
|
||||
<Toaster />
|
||||
<Router hook={useHashLocation}>
|
||||
<AIDrawerProvider>
|
||||
<SelectScrollRestore />
|
||||
<div className={appShellOuterClass}>
|
||||
<div className={appShellColumnClass}>
|
||||
<DesktopAIShell>
|
||||
<AppRouter />
|
||||
</DesktopAIShell>
|
||||
<NavConfigProvider>
|
||||
<SelectScrollRestore />
|
||||
<NavRouteGuard />
|
||||
<div className={appShellOuterClass}>
|
||||
<div className={appShellColumnClass}>
|
||||
<DesktopAIShell>
|
||||
<AppRouter />
|
||||
</DesktopAIShell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NavConfigProvider>
|
||||
</AIDrawerProvider>
|
||||
</Router>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- Inactive: muted icon + label
|
||||
============================================================ */
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import { useNavConfig } from "@/contexts/NavConfigContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
|
||||
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
||||
@@ -99,8 +100,15 @@ export default function BottomNav() {
|
||||
const isLgUp = useIsLgUp();
|
||||
const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer();
|
||||
const { deviceState } = useDevice();
|
||||
const { isNavHidden } = useNavConfig();
|
||||
const festival = useFestival();
|
||||
|
||||
// 按 MAC 下发的隐藏配置过滤入口(fail-open:无配置时全部显示)
|
||||
const navItems = useMemo(
|
||||
() => NAV_ITEMS.filter((item) => !isNavHidden(item.labelKey as string)),
|
||||
[isNavHidden],
|
||||
);
|
||||
|
||||
const navText = useMemo((): BottomNavLocale => {
|
||||
const pack = resolveLocalePack(deviceState?.language);
|
||||
return (pack.bottomNav ?? localeEn.bottomNav) as BottomNavLocale;
|
||||
@@ -122,7 +130,7 @@ export default function BottomNav() {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-stretch justify-around">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
{navItems.map((item) => {
|
||||
const isActive =
|
||||
item.path === "/ai"
|
||||
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* ============================================================
|
||||
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;
|
||||
}
|
||||
@@ -1159,3 +1159,22 @@ export async function deleteShareCode(
|
||||
if (!res.ok) throw new Error(`shareDelete HTTP ${res.status}`);
|
||||
return (await res.json()) as ShareCodeDeleteResponse;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Nav Config API — getNavConfig(按 MAC 下发要隐藏的导航入口)
|
||||
// ============================================================
|
||||
|
||||
export interface NavConfigResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: { hidden?: string[] };
|
||||
}
|
||||
|
||||
/** GET `/audio/getNavConfig?mac=...` — 查询该 MAC 需要隐藏的导航入口(nav key)。未命中返回空数组。 */
|
||||
export async function getNavConfig(mac: string): Promise<NavConfigResponse> {
|
||||
const res = await fetch(
|
||||
buildLuxsinAudioUrl(`getNavConfig?mac=${encodeURIComponent(mac)}`),
|
||||
);
|
||||
if (!res.ok) throw new Error(`getNavConfig HTTP ${res.status}`);
|
||||
return (await res.json()) as NavConfigResponse;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user