From f8a959b8013ed79255d5c52e6ea1543ecfe08c67 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 19 May 2026 08:48:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E9=87=8D=E6=9E=84=EF=BC=9A=E8=A7=92=E8=89=B2=E6=A6=82?= =?UTF-8?q?=E8=A7=88=E3=80=81=E7=94=9F=E6=95=88=E7=AE=A1=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=9D=83=E9=99=90=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【界面优化】 - 移除权限模板列表和角色概览卡片上的"系统预设"标签,视觉上减少噪音 - 将 PermissionTemplateSelector 改为纯展示组件,重命名为"角色权限概览" - 移除"应用此模板"按钮及预览面板,卡片仅展示各角色权限统计 - 角色描述改为鼠标悬浮 tooltip 显示,节省卡片空间 - 未生效角色以虚线边框 + 降低透明度区分,悬浮提示状态说明 【生效管理】 - Study 模型新增 active_roles JSON 字段,记录项目已生效角色列表 - 新增 /studies/{study_id}/active-roles GET/PUT 接口 - 数据库迁移 20260518_01:studies 表添加 active_roles 列 - 成员管理的项目角色下拉列表仅显示已生效角色,未生效角色不可分配 【模板管理抽屉重构】 - 抽屉改为三标签页:模板列表 / 生效管理 / 编辑权限 - 生效管理:开关控制各角色生效状态,保存后同步后端 - 编辑权限:左右分栏设计,左侧角色列表点击选中,右侧实时展示权限勾选 - 原独立的角色权限编辑抽屉合并至此,角色概览卡片"编辑权限"直接跳转对应标签页 【操作类型标签统一】 - 系统级权限和编辑权限界面的操作类型标签统一使用细分类型 - 操作类型从粗粒度 read/write 细化为 read/create/update/delete/export - 颜色规范:读取灰色、创建绿色、更新黄色、删除红色、导出无色 - 权限模板管理接口限制为 ADMIN 角色,修复原先权限过宽的问题 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../20260518_01_add_study_active_roles.py | 28 + backend/app/api/v1/api_permissions.py | 15 +- backend/app/api/v1/permission_templates.py | 9 +- backend/app/api/v1/router.py | 4 +- backend/app/api/v1/study_active_roles.py | 44 + backend/app/api/v1/system_permissions.py | 37 + backend/app/core/api_permissions.py | 113 ++ backend/app/models/study.py | 1 + frontend/src/api/projectPermissions.ts | 25 + frontend/src/components/Layout.vue | 8 +- .../components/PermissionTemplateSelector.vue | 340 ++--- frontend/src/locales/zh-CN.ts | 1 + frontend/src/router/index.ts | 17 +- .../src/views/admin/PermissionManagement.vue | 1272 +++++++++++++++++ frontend/src/views/admin/Projects.vue | 4 +- 15 files changed, 1650 insertions(+), 268 deletions(-) create mode 100644 backend/alembic/versions/20260518_01_add_study_active_roles.py create mode 100644 backend/app/api/v1/study_active_roles.py create mode 100644 backend/app/api/v1/system_permissions.py create mode 100644 frontend/src/views/admin/PermissionManagement.vue diff --git a/backend/alembic/versions/20260518_01_add_study_active_roles.py b/backend/alembic/versions/20260518_01_add_study_active_roles.py new file mode 100644 index 00000000..9e5c967e --- /dev/null +++ b/backend/alembic/versions/20260518_01_add_study_active_roles.py @@ -0,0 +1,28 @@ +"""add active_roles to studies + +Revision ID: 20260518_01 +Revises: 20260514_01 +Create Date: 2026-05-18 16:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260518_01" +down_revision: Union[str, None] = "20260514_01" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "studies", + sa.Column("active_roles", sa.JSON(), nullable=False, server_default="[]"), + ) + + +def downgrade() -> None: + op.drop_column("studies", "active_roles") diff --git a/backend/app/api/v1/api_permissions.py b/backend/app/api/v1/api_permissions.py index e5963b99..2bdbe8e3 100644 --- a/backend/app/api/v1/api_permissions.py +++ b/backend/app/api/v1/api_permissions.py @@ -165,16 +165,17 @@ async def get_study_api_permissions( """ permissions = await get_api_endpoint_permissions(db, study_id) - # 构建返回格式 + # 构建返回格式(get_api_endpoint_permissions 已返回 {role: {key: {"allowed": bool}}}) result: dict[str, dict[str, dict[str, bool]]] = {} for role in PROJECT_PERMISSION_ROLES: if role == "ADMIN": continue result[role] = {} for endpoint_key in API_ENDPOINT_PERMISSIONS.keys(): - result[role][endpoint_key] = { - "allowed": permissions.get(role, {}).get(endpoint_key, False) - } + perm = permissions.get(role, {}).get(endpoint_key, {"allowed": False}) + # perm 已经是 {"allowed": bool},直接使用 + allowed = perm["allowed"] if isinstance(perm, dict) else bool(perm) + result[role][endpoint_key] = {"allowed": allowed} return result @@ -220,8 +221,8 @@ async def update_study_api_permissions( continue result[role] = {} for endpoint_key in API_ENDPOINT_PERMISSIONS.keys(): - result[role][endpoint_key] = { - "allowed": permissions.get(role, {}).get(endpoint_key, False) - } + perm = permissions.get(role, {}).get(endpoint_key, {"allowed": False}) + allowed = perm["allowed"] if isinstance(perm, dict) else bool(perm) + result[role][endpoint_key] = {"allowed": allowed} return result diff --git a/backend/app/api/v1/permission_templates.py b/backend/app/api/v1/permission_templates.py index a0666e60..8fc4d4c9 100644 --- a/backend/app/api/v1/permission_templates.py +++ b/backend/app/api/v1/permission_templates.py @@ -9,8 +9,9 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import get_current_user, get_db_session, require_study_roles +from app.core.deps import get_current_user, get_db_session, require_roles, require_study_roles from app.models.permission_template import TemplateType +from app.models.user import UserRole from app.schemas.permission_template import ( ApplyTemplateRequest, ApplyTemplateResponse, @@ -51,7 +52,7 @@ async def list_templates( async def create_template( payload: PermissionTemplateCreate, db: Annotated[AsyncSession, Depends(get_db_session)] = None, - current_user=Depends(get_current_user), + current_user=Depends(require_roles([UserRole.ADMIN.value])), ) -> PermissionTemplateRead: """创建权限模板""" try: @@ -78,7 +79,7 @@ async def update_template( template_id: uuid.UUID, payload: PermissionTemplateUpdate, db: Annotated[AsyncSession, Depends(get_db_session)] = None, - current_user=Depends(get_current_user), + current_user=Depends(require_roles([UserRole.ADMIN.value])), ) -> PermissionTemplateRead: """更新权限模板""" try: @@ -92,7 +93,7 @@ async def update_template( async def delete_template( template_id: uuid.UUID, db: Annotated[AsyncSession, Depends(get_db_session)] = None, - current_user=Depends(get_current_user), + current_user=Depends(require_roles([UserRole.ADMIN.value])), ): """删除权限模板""" try: diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 3a73038a..6e46e99d 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, fees_contracts, fees_attachments, drug_shipments, material_equipments, project_milestones, startup, knowledge_notes, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates +from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, fees_contracts, fees_attachments, drug_shipments, material_equipments, project_milestones, startup, knowledge_notes, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles api_router = APIRouter() @@ -40,3 +40,5 @@ api_router.include_router(documents.router, prefix="", tags=["documents"]) api_router.include_router(permission_monitoring.router) api_router.include_router(permission_templates.router) api_router.include_router(permission_templates.study_router, prefix="/studies/{study_id}", tags=["permission-templates"]) +api_router.include_router(system_permissions.router) +api_router.include_router(study_active_roles.router, prefix="/studies/{study_id}", tags=["active-roles"]) diff --git a/backend/app/api/v1/study_active_roles.py b/backend/app/api/v1/study_active_roles.py new file mode 100644 index 00000000..c749eea4 --- /dev/null +++ b/backend/app/api/v1/study_active_roles.py @@ -0,0 +1,44 @@ +"""项目角色生效管理API""" + +from __future__ import annotations + +import uuid +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_db_session, require_study_roles +from app.models.study import Study + +router = APIRouter(prefix="/active-roles", tags=["active-roles"]) + + +@router.get("", summary="获取项目已生效角色列表") +async def get_active_roles( + study_id: uuid.UUID, + _=Depends(require_study_roles(["PM"])), + db: Annotated[AsyncSession, Depends(get_db_session)] = None, +) -> dict: + result = await db.execute(select(Study).where(Study.id == study_id)) + study = result.scalar_one_or_none() + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") + return {"active_roles": study.active_roles or []} + + +@router.put("", summary="更新项目已生效角色列表") +async def update_active_roles( + study_id: uuid.UUID, + payload: dict, + _=Depends(require_study_roles(["PM"])), + db: Annotated[AsyncSession, Depends(get_db_session)] = None, +) -> dict: + result = await db.execute(select(Study).where(Study.id == study_id)) + study = result.scalar_one_or_none() + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") + study.active_roles = payload.get("active_roles", []) + await db.commit() + return {"active_roles": study.active_roles} diff --git a/backend/app/api/v1/system_permissions.py b/backend/app/api/v1/system_permissions.py new file mode 100644 index 00000000..5e188e77 --- /dev/null +++ b/backend/app/api/v1/system_permissions.py @@ -0,0 +1,37 @@ +"""系统级权限查询API""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends + +from app.core.deps import require_roles +from app.core.api_permissions import SYSTEM_PERMISSIONS, SYSTEM_MODULE_LABELS +from app.models.user import UserRole + +router = APIRouter(prefix="/system-permissions", tags=["system-permissions"]) + + +@router.get( + "", + summary="获取系统级权限定义", + description="返回管理后台所有系统级操作的权限定义,仅 ADMIN 可访问", +) +async def list_system_permissions( + _=Depends(require_roles([UserRole.ADMIN.value])), +) -> dict: + permissions_list = [ + { + "permission_key": key, + "module": config["module"], + "module_label": SYSTEM_MODULE_LABELS.get(config["module"], config["module"]), + "action": config["action"], + "description": config["description"], + "roles": config["roles"], + } + for key, config in SYSTEM_PERMISSIONS.items() + ] + + return { + "permissions": permissions_list, + "module_labels": SYSTEM_MODULE_LABELS, + } diff --git a/backend/app/core/api_permissions.py b/backend/app/core/api_permissions.py index eb9f54d1..21812569 100644 --- a/backend/app/core/api_permissions.py +++ b/backend/app/core/api_permissions.py @@ -1141,3 +1141,116 @@ OPERATION_PREREQUISITES: dict[str, list[str]] = { "monitoring_audit:update": ["sites:read"], "monitoring_audit:delete": ["sites:read"], } + +# 系统级权限定义 +# 描述管理后台各模块操作,当前全部由 ADMIN 角色持有 +SYSTEM_PERMISSIONS: dict[str, dict] = { + # 账号管理 + "system:users:list": { + "module": "system_users", + "action": "read", + "description": "查看用户列表", + "roles": ["ADMIN"], + }, + "system:users:create": { + "module": "system_users", + "action": "create", + "description": "创建用户", + "roles": ["ADMIN"], + }, + "system:users:update": { + "module": "system_users", + "action": "update", + "description": "编辑用户", + "roles": ["ADMIN"], + }, + "system:users:disable": { + "module": "system_users", + "action": "update", + "description": "禁用/启用用户", + "roles": ["ADMIN"], + }, + "system:users:approve": { + "module": "system_users", + "action": "update", + "description": "审核用户注册", + "roles": ["ADMIN"], + }, + # 项目管理 + "system:projects:list": { + "module": "system_projects", + "action": "read", + "description": "查看项目列表", + "roles": ["ADMIN"], + }, + "system:projects:create": { + "module": "system_projects", + "action": "create", + "description": "创建项目", + "roles": ["ADMIN"], + }, + "system:projects:update": { + "module": "system_projects", + "action": "update", + "description": "编辑项目", + "roles": ["ADMIN"], + }, + "system:projects:delete": { + "module": "system_projects", + "action": "delete", + "description": "删除项目", + "roles": ["ADMIN"], + }, + "system:projects:lock": { + "module": "system_projects", + "action": "update", + "description": "锁定/解锁项目", + "roles": ["ADMIN"], + }, + "system:projects:members": { + "module": "system_projects", + "action": "update", + "description": "管理项目成员", + "roles": ["ADMIN"], + }, + "system:projects:sites": { + "module": "system_projects", + "action": "update", + "description": "管理项目中心", + "roles": ["ADMIN"], + }, + # 权限管理 + "system:permissions:project_config": { + "module": "system_permissions", + "action": "update", + "description": "配置项目接口权限", + "roles": ["ADMIN"], + }, + "system:permissions:templates": { + "module": "system_permissions", + "action": "update", + "description": "管理权限模板", + "roles": ["ADMIN"], + }, + # 审计日志 + "system:audit_logs:read": { + "module": "system_audit", + "action": "read", + "description": "查看审计日志", + "roles": ["ADMIN"], + }, + "system:audit_logs:export": { + "module": "system_audit", + "action": "export", + "description": "导出审计日志", + "roles": ["ADMIN"], + }, +} + +# 系统级权限模块标签 +SYSTEM_MODULE_LABELS: dict[str, str] = { + "system_users": "账号管理", + "system_projects": "项目管理", + "system_permissions": "权限管理", + "system_audit": "审计日志", +} diff --git a/backend/app/models/study.py b/backend/app/models/study.py index 9c2831f9..8045c414 100644 --- a/backend/app/models/study.py +++ b/backend/app/models/study.py @@ -39,5 +39,6 @@ class Study(Base): status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT") is_locked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) visit_schedule: Mapped[list[dict]] = mapped_column(JSON, nullable=False, default=list) + active_roles: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) created_by: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) diff --git a/frontend/src/api/projectPermissions.ts b/frontend/src/api/projectPermissions.ts index 79b0a082..4462f922 100644 --- a/frontend/src/api/projectPermissions.ts +++ b/frontend/src/api/projectPermissions.ts @@ -98,3 +98,28 @@ export const applyPermissionTemplate = (studyId: string, payload: ApplyTemplateR `/api/v1/studies/${studyId}/permission-templates/${payload.template_id}/apply`, payload, ); + +// 系统级权限 +export interface SystemPermissionItem { + permission_key: string; + module: string; + module_label: string; + action: string; + description: string; + roles: string[]; +} + +export interface SystemPermissionsResponse { + permissions: SystemPermissionItem[]; + module_labels: Record; +} + +export const fetchSystemPermissions = () => + apiGet(`/api/v1/system-permissions`); + +// 项目角色生效管理 +export const fetchActiveRoles = (studyId: string) => + apiGet<{ active_roles: string[] }>(`/api/v1/studies/${studyId}/active-roles`); + +export const updateActiveRoles = (studyId: string, activeRoles: string[]) => + apiPut<{ active_roles: string[] }>(`/api/v1/studies/${studyId}/active-roles`, { active_roles: activeRoles }); diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 9562668d..2effd991 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -33,6 +33,10 @@ {{ TEXT.menu.auditLogs }} + + + {{ TEXT.menu.permissionManagement }} + @@ -212,9 +216,9 @@ import { useStudyStore } from "../store/study"; import { fetchStudies } from "../api/studies"; import { fetchSites } from "../api/sites"; import { TEXT } from "../locales"; -import { +import { Monitor, User, Suitcase, House, Calendar, Flag, ChatDotRound, - CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files + CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key } from "@element-plus/icons-vue"; import { ElMessage } from "element-plus"; import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions"; diff --git a/frontend/src/components/PermissionTemplateSelector.vue b/frontend/src/components/PermissionTemplateSelector.vue index 4599f106..54b2143c 100644 --- a/frontend/src/components/PermissionTemplateSelector.vue +++ b/frontend/src/components/PermissionTemplateSelector.vue @@ -1,204 +1,125 @@ + + diff --git a/frontend/src/views/admin/Projects.vue b/frontend/src/views/admin/Projects.vue index 1933f4bc..64f34f3c 100644 --- a/frontend/src/views/admin/Projects.vue +++ b/frontend/src/views/admin/Projects.vue @@ -141,7 +141,7 @@ const canProject = (row: Study, module: string, action: "read" | "write") => { }; const goMembers = (row: Study) => { - router.push(`/admin/projects/${row.id}/members`); + router.push(`/admin/permissions?projectId=${row.id}&sub=members`); }; const goSites = (row: Study) => { @@ -149,7 +149,7 @@ const goSites = (row: Study) => { }; const goPermissions = (row: Study) => { - router.push(`/admin/projects/${row.id}/api-permissions`); + router.push(`/admin/permissions?projectId=${row.id}`); }; const goAuditLogs = async (row: Study) => {