根据 mac 地址屏蔽导航栏入口
This commit is contained in:
+28
-7
@@ -18,6 +18,7 @@ import SelectPage, { consumeSelectReturnScroll } from "./pages/SelectPage";
|
|||||||
import CommunityPage from "./pages/CommunityPage";
|
import CommunityPage from "./pages/CommunityPage";
|
||||||
import ChangelogPage from "./pages/ChangelogPage";
|
import ChangelogPage from "./pages/ChangelogPage";
|
||||||
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
||||||
|
import { NavConfigProvider, useNavConfig } from "./contexts/NavConfigContext";
|
||||||
import DesktopAIShell from "./components/DesktopAIShell";
|
import DesktopAIShell from "./components/DesktopAIShell";
|
||||||
import { appShellColumnClass, appShellOuterClass } from "./lib/appShell";
|
import { appShellColumnClass, appShellOuterClass } from "./lib/appShell";
|
||||||
|
|
||||||
@@ -74,6 +75,23 @@ function SelectScrollRestore() {
|
|||||||
return null;
|
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() {
|
function AppRouter() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -104,14 +122,17 @@ function App() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<AIDrawerProvider>
|
<AIDrawerProvider>
|
||||||
<SelectScrollRestore />
|
<NavConfigProvider>
|
||||||
<div className={appShellOuterClass}>
|
<SelectScrollRestore />
|
||||||
<div className={appShellColumnClass}>
|
<NavRouteGuard />
|
||||||
<DesktopAIShell>
|
<div className={appShellOuterClass}>
|
||||||
<AppRouter />
|
<div className={appShellColumnClass}>
|
||||||
</DesktopAIShell>
|
<DesktopAIShell>
|
||||||
|
<AppRouter />
|
||||||
|
</DesktopAIShell>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</NavConfigProvider>
|
||||||
</AIDrawerProvider>
|
</AIDrawerProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
- Inactive: muted icon + label
|
- Inactive: muted icon + label
|
||||||
============================================================ */
|
============================================================ */
|
||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
|
import { useNavConfig } from "@/contexts/NavConfigContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
|
import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
|
||||||
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
||||||
@@ -99,8 +100,15 @@ export default function BottomNav() {
|
|||||||
const isLgUp = useIsLgUp();
|
const isLgUp = useIsLgUp();
|
||||||
const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer();
|
const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer();
|
||||||
const { deviceState } = useDevice();
|
const { deviceState } = useDevice();
|
||||||
|
const { isNavHidden } = useNavConfig();
|
||||||
const festival = useFestival();
|
const festival = useFestival();
|
||||||
|
|
||||||
|
// 按 MAC 下发的隐藏配置过滤入口(fail-open:无配置时全部显示)
|
||||||
|
const navItems = useMemo(
|
||||||
|
() => NAV_ITEMS.filter((item) => !isNavHidden(item.labelKey as string)),
|
||||||
|
[isNavHidden],
|
||||||
|
);
|
||||||
|
|
||||||
const navText = useMemo((): BottomNavLocale => {
|
const navText = useMemo((): BottomNavLocale => {
|
||||||
const pack = resolveLocalePack(deviceState?.language);
|
const pack = resolveLocalePack(deviceState?.language);
|
||||||
return (pack.bottomNav ?? localeEn.bottomNav) as BottomNavLocale;
|
return (pack.bottomNav ?? localeEn.bottomNav) as BottomNavLocale;
|
||||||
@@ -122,7 +130,7 @@ export default function BottomNav() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-stretch justify-around">
|
<div className="flex items-stretch justify-around">
|
||||||
{NAV_ITEMS.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
item.path === "/ai"
|
item.path === "/ai"
|
||||||
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
? 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;
|
||||||
|
}
|
||||||
@@ -935,3 +935,22 @@ export async function deleteShareCode(
|
|||||||
if (!res.ok) throw new Error(`shareDelete HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`shareDelete HTTP ${res.status}`);
|
||||||
return (await res.json()) as ShareCodeDeleteResponse;
|
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