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,222 +1,47 @@
# 自动生成的 GraphQL Federation 子图v2.1 M0
# 源文件msg.proto
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
extend type Query
type Notification @key(fields: "id") {
id: String
user_id: String
type: String
title: String
content: String
channel: String
is_read: Boolean
created_at: String
status: String
related_entity_type: String
related_entity_id: String
group_id: String
sender_id: String
template_id: String
event_id: String
read_at: String
updated_at: String
type Notification {
id: ID!
userId: String!
type: String!
title: String!
content: String!
channel: String!
isRead: Boolean!
status: String!
createdAt: DateTime!
updatedAt: DateTime!
relatedEntityType: String
relatedEntityId: String
groupId: String
senderId: String
templateId: String
eventId: String
readAt: DateTime
}
type MarkAsReadRequest @key(fields: "id") {
id: String
user_id: String
"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime
type NotificationTemplate {
id: ID!
code: String!
type: String!
titleTemplate: String!
contentTemplate: String!
defaultChannels: [String!]!
variables: [String!]!
locale: String!
status: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type NotificationPreference @key(fields: "id") {
id: String
user_id: String
type: String
channels: String
frequency_limit: Int
quiet_hours_start: String
quiet_hours_end: String
quiet_hours_timezone: String
enabled: Boolean
created_at: String
updated_at: String
}
type NotificationTemplate @key(fields: "id") {
id: String
code: String
type: String
title_template: String
content_template: String
default_channels: String
variables: String
locale: String
status: String
created_at: String
updated_at: String
}
type GetTemplateRequest @key(fields: "id") {
id: String
}
type UpdateTemplateRequest @key(fields: "id") {
id: String
title_template: String
content_template: String
default_channels: String
variables: String
status: String
}
type DeleteTemplateRequest @key(fields: "id") {
id: String
}
input SendNotificationRequestInput {
user_id: String
type: String
title: String
content: String
channel: String
related_entity_type: String
related_entity_id: String
group_id: String
sender_id: String
template_id: String
event_id: String
}
input BatchSendNotificationRequestInput {
items: SendNotificationRequest
group_id: String
}
input BatchSendNotificationResponseInput {
ids: String
failed: BatchSendFailure
}
type BatchSendFailure {
user_id: String
error: String
}
input ListNotificationsRequestInput {
user_id: String
only_unread: Boolean
type: String
page: Int
page_size: Int
}
input ListNotificationsResponseInput {
notifications: Notification
total: Int
}
input GetUnreadCountRequestInput {
user_id: String
}
input GetUnreadCountResponseInput {
count: Int
}
input BatchMarkAsReadRequestInput {
ids: String
user_id: String
}
input MarkAllAsReadRequestInput {
user_id: String
before: String
}
input SearchNotificationsRequestInput {
user_id: String
query: String
type: String
page: Int
page_size: Int
}
input SearchNotificationsResponseInput {
notifications: Notification
total: Int
}
input RecallNotificationRequestInput {
group_id: String
reason: String
}
input RecallNotificationResponseInput {
recalled_count: Int
}
input GetPreferencesRequestInput {
user_id: String
}
input GetPreferencesResponseInput {
preferences: NotificationPreference
}
input UpdatePreferencesRequestInput {
user_id: String
preferences: NotificationPreference
}
input CreateTemplateRequestInput {
code: String
type: String
title_template: String
content_template: String
default_channels: String
variables: String
locale: String
}
input ListTemplatesRequestInput {
type: String
status: String
}
input ListTemplatesResponseInput {
templates: NotificationTemplate
}
input RenderTemplateRequestInput {
code: String
locale: String
}
type RenderedNotification {
title: String
content: String
}
type Empty {
}
extend type Query {
notifications: [ListNotificationsResponse!]!
}
extend type Query {
unreadCount: GetUnreadCountResponse
}
extend type Query {
preferences: GetPreferencesResponse
}
extend type Query {
template: NotificationTemplate
}
extend type Query {
templates: [ListTemplatesResponse!]!
}
type Query {
notifications(userId: ID!): [Notification!]!
template(id: ID!): NotificationTemplate
}

View File

@@ -13,7 +13,7 @@
*/
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 { NotificationResolver } from "./resolvers/notification.resolver.js";
@@ -24,7 +24,7 @@ import { getRedis } from "../shared/redis/redis.client.js";
@Module({
imports: [
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 { AppModule } from "./app.module.js";