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:
SpecialX
2026-07-17 16:10:05 +08:00
parent f7e52b5b7f
commit 9cedf0c437
140 changed files with 10872 additions and 3192 deletions

View File

@@ -1,187 +1,31 @@
# 自动生成的 GraphQL Federation 子图v2.1 M0
# 源文件iam.proto
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
extend type Query
type UserInfo @key(fields: "id") {
id: String
email: String
name: String
roles: String
permissions: String
data_scope: String
status: String
type User {
userId: ID!
email: String!
name: String!
status: String!
dataScope: String!
}
input RegisterRequestInput {
email: String
password: String
name: String
type Role {
roleId: ID!
name: String!
description: String
roleType: String!
level: Float!
}
input LoginRequestInput {
email: String
password: String
type UserDataScope {
userId: ID!
classScopeToken: String!
studentScopeToken: String!
}
input RefreshTokenRequestInput {
refresh_token: String
}
input LogoutRequestInput {
refresh_token: String
user_id: String
}
input LogoutResponseInput {
success: Boolean
}
input AuthResponseInput {
user: UserInfo
tokens: TokenPair
}
type TokenPair {
access_token: String
refresh_token: String
expires_in: Int
}
input GetUserInfoRequestInput {
user_id: String
}
input BatchGetUsersRequestInput {
user_ids: String
}
input BatchGetUsersResponseInput {
users: UserInfo
}
input GetEffectivePermissionsRequestInput {
user_id: String
}
input EffectivePermissionsResponseInput {
permissions: String
}
input GetEffectiveAccessRequestInput {
user_id: String
permission: String
}
input EffectiveAccessResponseInput {
allowed: Boolean
data_scope: String
}
input GetEffectiveDataScopeRequestInput {
user_id: String
}
input DataScopeResponseInput {
data_scope: String
}
input GetViewportsRequestInput {
user_id: String
}
input ViewportsResponseInput {
viewports: ViewportItem
}
type ViewportItem {
key: String
label: String
route: String
icon: String
sort_order: String
required_permission: String
}
input GetPublicKeyRequestInput {
}
input PublicKeyResponseInput {
kid: String
alg: String
public_key_pem: String
}
input GetChildrenByParentRequestInput {
parent_id: String
}
input ChildrenResponseInput {
children: ChildInfo
}
type ChildInfo {
student_id: String
name: String
relation: String
}
type EffectiveDataScope {
user_id: String
level: String
scope_ids: String
school_id: String
}
input CreateUserRequestInput {
email: String
password: String
name: String
role_id: String
data_scope: String
}
input UpdateUserRequestInput {
user_id: String
name: String
email: String
status: String
data_scope: String
}
input DeleteUserRequestInput {
user_id: String
}
input DeleteUserResponseInput {
success: Boolean
}
extend type Query {
userInfo: UserInfo
}
extend type Query {
effectiveDataScope: EffectiveDataScope
}
extend type Query {
effectivePermissions: EffectivePermissionsResponse
}
extend type Query {
effectiveAccess: EffectiveAccessResponse
}
extend type Query {
viewports: ViewportsResponse
}
extend type Query {
publicKey: PublicKeyResponse
}
extend type Query {
childrenByParent: ChildrenResponse
}
type Query {
user(userId: ID!): User
role(roleId: ID!): Role
dataScope(userId: ID!): UserDataScope
}

View File

@@ -11,9 +11,10 @@
*/
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { join } from "node:path";
import { GraphqlContext } from "@edu/shared-ts/federation";
import { IamModule } from "../iam/iam.module.js";
import { UserResolver } from "./resolvers/user.resolver.js";
import { RoleResolver } from "./resolvers/role.resolver.js";
import { DataScopeResolver } from "./resolvers/datascope.resolver.js";
@@ -22,8 +23,9 @@ import { getRedis } from "../config/redis.js";
@Module({
imports: [
IamModule,
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
driver: ApolloFederationDriver,
// Federation 2 子图
autoSchemaFile: {
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),

View File

@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
}
override canActivate(ctx: ExecutionContext): boolean {
// DEV_MODE 优先放行(开发态跳过所有校验)
if (process.env.DEV_MODE === "true") {
return true;
}
const req = ctx.switchToHttp().getRequest<{
url: string;
}>();
// 仅 GraphQL 端点需要校验REST 路由放行
if (!req.url?.startsWith("/graphql")) {
if (!req?.url?.startsWith("/graphql")) {
return true;
}

View File

@@ -1,3 +1,4 @@
import "@edu/shared-ts/env-loader";
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { Transport, MicroserviceOptions } from "@nestjs/microservices";