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:
@@ -20,6 +20,7 @@ import { ConfigModule } from "../config-config/config.module.js";
|
||||
import { PluginResolver } from "./resolvers/plugin.resolver.js";
|
||||
import { LayoutTemplateResolver } from "./resolvers/layout-template.resolver.js";
|
||||
import { UserLayoutResolver } from "./resolvers/user-layout.resolver.js";
|
||||
import { PluginConfigResolver } from "./resolvers/plugin-config.resolver.js";
|
||||
import { DataLoaderService } from "./dataloader.service.js";
|
||||
|
||||
@Module({
|
||||
@@ -50,6 +51,7 @@ import { DataLoaderService } from "./dataloader.service.js";
|
||||
PluginResolver,
|
||||
LayoutTemplateResolver,
|
||||
UserLayoutResolver,
|
||||
PluginConfigResolver,
|
||||
DataLoaderService,
|
||||
],
|
||||
exports: [DataLoaderService],
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* PluginConfig Resolver(v2.1 M8)
|
||||
*
|
||||
* Apollo Federation 子图统一查询入口:portal-shell 通过 apollo-router
|
||||
* 查询 `pluginConfig(userId, role)` 获取三层合并后的 PluginConfigResponse。
|
||||
*
|
||||
* 设计意图(portal-shell spec §5.5 / §6.2):
|
||||
* - portal-shell 是 Next.js 前端,不直接调 gRPC,统一走 apollo-router GraphQL
|
||||
* - 本 Resolver 仅做 GraphQL 出参适配,业务逻辑复用 ConfigService.getPluginConfig
|
||||
* - role 由调用方(portal-shell RSC 从 x-user-role 头)传入,避免依赖 Router 头透传
|
||||
*
|
||||
* 关联:ADR-026 双入口策略、M8 验收点(portal-shell 查询走 apollo-router)
|
||||
*/
|
||||
import {
|
||||
Resolver,
|
||||
Query,
|
||||
Args,
|
||||
ID,
|
||||
ObjectType,
|
||||
Field,
|
||||
Int,
|
||||
} from "@nestjs/graphql";
|
||||
import { ConfigService } from "../../config-config/config.service.js";
|
||||
|
||||
@ObjectType()
|
||||
export class PluginConfigLayoutGql {
|
||||
@Field(() => ID)
|
||||
layoutId!: string;
|
||||
|
||||
@Field()
|
||||
displayName!: string;
|
||||
|
||||
@Field()
|
||||
description!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
availableSlots!: string[];
|
||||
|
||||
@Field()
|
||||
layoutSchemaJson!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PluginConfigSlotGql {
|
||||
@Field()
|
||||
slotName!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
navItems!: string[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PluginConfigPlacementGql {
|
||||
@Field(() => ID)
|
||||
pluginId!: string;
|
||||
|
||||
@Field()
|
||||
slot!: string;
|
||||
|
||||
@Field(() => Int)
|
||||
sortOrder!: number;
|
||||
|
||||
@Field()
|
||||
sizeJson!: string;
|
||||
|
||||
@Field()
|
||||
propsJson!: string;
|
||||
|
||||
@Field()
|
||||
isVisible!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PluginConfigRegistryItemGql {
|
||||
@Field(() => ID)
|
||||
pluginId!: string;
|
||||
|
||||
@Field()
|
||||
category!: string;
|
||||
|
||||
@Field()
|
||||
version!: string;
|
||||
|
||||
@Field()
|
||||
displayName!: string;
|
||||
|
||||
@Field()
|
||||
description!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
requiredRoles!: string[];
|
||||
|
||||
@Field()
|
||||
isBuiltin!: boolean;
|
||||
|
||||
@Field()
|
||||
isActive!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PluginConfigResponseGql {
|
||||
@Field(() => PluginConfigLayoutGql, { nullable: true })
|
||||
activeLayout!: PluginConfigLayoutGql | null;
|
||||
|
||||
@Field(() => [PluginConfigSlotGql])
|
||||
slots!: PluginConfigSlotGql[];
|
||||
|
||||
@Field(() => [PluginConfigPlacementGql])
|
||||
plugins!: PluginConfigPlacementGql[];
|
||||
|
||||
@Field(() => [PluginConfigRegistryItemGql])
|
||||
registry!: PluginConfigRegistryItemGql[];
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class PluginConfigResolver {
|
||||
constructor(private readonly service: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 获取用户合并后的插件配置(三层合并)。
|
||||
*
|
||||
* portal-shell RSC 调用:传入 userId(来自 x-user-id 头)与 role
|
||||
* (来自 x-user-role 头),返回与 gRPC GetPluginConfig 等价的结果。
|
||||
*/
|
||||
@Query(() => PluginConfigResponseGql)
|
||||
async pluginConfig(
|
||||
@Args("userId", { type: () => ID }) userId: string,
|
||||
@Args("role", {
|
||||
type: () => String,
|
||||
nullable: true,
|
||||
defaultValue: "student",
|
||||
})
|
||||
role: string,
|
||||
): Promise<PluginConfigResponseGql> {
|
||||
const result = await this.service.getPluginConfig(userId, role);
|
||||
return result as PluginConfigResponseGql;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user