Files
Edu/apps/portal-shell/src/shell/LayoutManager.tsx
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等
- Tailwind v4 + @theme inline,移除 tailwind.config.js
- React 19 use() + Suspense 流式渲染,首屏骨架秒出
- 三级错误边界:Route → Section → Widget 层层兜底
- 错误上报:useErrorReport → sendBeacon → /api/log mock 端点
- 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围
- 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99%
- notify 统一 Toast 封装,禁止业务直接 import sonner
- PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套)

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
2026-07-17 16:10:05 +08:00

153 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* LayoutManager - 5 种 Layout 模板渲染器v2.1 M8 + shadcn 令牌)
*
* | layoutId | 布局 | slots |
* | -------- | --------------------------------- | ---------------------------------- |
* | classic | TopBar + SideNav + Main | top / side / main |
* | focus | TopBar + 全宽 Main | top / main |
* | split | TopBar + 左右等分 Main | top / main-left / main-right |
* | triple | TopBar + SideNav + Main + RightRail | top / side / main / right |
* | canvas | TopBar + 自由摆放 | top / canvas-grid |
*
* 令牌迁移v2.0 shadcn 标准令牌
* - bg-paper → bg-background页面底色
* - bg-surface → bg-card容器表面
* - border-rule → border默认边框色
* - p-md/p-lg → p-4/p-6间距阶梯
* - gap-md → gap-4
*
* 关联portal-shell spec §4.1、§4.2、README v2.0 §3.5 shadcn 令牌
*/
import { type ReactNode } from "react";
import { SlotRenderer, type SlotRendererProps } from "./SlotRenderer";
type SlotRendererInput = Omit<SlotRendererProps, "slotName" | "layoutId">;
export interface LayoutManagerProps extends SlotRendererInput {
layoutId: string;
}
export function LayoutManager(props: LayoutManagerProps): ReactNode {
const { layoutId, ...slotInput } = props;
switch (layoutId) {
case "focus":
return <FocusLayout slotInput={slotInput} layoutId={layoutId} />;
case "split":
return <SplitLayout slotInput={slotInput} layoutId={layoutId} />;
case "triple":
return <TripleLayout slotInput={slotInput} layoutId={layoutId} />;
case "canvas":
return <CanvasLayout slotInput={slotInput} layoutId={layoutId} />;
case "classic":
default:
return <ClassicLayout slotInput={slotInput} layoutId={layoutId} />;
}
}
interface LayoutShellProps {
slotInput: SlotRendererInput;
layoutId: string;
}
/** classicTopBar + SideNav + Main */
function ClassicLayout({ slotInput, layoutId }: LayoutShellProps): ReactNode {
return (
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b bg-card">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1">
<aside className="w-64 border-r bg-card p-4">
<SlotRenderer slotName="side" layoutId={layoutId} {...slotInput} />
</aside>
<main className="flex-1 p-6">
<SlotRenderer slotName="main" layoutId={layoutId} {...slotInput} />
</main>
</div>
</div>
);
}
/** focusTopBar + 全宽 Main */
function FocusLayout({ slotInput, layoutId }: LayoutShellProps): ReactNode {
return (
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b bg-card">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<main className="flex-1 p-6">
<SlotRenderer slotName="main" layoutId={layoutId} {...slotInput} />
</main>
</div>
);
}
/** splitTopBar + 左右等分 Main */
function SplitLayout({ slotInput, layoutId }: LayoutShellProps): ReactNode {
return (
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b bg-card">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1 gap-4 p-6">
<section className="flex-1">
<SlotRenderer
slotName="main-left"
layoutId={layoutId}
{...slotInput}
/>
</section>
<section className="flex-1">
<SlotRenderer
slotName="main-right"
layoutId={layoutId}
{...slotInput}
/>
</section>
</div>
</div>
);
}
/** tripleTopBar + SideNav + Main + RightRail */
function TripleLayout({ slotInput, layoutId }: LayoutShellProps): ReactNode {
return (
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b bg-card">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1">
<aside className="w-64 border-r bg-card p-4">
<SlotRenderer slotName="side" layoutId={layoutId} {...slotInput} />
</aside>
<main className="flex-1 p-6">
<SlotRenderer slotName="main" layoutId={layoutId} {...slotInput} />
</main>
<aside className="w-72 border-l bg-card p-4">
<SlotRenderer slotName="right" layoutId={layoutId} {...slotInput} />
</aside>
</div>
</div>
);
}
/** canvasTopBar + 自由摆放 gridMVP 按 grid 排列,不实现拖拽) */
function CanvasLayout({ slotInput, layoutId }: LayoutShellProps): ReactNode {
return (
<div className="flex min-h-screen flex-col bg-background">
<header className="border-b bg-card">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<main className="flex-1 p-6">
<SlotRenderer
slotName="canvas-grid"
layoutId={layoutId}
{...slotInput}
/>
</main>
</div>
);
}