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,516 @@
/**
* MSW Handlers
*
* 仲裁 ARB-004 §4MSW 拦截所有 GraphQL/HTTP 请求
* 端点:
* - POST /api/auth/login - mock 登录
* - POST /api/admin/graphql - admin 命名空间 GraphQL
* - GET /ws - WebSocketmock-socket 在 use-websocket.ts 中处理)
*/
import { graphql, http, HttpResponse } from "msw";
import {
mockCurrentUser,
mockUsers,
mockRoles,
mockPermissions,
mockViewports,
mockOrganization,
mockClasses,
mockTeachers,
mockStudents,
mockAuditLogs,
mockDashboard,
mockSystemSettings,
genId,
} from "./fixtures";
import type {
UserViewModel,
RoleViewModel,
ViewportConfigViewModel,
PaginatedResult,
ListFilter,
} from "@/types/view-models";
const BASE_URL = "";
// 内存中可变副本(支持 mock 增删改)
const db = {
users: [...mockUsers] as UserViewModel[],
roles: [...mockRoles] as RoleViewModel[],
viewports: [...mockViewports] as ViewportConfigViewModel[],
permissions: [...mockPermissions],
organization: [...mockOrganization],
classes: [...mockClasses],
teachers: [...mockTeachers],
students: [...mockStudents],
auditLogs: [...mockAuditLogs],
systemSettings: { ...mockSystemSettings },
};
function paginate<T>(
items: T[],
filter: Partial<ListFilter>,
): PaginatedResult<T> {
const page = filter.page ?? 1;
const pageSize = filter.pageSize ?? 20;
const start = (page - 1) * pageSize;
const end = start + pageSize;
const sliced = items.slice(start, end);
return {
items: sliced,
total: items.length,
page,
pageSize,
hasNext: end < items.length,
};
}
function recordAudit(
action: string,
resourceType: string,
resourceId: string,
afterState?: string,
): void {
db.auditLogs.unshift({
id: genId("al"),
eventId: genId("evt"),
occurredAt: Date.now(),
actorUserId: mockCurrentUser.id,
actorName: mockCurrentUser.name,
action,
resourceType,
resourceId,
ip: "127.0.0.1",
userAgent: "MSW-Mock",
beforeState: null,
afterState: afterState ?? null,
traceId: genId("trace"),
});
}
export const handlers = [
// ─── 认证 ────────────────────────────────────────────
http.post(`${BASE_URL}/api/auth/login`, async ({ request }) => {
const body = (await request.json()) as { email: string; password: string };
if (body.email === "admin@edu.test" && body.password === "admin123") {
return HttpResponse.json({
success: true,
data: {
user: mockCurrentUser,
tokens: { accessToken: "mock-jwt-token-admin-001" },
},
});
}
return HttpResponse.json(
{
success: false,
error: { message: "邮箱或密码错误" },
},
{ status: 401 },
);
}),
// ─── GraphQL admin 命名空间 ──────────────────────────
graphql.operation(({ query, variables }) => {
const operationName =
(query.match(/^\s*(query|mutation)\s+(\w+)/) || [])[2] ?? "";
// ── currentUser ──
if (operationName === "CurrentUser") {
return HttpResponse.json({ data: { currentUser: mockCurrentUser } });
}
// ── adminUsers ──
if (operationName === "AdminUsers") {
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
let items = [...db.users];
if (filter.search) {
const q = filter.search.toLowerCase();
items = items.filter(
(u) =>
u.name.toLowerCase().includes(q) ||
u.email.toLowerCase().includes(q),
);
}
if (filter.status && filter.status !== "all") {
items = items.filter((u) => u.status === filter.status);
}
const result = paginate(items, filter);
return HttpResponse.json({ data: { adminUsers: result } });
}
// ── adminUser ──
if (operationName === "AdminUser") {
const id = variables.id as string;
const user = db.users.find((u) => u.id === id) ?? null;
return HttpResponse.json({ data: { adminUser: user } });
}
// ── createUser ──
if (operationName === "CreateUser") {
const input = variables.input as {
email: string;
name: string;
password: string;
roleIds?: string[];
dataScope?: string;
organizationId?: string;
};
const roles = db.roles.filter((r) => input.roleIds?.includes(r.id));
const newUser: UserViewModel = {
id: genId("u"),
email: input.email,
name: input.name,
roles,
status: "active",
dataScope: (input.dataScope as UserViewModel["dataScope"]) ?? "SCHOOL",
organizationId: input.organizationId ?? null,
schoolName: "Edu 实验学校",
lastLoginAt: null,
createdAt: Date.now(),
updatedAt: Date.now(),
};
db.users.push(newUser);
recordAudit(
"USER_CREATE",
"iam.user",
newUser.id,
JSON.stringify({ email: newUser.email, name: newUser.name }),
);
return HttpResponse.json({
data: {
createUser: {
id: newUser.id,
email: newUser.email,
name: newUser.name,
status: newUser.status,
},
},
});
}
// ── updateUser ──
if (operationName === "UpdateUser") {
const id = variables.id as string;
const input = variables.input as {
name?: string;
email?: string;
roleIds?: string[];
dataScope?: string;
organizationId?: string;
};
const user = db.users.find((u) => u.id === id);
if (!user) {
return HttpResponse.json(
{ errors: [{ message: "用户不存在" }] },
{ status: 404 },
);
}
if (input.name) user.name = input.name;
if (input.email) user.email = input.email;
if (input.roleIds)
user.roles = db.roles.filter((r) => input.roleIds!.includes(r.id));
if (input.dataScope)
user.dataScope = input.dataScope as UserViewModel["dataScope"];
if (input.organizationId !== undefined)
user.organizationId = input.organizationId;
user.updatedAt = Date.now();
recordAudit("USER_UPDATE", "iam.user", user.id, JSON.stringify(input));
return HttpResponse.json({
data: {
updateUser: {
id: user.id,
email: user.email,
name: user.name,
status: user.status,
},
},
});
}
// ── toggleUserStatus ──
if (operationName === "ToggleUserStatus") {
const id = variables.id as string;
const newStatus = variables.status as UserViewModel["status"];
const user = db.users.find((u) => u.id === id);
if (!user) {
return HttpResponse.json(
{ errors: [{ message: "用户不存在" }] },
{ status: 404 },
);
}
const before = user.status;
user.status = newStatus;
user.updatedAt = Date.now();
recordAudit(
"USER_TOGGLE_STATUS",
"iam.user",
user.id,
JSON.stringify({ before, after: newStatus }),
);
return HttpResponse.json({
data: { toggleUserStatus: { id: user.id, status: user.status } },
});
}
// ── adminRoles ──
if (operationName === "AdminRoles") {
return HttpResponse.json({ data: { adminRoles: db.roles } });
}
// ── createRole ──
if (operationName === "CreateRole") {
const input = variables.input as {
name: string;
code: string;
description?: string;
dataScope?: string;
};
const newRole: RoleViewModel = {
id: genId("r"),
name: input.name,
code: input.code,
description: input.description ?? "",
permissions: [],
userCount: 0,
dataScope: (input.dataScope as RoleViewModel["dataScope"]) ?? "SCHOOL",
isSystem: false,
createdAt: Date.now(),
updatedAt: Date.now(),
};
db.roles.push(newRole);
recordAudit(
"ROLE_CREATE",
"iam.role",
newRole.id,
JSON.stringify({ name: newRole.name, code: newRole.code }),
);
return HttpResponse.json({
data: {
createRole: {
id: newRole.id,
name: newRole.name,
code: newRole.code,
},
},
});
}
// ── updateRolePermissions ──
if (operationName === "UpdateRolePermissions") {
const roleId = variables.roleId as string;
const codes = variables.permissionCodes as string[];
const role = db.roles.find((r) => r.id === roleId);
if (!role) {
return HttpResponse.json(
{ errors: [{ message: "角色不存在" }] },
{ status: 404 },
);
}
role.permissions = db.permissions.filter((p) => codes.includes(p.code));
role.updatedAt = Date.now();
recordAudit(
"ROLE_UPDATE",
"iam.role",
role.id,
JSON.stringify({ permissionCodes: codes }),
);
return HttpResponse.json({
data: {
updateRolePermissions: {
id: role.id,
permissions: role.permissions.map((p) => ({
id: p.id,
code: p.code,
})),
},
},
});
}
// ── adminPermissions ──
if (operationName === "AdminPermissions") {
return HttpResponse.json({ data: { adminPermissions: db.permissions } });
}
// ── adminViewports ──
if (operationName === "AdminViewports") {
const scope = variables.scope as string | undefined;
const items = scope
? db.viewports.filter((v) => v.scope === scope)
: db.viewports;
return HttpResponse.json({ data: { adminViewports: items } });
}
// ── updateViewport ──
if (operationName === "UpdateViewport") {
const id = variables.id as string;
const input = variables.input as {
label?: string;
sortOrder?: number;
isVisible?: boolean;
requiredPermission?: string | null;
};
const vp = db.viewports.find((v) => v.id === id);
if (!vp) {
return HttpResponse.json(
{ errors: [{ message: "视口不存在" }] },
{ status: 404 },
);
}
if (input.label !== undefined) vp.label = input.label;
if (input.sortOrder !== undefined) vp.sortOrder = input.sortOrder;
if (input.isVisible !== undefined) vp.isVisible = input.isVisible;
if (input.requiredPermission !== undefined)
vp.requiredPermission = input.requiredPermission;
recordAudit(
"VIEWPORT_UPDATE",
"iam.viewport",
vp.id,
JSON.stringify(input),
);
return HttpResponse.json({
data: {
updateViewport: {
id: vp.id,
key: vp.key,
label: vp.label,
sortOrder: vp.sortOrder,
isVisible: vp.isVisible,
},
},
});
}
// ── adminOrganization ──
if (operationName === "AdminOrganization") {
const parentId = variables.parentId as string | null | undefined;
const items =
parentId === undefined || parentId === null
? db.organization.filter((n) => n.parentId === null)
: db.organization.filter((n) => n.parentId === parentId);
return HttpResponse.json({ data: { adminOrganization: items } });
}
// ── adminClasses ──
if (operationName === "AdminClasses") {
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
let items = [...db.classes];
if (filter.search) {
const q = filter.search.toLowerCase();
items = items.filter(
(c) =>
c.name.toLowerCase().includes(q) ||
c.gradeName.toLowerCase().includes(q),
);
}
if (filter.schoolId) {
// mock单一学校
}
const result = paginate(items, filter);
return HttpResponse.json({ data: { adminClasses: result } });
}
// ── adminTeachers ──
if (operationName === "AdminTeachers") {
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
let items = [...db.teachers];
if (filter.search) {
const q = filter.search.toLowerCase();
items = items.filter(
(t) =>
t.name.toLowerCase().includes(q) ||
t.email.toLowerCase().includes(q),
);
}
if (filter.status && filter.status !== "all") {
items = items.filter((t) => t.status === filter.status);
}
const result = paginate(items, filter);
return HttpResponse.json({ data: { adminTeachers: result } });
}
// ── adminStudents ──
if (operationName === "AdminStudents") {
const filter = (variables.filter ?? {}) as Partial<ListFilter>;
let items = [...db.students];
if (filter.search) {
const q = filter.search.toLowerCase();
items = items.filter(
(s) =>
s.name.toLowerCase().includes(q) ||
s.email.toLowerCase().includes(q),
);
}
if (filter.status && filter.status !== "all") {
items = items.filter((s) => s.status === filter.status);
}
const result = paginate(items, filter);
return HttpResponse.json({ data: { adminStudents: result } });
}
// ── auditLogs ──
if (operationName === "AuditLogs") {
const filter = (variables.filter ?? {}) as Partial<ListFilter> & {
action?: string;
actorUserId?: string;
};
let items = [...db.auditLogs];
if (filter.search) {
const q = filter.search.toLowerCase();
items = items.filter(
(l) =>
l.actorName.toLowerCase().includes(q) ||
l.action.toLowerCase().includes(q) ||
l.resourceType.toLowerCase().includes(q),
);
}
if (filter.action) {
items = items.filter((l) => l.action === filter.action);
}
if (filter.actorUserId) {
items = items.filter((l) => l.actorUserId === filter.actorUserId);
}
const result = paginate(items, filter);
return HttpResponse.json({ data: { auditLogs: result } });
}
// ── adminDashboard ──
if (operationName === "AdminDashboard") {
return HttpResponse.json({ data: { adminDashboard: mockDashboard } });
}
// ── systemSettings ──
if (operationName === "SystemSettings") {
return HttpResponse.json({ data: { systemSettings: db.systemSettings } });
}
// ── updateSystemSettings ──
if (operationName === "UpdateSystemSettings") {
const input = variables.input as Partial<typeof mockSystemSettings>;
Object.assign(db.systemSettings, input);
recordAudit(
"SYSTEM_SETTINGS_UPDATE",
"admin.system",
"settings",
JSON.stringify(input),
);
return HttpResponse.json({
data: {
updateSystemSettings: {
schoolName: db.systemSettings.schoolName,
schoolCode: db.systemSettings.schoolCode,
academicYear: db.systemSettings.academicYear,
semester: db.systemSettings.semester,
},
},
});
}
// ── 未匹配的 operation ──
return HttpResponse.json(
{ errors: [{ message: `未实现的 GraphQL operation: ${operationName}` }] },
{ status: 501 },
);
}),
];