2026-05-26-checkup-management-page.md 30 KB

体检管理页面实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 新增独立的体检管理页面,以患者为维度管理中试服务体检全流程(登记、上传报告、费用结算)。

Architecture: 在现有 biz_pilot_checkup 表新增 pilot_patient_id 字段实现患者维度关联。后端新增 3 个 API(分页列表、待登记患者、批量创建),前端新增 3 个 Vue 文件(列表页 + 登记弹窗 + 详情弹窗)。

Tech Stack: FastAPI + SQLAlchemy(后端)、Vue 3 + Element Plus + TypeScript(前端)

Design Spec: docs/superpowers/specs/2026-05-26-checkup-management-page-design.md


File Map

Backend — 新建/修改

文件 操作 职责
docs/sql/2026-05-26-add-pilot-patient-id-to-checkup.sql 新建 ALTER TABLE 迁移 SQL
backend/app/models/models.py:2913-2938 修改 BizPilotCheckup 新增 pilot_patient_id 字段
backend/app/crud/pilot_checkup.py 修改 新增 3 个函数 + 更新现有 _row_to_dict_checkup/create_checkup
backend/app/api/pilot_checkup.py 修改 新增 3 个端点 + 更新 CheckupSaveBody

Frontend — 新建/修改

文件 操作 职责
web-admin/src/api/admin.ts 修改 新增 3 个 API 函数
web-admin/src/router/modules/sqxp.ts 修改 新增路由
web-admin/src/views/pilot-service/checkup-management/index.vue 新建 主列表页
web-admin/src/views/pilot-service/checkup-management/CheckupRegisterDialog.vue 新建 批量登记体检弹窗
web-admin/src/views/pilot-service/checkup-management/CheckupDetailDialog.vue 新建 详情弹窗(报告上传 + 实际项目)

Task 1: 数据库迁移 SQL

Files:

  • Create: docs/sql/2026-05-26-add-pilot-patient-id-to-checkup.sql

  • [ ] Step 1: 创建迁移 SQL 文件

-- 2026-05-26: biz_pilot_checkup 新增 pilot_patient_id 字段
-- 用于体检管理页面按患者维度管理体检记录

ALTER TABLE biz_pilot_checkup
ADD COLUMN pilot_patient_id VARCHAR(64) DEFAULT NULL COMMENT '中试患者ID'
AFTER pilot_application_id;

-- 为已有数据补充 pilot_patient_id(通过申请单关联患者)
-- 注意:一个申请单可能对应多个患者,这里仅标记第一个需要体检的患者
-- 新数据将通过 API 正确设置
CREATE INDEX idx_pilot_checkup_patient_id ON biz_pilot_checkup(pilot_patient_id);
  • Step 2: 在数据库执行迁移
cd backend && source venv/bin/activate
python -c "
from app.core.database import engine
from sqlalchemy import text
with engine.connect() as conn:
    conn.execute(text('ALTER TABLE biz_pilot_checkup ADD COLUMN pilot_patient_id VARCHAR(64) DEFAULT NULL COMMENT \"中试患者ID\" AFTER pilot_application_id'))
    conn.execute(text('CREATE INDEX idx_pilot_checkup_patient_id ON biz_pilot_checkup(pilot_patient_id)'))
    conn.commit()
print('Migration done')
"
  • Step 3: 提交
git add docs/sql/2026-05-26-add-pilot-patient-id-to-checkup.sql
git commit -m "feat(checkup): add pilot_patient_id column to biz_pilot_checkup table"

Task 2: 后端 Model 更新

Files:

  • Modify: backend/app/models/models.py (line ~2913, BizPilotCheckup class)

  • [ ] Step 1: 在 BizPilotCheckup 类中新增字段

在 pilot_application_id 字段之后(约 line 2919 后)添加:

pilot_patient_id = Column(String(64), nullable=True, comment="中试患者ID")

完整上下文:找到 BizPilotCheckup 类中的 pilot_application_id 行,在其下一行添加 pilot_patient_id。

  • Step 2: 验证 Model 能正常加载
cd backend && source venv/bin/activate
python -c "from app.models.models import BizPilotCheckup; print([c.name for c in BizPilotCheckup.__table__.columns])"

预期输出包含 pilot_patient_id。

  • Step 3: 提交
git add backend/app/models/models.py
git commit -m "feat(checkup): add pilot_patient_id to BizPilotCheckup model"

Task 3: 后端 CRUD 层更新

Files:

  • Modify: backend/app/crud/pilot_checkup.py

  • [ ] Step 1: 更新 _row_to_dict_checkup 函数 — 添加 pilotPatientId 字段

在 _row_to_dict_checkup 函数的字典构建中,"pilotApplicationId" 行之后添加:

"pilotPatientId": row.pilot_patient_id or "",

同时,在 if db and tenant_no: 块内(institutionName 查找之后),添加患者信息关联查询:

# patientName / sex / age / phoneNo — from BizPilotPatient
from app.models.models import BizPilotPatient, BizPilotApplication
if row.pilot_patient_id:
    patient = (
        db.query(BizPilotPatient)
        .filter(
            BizPilotPatient.id == row.pilot_patient_id,
            BizPilotPatient.del_flag == 0,
        )
        .first()
    )
    if patient:
        d["patientName"] = patient.patient_name or ""
        d["sex"] = patient.sex or ""
        d["age"] = patient.age
        d["phoneNo"] = patient.phone_no or ""
    else:
        d["patientName"] = ""
        d["sex"] = ""
        d["age"] = None
        d["phoneNo"] = ""
else:
    d["patientName"] = ""
    d["sex"] = ""
    d["age"] = None
    d["phoneNo"] = ""

# applicationNo / channelName — from BizPilotApplication
if row.pilot_application_id:
    app = (
        db.query(BizPilotApplication)
        .filter(
            BizPilotApplication.id == row.pilot_application_id,
            BizPilotApplication.del_flag == 0,
        )
        .first()
    )
    if app:
        d["applicationNo"] = app.application_no or ""
        d["channelName"] = app.channel_name or ""
    else:
        d["applicationNo"] = ""
        d["channelName"] = ""
else:
    d["applicationNo"] = ""
    d["channelName"] = ""

注意:from app.models.models import BizPilotPatient, BizPilotApplication 已在文件顶部 import 区域(如果不存在需添加)。当前文件顶部已有:

from app.models.models import (
    BizCheckupInstitution,
    BizPilotCheckup,
    BizPilotCheckupActualItem,
    BizPilotCheckupReserveItem,
)

需要改为:

from app.models.models import (
    BizCheckupInstitution,
    BizPilotApplication,
    BizPilotCheckup,
    BizPilotCheckupActualItem,
    BizPilotCheckupReserveItem,
    BizPilotPatient,
)
  • Step 2: 更新 create_checkup 函数 — 支持 pilot_patient_id

在 create_checkup 函数中,BizPilotCheckup(...) 构造参数里添加:

pilot_patient_id=data.get("pilotPatientId") or None,
  • Step 3: 新增 list_checkups_paginated 函数

在文件末尾(delete_actual_item 函数之后)添加:

# ────────────────────────────────────────────
# Checkup List (Paginated)
# ────────────────────────────────────────────


def list_checkups_paginated(
    db: Session,
    tenant_no: str,
    page: int = 1,
    page_size: int = 10,
    checkup_status: Optional[str] = None,
    fee_status: Optional[str] = None,
    checkup_institution_id: Optional[str] = None,
    application_no: Optional[str] = None,
    patient_name: Optional[str] = None,
    channel_id: Optional[str] = None,
) -> Dict[str, Any]:
    q = db.query(BizPilotCheckup).filter(
        BizPilotCheckup.tenant_no == tenant_no,
        BizPilotCheckup.del_flag == 0,
    )

    if checkup_status:
        statuses = [s.strip() for s in checkup_status.split(",") if s.strip()]
        if statuses:
            q = q.filter(BizPilotCheckup.checkup_status.in_(statuses))

    if fee_status:
        statuses = [s.strip() for s in fee_status.split(",") if s.strip()]
        if statuses:
            q = q.filter(BizPilotCheckup.fee_status.in_(statuses))

    if checkup_institution_id:
        q = q.filter(BizPilotCheckup.checkup_institution_id == checkup_institution_id)

    if application_no:
        app_subq = (
            db.query(BizPilotApplication.id)
            .filter(
                BizPilotApplication.application_no.like(f"%{application_no}%"),
                BizPilotApplication.del_flag == 0,
            )
            .subquery()
        )
        q = q.filter(BizPilotCheckup.pilot_application_id.in_(app_subq))

    if patient_name:
        patient_subq = (
            db.query(BizPilotPatient.id)
            .filter(
                BizPilotPatient.patient_name.like(f"%{patient_name}%"),
                BizPilotPatient.del_flag == 0,
            )
            .subquery()
        )
        q = q.filter(BizPilotCheckup.pilot_patient_id.in_(patient_subq))

    if channel_id:
        app_subq = (
            db.query(BizPilotApplication.id)
            .filter(
                BizPilotApplication.channel_id == channel_id,
                BizPilotApplication.del_flag == 0,
            )
            .subquery()
        )
        q = q.filter(BizPilotCheckup.pilot_application_id.in_(app_subq))

    total = q.order_by(None).with_entities(func.count(BizPilotCheckup.id)).scalar() or 0
    page = max(1, page)
    page_size = min(max(1, page_size), 100)
    rows = (
        q.order_by(BizPilotCheckup.create_time.desc())
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all()
    )
    items = [_row_to_dict_checkup(r, db, tenant_no) for r in rows]
    return {
        "list": items,
        "pagination": {"page": page, "pageSize": page_size, "total": total},
    }
  • Step 4: 新增 list_pending_patients 函数

紧接上面函数之后添加:

def list_pending_patients(
    db: Session,
    tenant_no: str,
    page: int = 1,
    page_size: int = 10,
    application_no: Optional[str] = None,
    patient_name: Optional[str] = None,
    channel_id: Optional[str] = None,
) -> Dict[str, Any]:
    """查询 need_checkup=1 但尚未有 biz_pilot_checkup 记录的患者"""
    # 已有 checkup 的 patient_id 集合
    checked_patient_ids = (
        db.query(BizPilotCheckup.pilot_patient_id)
        .filter(
            BizPilotCheckup.tenant_no == tenant_no,
            BizPilotCheckup.del_flag == 0,
            BizPilotCheckup.pilot_patient_id.isnot(None),
            BizPilotCheckup.pilot_patient_id != "",
        )
        .subquery()
    )

    q = db.query(BizPilotPatient).filter(
        BizPilotPatient.need_checkup == 1,
        BizPilotPatient.tenant_no == tenant_no,
        BizPilotPatient.del_flag == 0,
        ~BizPilotPatient.id.in_(checked_patient_ids),
    )

    if patient_name:
        q = q.filter(BizPilotPatient.patient_name.like(f"%{patient_name}%"))

    if application_no or channel_id:
        app_q = db.query(BizPilotApplication.id).filter(
            BizPilotApplication.tenant_no == tenant_no,
            BizPilotApplication.del_flag == 0,
        )
        if application_no:
            app_q = app_q.filter(
                BizPilotApplication.application_no.like(f"%{application_no}%")
            )
        if channel_id:
            app_q = app_q.filter(BizPilotApplication.channel_id == channel_id)
        app_subq = app_q.subquery()
        q = q.filter(BizPilotPatient.pilot_application_id.in_(app_subq))

    total = q.order_by(None).with_entities(func.count(BizPilotPatient.id)).scalar() or 0
    page = max(1, page)
    page_size = min(max(1, page_size), 100)
    rows = (
        q.order_by(BizPilotPatient.create_time.desc())
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all()
    )

    items = []
    for r in rows:
        d = {
            "id": r.id,
            "pilotApplicationId": r.pilot_application_id or "",
            "patientName": r.patient_name or "",
            "sex": r.sex or "",
            "age": r.age,
            "phoneNo": r.phone_no or "",
        }
        # 关联申请单信息
        app = (
            db.query(BizPilotApplication)
            .filter(
                BizPilotApplication.id == r.pilot_application_id,
                BizPilotApplication.del_flag == 0,
            )
            .first()
        )
        if app:
            d["applicationNo"] = app.application_no or ""
            d["channelName"] = app.channel_name or ""
            d["channelId"] = app.channel_id or ""
        else:
            d["applicationNo"] = ""
            d["channelName"] = ""
            d["channelId"] = ""
        items.append(d)

    return {
        "list": items,
        "pagination": {"page": page, "pageSize": page_size, "total": total},
    }
  • Step 5: 新增 batch_create_checkups 函数

紧接上面函数之后添加:

def batch_create_checkups(
    db: Session,
    patient_ids: list,
    checkup_institution_id: str,
    reserve_time: Optional[str],
    reserve_items: list,
    tenant_no: str,
    user_id: str,
) -> list:
    """为多个患者批量创建体检记录 + 预约项目"""
    results = []
    now = datetime.now()

    for patient_id in patient_ids:
        # 查找患者信息以获取 application_id
        patient = (
            db.query(BizPilotPatient)
            .filter(
                BizPilotPatient.id == patient_id,
                BizPilotPatient.tenant_no == tenant_no,
                BizPilotPatient.del_flag == 0,
            )
            .first()
        )
        if not patient:
            continue

        # 创建 checkup 主记录
        checkup_row = BizPilotCheckup(
            id=uuid.uuid4().hex,
            pilot_application_id=patient.pilot_application_id,
            pilot_patient_id=patient_id,
            checkup_institution_id=checkup_institution_id or "",
            reserve_time=reserve_time or None,
            reserve_fee=sum(
                float(item.get("projectFee") or 0) for item in reserve_items
            ) if reserve_items else None,
            actual_checkup_time=None,
            actual_checkup_fee=None,
            fee_settled=0,
            checkup_status="dtj",
            fee_status="wjs",
            checkup_remark="",
            report_attachments="",
            tenant_no=tenant_no,
            order_num=0,
            remark="",
            create_by=user_id,
            create_dept_id="",
            create_time=now,
            update_by=user_id,
            update_time=now,
            del_flag=0,
        )
        db.add(checkup_row)
        db.flush()  # 确保 checkup_row.id 可用

        # 创建预约项目
        for idx, item in enumerate(reserve_items):
            ri = BizPilotCheckupReserveItem(
                id=uuid.uuid4().hex,
                pilot_checkup_id=checkup_row.id,
                project_name=item.get("projectName") or item.get("project_name") or "",
                project_fee=item.get("projectFee") if item.get("projectFee") is not None else (
                    item.get("project_fee") if item.get("project_fee") is not None else None
                ),
                tenant_no=tenant_no,
                order_num=idx + 1,
                remark="",
                create_by=user_id,
                create_dept_id="",
                create_time=now,
                update_by=user_id,
                update_time=now,
                del_flag=0,
            )
            db.add(ri)

        results.append(_row_to_dict_checkup(checkup_row, db, tenant_no))

    db.commit()
    return results
  • Step 6: 验证 CRUD 能正常加载
cd backend && source venv/bin/activate
python -c "from app.crud import pilot_checkup; print([f for f in dir(pilot_checkup) if not f.startswith('_')])"

预期输出包含 list_checkups_paginated、list_pending_patients、batch_create_checkups。

  • Step 7: 提交
git add backend/app/crud/pilot_checkup.py
git commit -m "feat(checkup): add paginated list, pending patients, and batch create CRUD functions"

Task 4: 后端 API 层更新

Files:

  • Modify: backend/app/api/pilot_checkup.py

  • [ ] Step 1: 更新 CheckupSaveBody — 新增 patient_id 字段

在 CheckupSaveBody 类中添加:

patient_id: Optional[str] = None
  • Step 2: 更新 api_save_checkup — 传递 patient_id

在 api_save_checkup 函数中,data 字典构建添加:

"pilotPatientId": body.patient_id,
  • Step 3: 新增 BatchCheckupSaveBody Schema

在 Pydantic Schemas 区域(CheckupItemsSaveBody 之后)添加:

class BatchCheckupSaveBody(BaseModel):
    patientIds: List[str]
    checkupInstitutionId: Optional[str] = None
    reserveTime: Optional[str] = None
    reserveItems: Optional[List[CheckupItemBody]] = None
  • Step 4: 新增 api_list_checkups 端点

在"1. 体检信息 CRUD"区域末尾(api_settle_fee 之后)添加:

@router.get("/pilot-checkup/list")
async def api_list_checkups(
    page: int = Query(1, ge=1),
    pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
    checkupStatus: Optional[str] = Query(None, alias="checkupStatus"),
    feeStatus: Optional[str] = Query(None, alias="feeStatus"),
    checkupInstitutionId: Optional[str] = Query(None, alias="checkupInstitutionId"),
    applicationNo: Optional[str] = Query(None, alias="applicationNo"),
    patientName: Optional[str] = Query(None, alias="patientName"),
    channelId: Optional[str] = Query(None, alias="channelId"),
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    tenant_no = _get_tenant_no(db, user)
    result = pilot_checkup.list_checkups_paginated(
        db,
        tenant_no=tenant_no,
        page=page,
        page_size=pageSize,
        checkup_status=checkupStatus,
        fee_status=feeStatus,
        checkup_institution_id=checkupInstitutionId,
        application_no=applicationNo,
        patient_name=patientName,
        channel_id=channelId,
    )
    return success_response(result)
  • Step 5: 新增 api_list_pending_patients 端点
@router.get("/pilot-checkup/pending-patients")
async def api_list_pending_patients(
    page: int = Query(1, ge=1),
    pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
    applicationNo: Optional[str] = Query(None, alias="applicationNo"),
    patientName: Optional[str] = Query(None, alias="patientName"),
    channelId: Optional[str] = Query(None, alias="channelId"),
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    tenant_no = _get_tenant_no(db, user)
    result = pilot_checkup.list_pending_patients(
        db,
        tenant_no=tenant_no,
        page=page,
        page_size=pageSize,
        application_no=applicationNo,
        patient_name=patientName,
        channel_id=channelId,
    )
    return success_response(result)
  • Step 6: 新增 api_batch_save_checkup 端点
@router.post("/pilot-checkup/batch-save")
async def api_batch_save_checkup(
    body: BatchCheckupSaveBody,
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    try:
        tenant_no = _get_tenant_no(db, user)
        items_json = [item.dict() for item in body.reserveItems] if body.reserveItems else []
        result = pilot_checkup.batch_create_checkups(
            db,
            patient_ids=body.patientIds,
            checkup_institution_id=body.checkupInstitutionId or "",
            reserve_time=body.reserveTime or None,
            reserve_items=items_json,
            tenant_no=tenant_no,
            user_id=user.get("user_id"),
        )
        return success_response(result, message=f"成功为 {len(result)} 名患者创建体检记录")
    except ValueError as e:
        return error_response(400, str(e))
  • Step 7: 验证 API 能正常启动
cd backend && source venv/bin/activate
python -c "from app.api.pilot_checkup import router; routes = [r.path for r in router.routes]; print(routes)"

预期输出包含 /web/admin/pilot-checkup/list、/web/admin/pilot-checkup/pending-patients、/web/admin/pilot-checkup/batch-save。

  • Step 8: 提交
git add backend/app/api/pilot_checkup.py
git commit -m "feat(checkup): add list, pending-patients, and batch-save API endpoints"

Task 5: 前端 API 函数

Files:

  • Modify: web-admin/src/api/admin.ts

  • [ ] Step 1: 在"中试服务 - 体检管理"区域末尾(deletePilotCheckupActualItem 之后)添加 3 个新函数

// 体检记录分页列表
export const fetchPilotCheckupList = (params: any) =>
  request.get({ url: '/web/admin/pilot-checkup/list', params })

// 待登记体检的患者列表
export const fetchPilotCheckupPendingPatients = (params: any) =>
  request.get({ url: '/web/admin/pilot-checkup/pending-patients', params })

// 批量创建体检记录
export const batchSavePilotCheckup = (data: any) =>
  request.post({ url: '/web/admin/pilot-checkup/batch-save', params: data })
  • Step 2: 验证无语法错误
cd web-admin && npx tsc --noEmit src/api/admin.ts 2>&1 | head -5
  • Step 3: 提交
git add web-admin/src/api/admin.ts
git commit -m "feat(checkup): add frontend API functions for checkup management page"

Task 6: 前端路由

Files:

  • Modify: web-admin/src/router/modules/sqxp.ts

  • [ ] Step 1: 在 PilotService 的 children 数组末尾(PilotApplication 路由之后,即约 line 710 的 } 之前)添加新路由

{
  id: '80000000000000000000000000000011',
  name: 'CheckupManagement',
  path: 'checkup-management',
  component: '/pilot-service/checkup-management/index',
  meta: {
    title: '体检管理',
    keepAlive: true,
    menuCode: 'pilot-service:checkup-management:list'
  }
}
  • Step 2: 提交
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat(checkup): add checkup management route"

Task 7: 列表页 index.vue

Files:

  • Create: web-admin/src/views/pilot-service/checkup-management/index.vue

参考模板: pilot-service/channel-service-order/index.vue(复杂列表)+ checkup-institution/index.vue(同模块风格)

  • Step 1: 创建目录
mkdir -p web-admin/src/views/pilot-service/checkup-management
  • Step 2: 创建 index.vue

页面结构要求(遵循 AGENTS.md UI 规范):

  • page-container > page-head > h1 标题"体检管理"
  • panel > panel-top > h2 "体检记录"
  • filter-block > filter-grid(2行4列筛选项)
  • list-toolbar:已选 N 项 + 「登记体检」按钮
  • el-table 带 type="selection" 列
  • el-pagination
  • 操作列 fixed="right" min-width ≥ 260

筛选项:

  • 体检状态(el-select:全部/待体检/已体检)
  • 费用状态(el-select:全部/未结算/已结算)
  • 体检机构(el-select,调用 fetchCheckupInstitutionList 加载选项)
  • 申请单号(el-input)
  • 患者姓名(el-input)

表格列(min-width,无 width): | 列 | prop | 说明 | |---|---|---| | ☐选择 | — | type="selection" | | 序号 | — | type="index" | | 申请单号 | applicationNo | | | 患者姓名 | patientName | | | 性别 | sex | 显示"男"/"女" | | 年龄 | age | | | 渠道名称 | channelName | | | 体检机构 | institutionName | 未登记显示"—" | | 预约时间 | reserveTime | | | 体检状态 | checkupStatus | el-tag:checkupStatusTagType | | 费用状态 | feeStatus | el-tag:待结算 info / 已结算 success | | 实际费用 | actualCheckupFee | formatMoneyDisplay | | 操作 | — | 查看 / 标记已体检 / 费用结算 |

操作按钮条件:

  • 查看:始终可用 → 打开 CheckupDetailDialog
  • 标记已体检:checkupStatus === 'dtj' 可用
  • 费用结算:checkupStatus === 'ytj' && feeStatus === 'wjs' 可用

工具栏「登记体检」按钮:selectedRows.length > 0 且所有选中行 checkupStatus === 'dtj' 时可用,点击打开 CheckupRegisterDialog。

API 调用:

  • 列表数据:fetchPilotCheckupList
  • 机构下拉:fetchCheckupInstitutionList
  • 标记已体检:markPilotCheckupChecked
  • 费用结算:settlePilotCheckupFee

复用工具:

  • checkupStatusLabel / checkupStatusTagType from @/utils/status-tag-registry
  • formatMoneyDisplay from @/utils/format

  • [ ] Step 3: 验证页面加载

启动 dev server,访问 /#/pilot-service/checkup-management,确认页面正常渲染。

  • Step 4: 提交
git add web-admin/src/views/pilot-service/checkup-management/index.vue
git commit -m "feat(checkup): create checkup management list page"

Task 8: 登记体检弹窗 CheckupRegisterDialog.vue

Files:

  • Create: web-admin/src/views/pilot-service/checkup-management/CheckupRegisterDialog.vue

参考模式: CheckupPackagePanel.vue(项目选择器模式)

  • Step 1: 创建弹窗组件

组件结构:

  • el-dialog title="登记体检" width="1380px" class="dialog-form-layout" destroy-on-close
  • 内部分 3 个区域:患者选择区、体检信息区、项目选择区

患者选择区:

  • 搜索栏:申请单号 + 患者姓名 + 搜索按钮
  • el-table 带 type="selection":展示 fetchPilotCheckupPendingPatients 数据
  • 列:申请单号、患者姓名、性别、年龄、渠道名称
  • 底部显示"已选 N 名患者"

体检信息区(form-grid-2):

  • 体检机构:el-select 调用 fetchCheckupInstitutionList,选择后加载该机构的项目和套餐
  • 预约时间:el-date-picker type="datetime"

项目选择区:

  • el-tabs 两个 Tab:「从套餐添加」和「添加单项」
  • Tab 1(套餐):el-select 选套餐 → 点击"加载套餐项目" → 自动填充到项目列表
  • Tab 2(单项):el-button "添加项目" → 打开项目选择器(el-table 带 selection 展示该机构所有项目)
  • 已选项目列表:el-table 显示项目名称 + 费用,可删除
  • 底部合计费用

保存逻辑:

  • 校验:至少选 1 名患者、必选体检机构
  • 调用 batchSavePilotCheckup 传入 { patientIds, checkupInstitutionId, reserveTime, reserveItems }
  • 成功后 emit('success') 并关闭弹窗

API 调用:

  • fetchPilotCheckupPendingPatients — 搜索患者
  • fetchCheckupInstitutionList — 机构下拉
  • fetchCheckupInstitutionProjects(institutionId) — 机构项目列表
  • fetchCheckupInstitutionPackages(institutionId) — 机构套餐列表
  • fetchCheckupInstitutionPackageDetail(packageId) — 套餐详情(含项目)
  • batchSavePilotCheckup — 批量保存

  • [ ] Step 2: 在 index.vue 中引入并使用

在 index.vue 中 import 并注册 CheckupRegisterDialog,添加 v-model 绑定和 @success 回调(回调刷新列表)。

  • Step 3: 提交
git add web-admin/src/views/pilot-service/checkup-management/
git commit -m "feat(checkup): create batch checkup register dialog"

Task 9: 详情弹窗 CheckupDetailDialog.vue

Files:

  • Create: web-admin/src/views/pilot-service/checkup-management/CheckupDetailDialog.vue

参考模式: ApplicationDetailDialog.vue 体检 Tab 部分(约 line 2900-3500)

  • Step 1: 创建详情弹窗组件

组件结构:

  • el-dialog title="体检详情 — {patientName}" width="1200px" class="dialog-form-layout" destroy-on-close

基本信息区(只读展示,form-grid-2):

  • 申请单号、患者姓名、性别、年龄、渠道名称
  • 体检机构、预约时间
  • 体检状态 el-tag、费用状态 el-tag

预约项目区(只读):

  • el-table 展示预约项目列表(项目名称 + 费用)
  • 底部合计

实际项目区(可编辑):

  • el-button "添加单项" → 内联输入行(项目名称 input + 费用 input + 确认按钮)
  • el-table 展示实际项目列表(项目名称 + 费用 + 删除操作)
  • 底部合计

体检报告区(卡片化上传,参考 ProtocolFormDialog.vue):

  • el-upload 触发器为虚线卡片(140×96px, dashed border #a7f3d0)
  • 已上传文件展示为卡片行,每个卡片含文件名 + 下载/删除按钮
  • 上传调用 fetchUploadAttachment,URL 以逗号分隔存储到 reportAttachments

备注区:

  • el-input type="textarea" v-model 绑定 checkupRemark

底部操作按钮(根据状态显示):

  • 标记已体检:checkupStatus === 'dtj' 时显示
  • 费用结算:checkupStatus === 'ytj' && feeStatus === 'wjs' 时显示
  • 保存:保存备注和实际项目修改

API 调用:

  • 打开时加载:fetchPilotCheckupDetail({ application_id }) + fetchPilotCheckupReserveItems + fetchPilotCheckupActualItems
  • 保存实际项目:savePilotCheckupActualItems
  • 保存主信息:savePilotCheckup
  • 标记已体检:markPilotCheckupChecked
  • 费用结算:settlePilotCheckupFee
  • 上传附件:fetchUploadAttachment
  • 下载附件:fetchAttachmentDownloadUrl

  • [ ] Step 2: 在 index.vue 中引入并使用

在 index.vue 中 import 并注册 CheckupDetailDialog,点击"查看"或行双击时打开。

  • Step 3: 提交
git add web-admin/src/views/pilot-service/checkup-management/
git commit -m "feat(checkup): create checkup detail dialog with report upload"

Task 10: 端到端验证

  • Step 1: 启动后端
cd backend && source venv/bin/activate && ./start-local.sh
  • Step 2: 启动前端
cd web-admin && pnpm dev
  • Step 3: 验证关键流程
  1. 访问 /#/pilot-service/checkup-management,列表页正常加载
  2. 点击「登记体检」按钮,弹窗正常弹出
  3. 搜索患者、选择体检机构、添加项目、保存 — 成功创建体检记录
  4. 列表中出现新记录,状态为"待体检"
  5. 点击"查看",详情弹窗正常显示基本信息 + 预约项目
  6. 上传体检报告附件 — 成功
  7. 添加实际项目 — 成功
  8. 点击"标记已体检" — 状态变为"已体检"
  9. 点击"费用结算" — 状态变为"已结算"
  • Step 4: 检查 LSP 诊断
# 后端
cd backend && python -c "from app.api.pilot_checkup import router; print('OK')"
# 前端 — 在 IDE 中检查 index.vue 无报错
  • Step 5: 最终提交
git add -A
git commit -m "feat(checkup): complete checkup management page with register, detail, and report upload"