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:
117
apps/portal-shell/src/lib/config-fetcher.ts
Normal file
117
apps/portal-shell/src/lib/config-fetcher.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 配置获取(服务端 RSC 预取)
|
||||
*
|
||||
* 通过 apollo-router 查询 config-service 子图的 pluginConfig(userId, role),
|
||||
* 返回三层合并后的 PluginConfigResponse(M8 验收点:查询走 apollo-router)。
|
||||
*
|
||||
* 设计意图(portal-shell spec §5.5):
|
||||
* - portal-shell 是 Next.js 前端,不直接调 config-service gRPC
|
||||
* - 统一走 apollo-router GraphQL,由 Router 路由到 config-service 子图
|
||||
* - RSC 服务端预取消除 CSR 瀑布流,Config 随 HTML 直出
|
||||
*
|
||||
* 关联:portal-shell spec §5.5、§6.2、M8 验收标准
|
||||
*/
|
||||
import { gql } from "@apollo/client";
|
||||
import { createApolloClient } from "./apollo-client";
|
||||
import type { PluginConfigResponse, Role } from "./types";
|
||||
|
||||
/** 查询用户合并后的插件配置(走 apollo-router → config-service 子图) */
|
||||
export const GET_PLUGIN_CONFIG = gql`
|
||||
query GetPluginConfig($userId: ID!, $role: String) {
|
||||
pluginConfig(userId: $userId, role: $role) {
|
||||
activeLayout {
|
||||
layoutId
|
||||
displayName
|
||||
description
|
||||
availableSlots
|
||||
layoutSchemaJson
|
||||
}
|
||||
slots {
|
||||
slotName
|
||||
navItems
|
||||
}
|
||||
plugins {
|
||||
pluginId
|
||||
slot
|
||||
sortOrder
|
||||
sizeJson
|
||||
propsJson
|
||||
isVisible
|
||||
}
|
||||
registry {
|
||||
pluginId
|
||||
category
|
||||
version
|
||||
displayName
|
||||
description
|
||||
requiredRoles
|
||||
isBuiltin
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 获取用户合并后的插件配置(服务端调用)。
|
||||
*
|
||||
* @param userId 用户 ID(来自 RSC 的 x-user-id 头)
|
||||
* @param role 用户角色(来自 RSC 的 x-user-role 头)
|
||||
* @returns 三层合并后的 PluginConfigResponse;查询失败时返回默认 classic 配置
|
||||
*/
|
||||
export async function fetchPluginConfig(
|
||||
userId: string,
|
||||
role: Role,
|
||||
): Promise<PluginConfigResponse> {
|
||||
const client = createApolloClient();
|
||||
try {
|
||||
const { data, error } = await client.query<{
|
||||
pluginConfig: PluginConfigResponse;
|
||||
}>({
|
||||
query: GET_PLUGIN_CONFIG,
|
||||
variables: { userId, role },
|
||||
});
|
||||
if (error) {
|
||||
console.warn(
|
||||
`[portal-shell] fetchPluginConfig partial error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
if (data?.pluginConfig) {
|
||||
return data.pluginConfig;
|
||||
}
|
||||
return getDefaultConfig();
|
||||
} catch (err) {
|
||||
// Router 未就绪时降级为默认配置,保证 Shell 可渲染(开发态友好)
|
||||
console.warn(
|
||||
`[portal-shell] fetchPluginConfig failed, falling back to default config: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认 classic 布局配置(Router 未就绪 / 查询失败时降级)。
|
||||
* 保证 Shell 始终可渲染,不因下游不可用而白屏。
|
||||
*/
|
||||
export function getDefaultConfig(): PluginConfigResponse {
|
||||
return {
|
||||
activeLayout: {
|
||||
layoutId: "classic",
|
||||
displayName: "经典三栏",
|
||||
description: "TopBar + SideNav + Main",
|
||||
availableSlots: ["top", "side", "main"],
|
||||
layoutSchemaJson: JSON.stringify({
|
||||
grid: { rows: 1, cols: 1, areas: [["main"]] },
|
||||
}),
|
||||
},
|
||||
slots: [
|
||||
{ slotName: "top", navItems: [] },
|
||||
{ slotName: "side", navItems: [] },
|
||||
{ slotName: "main", navItems: [] },
|
||||
],
|
||||
plugins: [],
|
||||
registry: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user