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,75 @@
/**
* Apollo Clientv2.1 M8 验收点)
*
* 所有 portal-shell 查询走 apollo-routerGraphQL 联邦入口):
* portal-shell → apollo-router :3000/graphql → 各子图iam/core-edu/content/msg/data-ana/ai/config-service
*
* 双端使用:
* - 服务端RSCcreateApolloClient() 每次请求新建实例ssrMode=true
* - 客户端getApolloClient() 单例,复用 InMemoryCache
*
* 关联portal-shell spec §5.5 RSC 预取、§5.6 统一 Hook、M8 验收标准
*/
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
const APOLLO_ROUTER_URL =
process.env.NEXT_PUBLIC_APOLLO_ROUTER_URL ||
process.env.APOLLO_ROUTER_URL ||
"http://localhost:3000/graphql";
/**
* 创建 Apollo Client 实例。
*
* @param getAuthToken 可选,返回 JWT 用于注入 Authorization 头(客户端从 cookie/localStorage 读取)
*/
export function createApolloClient(
getAuthToken?: () => string | null,
): ApolloClient<unknown> {
const httpLink = new HttpLink({
uri: APOLLO_ROUTER_URL,
credentials: "include",
});
const authLink = setContext((_, { headers }) => {
const token = getAuthToken?.() ?? null;
return {
headers: {
...headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
};
});
return new ApolloClient({
link: from([authLink, httpLink]),
cache: new InMemoryCache(),
ssrMode: typeof window === "undefined",
defaultOptions: {
query: {
errorPolicy: "all",
fetchPolicy: typeof window === "undefined" ? "no-cache" : "cache-first",
},
watchQuery: {
errorPolicy: "all",
},
},
});
}
let clientSingleton: ApolloClient<unknown> | null = null;
/**
* 获取客户端 Apollo Client 单例(浏览器侧复用缓存)。
*/
export function getApolloClient(
getAuthToken?: () => string | null,
): ApolloClient<unknown> {
if (typeof window === "undefined") {
return createApolloClient(getAuthToken);
}
if (!clientSingleton) {
clientSingleton = createApolloClient(getAuthToken);
}
return clientSingleton;
}