feat(admin-portal): 完整实现 admin-portal 管理端微前端

包含 src 全部实现、Dockerfile、配置文件等
This commit is contained in:
SpecialX
2026-07-10 19:09:12 +08:00
parent 02d09c47fa
commit b3511910d1
69 changed files with 11955 additions and 693 deletions

View File

@@ -0,0 +1,79 @@
/**
* 认证工具token 存储 + 路由保护
*
* 仲裁ARB-002 复用 Shell /loginadmin-portal 不自行实现登录页
* 开发期standalone 壳提供 mock 登录MSW 拦截 /api/auth/login
*/
const TOKEN_KEY = "edu_access_token";
const USER_KEY = "edu_user_info";
import type { CurrentUser } from "@/types/view-models";
export function getToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(TOKEN_KEY);
}
export function setToken(token: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(TOKEN_KEY, token);
}
export function clearToken(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
export function getUser(): CurrentUser | null {
if (typeof window === "undefined") return null;
const raw = localStorage.getItem(USER_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as CurrentUser;
} catch {
return null;
}
}
export function setUser(user: CurrentUser): void {
if (typeof window === "undefined") return;
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
export function isAuthenticated(): boolean {
return getToken() !== null;
}
/** 调用登录 APIMSW 拦截 / api-gateway 代理到 iam */
export async function login(
email: string,
password: string,
): Promise<{ user: CurrentUser; token: string }> {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const json = (await res.json()) as {
success: boolean;
data?: { user: CurrentUser; tokens: { accessToken: string } };
error?: { message: string };
};
if (!json.success || !json.data) {
throw new Error(json.error?.message || "登录失败");
}
const user = json.data.user;
const token = json.data.tokens.accessToken;
setToken(token);
setUser(user);
return { user, token };
}
export function logout(): void {
clearToken();
if (typeof window !== "undefined") {
window.location.href = "/login";
}
}

View File

@@ -0,0 +1,333 @@
/**
* GraphQL client + operations
*
* 仲裁ARB-001 admin 命名空间 GraphQL + ARB-002 复用 Shell GraphQL client 单例
* 端点POST /api/admin/graphql经 api-gateway 代理到 teacher-bff admin 命名空间)
*/
import { createClient, fetchExchange, type Client } from "@urql/core";
const GRAPHQL_ENDPOINT = "/api/admin/graphql";
/** 创建 urql GraphQL clientstandalone 模式自建MF 模式复用 Shell 单例) */
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}` } } : {};
},
});
}
// ─── GraphQL Operationscontract §2.4 ───────────────────────────
export const CURRENT_USER_QUERY = `
query CurrentUser {
currentUser {
id
email
name
roles
permissions
dataScope
schoolId
schoolName
}
}
` as const;
export const ADMIN_USERS_QUERY = `
query AdminUsers($filter: AdminUserFilterInput) {
adminUsers(filter: $filter) {
items {
id
email
name
roles { id name code }
status
dataScope
organizationId
schoolName
lastLoginAt
createdAt
updatedAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const ADMIN_USER_QUERY = `
query AdminUser($id: ID!) {
adminUser(id: $id) {
id
email
name
roles { id name code }
status
dataScope
organizationId
schoolName
lastLoginAt
createdAt
updatedAt
}
}
` as const;
export const CREATE_USER_MUTATION = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
name
status
}
}
` as const;
export const UPDATE_USER_MUTATION = `
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
email
name
status
}
}
` as const;
export const TOGGLE_USER_STATUS_MUTATION = `
mutation ToggleUserStatus($id: ID!, $status: String!) {
toggleUserStatus(id: $id, status: $status) {
id
status
}
}
` as const;
export const ADMIN_ROLES_QUERY = `
query AdminRoles {
adminRoles {
id
name
code
description
permissions { id code name resource action }
userCount
dataScope
isSystem
createdAt
updatedAt
}
}
` as const;
export const CREATE_ROLE_MUTATION = `
mutation CreateRole($input: CreateRoleInput!) {
createRole(input: $input) { id name code }
}
` as const;
export const UPDATE_ROLE_PERMISSIONS_MUTATION = `
mutation UpdateRolePermissions($roleId: ID!, $permissionCodes: [String!]!) {
updateRolePermissions(roleId: $roleId, permissionCodes: $permissionCodes) {
id
permissions { id code }
}
}
` as const;
export const ADMIN_PERMISSIONS_QUERY = `
query AdminPermissions {
adminPermissions {
id
code
name
description
resource
action
isSystem
}
}
` as const;
export const ADMIN_VIEWPORTS_QUERY = `
query AdminViewports($scope: String) {
adminViewports(scope: $scope) {
id
key
label
route
icon
sortOrder
requiredPermission
scope
isVisible
}
}
` as const;
export const UPDATE_VIEWPORT_MUTATION = `
mutation UpdateViewport($id: ID!, $input: UpdateViewportInput!) {
updateViewport(id: $id, input: $input) {
id
key
label
sortOrder
isVisible
}
}
` as const;
export const ADMIN_ORGANIZATION_QUERY = `
query AdminOrganization($parentId: ID) {
adminOrganization(parentId: $parentId) {
id
name
type
parentId
childrenCount
sortOrder
}
}
` as const;
export const ADMIN_CLASSES_QUERY = `
query AdminClasses($filter: AdminClassFilterInput) {
adminClasses(filter: $filter) {
items {
id
name
gradeName
schoolName
headTeacherName
studentCount
createdAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const ADMIN_TEACHERS_QUERY = `
query AdminTeachers($filter: AdminTeacherFilterInput) {
adminTeachers(filter: $filter) {
items {
id
email
name
schoolName
subjects
classCount
status
lastLoginAt
}
total
page
pageSize
hasNext
}
}
` as const;
export const ADMIN_STUDENTS_QUERY = `
query AdminStudents($filter: AdminStudentFilterInput) {
adminStudents(filter: $filter) {
items {
id
email
name
className
gradeName
schoolName
guardianName
guardianPhone
status
}
total
page
pageSize
hasNext
}
}
` as const;
export const AUDIT_LOGS_QUERY = `
query AuditLogs($filter: AuditLogFilterInput) {
auditLogs(filter: $filter) {
items {
id
eventId
occurredAt
actorUserId
actorName
action
resourceType
resourceId
ip
userAgent
beforeState
afterState
traceId
}
total
page
pageSize
hasNext
}
}
` as const;
export const ADMIN_DASHBOARD_QUERY = `
query AdminDashboard {
adminDashboard {
totalTeachers
totalStudents
totalClasses
totalSchools
schoolAvgScore
activeUsersToday
auditEventsToday
trend { date teachers students avgScore }
serviceHealth { serviceName status latencyMs }
}
}
` as const;
export const SYSTEM_SETTINGS_QUERY = `
query SystemSettings {
systemSettings {
schoolName
schoolCode
academicYear
semester
contactEmail
contactPhone
address
timezone
locale
}
}
` as const;
export const UPDATE_SYSTEM_SETTINGS_MUTATION = `
mutation UpdateSystemSettings($input: SystemSettingsInput!) {
updateSystemSettings(input: $input) {
schoolName
schoolCode
academicYear
semester
}
}
` as const;

View File

@@ -0,0 +1,183 @@
/**
* 轻量 i18ncontract §1.7 命名空间admin.* / iam.error.* / bff.error.* / gateway.error.* / network.error.*
*
* 维护者ai16
* 仲裁F4 i18n key 模式 error.{{service}}.{{code_snake_case}}
*/
type MessageMap = Record<string, string>;
const messages: MessageMap = {
// 导航
"admin.nav.dashboard": "管理仪表盘",
"admin.nav.users": "用户管理",
"admin.nav.roles": "角色权限",
"admin.nav.permissions": "权限点",
"admin.nav.viewports": "视口配置",
"admin.nav.organization": "组织管理",
"admin.nav.system": "学校设置",
"admin.nav.classes": "班级管理",
"admin.nav.teachers": "教师管理",
"admin.nav.students": "学生管理",
"admin.nav.auditLogs": "审计日志",
// 通用
"admin.common.search": "搜索",
"admin.common.filter": "筛选",
"admin.common.create": "新建",
"admin.common.edit": "编辑",
"admin.common.delete": "删除",
"admin.common.save": "保存",
"admin.common.cancel": "取消",
"admin.common.confirm": "确认",
"admin.common.export": "导出",
"admin.common.actions": "操作",
"admin.common.status": "状态",
"admin.common.all": "全部",
"admin.common.loading": "加载中...",
"admin.common.empty": "暂无数据",
"admin.common.error": "加载失败",
"admin.common.retry": "重试",
"admin.common.page": "页",
"admin.common.total": "共",
"admin.common.records": "条",
"admin.common.active": "启用",
"admin.common.disabled": "禁用",
"admin.common.locked": "锁定",
"admin.common.yes": "是",
"admin.common.no": "否",
"admin.common.name": "姓名",
"admin.common.email": "邮箱",
"admin.common.role": "角色",
"admin.common.createdAt": "创建时间",
"admin.common.updatedAt": "更新时间",
"admin.common.lastLoginAt": "最后登录",
"admin.common.dataScope": "数据范围",
"admin.common.school": "学校",
"admin.common.grade": "年级",
"admin.common.class": "班级",
// 仪表盘
"admin.dashboard.title": "管理仪表盘",
"admin.dashboard.totalTeachers": "教师总数",
"admin.dashboard.totalStudents": "学生总数",
"admin.dashboard.totalClasses": "班级总数",
"admin.dashboard.totalSchools": "学校总数",
"admin.dashboard.schoolAvgScore": "全校平均分",
"admin.dashboard.activeUsersToday": "今日活跃用户",
"admin.dashboard.auditEventsToday": "今日审计事件",
"admin.dashboard.trend": "趋势",
"admin.dashboard.serviceHealth": "服务健康",
// 用户管理
"admin.users.title": "用户管理",
"admin.users.new": "新建用户",
"admin.users.edit": "编辑用户",
"admin.users.deleteConfirm": "确认删除该用户?",
"admin.users.toggleStatus": "切换状态",
"admin.users.assignRole": "分配角色",
"admin.users.password": "密码",
"admin.users.roles": "角色",
"admin.users.organization": "所属组织",
// 角色权限
"admin.roles.title": "角色权限管理",
"admin.roles.new": "新建角色",
"admin.roles.permissionMatrix": "权限矩阵",
"admin.roles.systemRole": "系统预置",
"admin.roles.userCount": "用户数",
"admin.roles.deleteConfirm": "确认删除该角色?",
"admin.roles.cannotDeleteSystem": "系统预置角色不可删除",
"admin.roles.cannotDeleteWithUsers": "角色下仍有用户,不可删除",
// 权限点
"admin.permissions.title": "权限点管理",
"admin.permissions.resource": "资源",
"admin.permissions.action": "操作",
"admin.permissions.code": "权限码",
// 视口配置
"admin.viewports.title": "视口配置",
"admin.viewports.key": "标识",
"admin.viewports.label": "标签",
"admin.viewports.route": "路由",
"admin.viewports.sortOrder": "排序",
"admin.viewports.requiredPermission": "所需权限",
"admin.viewports.scope": "作用域",
"admin.viewports.visible": "是否可见",
"admin.viewports.dragHint": "拖拽调整排序",
// 组织管理
"admin.organization.title": "组织管理",
"admin.organization.school": "学校",
"admin.organization.expand": "展开",
"admin.organization.collapse": "折叠",
// 学校设置
"admin.system.title": "学校设置",
"admin.system.schoolName": "学校名称",
"admin.system.schoolCode": "学校代码",
"admin.system.academicYear": "学年",
"admin.system.semester": "学期",
"admin.system.contactEmail": "联系邮箱",
"admin.system.contactPhone": "联系电话",
"admin.system.address": "地址",
"admin.system.timezone": "时区",
"admin.system.locale": "语言",
"admin.system.saveSuccess": "设置已保存",
// 班级管理
"admin.classes.title": "班级管理",
"admin.classes.headTeacher": "班主任",
"admin.classes.studentCount": "学生数",
// 教师管理
"admin.teachers.title": "教师管理",
"admin.teachers.subjects": "任教科目",
"admin.teachers.classCount": "班级数",
// 学生管理
"admin.students.title": "学生管理",
"admin.students.guardian": "家长",
"admin.students.guardianPhone": "家长电话",
// 审计日志
"admin.auditLogs.title": "审计日志",
"admin.auditLogs.actor": "操作人",
"admin.auditLogs.action": "操作",
"admin.auditLogs.resource": "资源",
"admin.auditLogs.resourceId": "资源ID",
"admin.auditLogs.ip": "IP地址",
"admin.auditLogs.time": "时间",
"admin.auditLogs.dateRange": "时间范围",
"admin.auditLogs.exportCsv": "导出CSV",
"admin.auditLogs.traceId": "追踪ID",
// 通知
"admin.notification.title": "通知中心",
"admin.notification.auditAlert": "审计告警",
"admin.notification.abnormalLogin": "异常登录",
"admin.notification.systemError": "系统异常",
// 退出
"admin.auth.logout": "退出登录",
"admin.auth.welcome": "Edu 管理端",
};
/**
* 翻译函数
* @param key i18n key如 "admin.nav.dashboard"
* @param params 模板参数(如 { count: 5 },替换 {{count}}
*/
export function t(
key: string,
params?: Record<string, string | number>,
): string {
let msg = messages[key] ?? key;
if (params) {
for (const [k, v] of Object.entries(params)) {
msg = msg.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
}
}
return msg;
}

View File

@@ -0,0 +1,78 @@
/**
* admin-portal 权限点常量
*
* 仲裁裁决ISSUE-046 §5.3 / ISSUE-045 §5.2
* - admin-portal 权限点统一加 ADMIN_ 前缀
* - 跨服务资源保留原前缀(如 IAM_USER_READ
* - 错误码前缀 ADMIN_合并 SCHOOL_ / ORG_ 入 ADMIN_
*
* 来源packages/contracts/src/permissions.tscoord 维护)
* admin-portal 引用,不硬编码
*/
/** admin 自身资源权限点ADMIN_ 前缀) */
export const ADMIN_PERMISSIONS = {
DASHBOARD_VIEW: "ADMIN_DASHBOARD_VIEW",
SYSTEM_MANAGE: "ADMIN_SYSTEM_MANAGE",
AUDIT_READ: "ADMIN_AUDIT_READ",
CLASS_READ: "ADMIN_CLASS_READ",
CLASS_MANAGE: "ADMIN_CLASS_MANAGE",
TEACHER_READ: "ADMIN_TEACHER_READ",
TEACHER_MANAGE: "ADMIN_TEACHER_MANAGE",
STUDENT_READ: "ADMIN_STUDENT_READ",
STUDENT_MANAGE: "ADMIN_STUDENT_MANAGE",
ORG_MANAGE: "ADMIN_ORG_MANAGE",
SCHOOL_READ: "ADMIN_SCHOOL_READ",
SCHOOL_MANAGE: "ADMIN_SCHOOL_MANAGE",
MONITORING_VIEW: "ADMIN_MONITORING_VIEW",
} as const;
/** 跨服务资源权限点保留原前缀iam 域) */
export const IAM_PERMISSIONS = {
USER_READ: "IAM_USER_READ",
USER_CREATE: "IAM_USER_CREATE",
USER_UPDATE: "IAM_USER_UPDATE",
USER_DELETE: "IAM_USER_DELETE",
ROLE_READ: "IAM_ROLE_READ",
ROLE_CREATE: "IAM_ROLE_CREATE",
ROLE_UPDATE: "IAM_ROLE_UPDATE",
ROLE_DELETE: "IAM_ROLE_DELETE",
PERMISSION_READ: "IAM_PERMISSION_READ",
VIEWPORT_READ: "IAM_VIEWPORT_READ",
VIEWPORT_UPDATE: "IAM_VIEWPORT_UPDATE",
} as const;
/** 所有权限点合集 */
export const PERMISSIONS = {
...ADMIN_PERMISSIONS,
...IAM_PERMISSIONS,
} as const;
export type PermissionCode = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
/** admin-portal 视口路由权限映射contract §1.2 */
export const ROUTE_PERMISSIONS: Record<string, string> = {
"/admin/dashboard": ADMIN_PERMISSIONS.DASHBOARD_VIEW,
"/admin/users": IAM_PERMISSIONS.USER_READ,
"/admin/roles": IAM_PERMISSIONS.ROLE_READ,
"/admin/permissions": IAM_PERMISSIONS.PERMISSION_READ,
"/admin/viewports": IAM_PERMISSIONS.VIEWPORT_READ,
"/admin/organization": ADMIN_PERMISSIONS.ORG_MANAGE,
"/admin/system": ADMIN_PERMISSIONS.SYSTEM_MANAGE,
"/admin/classes": ADMIN_PERMISSIONS.CLASS_READ,
"/admin/teachers": ADMIN_PERMISSIONS.TEACHER_READ,
"/admin/students": ADMIN_PERMISSIONS.STUDENT_READ,
"/admin/audit-logs": ADMIN_PERMISSIONS.AUDIT_READ,
};
/** admin-portal 错误码前缀ISSUE-045 §5.2 */
export const ADMIN_ERROR_PREFIX = "ADMIN_";
/** 消费的错误码前缀contract §2.5 */
export const CONSUMED_ERROR_PREFIXES = [
"IAM_",
"BFF_TEACHER_",
"GW_",
"NETWORK_",
"ADMIN_",
] as const;

View File

@@ -0,0 +1,43 @@
/**
* Web Vitals 采集A11y + 性能)
*
* 仲裁:所有前端应用必须采集 Web Vitals
* 端点NEXT_PUBLIC_WEB_VITALS_ENDPOINT默认 /api/admin/web-vitals
*/
import { onLCP, onCLS, onFCP, onINP, onTTFB } from "web-vitals";
type Metric = { name: string; value: number; rating: string; id: string };
const ENDPOINT =
process.env.NEXT_PUBLIC_WEB_VITALS_ENDPOINT ?? "/api/admin/web-vitals";
function sendMetric(metric: Metric): void {
if (typeof navigator === "undefined") return;
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
page: window.location.pathname,
ts: Date.now(),
});
// 使用 sendBeacon 避免阻塞卸载
if (navigator.sendBeacon) {
navigator.sendBeacon(ENDPOINT, body);
} else {
void fetch(ENDPOINT, { method: "POST", body, keepalive: true }).catch(
() => {
// 忽略上报失败
},
);
}
}
export function initWebVitals(): void {
if (typeof window === "undefined") return;
onLCP((m) => sendMetric(m as unknown as Metric));
onCLS((m) => sendMetric(m as unknown as Metric));
onFCP((m) => sendMetric(m as unknown as Metric));
onINP((m) => sendMetric(m as unknown as Metric));
onTTFB((m) => sendMetric(m as unknown as Metric));
}