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

重新设计权限系统所有 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
+65 -4
View File
@@ -17,6 +17,14 @@ from app.crud.user import ensure_admin_exists
from app.db.base import Base
from app.db.session import SessionLocal, engine
from app.services.visit_scheduler import run_daily_lost_visit_job
from app.services.permission_log_writer import start_log_writer, stop_log_writer
from app.services.permission_metric_aggregator import run_hourly_metric_aggregation
from app.services.security_access_log_writer import (
get_security_log_writer,
start_security_log_writer,
stop_security_log_writer,
)
from app.core.security import decode_token
logger = logging.getLogger("ctms.setup_config")
UUID_RE = re.compile(
@@ -29,6 +37,9 @@ setup_config_stats: dict[str, int] = defaultdict(int)
async def lifespan(_: FastAPI):
stop_event = asyncio.Event()
scheduler_task = asyncio.create_task(run_daily_lost_visit_job(stop_event))
aggregator_task = asyncio.create_task(run_hourly_metric_aggregation(stop_event))
await start_log_writer()
await start_security_log_writer()
# Ensure models are imported so metadata is populated
from app.models import user as user_model # noqa: F401
@@ -38,12 +49,12 @@ async def lifespan(_: FastAPI):
await conn.run_sync(Base.metadata.create_all)
async with SessionLocal() as session:
await ensure_admin_exists(session)
# Initialize API endpoint registry
# from app.core.decorators import initialize_api_endpoint_registry
# await initialize_api_endpoint_registry(session)
yield
stop_event.set()
await stop_log_writer()
await stop_security_log_writer()
await scheduler_task
await aggregator_task
async def _ensure_legacy_primary_keys(conn) -> None:
@@ -118,7 +129,9 @@ def create_app() -> FastAPI:
async def setup_config_monitoring_middleware(request, call_next):
path = request.url.path
is_setup_config_path = "/api/v1/studies/" in path and "/setup-config" in path
should_security_log = path.startswith("/api/")
started_at = time.perf_counter()
status_code = 500
try:
response = await call_next(request)
except Exception:
@@ -134,12 +147,14 @@ def create_app() -> FastAPI:
500,
duration_ms,
)
if should_security_log:
_enqueue_security_access_log(request, path, status_code, started_at)
raise
status_code = int(response.status_code)
if is_setup_config_path:
normalized_path = UUID_RE.sub("{study_id}", path)
duration_ms = int((time.perf_counter() - started_at) * 1000)
status_code = int(response.status_code)
status_bucket = f"{status_code // 100}xx"
key = f"{request.method} {normalized_path} {status_bucket}"
setup_config_stats[key] += 1
@@ -167,6 +182,8 @@ def create_app() -> FastAPI:
status_code,
duration_ms,
)
if should_security_log:
_enqueue_security_access_log(request, path, status_code, started_at)
return response
register_exception_handlers(app)
@@ -215,3 +232,47 @@ def create_app() -> FastAPI:
app = create_app()
def _resolve_client_ip(request) -> str | None:
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.headers.get("x-real-ip")
if real_ip:
return real_ip.strip()
return request.client.host if request.client else None
def _resolve_auth_context(request) -> tuple[str, str | None]:
authorization = request.headers.get("authorization") or ""
if not authorization.lower().startswith("bearer "):
return "ANONYMOUS", None
token = authorization.split(" ", 1)[1].strip()
if not token:
return "ANONYMOUS", None
try:
payload = decode_token(token)
except Exception:
return "INVALID_TOKEN", None
subject = payload.get("sub")
return ("AUTHENTICATED", str(subject)) if subject else ("INVALID_TOKEN", None)
def _enqueue_security_access_log(request, path: str, status_code: int, started_at: float) -> None:
writer = get_security_log_writer()
if not writer:
return
auth_status, user_identifier = _resolve_auth_context(request)
writer.enqueue(
{
"method": request.method,
"path": path,
"status_code": status_code,
"elapsed_ms": round((time.perf_counter() - started_at) * 1000, 2),
"client_ip": _resolve_client_ip(request),
"user_agent": request.headers.get("user-agent"),
"auth_status": auth_status,
"user_identifier": user_identifier,
}
)