页面出错了
{error.message}
"use client"; /** * AppShell - MF Shell 布局容器 * * 职责(02-architecture-design.md §1.2): * 1. 左侧导航树(viewports GraphQL query,ARB-001 §1.2) * 2. 主内容区(children slot) * 3. 路由守卫(未登录跳转 /login) * 4. 用户信息 + 登出 * 5. 按 requiredPermission 过滤视口(F7 权限点) * * 数据来源(ARB-002 §2.4 ai13 执行项 #3): * - viewports: GraphQL ViewportsQuery(替代 REST /api/v1/teacher/viewports) * - user: GraphQL MeQuery(在 DashboardPage 已加载,AppShell 从 localStorage 兜底) * * 维护者:ai13(teacher-portal) */ import { useEffect, useMemo } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import { useQuery } from "urql"; import { ErrorBoundary, Loading } from "@edu/ui-components"; import { usePermission } from "@edu/hooks"; import { ViewportsQuery, MeQuery } from "@/lib/graphql"; import type { ViewportItem, User } from "@/lib/graphql"; import { getToken, getUser, logout } from "@/lib/auth"; /** * 权限上下文:从 localStorage 读取(登录时由 iam 返回并存储)。 * P3+ 改为 GraphQL me query 实时拉取(usePermission hook 接受 context props)。 */ function buildPermissionContext(user: User | null) { if (!user) return null; // localStorage UserInfo 包含 permissions 字段(iam /iam/permissions/effective 返回) const stored = getUser(); return { permissions: stored?.permissions ?? [], roles: user.roles, dataScope: user.dataScope, }; } export default function AppShell({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); // 路由守卫:未登录跳转 /login useEffect(() => { const token = getToken(); if (!token) { router.replace("/login"); } }, [router]); // GraphQL: viewports query(替代 REST /api/v1/teacher/viewports) const [viewportsResult] = useQuery({ query: ViewportsQuery, }); // GraphQL: me query(实时获取当前用户信息,替代 localStorage 读取) const [meResult] = useQuery({ query: MeQuery, }); const user = meResult.data?.me ?? null; const permissionContext = useMemo( () => buildPermissionContext(user ?? null), [user], ); const { hasPermission } = usePermission({ context: permissionContext }); // 按权限过滤视口(F7: requiredPermission 为 null 表示公开视口) const visibleViewports = useMemo(() => { const list: ViewportItem[] = viewportsResult.data?.viewports ?? []; return list.filter( (vp) => !vp.requiredPermission || hasPermission(vp.requiredPermission), ); }, [viewportsResult, hasPermission]); // 视口加载中:占位(避免导航闪烁) if (viewportsResult.fetching && visibleViewports.length === 0) { return (
导航加载失败:{viewportsResult.error.message}
) : null; return ({error.message}