From 6d682103f34d01c1d878dd26851c453a29c7b5a0 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Wed, 20 May 2026 16:53:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=80=BB=E8=A7=88=E7=95=8C?= =?UTF-8?q?=E9=9D=A2UI=E7=BE=8E=E5=8C=96=E4=B8=8E=E5=85=A5=E7=BB=84?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构总览页面布局:卡片化设计、顶部标题栏、图标装饰 - 美化进度时间线:hover高亮、呼吸动画、渐变连接线 - 重写入组图表:去除边框、虚线网格、渐变柱子、柔和背景 - 修复入组统计:同时匹配enrollment_date和ENROLLED状态 - 移除adapter中hasEnrollmentStage过滤,显示真实入组数 Co-Authored-By: Claude Opus 4.7 --- backend/app/crud/overview.py | 47 ++-- frontend/src/views/admin/ProjectDetail.vue | 25 ++- frontend/src/views/ia/ProjectOverview.vue | 206 ++++++++++++++---- .../ia/project-overview/CenterProgressRow.vue | 30 +-- .../project-overview/EnrollmentBarChart.vue | 192 +++++++--------- .../views/ia/project-overview/StageNode.vue | 22 +- .../ia/project-overview/overview.adapter.ts | 3 +- 7 files changed, 323 insertions(+), 202 deletions(-) diff --git a/backend/app/crud/overview.py b/backend/app/crud/overview.py index e2e5ff19..217c1d22 100644 --- a/backend/app/crud/overview.py +++ b/backend/app/crud/overview.py @@ -3,7 +3,7 @@ import uuid from datetime import datetime from typing import Dict, List, Tuple -from sqlalchemy import func, select, and_ +from sqlalchemy import Date, cast, func, select, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession from app.models.site import Site @@ -86,20 +86,18 @@ async def get_project_overview(db: AsyncSession, study_id: uuid.UUID) -> Dict: startup_status = "NOT_STARTED" startup_completed = kickoff.get("kickoff_date") if contract_status == "COMPLETED" else None - # 入组状态 - 依赖启动会完成 + # 入组状态 enrollment = enrollment_data.get(site_id_str, {"actual": 0}) enrollment_actual = enrollment["actual"] enrollment_target = site.enrollment_target or 0 - - if startup_status == "COMPLETED": - if enrollment_actual >= enrollment_target and enrollment_target > 0: - enrollment_status = "COMPLETED" - elif enrollment_actual > 0: - enrollment_status = "IN_PROGRESS" - else: - enrollment_status = "NOT_STARTED" + + if enrollment_actual >= enrollment_target and enrollment_target > 0: + enrollment_status = "COMPLETED" + elif enrollment_actual > 0: + enrollment_status = "IN_PROGRESS" + elif startup_status == "COMPLETED": + enrollment_status = "NOT_STARTED" else: - # 启动会未完成,入组强制为未开始 enrollment_status = "NOT_STARTED" # 检查状态 - 依赖入组完成 @@ -265,35 +263,44 @@ async def _get_enrollment_data(db: AsyncSession, study_id: uuid.UUID, site_ids: and_( Subject.study_id == study_id, Subject.site_id.in_(site_ids), - Subject.enrollment_date.isnot(None) + or_( + Subject.enrollment_date.isnot(None), + Subject.status == "ENROLLED" + ) ) ).group_by(Subject.site_id) - + result = await db.execute(stmt) rows = result.all() - + data = {} for row in rows: data[str(row.site_id)] = {"actual": row.count} - + return data async def _get_monthly_enrollment(db: AsyncSession, study_id: uuid.UUID) -> List[Dict]: """获取月度入组统计""" - month_col = func.to_char(Subject.enrollment_date, 'YYYY-MM').label("month") - + month_col = func.to_char( + func.coalesce(Subject.enrollment_date, func.cast(Subject.created_at, Date)), + 'YYYY-MM' + ).label("month") + stmt = select( month_col, func.count(Subject.id).label("count") ).where( and_( Subject.study_id == study_id, - Subject.enrollment_date.isnot(None) + or_( + Subject.enrollment_date.isnot(None), + Subject.status == "ENROLLED" + ) ) ).group_by(month_col).order_by(month_col) - + result = await db.execute(stmt) rows = result.all() - + return [{"month": row.month, "count": row.count} for row in rows] diff --git a/frontend/src/views/admin/ProjectDetail.vue b/frontend/src/views/admin/ProjectDetail.vue index 671c1333..ae798141 100644 --- a/frontend/src/views/admin/ProjectDetail.vue +++ b/frontend/src/views/admin/ProjectDetail.vue @@ -7099,30 +7099,31 @@ onBeforeUnmount(() => { .setup-content-step-nav { display: flex; align-items: center; - gap: 8px; + gap: 6px; } .setup-content-step-nav :deep(.el-button.is-circle) { - width: 28px; - height: 28px; + width: 32px; + height: 32px; padding: 0; - background: transparent !important; - border: 1px solid #e2e8f0 !important; - color: #64748b !important; + background: #f1f5f9 !important; + border: 1.5px solid #cbd5e1 !important; + color: #475569 !important; + font-size: 14px; transition: all 0.2s; } .setup-content-step-nav :deep(.el-button.is-circle:hover) { - background: #f8fafc !important; - border-color: #cbd5e1 !important; - color: #3b82f6 !important; + background: #e2e8f0 !important; + border-color: #94a3b8 !important; + color: #1e40af !important; } .setup-content-step-nav :deep(.el-button.is-circle.is-disabled), .setup-content-step-nav :deep(.el-button.is-circle.is-disabled:hover) { - background: transparent !important; - border-color: #f1f5f9 !important; - color: #e2e8f0 !important; + background: #f8fafc !important; + border-color: #e2e8f0 !important; + color: #cbd5e1 !important; cursor: not-allowed; } diff --git a/frontend/src/views/ia/ProjectOverview.vue b/frontend/src/views/ia/ProjectOverview.vue index c8991d62..75cc6bab 100644 --- a/frontend/src/views/ia/ProjectOverview.vue +++ b/frontend/src/views/ia/ProjectOverview.vue @@ -1,14 +1,31 @@