feat(perm/frontend): PM 视角的权限管理界面与个人有效权限接入

- store/study 与 router 改为读取 /api-permissions/me 拉取当前用户在
  当前项目的有效权限,菜单与路由守卫据此判定,并在 PM 项目缺省时
  主动 ensureDefaultPmStudy。
- api/projectPermissions 暴露 fetchMyApiEndpointPermissions,并允许
  调用方关闭统一错误提示,便于在批量项目权限拉取场景下静默失败。
- 项目管理:Projects 页根据每个项目的有效权限决定项目入口的可点击
  状态,缺权限时禁用项目名跳转;ProjectDetail 据 setup_config 写权限
  渲染保存/回填/清空/发布按钮,并在无读取权限时提示并退出页面。
- 权限管理:PermissionManagement 切换为按角色提交,Admin 视角才能
  编辑 PM 行;按系统模块美化系统级权限展示并标注 PM 可访问项;项目
  成员表的角色下拉受 PM 等级约束,自定义角色入口下线。
- 监控:PermissionMonitoring/PermissionAccessLogs 增加 isAdmin 与
  showSecurityLog 入参,仅 ADMIN 才能看到底层安全日志与统计。
- ApiEndpointPermissions 表格根据当前用户角色禁用不可写行,避免
  误操作 PM 行;MODULE_LABELS 增加 setup_config 的中文展示。
- 测试同步覆盖以上行为:新版 router/Layout/Projects/PermissionManagement
  /PermissionAccessLogs/PermissionMonitoring/PermissionIpLocations 等
  组件均补足检测,并按需为相关组件提供 pinia + localStorage stub。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-25 12:38:54 +08:00
parent 747dd55225
commit 934d114a29
22 changed files with 501 additions and 145 deletions
+31 -4
View File
@@ -426,7 +426,7 @@ const routes: RouteRecordRaw[] = [
path: "projects/:projectId",
name: "AdminProjectDetail",
component: ProjectDetail,
meta: { title: TEXT.modules.adminProjects.detailTitle, requiresAdmin: true },
meta: { title: TEXT.modules.adminProjects.detailTitle, requiresProjectMember: true },
},
{
path: "projects/:projectId/members",
@@ -456,19 +456,19 @@ const routes: RouteRecordRaw[] = [
path: "permissions/system",
name: "AdminPermissionsSystem",
component: PermissionManagement,
meta: { title: "系统级权限", requiresAdmin: true },
meta: { title: "系统级权限", requiresProjectPm: true },
},
{
path: "permissions/project",
name: "AdminPermissionsProject",
component: PermissionManagement,
meta: { title: "项目权限配置", requiresAdmin: true },
meta: { title: "项目权限配置", requiresProjectPm: true },
},
{
path: "permissions/monitoring",
name: "AdminPermissionsMonitoring",
component: PermissionManagement,
meta: { title: "权限监控", requiresAdmin: true },
meta: { title: "权限监控", requiresProjectPm: true },
},
],
},
@@ -514,6 +514,15 @@ const ensureAdminProjectAccess = async (to: any, isAdmin: boolean) => {
return isApiPermissionAllowed(studyStore.currentPermissions?.[role]?.[operationKey]);
};
const ensureRouteProjectMember = async (to: any, studyStore: ReturnType<typeof useStudyStore>) => {
const routeProjectId = typeof to.params.projectId === "string" ? to.params.projectId : "";
if (!routeProjectId) return false;
const { data: project } = await fetchStudyDetail(routeProjectId);
studyStore.setCurrentStudy(project);
return true;
};
router.beforeEach(async (to, _from, next) => {
const auth = useAuthStore();
const studyStore = useStudyStore();
@@ -574,6 +583,24 @@ router.beforeEach(async (to, _from, next) => {
next({ path: "/admin/projects" });
return;
}
if (to.meta.requiresProjectMember && !isAdmin) {
const allowed = await ensureRouteProjectMember(to, studyStore).catch(() => false);
if (!allowed) {
next({ path: "/admin/projects" });
return;
}
}
if (to.meta.requiresProjectPm && !isAdmin) {
const hasPmProject = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM";
if (!hasPmProject) {
await studyStore.ensureDefaultPmStudy().catch(() => {});
}
const hasProjectPmAccess = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM";
if (!hasProjectPmAccess) {
next({ path: "/admin/projects" });
return;
}
}
if (to.name === "AdminAuditLogs") {
if (studyStore.currentStudy && !studyStore.currentPermissions) {
await studyStore.loadCurrentStudyPermissions().catch(() => {});