705 lines
32 KiB
Markdown
705 lines
32 KiB
Markdown
# 模块架构设计文档 — admin-portal
|
||
|
||
> AI:ai16(TS/React · 管理场景域前端 remote)
|
||
> 阶段:阶段 2 交付物(仲裁后修订版 v2)
|
||
> 日期:2026-07-10
|
||
> 关联:
|
||
>
|
||
> - [阶段 1 理解确认书](./01-understanding.md)
|
||
> - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4
|
||
> - [总统最终裁决](../../../docs/architecture/president-final-rulings.md) §5.1-5.5(ISSUE-044~048)
|
||
> - [admin-portal 对接契约](../../architecture/issues/contracts/admin-portal_contract.md)
|
||
> - [pending-features P6](../../../docs/architecture/roadmap/pending-features.md)
|
||
> - [known-issues §2.16](../../../docs/troubleshooting/known-issues.md)
|
||
> 状态:已实现(P2-P6 全阶段交付完成)
|
||
|
||
---
|
||
|
||
## 1. 模块内部分层图(admin-portal Remote + Shell 引用 + Gateway + push-gateway)
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Browser["浏览器(系统/校管理员)"]
|
||
URL[URL 路由 /admin/*]
|
||
end
|
||
|
||
subgraph Shell["teacher-portal(Shell 宿主,端口 4000)"]
|
||
AppShell[AppShell<br/>左栏导航 + 主内容区<br/>按 scope=admin 过滤视口]
|
||
RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
|
||
Router[Next.js App Router<br/>动态加载 admin Remote]
|
||
SharedDeps["共享依赖暴露(singleton)<br/>react/react-dom/urql/graphql<br/>@edu/ui-tokens/@edu/ui-components/@edu/hooks/@edu/contracts"]
|
||
GraphQLProviderShell[GraphQLProvider<br/>Shell 暴露 urql client 单例]
|
||
end
|
||
|
||
subgraph RemoteAdmin["admin-portal(Remote 子应用,端口 4003)"]
|
||
AdminApp[AdminApp<br/>MF Remote 入口 exposes ./AdminApp]
|
||
AdminPages[管理场景页面<br/>dashboard/users/roles/permissions/viewports/organization<br/>classes/teachers/students/audit-logs/system]
|
||
AdminComponents["admin 特有组件<br/>UserManagementTable/RolePermissionMatrix<br/>ViewportConfigEditor/OrganizationTree/NotificationPanel"]
|
||
AdminHooks["admin 业务 Hooks<br/>useUsers/useRoles/usePermissions/useViewports<br/>useOrganization/useClasses/useTeachers/useStudents<br/>useAuditLogs/useDashboard/useSystemSettings/useWebSocket"]
|
||
end
|
||
|
||
subgraph Gateway["api-gateway"]
|
||
GW[Gin 路由/鉴权/限流]
|
||
end
|
||
|
||
subgraph PushGateway["push-gateway"]
|
||
WS[WebSocket :8081/ws]
|
||
end
|
||
|
||
subgraph Backend["后端服务"]
|
||
IAM[iam<br/>用户/角色/权限/视口 CRUD + AuditEvent 发布]
|
||
BFF[teacher-bff<br/>GraphQL admin 命名空间 聚合]
|
||
end
|
||
|
||
Browser --> URL
|
||
URL --> RootLayout
|
||
RootLayout --> AppShell
|
||
AppShell --> Router
|
||
Router -->|动态加载 /admin/*| AdminApp
|
||
AdminApp --> AdminPages
|
||
|
||
AdminPages -->|消费 singleton| SharedDeps
|
||
SharedDeps --> GraphQLProviderShell
|
||
AdminPages --> AdminComponents
|
||
AdminPages --> AdminHooks
|
||
|
||
AdminHooks -->|useGraphQuery/useGraphMutation| GraphQLProviderShell
|
||
GraphQLProviderShell -->|POST /api/admin/graphql| GW
|
||
GW --> BFF
|
||
BFF --> IAM
|
||
|
||
AdminHooks -->|useWebSocket| WS
|
||
IAM -.->|Kafka edu.iam.audit.created| BFF
|
||
|
||
AppShell -->|fetch /api/auth/login| GW
|
||
```
|
||
|
||
### 1.1 Remote 与 Shell 的职责边界
|
||
|
||
| 职责 | 归属 | 说明 |
|
||
| -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------ |
|
||
| RootLayout(字体/令牌/Provider) | teacher-portal Shell | admin-portal MF 模式复用;standalone 模式自建 |
|
||
| AppShell(左栏 + 主内容区) | admin-portal 内部 | `admin-shell.tsx`,11 项导航 + 跳过链接 A11y |
|
||
| 路由表 `/admin/*` | admin-portal | `src/app/admin/*/page.tsx` |
|
||
| 登录页 | admin-portal | standalone 模式 `src/app/login/page.tsx`(mock 登录);MF 模式复用 Shell `/login` |
|
||
| rewrites `/api/admin/graphql` | admin-portal | `next.config.js` rewrites 代理到 api-gateway |
|
||
| GraphQLProvider | Shell 暴露 / 自建 | MF 模式复用 Shell 单例(ARB-004);standalone 模式 `graphql-provider.tsx` 自建 urql client |
|
||
| 管理场景页面 | admin-portal Remote | `/admin/*` 下的所有 page.tsx |
|
||
| 管理特有组件 | admin-portal Remote | UserManagementTable 等 6 个 |
|
||
| 管理业务 Hooks | admin-portal Remote | 13 个业务 Hook + useWebSocket |
|
||
| 共享组件库 | Shell 暴露 | `@edu/ui-components` |
|
||
| 共享 Hooks | Shell 暴露 | `@edu/hooks` |
|
||
|
||
### 1.2 MF 配置(admin-portal/next.config.js,Remote 角色)
|
||
|
||
```javascript
|
||
const NextFederationPlugin = require("@module-federation/nextjs-mf");
|
||
|
||
const nextConfig = {
|
||
reactStrictMode: true,
|
||
transpilePackages: ["@edu/ui-tokens", "@edu/ui-components", "@edu/hooks"],
|
||
async rewrites() {
|
||
const gatewayUrl = process.env.API_GATEWAY_URL || "http://localhost:8080";
|
||
return [
|
||
{
|
||
source: "/api/admin/graphql",
|
||
destination: `${gatewayUrl}/api/admin/graphql`,
|
||
},
|
||
{ source: "/api/:path*", destination: `${gatewayUrl}/api/:path*` },
|
||
];
|
||
},
|
||
webpack(config, { isServer }) {
|
||
if (process.env.NEXT_PUBLIC_MF_ENABLED === "true") {
|
||
config.plugins.push(
|
||
new NextFederationPlugin({
|
||
name: "admin_app",
|
||
filename: "static/chunks/remoteEntry.js",
|
||
exposes: { "./AdminApp": "./src/app/admin-app.tsx" },
|
||
remotes: {
|
||
teacher: `teacher_app@http://localhost:4000/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
|
||
},
|
||
shared: {
|
||
react: { singleton: true, requiredVersion: "^18.3.0" },
|
||
"react-dom": { singleton: true, requiredVersion: "^18.3.0" },
|
||
urql: { singleton: true },
|
||
graphql: { singleton: true },
|
||
"@edu/ui-tokens": { singleton: true },
|
||
"@edu/ui-components": { singleton: true },
|
||
"@edu/hooks": { singleton: true },
|
||
},
|
||
extraOptions: { exposePages: false },
|
||
}),
|
||
);
|
||
}
|
||
return config;
|
||
},
|
||
};
|
||
module.exports = nextConfig;
|
||
```
|
||
|
||
> **说明**:
|
||
>
|
||
> - `NEXT_PUBLIC_MF_ENABLED` 环境变量控制 MF 启用(standalone 模式默认 `false`,MF 模式设 `true`)
|
||
> - admin-portal 暴露 `./AdminApp` 模块(非旧文档的 `./pages`),供 Shell 动态 import
|
||
> - `shared` 全部 `singleton: true`,确保 urql/graphql/react 单例(避免多实例报错)
|
||
> - admin-portal 自身实现 `rewrites`(代理 `/api/admin/graphql` 到 api-gateway),standalone 模式可独立运行
|
||
|
||
## 2. 领域模型(前端视角)
|
||
|
||
前端不持有业务聚合根,仅持有"视图模型"(ViewModel)和"会话状态"。定义于 `src/types/view-models.ts`。
|
||
|
||
### 2.1 视图模型清单
|
||
|
||
```typescript
|
||
// 数据范围
|
||
type DataScope = "ALL" | "SCHOOL" | "GRADE" | "CLASS" | "DISTRICT";
|
||
|
||
// 用户管理
|
||
interface UserViewModel {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
roles: { id: string; name: string; code: string }[];
|
||
status: "active" | "disabled" | "locked";
|
||
dataScope: DataScope;
|
||
organizationId: string | null;
|
||
schoolName?: string;
|
||
lastLoginAt: number | null;
|
||
createdAt: number;
|
||
updatedAt: number;
|
||
}
|
||
|
||
// 角色管理
|
||
interface RoleViewModel {
|
||
id: string;
|
||
name: string;
|
||
code: string;
|
||
description: string;
|
||
permissions: PermissionViewModel[];
|
||
userCount: number;
|
||
dataScope: DataScope;
|
||
isSystem: boolean;
|
||
createdAt: number;
|
||
updatedAt: number;
|
||
}
|
||
|
||
// 权限管理
|
||
interface PermissionViewModel {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
description: string;
|
||
resource: string;
|
||
action: string;
|
||
isSystem: boolean;
|
||
}
|
||
|
||
// 视口配置
|
||
interface ViewportConfigViewModel {
|
||
id: string;
|
||
key: string;
|
||
label: string;
|
||
route: string;
|
||
icon: string | null;
|
||
sortOrder: number;
|
||
requiredPermission: string | null;
|
||
scope: "teacher" | "student" | "parent" | "admin";
|
||
isVisible: boolean;
|
||
}
|
||
|
||
// 组织树
|
||
interface OrganizationNode {
|
||
id: string;
|
||
name: string;
|
||
type: "school" | "grade" | "class";
|
||
parentId: string | null;
|
||
childrenCount: number;
|
||
path: string;
|
||
}
|
||
|
||
// 学校设置
|
||
interface SystemSettingsViewModel {
|
||
schoolName: string;
|
||
schoolYear: string;
|
||
semester: string;
|
||
timezone: string;
|
||
locale: string;
|
||
// ... 更多设置项
|
||
}
|
||
|
||
// 班级/教师/学生(admin 全局视角)
|
||
interface AdminClassViewModel {
|
||
id: string;
|
||
name: string;
|
||
grade: string;
|
||
headTeacher: string;
|
||
studentCount: number;
|
||
status: string;
|
||
}
|
||
interface AdminTeacherViewModel {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
subjects: string[];
|
||
classes: string[];
|
||
status: string;
|
||
}
|
||
interface AdminStudentViewModel {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
className: string;
|
||
grade: string;
|
||
status: string;
|
||
}
|
||
|
||
// 审计日志
|
||
interface AuditLogViewModel {
|
||
id: string;
|
||
userId: string;
|
||
userName: string;
|
||
action: string;
|
||
resourceType: string;
|
||
resourceId: string;
|
||
beforeState: string | null;
|
||
afterState: string | null;
|
||
ip: string;
|
||
userAgent: string;
|
||
occurredAt: number;
|
||
}
|
||
|
||
// 管理仪表盘
|
||
interface AdminDashboardViewModel {
|
||
totalUsers: number;
|
||
totalTeachers: number;
|
||
totalStudents: number;
|
||
totalClasses: number;
|
||
activeSessions: number;
|
||
serviceHealth: {
|
||
serviceName: string;
|
||
status: "healthy" | "degraded" | "down";
|
||
latencyMs: number;
|
||
}[];
|
||
recentActivity: {
|
||
timestamp: number;
|
||
action: string;
|
||
user: string;
|
||
resource: string;
|
||
}[];
|
||
}
|
||
|
||
// WebSocket 通知
|
||
interface WsNotification {
|
||
id: string;
|
||
type: "audit_alert" | "abnormal_login" | "system_error" | "info";
|
||
severity: "info" | "warning" | "error";
|
||
title: string;
|
||
message: string;
|
||
timestamp: number;
|
||
}
|
||
```
|
||
|
||
### 2.2 当前用户与会话
|
||
|
||
```typescript
|
||
interface CurrentUser {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
roles: string[];
|
||
permissions: string[];
|
||
dataScope: DataScope;
|
||
schoolId: string | null;
|
||
schoolName: string | null;
|
||
}
|
||
```
|
||
|
||
会话状态由 `AuthProvider` 管理,token 存 `localStorage`(`edu_access_token`),用户信息存 `localStorage`(`edu_user`)。
|
||
|
||
## 3. 数据模型(前端缓存层)
|
||
|
||
admin-portal 无数据库,仅有 urql GraphQL client 缓存层。
|
||
|
||
| 数据类型 | 存储 | 缓存策略 |
|
||
| ----------------------- | ---------------------------- | --------------------------------------------- |
|
||
| Session(token + user) | localStorage + React Context | access token 持久化;用户信息持久化;登出清除 |
|
||
| GraphQL 查询缓存 | urql document cache | 默认缓存,mutation 后手动 invalidate |
|
||
| 视口配置(拖拽态) | React useState | 本地编辑,保存时批量 mutation |
|
||
| 表单临时态 | react-hook-form | 卸载即销毁 |
|
||
| WebSocket 通知 | React useState(限 20 条) | 新通知头部插入,超 20 条裁剪 |
|
||
|
||
> **缓存策略说明**:admin 数据低频变,urql 默认 document cache 已满足;监控指标经 WebSocket 实时推送(不再轮询);视口配置支持本地编辑 + 批量保存。
|
||
|
||
## 4. API 设计(GraphQL 为主)
|
||
|
||
### 4.1 GraphQL Client(urql,ARB-004)
|
||
|
||
**standalone 模式**(`src/lib/graphql-client.ts`):
|
||
|
||
```typescript
|
||
import { createClient, fetchExchange, type Client } from "@urql/core";
|
||
|
||
const GRAPHQL_ENDPOINT = "/api/admin/graphql";
|
||
|
||
export function createGraphQLClient(): Client {
|
||
return createClient({
|
||
url: GRAPHQL_ENDPOINT,
|
||
exchanges: [fetchExchange],
|
||
fetchOptions: () => {
|
||
const token =
|
||
typeof window !== "undefined"
|
||
? localStorage.getItem("edu_access_token")
|
||
: null;
|
||
return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
|
||
},
|
||
});
|
||
}
|
||
```
|
||
|
||
**MF 模式**:复用 Shell 暴露的 `GraphQLProvider` + `useGraphQLClient`(singleton,ARB-004)。
|
||
|
||
**urql 类型断言技巧**:urql 泛型与纯字符串 query 兼容性有限,用 `as unknown as` 从 unknown 转换:
|
||
|
||
```typescript
|
||
// use-graphql.ts
|
||
const [executeMutation] = useMutation(mutation) as unknown as [
|
||
(variables: V) => { toPromise: () => Promise<OperationResult<T, V>> },
|
||
unknown,
|
||
];
|
||
```
|
||
|
||
### 4.2 通用 GraphQL Hooks(`src/hooks/use-graphql.ts`)
|
||
|
||
```typescript
|
||
export function useGraphQuery<T, V = Record<string, never>>(
|
||
query: string,
|
||
variables?: V,
|
||
): QueryResult<T> { ... }
|
||
|
||
export function useGraphMutation<T, V>(
|
||
mutation: string,
|
||
): [MutationFn<T, V>, MutationResult] { ... }
|
||
```
|
||
|
||
### 4.3 业务 Hooks 清单
|
||
|
||
| Hook | 职责 |
|
||
| ---------------------------- | ------------------------------------------------ |
|
||
| `useUsers(filter)` | 用户列表查询(含筛选/分页) |
|
||
| `useUser(userId)` | 用户详情 |
|
||
| `useCreateUser()` | 创建用户 mutation |
|
||
| `useUpdateUser()` | 更新用户 mutation |
|
||
| `useToggleUserStatus()` | 启用/禁用用户 mutation |
|
||
| `useUserFilter()` | 用户筛选状态(搜索/角色/状态/组织) |
|
||
| `useRoles()` | 角色列表查询 |
|
||
| `useCreateRole()` | 创建角色 mutation |
|
||
| `useUpdateRolePermissions()` | 更新角色权限 mutation(RolePermissionMatrix 用) |
|
||
| `usePermissions()` | 全量权限列表 |
|
||
| `useViewports()` | 视口配置列表 |
|
||
| `useUpdateViewport()` | 更新视口配置 mutation |
|
||
| `useOrganization(parentId)` | 组织树查询(按 parentId 递归) |
|
||
| `useClasses(filter)` | 班级列表查询 |
|
||
| `useClassFilter()` | 班级筛选状态 |
|
||
| `useTeachers(filter)` | 教师列表查询 |
|
||
| `useTeacherFilter()` | 教师筛选状态 |
|
||
| `useStudents(filter)` | 学生列表查询 |
|
||
| `useStudentFilter()` | 学生筛选状态 |
|
||
| `useAuditLogs(filter)` | 审计日志查询 |
|
||
| `useAuditLogFilter()` | 审计日志筛选状态 |
|
||
| `exportAuditLogsCsv(logs)` | 审计日志 CSV 导出(BOM + UTF-8) |
|
||
| `useDashboard()` | 管理仪表盘聚合查询 |
|
||
| `useSystemSettings()` | 学校设置查询 |
|
||
| `useUpdateSystemSettings()` | 学校设置更新 mutation |
|
||
| `useWebSocket(maxItems)` | WebSocket 实时通知(mock 模式 30s 定时推送) |
|
||
|
||
## 5. 事件设计
|
||
|
||
### 5.1 WebSocket 实时通知(ARB-006)
|
||
|
||
admin-portal 接入 push-gateway `GET /ws`,消费 3 类通知:
|
||
|
||
```typescript
|
||
// src/hooks/use-websocket.ts
|
||
export function useWebSocket(maxItems = 20) {
|
||
// 真实模式:连接 push-gateway NEXT_PUBLIC_WS_URL
|
||
// mock 模式:30s 定时器模拟推送
|
||
// 返回:{ notifications, connected, dismiss, clear }
|
||
}
|
||
```
|
||
|
||
| 通知类型 | severity | 触发场景 |
|
||
| ---------------- | -------- | ----------------------------- |
|
||
| `audit_alert` | warning | 敏感操作(权限变更/批量删除) |
|
||
| `abnormal_login` | error | 异地登录/异常时段登录 |
|
||
| `system_error` | error | 服务降级/宕机 |
|
||
|
||
### 5.2 不消费 Kafka
|
||
|
||
admin-portal 不直接订阅 Kafka。审计日志经 teacher-bff 聚合后通过 GraphQL `auditLogs` Query 消费(ARB-005):
|
||
|
||
**链路**:iam → Kafka `edu.iam.audit.created` → **teacher-bff 消费** → GraphQL `auditLogs` Query → admin-portal
|
||
|
||
> **不再用轮询**:旧文档的"60s 轮询监控 + 5min 轮询统计"已被 ARB-006 的 WebSocket 推送替换。
|
||
|
||
## 6. 横切关注点对齐清单
|
||
|
||
### 6.1 权限(前端等价,ARB-003)
|
||
|
||
权限点常量定义于 `src/lib/permissions.ts`:
|
||
|
||
- `ADMIN_PERMISSIONS`:admin 自身资源(`ADMIN_DASHBOARD_VIEW` / `ADMIN_SYSTEM_MANAGE` / `ADMIN_AUDIT_READ` / `ADMIN_CLASS_READ` / `ADMIN_TEACHER_READ` / `ADMIN_STUDENT_READ` / `ADMIN_ORG_MANAGE` 等)
|
||
- `IAM_PERMISSIONS`:跨服务资源(`IAM_USER_READ` / `IAM_ROLE_READ` / `IAM_PERMISSION_READ` / `IAM_VIEWPORT_READ` 等)
|
||
- `ROUTE_PERMISSIONS`:11 路由 → 权限点映射
|
||
|
||
权限校验:`usePermission().hasPermission("XXX")` Hook + `AuthGuard` 组件(路由级)。
|
||
|
||
### 6.2 错误码清单(前端 i18n 路由,ARB-002)
|
||
|
||
| 前缀 | 来源服务 | i18n key 模式 |
|
||
| -------------- | ----------- | ------------------------ |
|
||
| `IAM_` | iam | `iam.error.{{code}}` |
|
||
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
|
||
| `GW_` | api-gateway | `gateway.error.{{code}}` |
|
||
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
|
||
| `ADMIN_` | admin 域 | `error.admin.*` |
|
||
|
||
### 6.3 Web Vitals(`src/lib/web-vitals.ts`)
|
||
|
||
| 指标 | 类型 | 上报方式 |
|
||
| ---- | -------------- | ---------------------- |
|
||
| LCP | 最大内容绘制 | `navigator.sendBeacon` |
|
||
| CLS | 累积布局偏移 | 同上 |
|
||
| FCP | 首次内容绘制 | 同上 |
|
||
| INP | 交互到下一绘制 | 同上 |
|
||
| TTFB | 首字节时间 | 同上 |
|
||
|
||
仅 production 启用(`WebVitalsInitializer` 组件控制)。
|
||
|
||
### 6.4 健康检查
|
||
|
||
| 端点 | 用途 | 实现 |
|
||
| ----------------- | --------- | --------------------------------------------------- |
|
||
| `GET /api/health` | Liveness | 返回 `200 { status: "ok" }` |
|
||
| `GET /api/ready` | Readiness | 检查 gateway 可达性(2s 超时);mock 模式直接 ready |
|
||
|
||
### 6.5 A11y(WCAG 2.2 AA)
|
||
|
||
- `eslint-plugin-jsx-a11y`(error 级)
|
||
- skip-link(`admin-shell.tsx` 跳过到主内容区)
|
||
- `focus-visible` 样式(`globals.css`)
|
||
- 组织树:`<button role="treeitem" aria-expanded aria-selected onKeyDown>`
|
||
- 通知面板:`<li>` 内嵌 `<button aria-label>`
|
||
- Toast:`aria-live="polite"` + `aria-atomic="true"`
|
||
- 注:`@next/eslint-plugin-next` 14.x 与 ESLint 9 不兼容(`context.getAncestors` 已移除),已移除
|
||
|
||
### 6.6 横切关注点对齐清单(汇总)
|
||
|
||
| 对齐项 | 实现 |
|
||
| --------------------- | --------------------------------------------------------------- |
|
||
| 权限校验 | `usePermission()` + `AuthGuard`,`ADMIN_*`/`IAM_*` 前缀 |
|
||
| 错误码前缀 | 5 个前缀(`IAM_`/`BFF_TEACHER_`/`GW_`/`NETWORK_`/`ADMIN_`) |
|
||
| logger | console + 结构化(生产上报) |
|
||
| metrics | Web Vitals v4 → `navigator.sendBeacon` |
|
||
| /health + /ready | Next.js Route Handler |
|
||
| 优雅关闭 | N/A(Next.js 无长连接) |
|
||
| 测试覆盖率 ≥ 80% | Vitest + @testing-library/react(vitest.config.ts 已配置) |
|
||
| Dockerfile 多阶段构建 | `node:22-alpine` builder + runner,standalone 输出,EXPOSE 4003 |
|
||
| Zod 输入验证 | react-hook-form + zod |
|
||
| GlobalErrorFilter | React ErrorBoundary + Toast 错误处理 |
|
||
| 设计令牌三层 | `globals.css`(primitive/semantic)+ tailwind-theme |
|
||
| A11y | eslint-plugin-jsx-a11y + skip-link + focus-visible |
|
||
| Module Federation | `next.config.js` Remote,`NEXT_PUBLIC_MF_ENABLED` 控制 |
|
||
|
||
## 7. 共享组件库(复用 Shell + admin 特有)
|
||
|
||
### 7.1 复用 Shell 暴露的组件(`@edu/ui-components`)
|
||
|
||
AppShell 风格、ErrorBoundary、RequirePermission、Loading、Empty、Button/Input/Select 等。
|
||
|
||
### 7.2 admin-portal 特有组件(新建)
|
||
|
||
| 组件 | 用途 | 实现要点 |
|
||
| ---------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- |
|
||
| `AdminShell` | 左栏 11 项导航 + 主内容区 + 跳过链接 | 内部组件,11 项导航按 `ROUTE_PERMISSIONS` 过滤;纸面风格 |
|
||
| `UserManagementTable` | 用户管理表格(列表/筛选/分页) | 列:邮箱/姓名/角色/状态/数据范围/最后登录;筛选:搜索/角色/状态 |
|
||
| `UserFormModal` | 用户表单弹窗(创建/编辑) | react-hook-form + zod;字段:邮箱/姓名/角色/数据范围/组织 |
|
||
| `RolePermissionMatrix` | 角色-权限矩阵编辑器(checkbox 网格) | 行=角色,列=权限(按 resource 分组);全选/半选/单选;系统预置角色只读 |
|
||
| `OrganizationTree` | 组织树(递归 + A11y treeitem) | 按 parentId 懒加载;`<button role="treeitem" aria-expanded aria-selected onKeyDown>` |
|
||
| `NotificationPanel` | 通知面板(WebSocket 实时通知) | 固定右上角;显示最近 20 条;点击触发 Toast + dismiss;`<li>` 内嵌 `<button aria-label>` |
|
||
|
||
### 7.3 通用 UI 组件(`src/components/ui.tsx`)
|
||
|
||
PaperCard / PageHeader / Button(4 variant)/ Input / Select / Badge / Table / TableRow / TableCell / EmptyState / LoadingState / ErrorState / Pagination
|
||
|
||
### 7.4 不使用的组件(明确排除)
|
||
|
||
| 组件 | 归属 | 排除原因 |
|
||
| ----------------------------------- | -------------- | ------------------------------------- |
|
||
| `RichTextEditor` | teacher-portal | 管理端无富文本场景 |
|
||
| `ExamTaking` | student-portal | 管理端无作答场景 |
|
||
| `ChildSwitcher` | parent-portal | 管理端无多子女切换 |
|
||
| `PlatformMonitor`(Grafana iframe) | — | 已移除,改为管理仪表盘 + 服务健康表格 |
|
||
|
||
## 8. Mock 策略(MSW + mock-socket)
|
||
|
||
### 8.1 MSW handlers(`src/mocks/handlers.ts`)
|
||
|
||
开发期拦截全部 GraphQL/HTTP 请求:
|
||
|
||
- `/api/auth/login` → mock 登录(admin@edu.test / admin123)
|
||
- `graphql.operation` → 按 operationName 分发 18 个 GraphQL operation mock:
|
||
- CurrentUser / AdminUsers / AdminUser / CreateUser / UpdateUser / ToggleUserStatus
|
||
- AdminRoles / CreateRole / UpdateRolePermissions
|
||
- AdminPermissions / AdminViewports / UpdateViewport
|
||
- AdminOrganization / AdminClasses / AdminTeachers / AdminStudents
|
||
- AuditLogs / AdminDashboard / SystemSettings / UpdateSystemSettings
|
||
- 内存可变副本:支持增删改 + 自动审计记录(`recordAudit`)
|
||
- 分页:`paginate(items, page, pageSize)` 辅助函数
|
||
|
||
### 8.2 mock fixtures(`src/mocks/fixtures.ts`)
|
||
|
||
- `mockCurrentUser`:admin 角色,permissions=["*"]
|
||
- `mockPermissions`:24 项权限点
|
||
- `mockRoles`:4 个角色(admin/teacher/student/parent)
|
||
- `mockUsers`:6 个用户
|
||
- `mockViewports`:7 个视口配置
|
||
- `mockOrganization`:12 节点(school/grade/class)
|
||
- `mockClasses` / `mockTeachers` / `mockStudents`:6/6/5 条
|
||
- `mockAuditLogs`:5 条审计日志
|
||
- `mockDashboard`:仪表盘聚合数据
|
||
- `mockSystemSettings`:学校设置
|
||
|
||
### 8.3 WebSocket mock(`src/hooks/use-websocket.ts`)
|
||
|
||
MSW 不拦截 WebSocket,用定时器模拟推送:
|
||
|
||
- mock 模式:30s 定时器推送 1 条通知(轮换 3 类:audit_alert/abnormal_login/system_error)
|
||
- 真实模式:连接 `NEXT_PUBLIC_WS_URL`(push-gateway `GET /ws`)
|
||
|
||
### 8.4 环境切换
|
||
|
||
`NEXT_PUBLIC_API_MOCKING=enabled` 启用 MSW;`disabled` 使用真实后端。
|
||
|
||
## 9. 设计令牌三层(`src/app/globals.css`)
|
||
|
||
### 9.1 Layer 1 Primitive(原始色板)
|
||
|
||
```css
|
||
:root {
|
||
--paper: 0 0% 100%; /* 白纸 */
|
||
--ink: 30 10% 15%; /* 墨色 */
|
||
--ink-muted: 30 5% 45%;
|
||
--rule: 30 10% 90%; /* 分隔线 */
|
||
--accent: 210 60% 40%; /* 强调色 */
|
||
--accent-light: 210 60% 95%;
|
||
--danger: 0 70% 50%;
|
||
--success: 140 50% 40%;
|
||
--warning: 40 80% 50%;
|
||
}
|
||
```
|
||
|
||
### 9.2 Layer 2 Semantic
|
||
|
||
```css
|
||
:root {
|
||
--bg-paper: hsl(var(--paper));
|
||
--color-ink: hsl(var(--ink));
|
||
--color-ink-muted: hsl(var(--ink-muted));
|
||
--color-rule: hsl(var(--rule));
|
||
--color-accent: hsl(var(--accent));
|
||
--color-accent-light: hsl(var(--accent-light));
|
||
--color-danger: hsl(var(--danger));
|
||
--color-success: hsl(var(--success));
|
||
--color-warning: hsl(var(--warning));
|
||
}
|
||
```
|
||
|
||
### 9.3 Layer 3 Tailwind Theme(`tailwind.config.js`)
|
||
|
||
paper / ink / accent / rule / danger / success / warning 色板映射为 `bg-*` / `text-*` 类。
|
||
|
||
### 9.4 强制规则(project_rules §3.10)
|
||
|
||
- 禁止 `#hex` 字面量
|
||
- 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
|
||
- 禁止 `font-size: Npx`
|
||
- 禁止 Tailwind 任意值 `w-[Npx]`
|
||
|
||
## 10. 与其他模块的交互点(契约清单)
|
||
|
||
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
|
||
| ---- | --------------------- | --------------------- | ---------------------------------------- | ---------------------------- | ---- |
|
||
| 调用 | api-gateway | HTTP/GraphQL | `POST /api/admin/graphql`(经 rewrites) | 全部业务 GraphQL 查询 | P6+ |
|
||
| 调用 | api-gateway | HTTP | `POST /api/auth/login` | 登录(standalone 模式) | P6+ |
|
||
| 调用 | push-gateway | WebSocket | `GET /ws` | 实时通知(ARB-006) | P6+ |
|
||
| 消费 | teacher-bff | GraphQL(经 Gateway) | admin 命名空间 18 operations | 全部业务数据 | P6+ |
|
||
| 消费 | iam(经 teacher-bff) | GraphQL(间接) | 用户/角色/权限/视口 CRUD + AuditEvent | 管理数据 | P6+ |
|
||
| 依赖 | coord 维护 | — | `packages/contracts` | Permissions 常量 + 类型 | P6+ |
|
||
| 依赖 | coord 维护 | — | `packages/shared-proto` | TS 类型(仅 contracts 部分) | P6+ |
|
||
| 依赖 | teacher-portal Shell | — | Shell 暴露的 GraphQLProvider + 共享依赖 | 宿主环境(MF 模式) | P6+ |
|
||
|
||
> **proto 不直接消费**:前端不调用 gRPC,teacher-bff 把 gRPC 聚合为 GraphQL 暴露给前端。
|
||
|
||
## 11. 风险与假设
|
||
|
||
### 11.1 假设
|
||
|
||
1. **假设 teacher-bff admin 命名空间 schema 就绪**(ARB-001,contract §2.4 18 operations):若 ai03 未补齐,admin-portal 用 MSW mock 开发,上游就绪后切换。
|
||
2. **假设 teacher-portal Shell 已就绪**:MF 模式依赖 Shell 暴露 GraphQLProvider + 共享依赖。standalone 模式可独立运行。
|
||
3. **假设 push-gateway WebSocket 就绪**(ARB-006):若未就绪,用 mock-socket + 30s 定时器模拟。
|
||
4. _\*假设 `packages/contracts` ADMIN_* 权限点常量就绪_*(coord 维护):admin-portal `src/lib/permissions.ts` 引用,若 coord 未建立则本地定义。
|
||
|
||
### 11.2 技术风险
|
||
|
||
| 风险 | 影响 | 缓解 |
|
||
| ----------------------------- | -------------------------------------- | --------------------------------------------------------- |
|
||
| Shell 未就绪阻塞 admin | MF 模式无法挂载 | standalone 模式自建 Provider 链,独立可运行 |
|
||
| teacher-bff admin schema 缺失 | GraphQL 查询失败 | MSW mock 全覆盖 18 operations,上游就绪后切换 |
|
||
| urql 类型兼容性 | TypeScript 编译错误 | `as unknown as` 从 unknown 转换(见 §4.1) |
|
||
| ESLint 9 兼容性 | `@next/eslint-plugin-next` 14.x 不兼容 | 移除该插件,用 jsx-a11y + tseslint 替代 |
|
||
| WebSocket mock 不真实 | 开发期通知体验与生产不一致 | 30s 定时器 + 3 类通知轮换,接近真实推送频率 |
|
||
| 权限缓存陈旧 | 角色变更后前端仍用旧权限 | 登录时拉取最新权限;admin-portal 本身是管理端,可主动刷新 |
|
||
|
||
## 12. 端口矩阵
|
||
|
||
| 端 | dev 端口 | 生产端口 | 备注 |
|
||
| -------------------- | -------- | -------- | ---------------------- |
|
||
| admin-portal | 4003 | 4003 | Remote,挂载到 Shell |
|
||
| teacher-portal Shell | 4000 | 4000 | Shell 宿主 |
|
||
| api-gateway | 8080 | 8080 | GraphQL 代理 |
|
||
| push-gateway | 8081 | 8081 | WebSocket |
|
||
| teacher-bff | 3003 | 3003 | GraphQL admin 命名空间 |
|
||
|
||
## 13. 实施路线(已完成)
|
||
|
||
### P2-P6 全阶段交付(已完成)
|
||
|
||
1. ✅ **P2 骨架 Phase 1**:配置文件 + 设计令牌 + 类型定义
|
||
2. ✅ **P2 骨架 Phase 2**:GraphQL client + auth + permissions + providers + AdminShell + MF Remote 入口 + standalone 壳
|
||
3. ✅ **P2 骨架 Phase 3**:MSW mock(handlers + fixtures + browser)+ mock-socket WebSocket mock
|
||
4. ✅ **P3 业务**:用户管理(useUsers/useUser/CRUD hooks + UserManagementTable + /admin/users 页面)
|
||
5. ✅ **P3 业务**:角色权限矩阵 + 权限点管理 + 视口配置
|
||
6. ✅ **P4 业务**:组织管理 + 学校设置 + 班级/教师/学生全局管理
|
||
7. ✅ **P5 业务**:审计日志 + WebSocket 实时通知 + 管理仪表盘
|
||
8. ✅ **P6 硬化**:A11y + Web Vitals + Dockerfile + /api/health + /api/ready + 质量校验
|
||
9. ✅ **文档回写**:01/02 文档对齐仲裁结果 + known-issues §2.16 + arch:scan
|
||
|
||
### 验收标准
|
||
|
||
- ✅ 所有 11 个路由页面可访问,权限校验生效
|
||
- ✅ MF Remote 可被 Shell 加载(暴露 `./AdminApp`)
|
||
- ✅ standalone 模式独立可运行(自建 Provider 链 + mock 登录)
|
||
- ✅ GraphQL 18 operations 全部实现(MSW mock + 真实切换)
|
||
- ✅ WebSocket 实时通知(mock 模式 30s 定时推送)
|
||
- ✅ Web Vitals 采集(LCP/CLS/FCP/INP/TTFB → sendBeacon)
|
||
- ✅ A11y WCAG 2.2 AA(eslint-plugin-jsx-a11y error 级 + skip-link + focus-visible)
|
||
- ✅ `/api/health` + `/api/ready` 返回 200
|
||
- ✅ Dockerfile 多阶段构建(node:22-alpine,standalone 输出,EXPOSE 4003)
|
||
- ✅ `pnpm run typecheck` + `pnpm run lint` 通过(0 errors)
|
||
|
||
---
|
||
|
||
**AI Agent**: ai16 (admin-portal remote)
|
||
**Branch**: admin-portal-arbitration-done-TaFkyb
|
||
**Coordinator**: coord-ai
|