feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
@@ -9,6 +9,11 @@
|
||||
* - 统一走 apollo-router GraphQL,由 Router 路由到 config-service 子图
|
||||
* - RSC 服务端预取消除 CSR 瀑布流,Config 随 HTML 直出
|
||||
*
|
||||
* 开发态降级(spec §5.5 容错):
|
||||
* - 优先走 apollo-router(生产路径)
|
||||
* - Router 不可用时降级直连 config-service /graphql(仅开发态,由 CONFIG_SERVICE_URL 触发)
|
||||
* - 二者均失败时返回空默认配置,保证 Shell 可渲染
|
||||
*
|
||||
* 关联:portal-shell spec §5.5、§6.2、M8 验收标准
|
||||
*/
|
||||
import { gql } from "@apollo/client";
|
||||
@@ -55,6 +60,11 @@ export const GET_PLUGIN_CONFIG = gql`
|
||||
/**
|
||||
* 获取用户合并后的插件配置(服务端调用)。
|
||||
*
|
||||
* 查询顺序(开发态容错):
|
||||
* 1. apollo-router(生产路径,M8 验收点)
|
||||
* 2. config-service 直连(仅当 CONFIG_SERVICE_URL 配置时启用,开发态降级)
|
||||
* 3. 空默认配置(最后兜底)
|
||||
*
|
||||
* @param userId 用户 ID(来自 RSC 的 x-user-id 头)
|
||||
* @param role 用户角色(来自 RSC 的 x-user-role 头)
|
||||
* @returns 三层合并后的 PluginConfigResponse;查询失败时返回默认 classic 配置
|
||||
@@ -63,8 +73,9 @@ export async function fetchPluginConfig(
|
||||
userId: string,
|
||||
role: Role,
|
||||
): Promise<PluginConfigResponse> {
|
||||
const client = createApolloClient();
|
||||
// 1. 优先走 apollo-router(生产路径)
|
||||
try {
|
||||
const client = createApolloClient();
|
||||
const { data, error } = await client.query<{
|
||||
pluginConfig: PluginConfigResponse;
|
||||
}>({
|
||||
@@ -73,22 +84,106 @@ export async function fetchPluginConfig(
|
||||
});
|
||||
if (error) {
|
||||
console.warn(
|
||||
`[portal-shell] fetchPluginConfig partial error: ${error.message}`,
|
||||
`[portal-shell] fetchPluginConfig partial error from apollo-router: ${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: ${
|
||||
`[portal-shell] apollo-router query failed: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
return getDefaultConfig();
|
||||
}
|
||||
|
||||
// 2. 开发态降级:直连 config-service GraphQL
|
||||
const configServiceUrl =
|
||||
process.env.CONFIG_SERVICE_URL ||
|
||||
process.env.NEXT_PUBLIC_CONFIG_SERVICE_URL;
|
||||
if (configServiceUrl) {
|
||||
try {
|
||||
const result = await fetchPluginConfigDirect(
|
||||
userId,
|
||||
role,
|
||||
configServiceUrl,
|
||||
);
|
||||
if (result) {
|
||||
console.info(
|
||||
`[portal-shell] fetchPluginConfig fallback to config-service direct`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[portal-shell] config-service direct fallback failed: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 最终兜底:空默认配置
|
||||
console.warn(`[portal-shell] fetchPluginConfig returning empty default`);
|
||||
return getDefaultConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发态降级:直连 config-service GraphQL 查询 pluginConfig。
|
||||
*
|
||||
* 当 apollo-router 不可用时(如本地开发未启动 Router),
|
||||
* 直接请求 config-service 的 /graphql 端点获取插件配置。
|
||||
* 生产环境不应触发此路径(apollo-router 必须可用)。
|
||||
*/
|
||||
async function fetchPluginConfigDirect(
|
||||
userId: string,
|
||||
role: string,
|
||||
configServiceUrl: string,
|
||||
): Promise<PluginConfigResponse | null> {
|
||||
const endpoint = `${configServiceUrl.replace(/\/$/, "")}/graphql`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
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 }
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { userId, role },
|
||||
}),
|
||||
// RSC 服务端调用,不携带 cookie
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`config-service HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
data?: { pluginConfig?: PluginConfigResponse };
|
||||
errors?: unknown;
|
||||
};
|
||||
|
||||
if (json.errors) {
|
||||
throw new Error(
|
||||
`config-service GraphQL errors: ${JSON.stringify(json.errors)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return json.data?.pluginConfig ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user