权限监控与管理界面全面美化

重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效:
- 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式
- 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充
- 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮
- IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格
- 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片
- 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器
- 角色概览卡片:加进度条可视化,hover 微动效
- 接口权限矩阵:工具栏分离布局,表格圆角包裹
- 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-20 14:36:35 +08:00
parent e95eeed90e
commit 6cefa620e4
68 changed files with 6927 additions and 984 deletions
+6
View File
@@ -0,0 +1,6 @@
import type { ApiPermissionValue } from "../types/api";
export const isApiPermissionAllowed = (value: ApiPermissionValue | null | undefined): boolean => {
if (typeof value === "boolean") return value;
return !!value?.allowed;
};
+5 -2
View File
@@ -12,8 +12,11 @@ describe("permission project role model", () => {
expect(source).toContain("getProjectRole");
expect(source).toContain("study.currentStudyRole");
expect(source).toContain("study.currentPermissions");
expect(source).toContain('"faq.edit": { module: "faq", action: "write" }');
expect(source).not.toContain('"faq.edit": { module: "etmf"');
expect(source).toContain('"faq.edit": "faq:update"');
expect(source).toContain("isApiPermissionAllowed");
expect(source).toContain("projectPermissions.value?.[projectRole.value]?.[operationKey]");
expect(source).not.toContain("projectPermissions.value?.roles");
expect(source).not.toContain('"faq.edit": "etmf');
expect(source).not.toContain('"ADMIN", "PM"');
});
});
+25 -25
View File
@@ -3,6 +3,7 @@ import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import { TEXT } from "../locales";
import { getProjectRole, isSystemAdmin } from "./roles";
import { isApiPermissionAllowed } from "./apiPermissionValue";
const PERMISSIONS: Record<string, string[]> = {
"subject.create": ["ADMIN"],
@@ -45,32 +46,31 @@ export const usePermission = () => {
const can = (action: string): boolean => {
if (systemAdmin.value) return true;
if (!projectRole.value) return false;
const permissionMap: Record<string, { module: string; action: "read" | "write" }> = {
"subject.create": { module: "subjects", action: "write" },
"subject.enroll": { module: "subjects", action: "write" },
"subject.complete": { module: "subjects", action: "write" },
"subject.drop": { module: "subjects", action: "write" },
"ae.create": { module: "risk_issues", action: "write" },
"ae.close": { module: "risk_issues", action: "write" },
"project.overview.read": { module: "project_overview", action: "read" },
"faq.edit": { module: "faq", action: "write" },
"faq.create": { module: "faq", action: "write" },
"faq.reply": { module: "faq", action: "write" },
"knowledge.notes.write": { module: "shared_library", action: "write" },
"shared.library.read": { module: "shared_library", action: "read" },
"shared.library.write": { module: "shared_library", action: "write" },
"project.members.manage": { module: "project_members", action: "write" },
"site.manage": { module: "sites", action: "write" },
"site.cra.bind": { module: "sites", action: "write" },
"fees.contract.write": { module: "fees", action: "write" },
"fees.attachment.delete": { module: "fees", action: "write" },
"file.attachment.delete": { module: "file_versions", action: "write" },
"audit.export.read": { module: "audit_export", action: "read" },
const permissionMap: Record<string, string> = {
"subject.create": "subjects:create",
"subject.enroll": "subjects:update",
"subject.complete": "subjects:update",
"subject.drop": "subjects:update",
"ae.create": "risk_issues:create",
"ae.close": "risk_issues:update",
"project.overview.read": "project_overview:read",
"faq.edit": "faq:update",
"faq.create": "faq:create",
"faq.reply": "faq_reply:create",
"knowledge.notes.write": "knowledge_notes:create",
"shared.library.read": "knowledge_notes:read",
"shared.library.write": "knowledge_notes:create",
"project.members.manage": "project_members:update",
"site.manage": "sites:update",
"site.cra.bind": "sites:update",
"fees.contract.write": "fees_contracts:create",
"fees.attachment.delete": "fees_attachments:delete",
"file.attachment.delete": "attachments:delete",
"audit.export.read": "audit_logs:read",
};
const mapped = permissionMap[action];
if (!mapped) return false;
const allowed = projectPermissions.value?.roles?.[projectRole.value]?.[mapped.module]?.[mapped.action];
return !!allowed;
const operationKey = permissionMap[action];
if (!operationKey) return false;
return isApiPermissionAllowed(projectPermissions.value?.[projectRole.value]?.[operationKey]);
};
const reason = (action: string): string => {
@@ -2,18 +2,22 @@ import { describe, expect, it } from "vitest";
import { findFirstAccessibleProjectPath, getProjectRoutePermission, hasProjectPermission } from "./projectRoutePermissions";
describe("project route permissions", () => {
it("maps project routes to read permission modules", () => {
expect(getProjectRoutePermission("/project/overview")).toEqual({ module: "project_overview", action: "read" });
expect(getProjectRoutePermission("/knowledge/medical-consult/abc")).toEqual({ module: "faq", action: "read" });
expect(getProjectRoutePermission("/knowledge/notes/new")).toEqual({ module: "shared_library", action: "read" });
expect(getProjectRoutePermission("/startup/kickoff/new")).toEqual({ module: "startup_auth", action: "read" });
it("maps project routes to backend operation keys", () => {
expect(getProjectRoutePermission("/project/overview")).toEqual({ operationKey: "project_overview:read" });
expect(getProjectRoutePermission("/project/milestones")).toEqual({ operationKey: "project_milestones:read" });
expect(getProjectRoutePermission("/finance/contracts")).toEqual({ operationKey: "finance_contracts:read" });
expect(getProjectRoutePermission("/drug/shipments")).toEqual({ operationKey: "drug_shipments:read" });
expect(getProjectRoutePermission("/startup/feasibility/new")).toEqual({ operationKey: "ethics:read" });
expect(getProjectRoutePermission("/startup/kickoff/new")).toEqual({ operationKey: "startup_auth:read" });
expect(getProjectRoutePermission("/knowledge/medical-consult/abc")).toEqual({ operationKey: "faq:read" });
expect(getProjectRoutePermission("/knowledge/notes/new")).toEqual({ operationKey: "knowledge_notes:read" });
});
it("rejects project routes when the role lacks module read permission", () => {
it("rejects project routes when the role lacks operation permission", () => {
const matrix = {
CRA: {
project_overview: { read: false, write: false },
shared_library: { read: true, write: true },
"project_overview:read": { allowed: false },
"knowledge_notes:read": { allowed: true },
},
} as any;
@@ -1,4 +1,5 @@
import type { ApiEndpointPermissionsResponse } from "../types/api";
import { isApiPermissionAllowed } from "./apiPermissionValue";
export type ProjectRoutePermission = {
operationKey: string;
@@ -7,10 +8,12 @@ export type ProjectRoutePermission = {
const routePermissions: Array<{ prefixes: string[]; permission: ProjectRoutePermission }> = [
{ prefixes: ["/project/overview"], permission: { operationKey: "project_overview:read" } },
{ prefixes: ["/project/milestones"], permission: { operationKey: "project_milestones:read" } },
{ prefixes: ["/fees/contracts", "/finance/contracts"], permission: { operationKey: "fees_contracts:read" } },
{ prefixes: ["/drug/shipments", "/materials/equipment"], permission: { operationKey: "material_equipments:read" } },
{ prefixes: ["/fees/contracts"], permission: { operationKey: "fees_contracts:read" } },
{ prefixes: ["/finance/contracts"], permission: { operationKey: "finance_contracts:read" } },
{ prefixes: ["/drug/shipments"], permission: { operationKey: "drug_shipments:read" } },
{ prefixes: ["/materials/equipment"], permission: { operationKey: "material_equipments:read" } },
{ prefixes: ["/file-versions", "/trial/", "/documents/"], permission: { operationKey: "documents:read" } },
{ prefixes: ["/startup/feasibility-ethics", "/startup/feasibility", "/startup/ethics"], permission: { operationKey: "startup_ethics:read" } },
{ prefixes: ["/startup/feasibility-ethics", "/startup/feasibility", "/startup/ethics"], permission: { operationKey: "ethics:read" } },
{ prefixes: ["/startup/meeting-auth", "/startup/kickoff", "/startup/training"], permission: { operationKey: "startup_auth:read" } },
{ prefixes: ["/subjects"], permission: { operationKey: "subjects:read" } },
{ prefixes: ["/risk-issues"], permission: { operationKey: "risk_issues:read" } },
@@ -57,7 +60,7 @@ export const hasProjectPermission = (
) => {
if (!permission || isAdmin) return true;
if (!role || !permissions) return false;
return !!permissions[role]?.[permission.operationKey]?.allowed;
return isApiPermissionAllowed(permissions[role]?.[permission.operationKey]);
};
export const findFirstAccessibleProjectPath = (