feat(portal-shell): implement portal-shell with apollo-router integration

M8: portal-shell unified frontend shell (Modular Monolith + micro-kernel).

- Apollo Client -> apollo-router (port 4010, RSC prefetch)

- 5 layouts: classic/focus/split/triple/canvas

- Registry + PluginLoader (dynamic import ssr:false)

- 3-layer props merge, Zustand PluginStore

- 4 widgets: grades/notification-bell/user-menu/class-selector

- config-service: new pluginConfig GraphQL resolver

- apollo-router: CORS + header propagation for portal-shell

- docker-compose.yml: portal-shell service block
This commit is contained in:
SpecialX
2026-07-15 08:06:09 +08:00
parent 47e950c664
commit 514e26ebb4
49 changed files with 2802 additions and 6 deletions

View File

@@ -0,0 +1,145 @@
"use client";
/**
* LayoutManager - 5 种 Layout 模板渲染器v2.1 M8
*
* | 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 |
*
* 关联portal-shell spec §4.1、§4.2
*/
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-paper">
<header className="border-b border-rule bg-surface">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1">
<aside className="w-64 border-r border-rule bg-surface p-md">
<SlotRenderer slotName="side" layoutId={layoutId} {...slotInput} />
</aside>
<main className="flex-1 p-lg">
<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-paper">
<header className="border-b border-rule bg-surface">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<main className="flex-1 p-lg">
<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-paper">
<header className="border-b border-rule bg-surface">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1 gap-md p-lg">
<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-paper">
<header className="border-b border-rule bg-surface">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<div className="flex flex-1">
<aside className="w-64 border-r border-rule bg-surface p-md">
<SlotRenderer slotName="side" layoutId={layoutId} {...slotInput} />
</aside>
<main className="flex-1 p-lg">
<SlotRenderer slotName="main" layoutId={layoutId} {...slotInput} />
</main>
<aside className="w-72 border-l border-rule bg-surface p-md">
<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-paper">
<header className="border-b border-rule bg-surface">
<SlotRenderer slotName="top" layoutId={layoutId} {...slotInput} />
</header>
<main className="flex-1 p-lg">
<SlotRenderer
slotName="canvas-grid"
layoutId={layoutId}
{...slotInput}
/>
</main>
</div>
);
}