diff --git a/apps/portal-shell/src/app/shell/layout.tsx b/apps/portal-shell/src/app/shell/layout.tsx index 45a69f4..20dc772 100644 --- a/apps/portal-shell/src/app/shell/layout.tsx +++ b/apps/portal-shell/src/app/shell/layout.tsx @@ -1,12 +1,7 @@ import { headers } from "next/headers"; import type { Role } from "@edu/shared-ts/contracts"; -import { batchCheckRoutePermission } from "@/shared/lib/route-permissions"; -import { getNavigationItemsForRole } from "@/shared/lib/navigation"; -import { - AppSidebar, - type NavItem, -} from "@/shared/components/layout/app-sidebar"; +import { ShellSidebar } from "@/shared/components/layout/shell-sidebar"; import { SidebarProvider } from "@/shared/components/layout/sidebar-provider"; import { SiteHeader } from "@/shared/components/layout/site-header"; import { UserMenu } from "@/shared/components/layout/user-menu"; @@ -14,10 +9,18 @@ import { UserMenu } from "@/shared/components/layout/user-menu"; /** * AppFrame - 全站页面框架(RSC,ARCHITECTURE.md §7.2 / §10 P1-1) * - * 结构:SidebarProvider > AppSidebar + (SiteHeader + main) - * - 从 middleware 注入的请求头读取身份(fail-closed,§11.7 红线 #5) - * - batchCheckRoutePermission 按用户位图过滤导航项 - * - PREFIX 路由以 "/" 结尾,需同时检查 href 和 href+"/" + * 结构:SidebarProvider > ShellSidebar + (SiteHeader + main) + * + * fail-closed(P0-2,§11.7 红线 #5): + * - middleware 已保证到达此处的请求必带 x-user-id / x-user-role / x-user-permissions + * - 头缺失 = middleware 未运行(异常路径)→ 抛错触发 error.tsx,禁止默认 teacher + * + * RSC 边界(P1-1 修正): + * - 本 layout 是 RSC,仅向 Client Components 传可序列化数据(string) + * - 导航项过滤 + icon 函数引用由 ShellSidebar(Client Component)持有 + * - 不直接传 lucide icon 或 hasPermission 函数给 Client Components + * + * 关联:portal-shell ARCHITECTURE.md §7.2、§10 P1-1、§11.7 红线 #5 */ export default async function ShellLayout({ children, @@ -38,17 +41,10 @@ export default async function ShellLayout({ } const role = roleHeader as Role; - const roleItems = getNavigationItemsForRole(role); - // PREFIX 路由以 "/" 结尾,需同时检查 href 和 href+"/" - const pathsToCheck = roleItems.flatMap((i) => [i.href, `${i.href}/`]); - const permMap = batchCheckRoutePermission(pathsToCheck, permsBitmap, role); - const visibleItems: NavItem[] = roleItems - .filter((i) => permMap[i.href] === true || permMap[`${i.href}/`] === true) - .map((i) => ({ title: i.label, href: i.href, icon: i.icon })); return ( - true} /> +
} />
{children}
diff --git a/apps/portal-shell/src/shared/components/layout/shell-sidebar.tsx b/apps/portal-shell/src/shared/components/layout/shell-sidebar.tsx new file mode 100644 index 0000000..e675778 --- /dev/null +++ b/apps/portal-shell/src/shared/components/layout/shell-sidebar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import type { Role } from "@edu/shared-ts/contracts"; + +import { AppSidebar, type NavItem } from "./app-sidebar"; +import { getNavigationItemsForRole } from "@/shared/lib/navigation"; +import { batchCheckRoutePermission } from "@/shared/lib/route-permissions"; + +/** + * ShellSidebar - portal-shell 专用侧边栏容器(Client Component) + * + * 职责:在 Client 侧按 role + permsBitmap 过滤导航项,渲染 AppSidebar。 + * + * 为何是 Client Component: + * - navigation.ts 的 `icon` 字段是 lucide-react 组件(函数), + * 不能从 RSC 直接传给 Client Component(Next.js RSC 边界限制)。 + * - 将过滤逻辑放到 Client 侧,icon 函数引用不出 Client 边界。 + * + * 关联:ARCHITECTURE.md §7.2 AppFrame / §10 P1-1 + */ +export interface ShellSidebarProps { + role: Role; + permsBitmap: string; +} + +export function ShellSidebar({ + role, + permsBitmap, +}: ShellSidebarProps): React.ReactNode { + const roleItems = getNavigationItemsForRole(role); + // PREFIX 路由以 "/" 结尾,需同时检查 href 和 href+"/" + const pathsToCheck = roleItems.flatMap((i) => [i.href, `${i.href}/`]); + const permMap = batchCheckRoutePermission(pathsToCheck, permsBitmap, role); + const visibleItems: NavItem[] = roleItems + .filter((i) => permMap[i.href] === true || permMap[`${i.href}/`] === true) + .map((i) => ({ title: i.label, href: i.href, icon: i.icon })); + + // layout 已按权限过滤,AppSidebar 内部无需再次过滤 + return true} />; +}