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

重新设计权限系统所有 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
+42 -6
View File
@@ -409,7 +409,7 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM"],
},
# 项目概览
"overview:read": {
"project_overview:read": {
"module": "project_overview",
"action": "read",
"description": "查询项目概览",
@@ -583,6 +583,13 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CRA"],
"prerequisite_permissions": ["subjects:read", "sites:read"],
},
"subject_pds:delete": {
"module": "subjects",
"action": "write",
"description": "删除参与者PDS",
"default_roles": ["PM"],
"prerequisite_permissions": ["subjects:read", "sites:read"],
},
# 审计日志管理
"audit_logs:list": {
"module": "audit_export",
@@ -665,18 +672,43 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"],
},
# 项目里程碑管理
"milestones:list": {
"project_milestones:read": {
"module": "project_milestones",
"action": "read",
"description": "查询项目里程碑列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
},
"milestones:update": {
"project_milestones:update": {
"module": "project_milestones",
"action": "write",
"description": "更新项目里程碑",
"default_roles": ["PM"],
},
# 启动与授权
"startup_auth:create": {
"module": "startup_auth",
"action": "write",
"description": "创建启动会或培训授权",
"default_roles": ["PM"],
},
"startup_auth:read": {
"module": "startup_auth",
"action": "read",
"description": "查询启动会或培训授权",
"default_roles": ["PM", "CRA", "PV", "IMP"],
},
"startup_auth:update": {
"module": "startup_auth",
"action": "write",
"description": "更新启动会或培训授权",
"default_roles": ["PM"],
},
"startup_auth:delete": {
"module": "startup_auth",
"action": "write",
"description": "删除培训授权",
"default_roles": ["PM"],
},
# 附件管理
"attachments:create": {
"module": "attachments",
@@ -981,12 +1013,16 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"startup_auth": {
"read": [
"startup_auth:read",
"budget:list",
"budget:read",
"timeline:list",
"timeline:read",
],
"write": [
"startup_auth:create",
"startup_auth:update",
"startup_auth:delete",
"budget:create",
"budget:update",
"budget:delete",
@@ -997,7 +1033,7 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"project_overview": {
"read": [
"overview:read",
"project_overview:read",
],
"write": [],
},
@@ -1083,10 +1119,10 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"project_milestones": {
"read": [
"milestones:list",
"project_milestones:read",
],
"write": [
"milestones:update",
"project_milestones:update",
],
},
}
+2
View File
@@ -24,6 +24,8 @@ class Settings(BaseSettings):
LOGIN_RSA_KEY_ID: str = "default"
LOGIN_CHALLENGE_TTL_SECONDS: int = 120
LOGIN_CHALLENGE_MAX_ACTIVE: int = 1000
IP2REGION_XDB_PATH: Optional[str] = None
IP2REGION_IPV6_XDB_PATH: Optional[str] = None
@lru_cache
+54 -4
View File
@@ -1,4 +1,5 @@
from typing import Annotated, AsyncGenerator, Callable, Iterable
import time
import uuid
from fastapi import Depends, HTTPException, Request, status
@@ -129,12 +130,17 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
check_prerequisites: 是否检查前置权限
"""
async def dependency(
request: Request,
study_id: uuid.UUID,
current_user=Depends(get_current_user),
db: AsyncSession = Depends(get_db_session),
):
from app.core.permission_monitor import get_permission_monitor
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
if allow_system_admin and role_value == "ADMIN":
_enqueue_permission_log(
study_id, current_user.id, endpoint_key, "ADMIN", True, 0.0, request
)
return current_user
membership = await member_crud.get_member(db, study_id, current_user.id)
if not membership or not membership.is_active:
@@ -143,11 +149,29 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
message="不是该项目成员",
status_code=status.HTTP_403_FORBIDDEN,
)
allowed = await role_has_api_permission(
db, study_id, membership.role_in_study, endpoint_key, check_prerequisites=check_prerequisites
)
start_time = time.perf_counter()
error = None
try:
allowed = await role_has_api_permission(
db, study_id, membership.role_in_study, endpoint_key, check_prerequisites=check_prerequisites
)
except Exception as e:
error = e
allowed = False
raise
finally:
elapsed_ms = (time.perf_counter() - start_time) * 1000
monitor = get_permission_monitor()
monitor.record_permission_check(
allowed=allowed, elapsed_time=elapsed_ms / 1000, error=error
)
_enqueue_permission_log(
study_id, current_user.id, endpoint_key,
membership.role_in_study, allowed, elapsed_ms, request
)
if not allowed:
# 获取缺失的前置权限,用于错误提示
missing_prereqs = await get_missing_prerequisites(
db, study_id, membership.role_in_study, endpoint_key
)
@@ -168,6 +192,32 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
return dependency
def _enqueue_permission_log(
study_id: uuid.UUID,
user_id: uuid.UUID,
endpoint_key: str,
role: str,
allowed: bool,
elapsed_ms: float,
request: Request,
) -> None:
from app.services.permission_log_writer import get_log_writer
writer = get_log_writer()
if writer:
forwarded = request.headers.get("x-forwarded-for")
ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else None)
writer.enqueue({
"study_id": study_id,
"user_id": user_id,
"endpoint_key": endpoint_key,
"role": role,
"allowed": allowed,
"elapsed_ms": elapsed_ms,
"ip_address": ip,
})
async def get_cra_site_scope(
db: AsyncSession,
study_id: uuid.UUID,
+5 -1
View File
@@ -6,6 +6,7 @@ from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.api_endpoint_permission import ApiEndpointPermission
from app.models.study import Study
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES
from app.core.permission_cache import get_permission_cache
@@ -102,7 +103,10 @@ async def get_api_endpoint_permissions(
"""
overrides = await _get_project_permission_overrides(db, study_id)
roles = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
study_result = await db.execute(select(Study).where(Study.id == study_id))
study = study_result.scalar_one_or_none()
active_roles = [role for role in (study.active_roles if study else []) if isinstance(role, str) and role.strip()]
roles = list(dict.fromkeys(["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", *active_roles, *overrides.keys()]))
matrix: dict[str, dict[str, dict[str, bool]]] = {}
for role in roles:
matrix[role] = {}