Files
Edu/apps/portal-shell/src/shell/SlotRenderer.tsx
SpecialX 514e26ebb4 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
2026-07-15 08:06:09 +08:00

108 lines
3.0 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";
/**
* SlotRenderer - 按 Config 渲染插件列表v2.1 M8
*
* 给定一个 slot 名称,从配置中过滤出该 slot 的可见插件,按 sortOrder 排序,
* 查 Registry 取组件,注入 PluginProps经 PluginLoader 挂载(含 ErrorBoundary
*
* 关联portal-shell spec §2.2、§5.1
*/
import { useMemo, type ReactNode } from "react";
import { REGISTRY, isPluginRegistered } from "./Registry";
import { PluginLoader, PluginSkeleton } from "./PluginLoader";
import { parsePropsJson, parseSizeJson } from "./PropsMerger";
import type { PluginPlacement, PluginProps, Role } from "@/lib/types";
import type { AuthUser } from "@/providers/AuthProvider";
export interface SlotRendererProps {
slotName: string;
layoutId: string;
plugins: PluginPlacement[];
user: AuthUser;
role: Role;
userId: string;
}
export function SlotRenderer({
slotName,
layoutId,
plugins,
user,
role,
userId,
}: SlotRendererProps): ReactNode {
const visible = useMemo(
() =>
plugins
.filter((p) => p.slot === slotName && p.isVisible)
.sort((a, b) => a.sortOrder - b.sortOrder),
[plugins, slotName],
);
if (visible.length === 0) {
// 空 slotmain 区显示占位topbar/side 区不渲染
if (slotName === "top" || slotName === "side") return null;
return (
<div className="rounded-card border border-rule bg-surface p-md text-ink-muted text-small">
</div>
);
}
return (
<div className="space-y-md">
{visible.map((placement) => {
if (!isPluginRegistered(placement.pluginId)) {
return (
<div
key={placement.pluginId}
className="rounded-card border border-rule bg-surface p-md text-ink-muted text-small"
>
{placement.pluginId}
</div>
);
}
const manifest = REGISTRY[placement.pluginId];
if (!manifest) {
return null;
}
const pluginProps: PluginProps = {
instanceId: `${placement.pluginId}-${slotName}-${placement.sortOrder}`,
role,
user: {
id: userId,
name: user.name,
email: user.email,
dataScope: user.dataScope,
},
slot: {
name: slotName,
layoutId,
size: parseSizeJson(placement.sizeJson),
},
props: parsePropsJson(placement.propsJson),
};
return (
<PluginLoader
key={pluginProps.instanceId}
Component={manifest.Component}
pluginProps={pluginProps}
/>
);
})}
</div>
);
}
/** Slot 加载态占位layout 切换瞬间) */
export function SlotSkeleton({ count = 1 }: { count?: number }): ReactNode {
return (
<div className="space-y-md">
{Array.from({ length: count }).map((_, i) => (
<PluginSkeleton key={i} variant="card" />
))}
</div>
);
}