Files
hunjing/frontend/src/app/(app)/layout.tsx
T
2026-08-21 16:00:13 +08:00

54 lines
1.8 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { useAuthStore } from "@/lib/stores/auth-store";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { AppShell } from "@/components/layout/AppShell";
import { LoadingState } from "@/components/shared/StateViews";
/**
* 主域壳层:日常对话 / 人物 / 人格 / 记忆 / 轨迹等路由共用 AppShell。
*
* 认证状态由 AuthInitializer(根布局)通过 BFF 初始化(user 首屏防闪用 localStorage),
* 此布局仅做路由守卫:未认证时重定向到 /login。
*/
export default function AppGroupLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const logout = useAuthStore((s) => s.logout);
const [ready, setReady] = useState(false);
// 等待 AuthInitializer 完成(给 loadFromBff 机会)
useEffect(() => {
const timer = setTimeout(() => setReady(true), 100);
return () => clearTimeout(timer);
}, []);
// 认证失败时登出并跳转登录页
const handleAuthError = useCallback(() => {
logout();
router.push("/login");
}, [logout, router]);
// 全局监听 ApiError "unauthorized"(通过自定义事件)
useEffect(() => {
const handler = () => handleAuthError();
window.addEventListener("auth:unauthorized", handler);
return () => window.removeEventListener("auth:unauthorized", handler);
}, [handleAuthError]);
useEffect(() => {
if (ready && !isAuthenticated) {
router.push("/login");
}
}, [ready, isAuthenticated, router]);
return (
<AppShell>
{ready && isAuthenticated ? children : <LoadingState label="辰光未启…" />}
</AppShell>
);
}