feat(admin-portal): 完整实现 admin-portal 管理端微前端
包含 src 全部实现、Dockerfile、配置文件等
This commit is contained in:
79
apps/admin-portal/src/lib/auth.ts
Normal file
79
apps/admin-portal/src/lib/auth.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 认证工具:token 存储 + 路由保护
|
||||
*
|
||||
* 仲裁:ARB-002 复用 Shell /login,admin-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;
|
||||
}
|
||||
|
||||
/** 调用登录 API(MSW 拦截 / 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";
|
||||
}
|
||||
}
|
||||
333
apps/admin-portal/src/lib/graphql-client.ts
Normal file
333
apps/admin-portal/src/lib/graphql-client.ts
Normal 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 client(standalone 模式自建,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 Operations(contract §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;
|
||||
183
apps/admin-portal/src/lib/i18n.ts
Normal file
183
apps/admin-portal/src/lib/i18n.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 轻量 i18n(contract §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;
|
||||
}
|
||||
78
apps/admin-portal/src/lib/permissions.ts
Normal file
78
apps/admin-portal/src/lib/permissions.ts
Normal 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.ts(coord 维护)
|
||||
* 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;
|
||||
43
apps/admin-portal/src/lib/web-vitals.ts
Normal file
43
apps/admin-portal/src/lib/web-vitals.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user