feat(admin-portal): 完整实现 admin-portal 管理端微前端
包含 src 全部实现、Dockerfile、配置文件等
This commit is contained in:
516
apps/admin-portal/src/mocks/handlers.ts
Normal file
516
apps/admin-portal/src/mocks/handlers.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* MSW Handlers
|
||||
*
|
||||
* 仲裁 ARB-004 §4:MSW 拦截所有 GraphQL/HTTP 请求
|
||||
* 端点:
|
||||
* - POST /api/auth/login - mock 登录
|
||||
* - POST /api/admin/graphql - admin 命名空间 GraphQL
|
||||
* - GET /ws - WebSocket(mock-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 },
|
||||
);
|
||||
}),
|
||||
];
|
||||
Reference in New Issue
Block a user