zlwu vor 5 Monaten
Ursprung
Commit
4ff289a8cf
54 geänderte Dateien mit 2986 neuen und 1679 gelöschten Zeilen
  1. 1 1
      backend/app.pid
  2. 4 0
      backend/app/api/production.py
  3. 4 0
      backend/app/api/qc.py
  4. 134 4
      backend/app/crud/pilot_service.py
  5. 204 58
      backend/app/crud/production_task.py
  6. 91 14
      backend/app/crud/production_work_order.py
  7. 67 10
      backend/app/crud/qc_work_order.py
  8. 0 0
      backend/app/graphify-out/cache/ast/07ec3ea29be2732d90417054a59d9d69140d3fd78bcaa15fe21b20c6a34001ff.json
  9. 0 0
      backend/app/graphify-out/cache/ast/28a4e43f54d5740d737e744003706d4f00f6f3e5fff700c533c799354c939d75.json
  10. 0 0
      backend/app/graphify-out/cache/ast/31e8f88f498b056204d0004827268e2343cb91ce8cb912d14b723a04554c204a.json
  11. 0 0
      backend/app/graphify-out/cache/ast/33253a4fa77a956810654663f2b76236f5d3112fccb350064d52b7c249c55987.json
  12. 0 0
      backend/app/graphify-out/cache/ast/78a98df0dce63f59bbe8dcdc780b837b6c661cb35c70b971d51fef4bb0db32e2.json
  13. 0 0
      backend/app/graphify-out/cache/ast/96e0467c778a331812124169c3122913ed010e06c8b4dd73794eaf04542232eb.json
  14. 0 0
      backend/app/graphify-out/cache/ast/99b7078e54dba968a38a2fa41262e71a4d92bc88fb8d5e315d0f53acd027744e.json
  15. 0 0
      backend/app/graphify-out/cache/ast/bc6c77611e00aea4846b14dae8540c402e1f66815522fcaa743b05d2a7c9d134.json
  16. 0 0
      backend/app/graphify-out/cache/ast/cb5592dd7e951b8b2f4896aca347f6ec496d4fab8c3a1fa26e45d7d4d0694a73.json
  17. 0 0
      backend/app/graphify-out/cache/ast/ff12badbb746786c5e0cdd55a90a038f9587817f27188643bf193e9f06f6471f.json
  18. 2 0
      backend/app/schemas/production_work_order.py
  19. 292 493
      backend/graphify-out/.graphify_analysis.json
  20. 185 171
      backend/graphify-out/.graphify_extract.json
  21. 87 132
      backend/graphify-out/GRAPH_REPORT.md
  22. 1 1
      backend/graphify-out/graph.html
  23. 112 112
      backend/graphify-out/graph.json
  24. 7 7
      backend/graphify-out/manifest.json
  25. 579 438
      web-admin/graphify-out/.graphify_analysis.json
  26. 343 83
      web-admin/graphify-out/.graphify_extract.json
  27. 84 119
      web-admin/graphify-out/GRAPH_REPORT.md
  28. 1 1
      web-admin/graphify-out/graph.html
  29. 10 10
      web-admin/graphify-out/manifest.json
  30. 11 0
      web-admin/src/api/production-work-order.ts
  31. 8 0
      web-admin/src/api/qc-work-order.ts
  32. 0 0
      web-admin/src/graphify-out/cache/ast/008c2c2e8ed63fd16d970c51613ca92c809beb8a9122066efcdac68870df139f.json
  33. 0 0
      web-admin/src/graphify-out/cache/ast/0baebf5301228b4cf583b82f343705cb23dc82efdefc002ec7632583c628ff49.json
  34. 1 0
      web-admin/src/graphify-out/cache/ast/1616dcb645c7804452121a05b3f0d7ae0da2c5e32faacc2ab91f172bc079de40.json
  35. 0 0
      web-admin/src/graphify-out/cache/ast/3037500e5ddec59e7665b45ec876c836845c14804de1d40ff2d8af1b7c3bef5f.json
  36. 1 0
      web-admin/src/graphify-out/cache/ast/3393fb35d5053a87679927dd66e85de1072f3458383eecc4e43e0eb965cd8544.json
  37. 1 0
      web-admin/src/graphify-out/cache/ast/46195c37337d139b577c474741918a78c2fd5bd24671d650a0c7cf44739b641d.json
  38. 1 0
      web-admin/src/graphify-out/cache/ast/5799d4c47c7d97432fc923e3af804098a7219b82825875b35de848afff54474b.json
  39. 0 0
      web-admin/src/graphify-out/cache/ast/66123bb7e450062acafbfc41919fe7eeb763f2057b3cae5208ba8b63689a7e2c.json
  40. 0 0
      web-admin/src/graphify-out/cache/ast/6c7fcfe0582ddec0006a3a1dae383a75ce05dcc0c12586f1a5b05b2c347e74de.json
  41. 0 0
      web-admin/src/graphify-out/cache/ast/797a3d629722af7e6ed25e1c2fce999313fc5121ae54c8045b00294e11a2d32f.json
  42. 0 0
      web-admin/src/graphify-out/cache/ast/ada2b9c3d52a727100298f5daa6a051b488a2003d6d194a043a0c17f2c640593.json
  43. 0 0
      web-admin/src/graphify-out/cache/ast/b7de30d2ebc33ebf24898d0c1142460ecf7a4bfc05f679a815fcebbf7bdb30d1.json
  44. 1 0
      web-admin/src/graphify-out/cache/ast/bb280d7b09bce620ca659c06c0f6c3849f3f121c605684b6bf1d66e006be04c9.json
  45. 0 0
      web-admin/src/graphify-out/cache/ast/d9b16a6044f207e63d8cd9a10d306fa43e73bb75b40f24b019e40a17c31320c8.json
  46. 1 0
      web-admin/src/graphify-out/cache/ast/ef2cc77c90f0280a93b8aedb3bc1feda69c2a0879db0f124e8a84b6e78e09ba7.json
  47. 1 0
      web-admin/src/graphify-out/cache/ast/effad9351ebe81b878dd1ccfa6ef88feb7c452f4b69f9bae1a8d44e867a03fcb.json
  48. 5 1
      web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue
  49. 531 3
      web-admin/src/views/pilot-service/checkup-management/CheckupMarkCheckedDialog.vue
  50. 23 0
      web-admin/src/views/pilot-service/checkup-management/checkup-reserve-items.ts
  51. 1 1
      web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue
  52. 76 12
      web-admin/src/views/production/production-work-order/ProductionTaskTab.vue
  53. 58 4
      web-admin/src/views/production/production-work-order/index.vue
  54. 58 4
      web-admin/src/views/quality-inspection/qc-work-order/index.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-83274
+82145

+ 4 - 0
backend/app/api/production.py

@@ -1598,6 +1598,8 @@ async def list_production_work_orders(
     sampleCode: Optional[str] = None,
     createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
     createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
+    infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
+    infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
     user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
@@ -1610,6 +1612,8 @@ async def list_production_work_orders(
         protocol_no=protocolNo, sample_code=sampleCode,
         create_time_start=createTimeStart,
         create_time_end=createTimeEnd,
+        infusion_time_start=infusionTimeStart,
+        infusion_time_end=infusionTimeEnd,
     )
     result = production_work_order_svc.list_production_work_orders(
         db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize,

+ 4 - 0
backend/app/api/qc.py

@@ -599,6 +599,8 @@ async def list_qc_work_orders(
     bizType: Optional[str] = Query(None, alias="bizType"),
     createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
     createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
+    infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
+    infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
     db: Session = Depends(get_db),
     user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
 ):
@@ -614,6 +616,8 @@ async def list_qc_work_orders(
             biz_type=bizType,
             create_time_start=createTimeStart,
             create_time_end=createTimeEnd,
+            infusion_time_start=infusionTimeStart,
+            infusion_time_end=infusionTimeEnd,
             tenant_no=tenant_no,
             user_info=user,
         )

+ 134 - 4
backend/app/crud/pilot_service.py

@@ -2519,8 +2519,114 @@ def list_channel_product_quotas(db, channel_id, tenant_no=None):
 # Entity 17: BizChannelServiceOrder  渠道服务订单
 # ────────────────────────────────────────────
 
-def _row_to_dict_service_order(row, db=None, tenant_no=None) -> Dict[str, Any]:
+def _calc_gmcp_purchase_total_from_db(db, order_id, tenant_no=None) -> float:
+    """从订单产品持仓汇总购买总额(协议单价 × 累计购买份数)。"""
+    q = db.query(BizChannelServiceOrderProduct).filter(
+        and_(
+            BizChannelServiceOrderProduct.service_order_id == order_id,
+            BizChannelServiceOrderProduct.del_flag == 0,
+        )
+    )
+    if tenant_no:
+        q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
+    total = 0.0
+    for prod in q.all():
+        price = float(prod.agreement_unit_price or 0)
+        qty = float(prod.total_buy_qty or 0)
+        total += price * qty
+    return total
+
+
+def _batch_gmcp_purchase_totals(db, order_ids, tenant_no=None) -> Dict[str, float]:
+    if not order_ids:
+        return {}
+    amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.total_buy_qty
+    q = db.query(
+        BizChannelServiceOrderProduct.service_order_id,
+        func.coalesce(func.sum(amount_expr), 0),
+    ).filter(
+        BizChannelServiceOrderProduct.del_flag == 0,
+        BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
+    )
+    if tenant_no:
+        q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
+    return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
+
+
+def _calc_gmcp_current_holding_amount_from_db(db, order_id, tenant_no=None) -> float:
+    """从订单产品持仓汇总当前余额(协议单价 × 当前持有份数)。"""
+    q = db.query(BizChannelServiceOrderProduct).filter(
+        and_(
+            BizChannelServiceOrderProduct.service_order_id == order_id,
+            BizChannelServiceOrderProduct.del_flag == 0,
+        )
+    )
+    if tenant_no:
+        q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
+    total = 0.0
+    for prod in q.all():
+        price = float(prod.agreement_unit_price or 0)
+        qty = float(prod.current_qty or 0)
+        total += price * qty
+    return total
+
+
+def _batch_gmcp_current_holding_amounts(db, order_ids, tenant_no=None) -> Dict[str, float]:
+    if not order_ids:
+        return {}
+    amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.current_qty
+    q = db.query(
+        BizChannelServiceOrderProduct.service_order_id,
+        func.coalesce(func.sum(amount_expr), 0),
+    ).filter(
+        BizChannelServiceOrderProduct.del_flag == 0,
+        BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
+    )
+    if tenant_no:
+        q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
+    return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
+
+
+def _resolve_service_order_purchase_total(row, db=None, tenant_no=None, purchase_total_override=None) -> float:
+    stored = float(row.purchase_total_amount) if row.purchase_total_amount else 0
+    if (row.order_type or "") != "gmcp":
+        return stored
+    if purchase_total_override is not None:
+        return float(purchase_total_override)
+    if db:
+        computed = _calc_gmcp_purchase_total_from_db(db, row.id, tenant_no or row.tenant_no)
+        if computed > 0 or not row.purchase_total_amount:
+            return computed
+    return stored
+
+
+def _resolve_service_order_current_balance(
+    row, db=None, tenant_no=None, current_balance_override=None
+) -> float:
+    if (row.order_type or "") == "gmcp":
+        if current_balance_override is not None:
+            return float(current_balance_override)
+        if db:
+            return _calc_gmcp_current_holding_amount_from_db(db, row.id, tenant_no or row.tenant_no)
+        return 0.0
+    return float(row.current_balance) if row.current_balance else 0
+
+
+def _row_to_dict_service_order(
+    row,
+    db=None,
+    tenant_no=None,
+    *,
+    purchase_total_override=None,
+    current_balance_override=None,
+) -> Dict[str, Any]:
     tn = tenant_no or row.tenant_no
+    purchase_total = _resolve_service_order_purchase_total(
+        row, db, tn, purchase_total_override=purchase_total_override
+    )
+    current_balance = _resolve_service_order_current_balance(
+        row, db, tn, current_balance_override=current_balance_override
+    )
     d = {
         "id": row.id,
         "channelId": row.channel_id or "",
@@ -2529,12 +2635,12 @@ def _row_to_dict_service_order(row, db=None, tenant_no=None) -> Dict[str, Any]:
         "deptName": row.dept_name or "",
         "orderType": row.order_type or "",
         "prepayAmount": float(row.prepay_amount) if row.prepay_amount else 0,
-        "purchaseTotalAmount": float(row.purchase_total_amount) if row.purchase_total_amount else 0,
+        "purchaseTotalAmount": purchase_total,
         "orderStatus": row.order_status or "",
         "channelAgreementId": row.channel_agreement_id or "",
         "totalRechargeAmount": float(row.total_recharge_amount) if row.total_recharge_amount else 0,
         "totalRefundAmount": float(row.total_refund_amount) if row.total_refund_amount else 0,
-        "currentBalance": float(row.current_balance) if row.current_balance else 0,
+        "currentBalance": current_balance,
         "balanceBefore": float(row.balance_before) if row.balance_before else 0,
         "balanceAfter": float(row.balance_after) if row.balance_after else 0,
         "financeSubmitBy": row.finance_submit_by or "",
@@ -2828,9 +2934,20 @@ def list_channel_service_orders(
         if x.finance_submit_by:
             user_ids.append(x.finance_submit_by)
     name_map = _user_nick_name_map(db, user_ids) if db else {}
+    gmcp_ids = [x.id for x in rows if (x.order_type or "") == "gmcp"]
+    purchase_total_map = _batch_gmcp_purchase_totals(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
+    holding_amount_map = (
+        _batch_gmcp_current_holding_amounts(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
+    )
     return [
         {
-            **_row_to_dict_service_order(x, db, tenant_no),
+            **_row_to_dict_service_order(
+                x,
+                db,
+                tenant_no,
+                purchase_total_override=purchase_total_map.get(x.id),
+                current_balance_override=holding_amount_map.get(x.id),
+            ),
             "createByName": name_map.get(x.create_by or "", "") or (x.create_by or ""),
             "financeSubmitByName": name_map.get(x.finance_submit_by or "", "") or (x.finance_submit_by or ""),
         }
@@ -3678,6 +3795,10 @@ def create_purchase_txn(db, order_id, *, products, remark=None, tenant_no=None,
         update_time=now,
         del_flag=0,
     )
+    order.purchase_total_amount = float(order.purchase_total_amount or 0) + total_amount
+    order.update_by = user_id
+    order.update_time = now
+    db.add(order)
     db.add(txn)
     db.commit()
     db.refresh(txn)
@@ -4163,6 +4284,15 @@ def void_txn(db, txn_id, *, tenant_no=None, user_id=None):
                 existing.update_by = user_id
                 existing.update_time = now
                 db.add(existing)
+        txn_amount = float(txn.amount or 0)
+        if txn_amount > 0:
+            order.purchase_total_amount = max(
+                0.0,
+                float(order.purchase_total_amount or 0) - txn_amount,
+            )
+            order.update_by = user_id
+            order.update_time = now
+            db.add(order)
 
     elif txn.txn_type == "unsubscribe" and order:
         # Rollback unsubscribe: restore product holdings

+ 204 - 58
backend/app/crud/production_task.py

@@ -51,6 +51,16 @@ _RECEPTION_SOURCE_LABELS = {
     "cell_storage": "细胞存储",
     "research_donation": "科研捐赠",
 }
+_OUTBOUND_BIZ_TYPE_LABELS = {
+    "pilot_service": "中试服务",
+    "clinical_application": "临床应用",
+}
+_APPLICATION_STATUS_LABELS = {
+    "dks": "待开始",
+    "jxz": "进行中",
+    "ywc": "已完成",
+    "yzf": "已作废",
+}
 _PRODUCTION_CONCLUSION_LABELS = {
     "cg": "成功",
     "sb": "失败",
@@ -75,6 +85,95 @@ def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
     return val
 
 
+def _enrich_outbound_task_items(
+    db: Session,
+    tenant_no: str,
+    items: List[Dict[str, Any]],
+    wo_ctx_map: Dict[str, Dict[str, Any]],
+) -> None:
+    """出库生产任务:来源类型=业务类型,协议号=申请单号,协议状态=申请单状态。"""
+    from app.models.models import (
+        BizPilotApplication,
+        BizPilotInfusionPlan,
+        BizPilotInfusionSchedule,
+    )
+
+    schedule_ids = [
+        ctx.get("infusionScheduleId")
+        for ctx in wo_ctx_map.values()
+        if ctx.get("workOrderType") == "outbound" and ctx.get("infusionScheduleId")
+    ]
+    if not schedule_ids:
+        return
+
+    schedules = {
+        s.id: s
+        for s in db.query(BizPilotInfusionSchedule)
+        .filter(
+            BizPilotInfusionSchedule.id.in_(schedule_ids),
+            BizPilotInfusionSchedule.tenant_no == tenant_no,
+            BizPilotInfusionSchedule.del_flag == 0,
+        )
+        .all()
+    }
+    plan_ids = list({s.infusion_plan_id for s in schedules.values() if s.infusion_plan_id})
+    plan_map = {}
+    if plan_ids:
+        plan_map = {
+            p.id: p
+            for p in db.query(BizPilotInfusionPlan)
+            .filter(
+                BizPilotInfusionPlan.id.in_(plan_ids),
+                BizPilotInfusionPlan.tenant_no == tenant_no,
+                BizPilotInfusionPlan.del_flag == 0,
+            )
+            .all()
+        }
+    app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
+    app_map = {}
+    if app_ids:
+        app_map = {
+            a.id: a
+            for a in db.query(BizPilotApplication)
+            .filter(
+                BizPilotApplication.id.in_(app_ids),
+                BizPilotApplication.tenant_no == tenant_no,
+                BizPilotApplication.del_flag == 0,
+            )
+            .all()
+        }
+
+    schedule_app_map: Dict[str, BizPilotApplication] = {}
+    for sid, sched in schedules.items():
+        plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
+        app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None
+        if app:
+            schedule_app_map[sid] = app
+
+    for item in items:
+        ctx = wo_ctx_map.get(item.get("workOrderId") or "")
+        if not ctx or ctx.get("workOrderType") != "outbound":
+            continue
+        biz_type = ctx.get("bizType") or ""
+        biz_label = _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, biz_type)
+        item["bizType"] = biz_type
+        item["bizTypeLabel"] = biz_label
+        item["receptionSource"] = biz_type
+        item["receptionSourceLabel"] = biz_label
+
+        sched_id = ctx.get("infusionScheduleId") or ""
+        app = schedule_app_map.get(sched_id)
+        if app:
+            app_status = app.application_status or ""
+            item["protocolNo"] = app.application_no or ""
+            item["protocolStatus"] = app_status
+            item["protocolStatusLabel"] = _APPLICATION_STATUS_LABELS.get(app_status, app_status)
+        else:
+            item["protocolNo"] = ""
+            item["protocolStatus"] = ""
+            item["protocolStatusLabel"] = ""
+
+
 def _generate_task_no(db: Session, tenant_no: str) -> str:
     day = datetime.now().strftime("%Y%m%d")
     like = f"RW{day}%"
@@ -497,20 +596,25 @@ def list_production_tasks(
         kw = (keyword or "").strip()
         if kw:
             like = f"%{kw}%"
+            keyword_conditions = [
+                BizProductionTask.task_no.like(like),
+                BizProductionWorkOrder.work_order_no.like(like),
+                BizProductionWorkOrder.protocol_no.like(like),
+                BizProductionWorkOrder.sample_code.like(like),
+            ]
+            if tenant_no:
+                from app.crud.production_work_order import _outbound_work_order_ids_by_keyword
+
+                outbound_ids = _outbound_work_order_ids_by_keyword(db, tenant_no, kw)
+                if outbound_ids:
+                    keyword_conditions.append(BizProductionWorkOrder.id.in_(outbound_ids))
             q = q.outerjoin(
                 BizProductionWorkOrder,
                 and_(
                     BizProductionWorkOrder.id == BizProductionTask.work_order_id,
                     BizProductionWorkOrder.del_flag == 0,
                 ),
-            ).filter(
-                or_(
-                    BizProductionTask.task_no.like(like),
-                    BizProductionWorkOrder.work_order_no.like(like),
-                    BizProductionWorkOrder.protocol_no.like(like),
-                    BizProductionWorkOrder.sample_code.like(like),
-                )
-            )
+            ).filter(or_(*keyword_conditions))
         elif task_no and task_no.strip():
             q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
         if work_order_id and work_order_id.strip():
@@ -557,11 +661,15 @@ def list_production_tasks(
     # 批量补充工单号 + 协议号 + 样本编码
     wo_ids = [x.work_order_id for x in rows if x.work_order_id]
     wo_map: Dict[str, Any] = {}
+    wo_ctx_map: Dict[str, Dict[str, Any]] = {}
     if wo_ids:
         wo_rows = (
             db.query(
                 BizProductionWorkOrder.id,
                 BizProductionWorkOrder.work_order_no,
+                BizProductionWorkOrder.work_order_type,
+                BizProductionWorkOrder.biz_type,
+                BizProductionWorkOrder.infusion_schedule_id,
                 BizProductionWorkOrder.protocol_no,
                 BizProductionWorkOrder.sample_code,
                 BizProductionWorkOrder.reception_id,
@@ -574,8 +682,26 @@ def list_production_tasks(
         )
         for w in wo_rows:
             wo_map[w.id] = w
-        cell_wo_ids = [w.collection_protocol_id for w in wo_rows if (w.reception_source or "cell_storage") != "research_donation" and w.collection_protocol_id]
-        donation_wo_ids = [w.collection_protocol_id for w in wo_rows if (w.reception_source or "") == "research_donation" and w.collection_protocol_id]
+            wo_type = getattr(w, "work_order_type", None) or "inbound"
+            wo_ctx_map[w.id] = {
+                "workOrderType": wo_type,
+                "bizType": getattr(w, "biz_type", None) or "",
+                "infusionScheduleId": getattr(w, "infusion_schedule_id", None) or "",
+            }
+        cell_wo_ids = [
+            w.collection_protocol_id
+            for w in wo_rows
+            if (w.reception_source or "cell_storage") != "research_donation"
+            and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
+            and w.collection_protocol_id
+        ]
+        donation_wo_ids = [
+            w.collection_protocol_id
+            for w in wo_rows
+            if (w.reception_source or "") == "research_donation"
+            and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
+            and w.collection_protocol_id
+        ]
         proto_status_map: Dict[str, str] = {}
         if cell_wo_ids:
             for p in (
@@ -596,17 +722,22 @@ def list_production_tasks(
             wo = wo_map.get(row.work_order_id)
             if wo:
                 item["workOrderNo"] = wo.work_order_no or ""
-                item["protocolNo"] = wo.protocol_no or ""
-                item["sampleCode"] = wo.sample_code or ""
-                _source = wo.reception_source or "cell_storage"
-                if _source == "research_donation":
-                    _st = donation_status_map.get(wo.collection_protocol_id, "")
-                    item["protocolStatus"] = _st
-                    item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
+                wo_type = getattr(wo, "work_order_type", None) or "inbound"
+                if wo_type != "outbound":
+                    item["protocolNo"] = wo.protocol_no or ""
+                    item["sampleCode"] = wo.sample_code or ""
+                    _source = wo.reception_source or "cell_storage"
+                    if _source == "research_donation":
+                        _st = donation_status_map.get(wo.collection_protocol_id, "")
+                        item["protocolStatus"] = _st
+                        item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
+                    else:
+                        _st = proto_status_map.get(wo.collection_protocol_id, "")
+                        item["protocolStatus"] = _st
+                        item["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
                 else:
-                    _st = proto_status_map.get(wo.collection_protocol_id, "")
-                    item["protocolStatus"] = _st
-                    item["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
+                    item["sampleCode"] = wo.sample_code or ""
+    _enrich_outbound_task_items(db, tenant_no, items, wo_ctx_map)
     # 批量补充接收人、接收时间、样本类型(与质检任务/工单列表一致:优先接收记录样本 id)
     from app.crud.qc_work_order import (
         _effective_donation_sample_id,
@@ -679,25 +810,29 @@ def list_production_tasks(
     # 将补充字段写入 items
     for item, row in zip(items, rows):
         wo = wo_map.get(row.work_order_id)
-        if wo:
-            item["protocolNo"] = wo.protocol_no or ""
-            item["sampleCode"] = wo.sample_code or ""
-            rec = reception_map.get(wo.reception_id) if wo.reception_id else None
-            item["receiverName"] = rec.handler_name or "" if rec else ""
-            item["receptionTime"] = _fmt_dt(rec.reception_time) if rec else ""
-            ctx = wo_item_ctx.get(wo.id, {})
-            source = _effective_reception_source(ctx, rec)
-            item["sampleTypeName"] = _sample_type_name_for_item(
-                source=source,
-                donation_sample_id=_effective_donation_sample_id(ctx, rec),
-                protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
-                type_map=type_map,
-                protocol_samples=protocol_samples,
-                donation_samples=donation_samples,
-            )
-            _rs = source
-            item["receptionSource"] = _rs
-            item["receptionSourceLabel"] = _RECEPTION_SOURCE_LABELS.get(_rs, _rs)
+        if not wo:
+            continue
+        wo_type = getattr(wo, "work_order_type", None) or "inbound"
+        if wo_type == "outbound":
+            continue
+        item["protocolNo"] = wo.protocol_no or ""
+        item["sampleCode"] = wo.sample_code or ""
+        rec = reception_map.get(wo.reception_id) if wo.reception_id else None
+        item["receiverName"] = rec.handler_name or "" if rec else ""
+        item["receptionTime"] = _fmt_dt(rec.reception_time) if rec else ""
+        ctx = wo_item_ctx.get(wo.id, {})
+        source = _effective_reception_source(ctx, rec)
+        item["sampleTypeName"] = _sample_type_name_for_item(
+            source=source,
+            donation_sample_id=_effective_donation_sample_id(ctx, rec),
+            protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
+            type_map=type_map,
+            protocol_samples=protocol_samples,
+            donation_samples=donation_samples,
+        )
+        _rs = source
+        item["receptionSource"] = _rs
+        item["receptionSourceLabel"] = _RECEPTION_SOURCE_LABELS.get(_rs, _rs)
     return items, int(total)
 
 
@@ -715,27 +850,38 @@ def get_production_task_detail(db: Session, task_id: str, tenant_no: str) -> Opt
     wo = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.id == task.work_order_id).first()
     result = _task_to_dict(task, work_order_no=wo.work_order_no if wo else "")
     if wo:
+        wo_type = getattr(wo, "work_order_type", None) or "inbound"
         result["sampleCode"] = wo.sample_code or ""
-        result["protocolNo"] = wo.protocol_no or ""
-        _src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
-        _cpid = wo.collection_protocol_id or ""
-        if _src == "research_donation" and _cpid:
-            d = db.query(BizResearchDonation.donation_status).filter(
-                BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0
-            ).first()
-            _st = d.donation_status or "" if d else ""
-            result["protocolStatus"] = _st
-            result["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
-        elif _cpid:
-            p = db.query(BizCollectionProtocol.protocol_status).filter(
-                BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0
-            ).first()
-            _st = p.protocol_status or "" if p else ""
-            result["protocolStatus"] = _st
-            result["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
+        if wo_type == "outbound":
+            wo_ctx = {
+                wo.id: {
+                    "workOrderType": wo_type,
+                    "bizType": getattr(wo, "biz_type", None) or "",
+                    "infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or "",
+                }
+            }
+            _enrich_outbound_task_items(db, tenant_no, [result], wo_ctx)
         else:
-            result["protocolStatus"] = ""
-            result["protocolStatusLabel"] = ""
+            result["protocolNo"] = wo.protocol_no or ""
+            _src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
+            _cpid = wo.collection_protocol_id or ""
+            if _src == "research_donation" and _cpid:
+                d = db.query(BizResearchDonation.donation_status).filter(
+                    BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0
+                ).first()
+                _st = d.donation_status or "" if d else ""
+                result["protocolStatus"] = _st
+                result["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
+            elif _cpid:
+                p = db.query(BizCollectionProtocol.protocol_status).filter(
+                    BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0
+                ).first()
+                _st = p.protocol_status or "" if p else ""
+                result["protocolStatus"] = _st
+                result["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
+            else:
+                result["protocolStatus"] = ""
+                result["protocolStatusLabel"] = ""
 
     steps = (
         db.query(BizProductionTaskStep)

+ 91 - 14
backend/app/crud/production_work_order.py

@@ -124,12 +124,19 @@ def _enrich_wo_items(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str,
 _BIZ_TYPE_LABELS = {"pilot_service": "中试服务", "clinical_application": "临床应用"}
 
 
-def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
-    """为 outbound 类型工单附加申请单号、细胞类型、规格、输注时间等关联数据。"""
+def _enrich_outbound_items(
+    db: Session, tenant_no: str, items: List[Dict[str, Any]]
+) -> List[Dict[str, Any]]:
+    """为 outbound 类型工单附加申请单号、用户姓名、疗程序号、细胞类型、规格、输注时间等关联数据。"""
+    from app.crud.pilot_infusion_schedule_mgmt import (
+        _get_plan_schedule_course_meta,
+        _resolve_course_display,
+    )
     from app.models.models import (
         BizPilotApplication,
         BizPilotInfusionPlan,
         BizPilotInfusionSchedule,
+        BizPilotPatient,
     )
 
     outbound_ids = [
@@ -144,6 +151,7 @@ def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dic
         db.query(BizPilotInfusionSchedule)
         .filter(
             BizPilotInfusionSchedule.id.in_(outbound_ids),
+            BizPilotInfusionSchedule.tenant_no == tenant_no,
             BizPilotInfusionSchedule.del_flag == 0,
         )
         .all()
@@ -153,14 +161,43 @@ def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dic
     plan_ids = list({s.infusion_plan_id for s in schedules if s.infusion_plan_id})
     plan_map = {}
     if plan_ids:
-        plans = db.query(BizPilotInfusionPlan).filter(BizPilotInfusionPlan.id.in_(plan_ids)).all()
+        plans = (
+            db.query(BizPilotInfusionPlan)
+            .filter(
+                BizPilotInfusionPlan.id.in_(plan_ids),
+                BizPilotInfusionPlan.tenant_no == tenant_no,
+                BizPilotInfusionPlan.del_flag == 0,
+            )
+            .all()
+        )
         plan_map = {p.id: p for p in plans}
 
     app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
     app_map = {}
+    patient_map: Dict[str, BizPilotPatient] = {}
     if app_ids:
-        apps = db.query(BizPilotApplication).filter(BizPilotApplication.id.in_(app_ids)).all()
+        apps = (
+            db.query(BizPilotApplication)
+            .filter(
+                BizPilotApplication.id.in_(app_ids),
+                BizPilotApplication.tenant_no == tenant_no,
+                BizPilotApplication.del_flag == 0,
+            )
+            .all()
+        )
         app_map = {a.id: a for a in apps}
+        patients = (
+            db.query(BizPilotPatient)
+            .filter(
+                BizPilotPatient.pilot_application_id.in_(app_ids),
+                BizPilotPatient.tenant_no == tenant_no,
+                BizPilotPatient.del_flag == 0,
+            )
+            .all()
+        )
+        patient_map = {p.pilot_application_id: p for p in patients}
+
+    plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
 
     for it in items:
         if it.get("workOrderType") != "outbound" or not it.get("infusionScheduleId"):
@@ -170,8 +207,20 @@ def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dic
             continue
         plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
         app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None
+        patient = (
+            patient_map.get(plan.pilot_application_id)
+            if plan and plan.pilot_application_id
+            else None
+        )
+        plan_meta = (
+            plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
+        )
+        course = _resolve_course_display(sched, plan, plan_meta)
 
         it["applicationNo"] = app.application_no if app else ""
+        it["patientName"] = patient.patient_name if patient else ""
+        it["sortNo"] = course.get("sortNo")
+        it["totalCourseCount"] = course.get("totalCourseCount")
         it["cellType"] = sched.cell_type or ""
         it["specText"] = sched.spec_text or ""
         it["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
@@ -487,16 +536,44 @@ def list_production_work_orders(
         if query.sample_code and query.sample_code.strip():
             like = f"%{query.sample_code.strip()}%"
             q0 = q0.filter(BizProductionWorkOrder.sample_code.like(like))
-        if query.create_time_start and query.create_time_start.strip():
-            q0 = q0.filter(
-                BizProductionWorkOrder.create_time
-                >= _normalize_date_bound(query.create_time_start.strip())
-            )
-        if query.create_time_end and query.create_time_end.strip():
-            q0 = q0.filter(
-                BizProductionWorkOrder.create_time
-                <= _normalize_date_bound(query.create_time_end.strip(), end_of_day=True)
+        use_infusion_time = wo_type == "outbound" and (
+            (query.infusion_time_start and query.infusion_time_start.strip())
+            or (query.infusion_time_end and query.infusion_time_end.strip())
+        )
+        if use_infusion_time:
+            from app.models.models import BizPilotInfusionSchedule
+
+            q0 = q0.join(
+                BizPilotInfusionSchedule,
+                eq_unicode(
+                    BizProductionWorkOrder.infusion_schedule_id,
+                    BizPilotInfusionSchedule.id,
+                ),
+            ).filter(
+                BizPilotInfusionSchedule.del_flag == 0,
+                BizPilotInfusionSchedule.tenant_no == tenant_no,
             )
+            if query.infusion_time_start and query.infusion_time_start.strip():
+                q0 = q0.filter(
+                    BizPilotInfusionSchedule.infusion_time
+                    >= _normalize_date_bound(query.infusion_time_start.strip())
+                )
+            if query.infusion_time_end and query.infusion_time_end.strip():
+                q0 = q0.filter(
+                    BizPilotInfusionSchedule.infusion_time
+                    <= _normalize_date_bound(query.infusion_time_end.strip(), end_of_day=True)
+                )
+        else:
+            if query.create_time_start and query.create_time_start.strip():
+                q0 = q0.filter(
+                    BizProductionWorkOrder.create_time
+                    >= _normalize_date_bound(query.create_time_start.strip())
+                )
+            if query.create_time_end and query.create_time_end.strip():
+                q0 = q0.filter(
+                    BizProductionWorkOrder.create_time
+                    <= _normalize_date_bound(query.create_time_end.strip(), end_of_day=True)
+                )
         if user_info:
             from app.core.data_scope import apply_data_scope
             q0 = apply_data_scope(q0, BizProductionWorkOrder, user_info, db, tenant_no)
@@ -516,7 +593,7 @@ def list_production_work_orders(
         .all()
     )
     items = _enrich_wo_items(db, [_wo_to_dict(x) for x in rows])
-    items = _enrich_outbound_items(db, items)
+    items = _enrich_outbound_items(db, tenant_no, items)
     return {"items": items, "total": int(total)}
 
 

+ 67 - 10
backend/app/crud/qc_work_order.py

@@ -143,14 +143,19 @@ def _enrich_work_order_cell_products(
 def _enrich_outbound_work_orders(
     db: Session, tenant_no: str, items: List[Dict[str, Any]]
 ) -> List[Dict[str, Any]]:
-    """为 outbound 类型工单批量补充申请单/输注计划信息。"""
+    """为 outbound 类型工单批量补充申请单/用户姓名/疗程序号/输注计划信息。"""
+    from app.crud.pilot_infusion_schedule_mgmt import (
+        _get_plan_schedule_course_meta,
+        _resolve_course_display,
+    )
+
     outbound = [x for x in items if x.get("workOrderType") == "outbound"]
     if not outbound:
         return items
     schedule_ids = list({x.get("infusionScheduleId") for x in outbound if x.get("infusionScheduleId")})
     if not schedule_ids:
         return items
-    # 查输注计划 → 输注方案 → 申请单
+    # 查输注计划 → 输注方案 → 申请单 → 患者
     schedules = {
         s.id: s
         for s in db.query(BizPilotInfusionSchedule)
@@ -183,6 +188,21 @@ def _enrich_outbound_work_orders(
         )
         .all()
     } if app_ids else {}
+    patient_map: Dict[str, BizPilotPatient] = {}
+    if app_ids:
+        patients = (
+            db.query(BizPilotPatient)
+            .filter(
+                BizPilotPatient.pilot_application_id.in_(app_ids),
+                BizPilotPatient.tenant_no == tenant_no,
+                BizPilotPatient.del_flag == 0,
+            )
+            .all()
+        )
+        patient_map = {p.pilot_application_id: p for p in patients}
+
+    plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
+
     for item in outbound:
         sid = item.get("infusionScheduleId") or ""
         sched = schedules.get(sid)
@@ -190,9 +210,22 @@ def _enrich_outbound_work_orders(
             continue
         plan = plans.get(sched.infusion_plan_id or "")
         app = apps.get(plan.pilot_application_id or "") if plan else None
+        patient = (
+            patient_map.get(plan.pilot_application_id)
+            if plan and plan.pilot_application_id
+            else None
+        )
+        plan_meta = (
+            plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
+        )
+        course = _resolve_course_display(sched, plan, plan_meta)
+
         item["applicationNo"] = app.application_no or "" if app else ""
         item["applicationName"] = app.application_name or "" if app else ""
         item["channelName"] = app.channel_name or "" if app else ""
+        item["patientName"] = patient.patient_name if patient else ""
+        item["sortNo"] = course.get("sortNo")
+        item["totalCourseCount"] = course.get("totalCourseCount")
         item["infusionTime"] = _fmt_dt(sched.infusion_time)
     return items
 
@@ -665,6 +698,8 @@ def list_work_orders(
     biz_type: Optional[str] = None,
     create_time_start: Optional[str] = None,
     create_time_end: Optional[str] = None,
+    infusion_time_start: Optional[str] = None,
+    infusion_time_end: Optional[str] = None,
     tenant_no: Optional[str] = None,
     user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
@@ -694,15 +729,37 @@ def list_work_orders(
         st = (work_order_status or "").strip()
         if st:
             q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
-        if create_time_start:
-            q0 = q0.filter(
-                BizQcWorkOrder.create_time >= _normalize_date_bound(create_time_start)
-            )
-        if create_time_end:
-            q0 = q0.filter(
-                BizQcWorkOrder.create_time
-                <= _normalize_date_bound(create_time_end, end_of_day=True)
+        use_infusion_time = wt == "outbound" and (
+            (infusion_time_start or "").strip() or (infusion_time_end or "").strip()
+        )
+        if use_infusion_time:
+            q0 = q0.join(
+                BizPilotInfusionSchedule,
+                eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id),
+            ).filter(
+                BizPilotInfusionSchedule.del_flag == 0,
+                BizPilotInfusionSchedule.tenant_no == tenant_no,
             )
+            if (infusion_time_start or "").strip():
+                q0 = q0.filter(
+                    BizPilotInfusionSchedule.infusion_time
+                    >= _normalize_date_bound(infusion_time_start.strip())
+                )
+            if (infusion_time_end or "").strip():
+                q0 = q0.filter(
+                    BizPilotInfusionSchedule.infusion_time
+                    <= _normalize_date_bound(infusion_time_end.strip(), end_of_day=True)
+                )
+        else:
+            if create_time_start:
+                q0 = q0.filter(
+                    BizQcWorkOrder.create_time >= _normalize_date_bound(create_time_start)
+                )
+            if create_time_end:
+                q0 = q0.filter(
+                    BizQcWorkOrder.create_time
+                    <= _normalize_date_bound(create_time_end, end_of_day=True)
+                )
         if user_info:
             from app.core.data_scope import apply_data_scope
             q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/07ec3ea29be2732d90417054a59d9d69140d3fd78bcaa15fe21b20c6a34001ff.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/28a4e43f54d5740d737e744003706d4f00f6f3e5fff700c533c799354c939d75.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/31e8f88f498b056204d0004827268e2343cb91ce8cb912d14b723a04554c204a.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/33253a4fa77a956810654663f2b76236f5d3112fccb350064d52b7c249c55987.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/78a98df0dce63f59bbe8dcdc780b837b6c661cb35c70b971d51fef4bb0db32e2.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/96e0467c778a331812124169c3122913ed010e06c8b4dd73794eaf04542232eb.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/99b7078e54dba968a38a2fa41262e71a4d92bc88fb8d5e315d0f53acd027744e.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/bc6c77611e00aea4846b14dae8540c402e1f66815522fcaa743b05d2a7c9d134.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/cb5592dd7e951b8b2f4896aca347f6ec496d4fab8c3a1fa26e45d7d4d0694a73.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
backend/app/graphify-out/cache/ast/ff12badbb746786c5e0cdd55a90a038f9587817f27188643bf193e9f06f6471f.json


+ 2 - 0
backend/app/schemas/production_work_order.py

@@ -18,6 +18,8 @@ class ProductionWorkOrderListQuery(BaseModel):
     sample_code: Optional[str] = None
     create_time_start: Optional[str] = None
     create_time_end: Optional[str] = None
+    infusion_time_start: Optional[str] = None
+    infusion_time_end: Optional[str] = None
 
 
 class ProductionWorkOrderComplete(BaseModel):

Datei-Diff unterdrückt, da er zu groß ist
+ 292 - 493
backend/graphify-out/.graphify_analysis.json


Datei-Diff unterdrückt, da er zu groß ist
+ 185 - 171
backend/graphify-out/.graphify_extract.json


+ 87 - 132
backend/graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - app  (2026-05-28)
 
 ## Corpus Check
-- 172 files · ~178,200 words
+- 172 files · ~179,323 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 3259 nodes · 14420 edges · 55 communities detected
-- Extraction: 38% EXTRACTED · 62% INFERRED · 0% AMBIGUOUS · INFERRED: 8974 edges (avg confidence: 0.62)
+- 3269 nodes · 14547 edges · 46 communities detected
+- Extraction: 38% EXTRACTED · 62% INFERRED · 0% AMBIGUOUS · INFERRED: 9083 edges (avg confidence: 0.61)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -50,26 +50,17 @@
 - [[_COMMUNITY_Community 37|Community 37]]
 - [[_COMMUNITY_Community 38|Community 38]]
 - [[_COMMUNITY_Community 39|Community 39]]
-- [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 41|Community 41]]
 - [[_COMMUNITY_Community 42|Community 42]]
 - [[_COMMUNITY_Community 43|Community 43]]
 - [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 45|Community 45]]
 - [[_COMMUNITY_Community 46|Community 46]]
 - [[_COMMUNITY_Community 47|Community 47]]
-- [[_COMMUNITY_Community 48|Community 48]]
-- [[_COMMUNITY_Community 51|Community 51]]
-- [[_COMMUNITY_Community 52|Community 52]]
-- [[_COMMUNITY_Community 53|Community 53]]
-- [[_COMMUNITY_Community 54|Community 54]]
-- [[_COMMUNITY_Community 55|Community 55]]
-- [[_COMMUNITY_Community 56|Community 56]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `success_response()` - 518 edges
 2. `error_response()` - 418 edges
-3. `SysUser` - 342 edges
+3. `SysUser` - 344 edges
 4. `BizQcTaskResult` - 267 edges
 5. `BizQcMaterialUsage` - 262 edges
 6. `BizQcMaterial` - 259 edges
@@ -93,265 +84,229 @@
 ## Communities
 
 ### Community 0 - "Community 0"
-Cohesion: 0.02
-Nodes (585): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+577 more)
+Cohesion: 0.01
+Nodes (604): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+596 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.01
-Nodes (493): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+485 more)
+Nodes (523): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+515 more)
 
 ### Community 2 - "Community 2"
-Cohesion: 0.01
-Nodes (326): list_collection_regions(), _persist_protocol_sub_resources(), create_access_log(), delete_access_log(), delete_clean_room(), delete_storage_facility(), delete_storage_slot(), _get_tenant_no() (+318 more)
+Cohesion: 0.04
+Nodes (445): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+437 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.05
-Nodes (331): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody (+323 more)
+Cohesion: 0.02
+Nodes (168): list_collection_regions(), list_protocol_nos(), _persist_protocol_sub_resources(), list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes() (+160 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.06
-Nodes (216): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+208 more)
+Cohesion: 0.05
+Nodes (183): _deduct_fee_on_delivery(), 中试交付管理 CRUD(交付信息、交付物料、交付方式 + 费用扣减), 创建交付时扣减费用。      流程:     1. schedule → plan → application     2. application.chan, List all deliveries for an application (through plan → schedule → delivery)., Bulk save: soft-delete existing + insert new., Create or update delivery method (upsert pattern)., activate_channel_agreement(), _apply_gmcp_order_quotas() (+175 more)
 
 ### Community 5 - "Community 5"
 Cohesion: 0.02
-Nodes (106): get_designer_data(), _error_json(), lifespan(), 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+98 more)
+Nodes (167): generate_uuid(), _access_log_to_dict(), create_access_log(), create_clean_room(), get_access_log(), get_clean_room(), list_access_logs(), list_clean_rooms() (+159 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.03
-Nodes (154): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), _format_attachment_file_size(), urls_to_attachment_list() (+146 more)
+Nodes (146): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), _format_attachment_file_size(), urls_to_attachment_list() (+138 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.02
-Nodes (120): _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。 (+112 more)
+Nodes (73): _error_json(), lifespan(), 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware, parse_admin_request_json() (+65 more)
 
 ### Community 8 - "Community 8"
 Cohesion: 0.04
-Nodes (101): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_batch() (+93 more)
+Nodes (69): get_protocol_nos_used_in_protocols(), create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), _row_to_dict(), soft_delete_dict_type(), update_dict_type() (+61 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.06
-Nodes (66): api_save_application(), auto_create_payment(), auto_generate_schedules(), complete_application(), create_application(), create_infusion_plan(), create_schedule(), _create_schedule_products() (+58 more)
+Cohesion: 0.05
+Nodes (56): _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。 (+48 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.35
-Nodes (61): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+53 more)
+Cohesion: 0.07
+Nodes (60): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+52 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.09
-Nodes (42): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), _resolve_receiver_purpose(), revert_step_stock_changes(), _row_to_dict(), _user_nick_name_map() (+34 more)
+Cohesion: 0.07
+Nodes (58): complete_application(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd(), _generate_application_no(), get_application(), get_patient_by_application() (+50 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.12
-Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
+Cohesion: 0.07
+Nodes (55): _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict(), _handover_view_dict(), _parse_handover_time(), 获取出库交接编辑数据(物料 + 交接信息 + 摘要)。 (+47 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.16
-Nodes (26): add_step(), copy_step(), create_route(), _create_step_consumable(), _create_step_data_collection(), _create_step_output(), _create_step_reagent(), get_route() (+18 more)
+Cohesion: 0.07
+Nodes (51): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_batch() (+43 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.18
-Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
+Cohesion: 0.06
+Nodes (41): get_designer_data(), list_params_by_item(), _row_to_dict(), save_params_for_item(), list_results_by_task(), _list_saved_results_by_task(), _merge_saved_into_template(), _resolve_task_item_ref() (+33 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.25
-Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
+Cohesion: 0.09
+Nodes (48): allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。     须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_collection_protocol(), _active_link_query(), assert_biz_no_available() (+40 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.25
-Nodes (13): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), 采集协议号池 CRUD(biz_collection_protocol_no) (+5 more)
+Cohesion: 0.12
+Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.25
-Nodes (14): batch_stock_in(), batch_stock_out(), _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials() (+6 more)
+Cohesion: 0.18
+Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.27
-Nodes (11): _accept_conclusion_label(), _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), _normalize_date_bound(), resolve_notification() (+3 more)
+Cohesion: 0.25
+Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.26
-Nodes (11): _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), _normalize_date_bound(), resolve_notification(), _row_to_dict() (+3 more)
-
-### Community 20 - "Community 20"
-Cohesion: 0.26
-Nodes (12): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), 样本编码池 CRUD(biz_sample_code), _resolve_issued_user_display_name() (+4 more)
-
-### Community 21 - "Community 21"
 Cohesion: 0.24
 Nodes (9): 微信消息通知服务  支持微信小程序模板消息推送  使用方式:     from app.core.wechat_message import Wech, 发送模板消息                  Args:             openid: 用户openid             templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+1 more)
 
-### Community 22 - "Community 22"
+### Community 20 - "Community 20"
 Cohesion: 0.23
 Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
 
-### Community 23 - "Community 23"
+### Community 21 - "Community 21"
 Cohesion: 0.24
 Nodes (12): appointment_no_exists(), create_exhibition_appointment(), _format_visit_date(), _generate_appointment_no(), get_exhibition_appointment(), list_exhibition_appointments(), 展览预约 CRUD(biz_exhibition_appointment), 生成预约编号:YYYYMMDD + 6位序号(如 20260515000001) (+4 more)
 
-### Community 24 - "Community 24"
-Cohesion: 0.27
-Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
-
-### Community 25 - "Community 25"
-Cohesion: 0.31
-Nodes (10): deduct_for_task(), _display_remark(), list_usages(), list_usages_by_task(), _parse_receiver_purpose_from_remark(), 扣减物料库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose(), revert_task_material_usages() (+2 more)
+### Community 22 - "Community 22"
+Cohesion: 0.3
+Nodes (10): create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists(), _row_to_dict() (+2 more)
 
-### Community 26 - "Community 26"
+### Community 23 - "Community 23"
 Cohesion: 0.27
-Nodes (10): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose(), revert_step_stock_changes() (+2 more)
+Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
 
-### Community 27 - "Community 27"
+### Community 24 - "Community 24"
 Cohesion: 0.29
 Nodes (9): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu() (+1 more)
 
-### Community 28 - "Community 28"
-Cohesion: 0.27
-Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
+### Community 25 - "Community 25"
+Cohesion: 0.31
+Nodes (8): create_sop_document(), get_sop_document(), list_sop_documents(), SOP文档 CRUD(biz_sop_document), _row_to_dict(), soft_delete_sop_document(), sop_code_exists(), update_sop_document()
 
-### Community 29 - "Community 29"
+### Community 26 - "Community 26"
 Cohesion: 0.31
 Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
 
-### Community 30 - "Community 30"
+### Community 27 - "Community 27"
 Cohesion: 0.38
 Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
 
-### Community 31 - "Community 31"
-Cohesion: 0.27
-Nodes (8): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data(), update_dict_data()
-
-### Community 32 - "Community 32"
+### Community 28 - "Community 28"
 Cohesion: 0.31
 Nodes (8): category_code_exists(), create_process_category(), get_process_category(), list_process_categories(), 工艺工序分类 CRUD(biz_process_category), _row_to_dict(), soft_delete_process_category(), update_process_category()
 
-### Community 33 - "Community 33"
+### Community 29 - "Community 29"
 Cohesion: 0.31
 Nodes (8): create_item_unit(), get_item_unit(), list_item_units(), 物品单位 CRUD(biz_item_unit), _row_to_dict(), soft_delete_item_unit(), unit_code_exists(), update_item_unit()
 
-### Community 34 - "Community 34"
+### Community 30 - "Community 30"
 Cohesion: 0.31
 Nodes (8): create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type(), type_code_exists(), update_cell_sample_type()
 
-### Community 35 - "Community 35"
+### Community 31 - "Community 31"
 Cohesion: 0.31
 Nodes (8): create_output_type(), get_output_type(), list_output_types(), 工艺产出物类型 CRUD(biz_output_type), _row_to_dict(), soft_delete_output_type(), type_code_exists(), update_output_type()
 
-### Community 36 - "Community 36"
-Cohesion: 0.36
-Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
-
-### Community 37 - "Community 37"
+### Community 32 - "Community 32"
 Cohesion: 0.36
 Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
 
-### Community 38 - "Community 38"
-Cohesion: 0.39
-Nodes (8): create_lab_item(), get_lab_item(), item_code_exists(), list_active_lab_items(), list_lab_items(), _row_to_dict(), soft_delete_lab_item(), update_lab_item()
-
-### Community 39 - "Community 39"
-Cohesion: 0.36
-Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
-
-### Community 40 - "Community 40"
+### Community 33 - "Community 33"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 41 - "Community 41"
+### Community 34 - "Community 34"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 42 - "Community 42"
-Cohesion: 0.6
-Nodes (5): _dec(), _fmt_dt(), list_stock_logs(), _row_to_dict(), _user_nick_name_map()
-
-### Community 43 - "Community 43"
-Cohesion: 0.4
-Nodes (4): list_ethnics_for_select(), list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict()
-
-### Community 44 - "Community 44"
+### Community 35 - "Community 35"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 45 - "Community 45"
+### Community 36 - "Community 36"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 46 - "Community 46"
+### Community 37 - "Community 37"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,13 个一级菜单)
 
-### Community 47 - "Community 47"
+### Community 38 - "Community 38"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 48 - "Community 48"
+### Community 39 - "Community 39"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 51 - "Community 51"
+### Community 42 - "Community 42"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 52 - "Community 52"
+### Community 43 - "Community 43"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 53 - "Community 53"
+### Community 44 - "Community 44"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 54 - "Community 54"
+### Community 45 - "Community 45"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 55 - "Community 55"
+### Community 46 - "Community 46"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 56 - "Community 56"
+### Community 47 - "Community 47"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
 - **96 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+91 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 40`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 33`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 36`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,13 个一级菜单)`
+- **Thin community `Community 37`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,13 个一级菜单)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 47`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 38`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 48`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 39`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 42`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 52`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 43`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 53`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 44`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 45`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 55`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 46`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 56`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 47`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `SysUser` connect `Community 1` to `Community 3`, `Community 4`, `Community 7`, `Community 8`, `Community 10`, `Community 16`, `Community 20`, `Community 25`, `Community 26`?**
-  _High betweenness centrality (0.122) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 2`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 43`, `Community 46`?**
-  _High betweenness centrality (0.078) - this node is a cross-community bridge._
-- **Why does `generate_uuid()` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`?**
-  _High betweenness centrality (0.059) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 1` to `Community 2`, `Community 3`, `Community 4`, `Community 8`, `Community 9`, `Community 10`, `Community 12`?**
+  _High betweenness centrality (0.106) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 3`, `Community 37`, `Community 9`, `Community 13`, `Community 14`?**
+  _High betweenness centrality (0.090) - this node is a cross-community bridge._
+- **Why does `generate_uuid()` connect `Community 5` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`?**
+  _High betweenness centrality (0.072) - this node is a cross-community bridge._
 - **Are the 517 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
   _`success_response()` has 517 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 503 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `extract_login_credentials()`) actually correct?**
   _`str` has 503 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 417 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
   _`error_response()` has 417 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 340 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
-  _`SysUser` has 340 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 342 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
+  _`SysUser` has 342 INFERRED edges - model-reasoned connections that need verification._

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 1
backend/graphify-out/graph.html


Datei-Diff unterdrückt, da er zu groß ist
+ 112 - 112
backend/graphify-out/graph.json


+ 7 - 7
backend/graphify-out/manifest.json

@@ -40,7 +40,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/outbound_release_review.py": 1779808997.3299003,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/price_list.py": 1778578258.096609,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/user.py": 1778578258.099944,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_work_order.py": 1779692778.4620268,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_work_order.py": 1779924314.060957,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/storage_occupancy.py": 1779418054.7652438,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/quotation.py": 1778578258.0991468,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter_value.py": 1778578258.0980055,
@@ -77,7 +77,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/post.py": 1779265648.38155,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_result.py": 1779255909.8554008,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/user.py": 1778775532.8870275,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779890270.3297482,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779924316.767352,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/material.py": 1779283386.3879013,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/dict_type.py": 1779265621.5212462,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_occupancy.py": 1779418213.0446064,
@@ -86,7 +86,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/tenant.py": 1779265611.0532296,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable.py": 1779512620.86979,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sop_document.py": 1779265721.9928973,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779893693.1517875,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779924808.9420185,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_item.py": 1779265661.482352,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent.py": 1779512669.9847593,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1779882417.575858,
@@ -115,11 +115,11 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_checkup.py": 1779767080.896167,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_category.py": 1779265700.0606413,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/item_unit.py": 1779265653.7504313,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1779892264.360863,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1779924321.8488002,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_handover_inventory.py": 1779830339.8633556,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_release_material.py": 1779827094.4205513,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1779309113.0447981,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779792399.6927543,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779923362.485726,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1779686149.7508218,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/workflow.py": 1779265993.677624,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_sms_notification.py": 1779691673.794283,
@@ -144,7 +144,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/outbound_release_review.py": 1779827082.0736034,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1779313409.0685387,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1779526648.041865,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779791675.3703022,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779924323.352671,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1779882422.004045,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1779882001.7519956,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
@@ -164,7 +164,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_sms_notification.py": 1779691678.688495,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1779692499.6698253,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1779281097.279193,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779692908.7915888,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779924318.5910363,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/clinical_payment.py": 1779835176.3325863,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,

Datei-Diff unterdrückt, da er zu groß ist
+ 579 - 438
web-admin/graphify-out/.graphify_analysis.json


Datei-Diff unterdrückt, da er zu groß ist
+ 343 - 83
web-admin/graphify-out/.graphify_extract.json


+ 84 - 119
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
 # Graph Report - src  (2026-05-28)
 
 ## Corpus Check
-- Large corpus: 668 files · ~413,987 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 668 files · ~415,647 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 1878 nodes · 2399 edges · 45 communities detected
-- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 532 edges (avg confidence: 0.8)
+- 1901 nodes · 2445 edges · 40 communities detected
+- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 547 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -39,25 +39,20 @@
 - [[_COMMUNITY_Community 27|Community 27]]
 - [[_COMMUNITY_Community 29|Community 29]]
 - [[_COMMUNITY_Community 30|Community 30]]
-- [[_COMMUNITY_Community 31|Community 31]]
+- [[_COMMUNITY_Community 32|Community 32]]
 - [[_COMMUNITY_Community 33|Community 33]]
 - [[_COMMUNITY_Community 34|Community 34]]
 - [[_COMMUNITY_Community 35|Community 35]]
 - [[_COMMUNITY_Community 36|Community 36]]
-- [[_COMMUNITY_Community 37|Community 37]]
-- [[_COMMUNITY_Community 38|Community 38]]
-- [[_COMMUNITY_Community 39|Community 39]]
 - [[_COMMUNITY_Community 40|Community 40]]
-- [[_COMMUNITY_Community 43|Community 43]]
+- [[_COMMUNITY_Community 41|Community 41]]
+- [[_COMMUNITY_Community 42|Community 42]]
 - [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 46|Community 46]]
-- [[_COMMUNITY_Community 47|Community 47]]
-- [[_COMMUNITY_Community 49|Community 49]]
-- [[_COMMUNITY_Community 51|Community 51]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `get()` - 203 edges
-2. `push()` - 34 edges
+2. `push()` - 36 edges
 3. `WebSocketClient` - 26 edges
 4. `emit()` - 16 edges
 5. `MenuProcessor` - 16 edges
@@ -68,245 +63,215 @@
 10. `buildOutboundPilotContextSections()` - 12 edges
 
 ## Surprising Connections (you probably didn't know these)
+- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
+  views/production/consumable/ConsumablePackTab.vue → utils/fullscreen-loading.ts
+- `handleReceptionAttachmentDownload()` --calls--> `downloadAttachmentFile()`  [INFERRED]
+  views/production/production-work-order/index.vue → utils/download-attachment.ts
 - `subscribeTokenRefresh()` --calls--> `push()`  [INFERRED]
   utils/http/index.ts → views/operations/storage-facility/index.vue
 - `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()`  [INFERRED]
   utils/http/index.ts → api/auth.ts
-- `fetchLeaveList()` --calls--> `get()`  [INFERRED]
-  api/leave.ts → views/production/process/ProcessRouteTab.vue
-- `fetchLeaveDetail()` --calls--> `get()`  [INFERRED]
-  api/leave.ts → views/production/process/ProcessRouteTab.vue
-- `fetchSampleReceptionCheckItemList()` --calls--> `get()`  [INFERRED]
-  api/sample-reception-check-item.ts → views/production/process/ProcessRouteTab.vue
+- `fetchConsumableList()` --calls--> `get()`  [INFERRED]
+  api/production.ts → views/production/process/ProcessRouteTab.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (107): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+99 more)
+Nodes (137): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+129 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.02
-Nodes (97): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupReserveItems() (+89 more)
+Cohesion: 0.01
+Nodes (10): fetchDeleteProjectSystemItem(), fetchDeptTree(), fetchGetRoleList(), fetchNotDeletedRegions(), fetchPostBoundUserIds(), fetchRegionList(), if(), loadRoleList() (+2 more)
 
 ### Community 2 - "Community 2"
-Cohesion: 0.02
-Nodes (55): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+47 more)
+Cohesion: 0.03
+Nodes (87): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail() (+79 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.03
-Nodes (40): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+32 more)
+Cohesion: 0.02
+Nodes (48): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+40 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.03
-Nodes (36): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+28 more)
+Nodes (37): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+29 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.05
-Nodes (24): fetchAttachmentDownloadUrl(), fullFileUrl(), handleDownloadAttachment(), handlePreviewAttachment(), isVideoFile(), handleDownloadAttachment(), attachmentFullUrl(), handleAttachmentDownload() (+16 more)
+Cohesion: 0.03
+Nodes (26): fetchAttachmentDownloadUrl(), collectIds(), fullFileUrl(), handleDownloadAttachment(), handlePreviewAttachment(), isVideoFile(), handleDownloadAttachment(), attachmentFullUrl() (+18 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.05
-Nodes (25): fetchCreateSite(), fetchDeleteSite(), fetchSiteList(), fetchUpdateSite(), fetchUserList(), collectIds(), formatNowDatetime(), formatHandoverUserLabel() (+17 more)
+Cohesion: 0.04
+Nodes (28): updateInfusionScheduleStatus(), submitOutboundReleaseReview(), fetchStorageReleaseReviewDetail(), fetchStorageReleaseReviewList(), submitStorageReleaseReview(), disableDevTools(), handleLock(), handleUnlock() (+20 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.06
-Nodes (28): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList() (+20 more)
+Nodes (11): fetchGetLatestUpgradeLog(), getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager (+3 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.1
-Nodes (2): WebSocketClient, TableCache
+Cohesion: 0.06
+Nodes (27): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+19 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.07
-Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
+Cohesion: 0.06
+Nodes (27): for(), set(), walk(), annotateForkEdges(), chainApiToElements(), walkAncestorBranch(), walkAncestorParents(), if() (+19 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.05
-Nodes (18): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail() (+10 more)
+Cohesion: 0.1
+Nodes (2): WebSocketClient, TableCache
 
 ### Community 11 - "Community 11"
 Cohesion: 0.08
 Nodes (32): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+24 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.07
-Nodes (21): handleBlur(), handleFocus(), handleInput(), fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport() (+13 more)
+Cohesion: 0.08
+Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.09
-Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+Cohesion: 0.07
+Nodes (21): handleBlur(), handleFocus(), handleInput(), fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport() (+13 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.05
-Nodes (9): fetchQcLabTestItemList(), fetchQcLabTestItemsActive(), fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList(), fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive(), fetchSampleSmsNotificationList() (+1 more)
+Cohesion: 0.08
+Nodes (12): if(), maskIdCard(), buildOutboundPilotContextSections(), applicationStatusLabel(), applicationStatusTagType(), infusionMethodLabel(), infusionStatusLabel(), infusionStatusTagType() (+4 more)
 
 ### Community 15 - "Community 15"
 Cohesion: 0.08
-Nodes (11): maskIdCard(), buildOutboundPilotContextSections(), applicationStatusLabel(), applicationStatusTagType(), infusionMethodLabel(), infusionStatusLabel(), infusionStatusTagType(), qcStatusLabel() (+3 more)
+Nodes (24): fetchCreateSite(), fetchDeleteSite(), fetchDictDataOptions(), fetchSiteList(), fetchUpdateSite(), fetchUserList(), formatHandoverUserLabel(), formatNowDatetime() (+16 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.09
-Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
+Cohesion: 0.11
+Nodes (21): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolDetail(), buildInsuranceAttachmentsValue(), buildInsurancePayload(), formatYmdFromAny(), fullFileUrl(), handleBatchEdit() (+13 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.12
-Nodes (22): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), handleBatchComplete(), handleBatchStart(), handleBatchVoid() (+14 more)
+Cohesion: 0.11
+Nodes (22): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchRoleOptions(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd() (+14 more)
 
 ### Community 18 - "Community 18"
 Cohesion: 0.12
-Nodes (20): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleBatchDelete() (+12 more)
+Nodes (22): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), handleBatchComplete(), handleBatchStart(), handleBatchVoid() (+14 more)
 
 ### Community 19 - "Community 19"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
 ### Community 20 - "Community 20"
-Cohesion: 0.14
-Nodes (19): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+11 more)
+Cohesion: 0.1
+Nodes (6): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), handleClose(), handleSubmit(), loadTaskDetail()
 
 ### Community 21 - "Community 21"
 Cohesion: 0.14
-Nodes (16): fetchCheckupInstitutionList(), fetchPilotCheckupList(), canEditCheckup(), canFillCheckupInfo(), canMarkChecked(), handleDropdownCommand(), handleEdit(), handleReset() (+8 more)
+Nodes (19): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+11 more)
 
 ### Community 22 - "Community 22"
 Cohesion: 0.16
-Nodes (15): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), formatLocalDate(), handleAdd(), handleDelete(), handleDropdownCommand() (+7 more)
+Nodes (14): fetchPilotCheckupList(), canEditCheckup(), canFillCheckupInfo(), canMarkChecked(), handleDropdownCommand(), handleEdit(), handleReset(), handleSearch() (+6 more)
 
 ### Community 23 - "Community 23"
 Cohesion: 0.16
-Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
+Nodes (15): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), formatLocalDate(), handleAdd(), handleDelete(), handleDropdownCommand() (+7 more)
 
 ### Community 24 - "Community 24"
+Cohesion: 0.16
+Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
+
+### Community 25 - "Community 25"
 Cohesion: 0.2
 Nodes (2): ComponentLoader, RouteTransformer
 
-### Community 25 - "Community 25"
+### Community 26 - "Community 26"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 26 - "Community 26"
-Cohesion: 0.14
-Nodes (8): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), themeAnimation(), toggleTheme(), toggleTransition()
-
 ### Community 27 - "Community 27"
 Cohesion: 0.18
 Nodes (5): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData()
 
 ### Community 29 - "Community 29"
-Cohesion: 0.18
-Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
+Cohesion: 0.17
+Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
 
 ### Community 30 - "Community 30"
 Cohesion: 0.2
 Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
 
-### Community 31 - "Community 31"
-Cohesion: 0.28
-Nodes (1): FireworkSystem
-
-### Community 33 - "Community 33"
-Cohesion: 0.31
-Nodes (4): annotateForkEdges(), chainApiToElements(), walkAncestorBranch(), walkAncestorParents()
-
-### Community 34 - "Community 34"
+### Community 32 - "Community 32"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 35 - "Community 35"
+### Community 33 - "Community 33"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 36 - "Community 36"
-Cohesion: 0.25
-Nodes (2): fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList()
-
-### Community 37 - "Community 37"
+### Community 34 - "Community 34"
 Cohesion: 0.25
 Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
 
-### Community 38 - "Community 38"
+### Community 35 - "Community 35"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 39 - "Community 39"
-Cohesion: 0.29
-Nodes (2): fetchLeaveDetail(), fetchLeaveList()
-
-### Community 40 - "Community 40"
+### Community 36 - "Community 36"
 Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 43 - "Community 43"
-Cohesion: 0.33
-Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
-
-### Community 44 - "Community 44"
-Cohesion: 0.33
-Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
+### Community 40 - "Community 40"
+Cohesion: 0.5
+Nodes (3): reload(), handleMenuJump(), openExternalLink()
 
-### Community 46 - "Community 46"
+### Community 41 - "Community 41"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 47 - "Community 47"
+### Community 42 - "Community 42"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 49 - "Community 49"
+### Community 44 - "Community 44"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 51 - "Community 51"
+### Community 46 - "Community 46"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
 ## Knowledge Gaps
-- **Thin community `Community 8`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
+- **Thin community `Community 10`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 24`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
+- **Thin community `Community 25`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 - **Thin community `Community 30`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 31`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 35`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 36`** (8 nodes): `fetchCompleteProductionWorkOrder()`, `fetchConfigureProductionWorkOrder()`, `fetchCreateProductionWorkOrder()`, `fetchDeleteProductionWorkOrder()`, `fetchProductionWorkOrderDetail()`, `fetchProductionWorkOrderList()`, `fetchUpdateProductionWorkOrder()`, `production-work-order.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 38`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 39`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
+- **Thin community `Community 33`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 43`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
+- **Thin community `Community 34`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 44`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
+- **Thin community `Community 35`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 41`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 47`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 42`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 49`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 44`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 46`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 10`, `Community 12`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 27`, `Community 29`, `Community 30`, `Community 33`, `Community 36`, `Community 37`, `Community 39`, `Community 43`, `Community 44`?**
-  _High betweenness centrality (0.261) - this node is a cross-community bridge._
-- **Why does `push()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 11`, `Community 12`, `Community 15`, `Community 16`, `Community 31`?**
-  _High betweenness centrality (0.127) - this node is a cross-community bridge._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 27`, `Community 29`, `Community 30`, `Community 34`?**
+  _High betweenness centrality (0.260) - this node is a cross-community bridge._
+- **Why does `push()` connect `Community 2` to `Community 1`, `Community 4`, `Community 5`, `Community 7`, `Community 40`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`?**
+  _High betweenness centrality (0.143) - this node is a cross-community bridge._
 - **Are the 202 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
   _`get()` has 202 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 33 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
-  _`push()` has 33 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 35 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
+  _`push()` has 35 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 15 inferred relationships involving `emit()` (e.g. with `handleSubmit()` and `handleBlur()`) actually correct?**
   _`emit()` has 15 INFERRED edges - model-reasoned connections that need verification._
 - **Should `Community 0` be split into smaller, more focused modules?**
   _Cohesion score 0.01 - nodes in this community are weakly interconnected._
 - **Should `Community 1` be split into smaller, more focused modules?**
-  _Cohesion score 0.02 - nodes in this community are weakly interconnected._
+  _Cohesion score 0.01 - nodes in this community are weakly interconnected._

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 1
web-admin/graphify-out/graph.html


+ 10 - 10
web-admin/graphify-out/manifest.json

@@ -17,7 +17,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779897017.359845,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779924024.0394778,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779896972.9881423,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
@@ -210,7 +210,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-item.ts": 1779017836.101254,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-release-review.ts": 1779615813.3431153,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/workflow.ts": 1779790323.5709383,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-work-order.ts": 1779801394.2101724,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-work-order.ts": 1779924334.417812,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task-result.ts": 1779332042.258692,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-company.ts": 1779021425.3773527,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts": 1779790323.1950388,
@@ -232,7 +232,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material.ts": 1779283387.4196396,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception.ts": 1779692503.163169,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1778582067.8610063,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779800625.4572983,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779924334.8954625,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1779790342.0297484,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779866933.9349332,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779859546.0906804,
@@ -308,7 +308,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779882001.8321233,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779865978.3645008,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779863928.5798419,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779890767.190274,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779924329.2539678,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779790360.26504,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779790359.9189978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/CompleteProductionTaskDialog.vue": 1779790359.5464847,
@@ -317,7 +317,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/EditTaskInfoDialog.vue": 1779790359.6398873,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputList.vue": 1779525781.6822913,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779801620.1553593,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue": 1779891741.6961212,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue": 1779924839.307318,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue": 1779701250.4415438,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable-pack/index.vue": 1779863746.111673,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779863845.5777776,
@@ -327,7 +327,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779864070.4019275,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/OutputTypeTab.vue": 1779864078.964701,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1779525781.6949234,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779889606.295596,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779924334.895502,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue": 1779889633.846836,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcTaskTab.vue": 1779871381.9423103,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779857055.2939715,
@@ -361,7 +361,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/ServiceOrderTxnDialog.vue": 1779792403.5058112,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/index.vue": 1779871848.481235,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue": 1779790352.7457726,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue": 1779871859.199333,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue": 1779923364.6801205,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/clinical-refund-confirm/index.vue": 1779857041.790119,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue": 1779857039.564749,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/outbound-release-review/index.vue": 1779857036.1353672,
@@ -372,7 +372,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue": 1779841841.9942307,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/PilotApplicationTab.vue": 1779870527.5393612,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationViewDialog.vue": 1779830501.0475526,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue": 1779871148.701063,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue": 1779923781.8331578,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue": 1779835904.8876522,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/CheckupManagementTab.vue": 1779870993.5511904,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/cell-product-price/index.vue": 1779864955.5971668,
@@ -381,9 +381,9 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupViewDialog.vue": 1779790354.8122675,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupRegisterDialog.vue": 1779790354.7432914,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupDetailDialog.vue": 1779790354.4793391,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/checkup-reserve-items.ts": 1779790354.395297,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/checkup-reserve-items.ts": 1779923701.3416343,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupFillDialog.vue": 1779790354.549774,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupMarkCheckedDialog.vue": 1779790354.6605508,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-management/CheckupMarkCheckedDialog.vue": 1779923727.345238,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/clinical-payment-info/index.vue": 1779857023.6723864,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order-finance/index.vue": 1779857021.6808643,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/project-price-system/index.vue": 1779864957.7299743,

+ 11 - 0
web-admin/src/api/production-work-order.ts

@@ -22,6 +22,15 @@ export interface ProductionWorkOrderItem {
   workOrderStatus: string
   createTime?: string
   remark?: string
+  /** 出库工单:关联申请单号 */
+  applicationNo?: string
+  /** 出库工单:患者/用户姓名 */
+  patientName?: string
+  /** 出库工单:当前为第几次输注计划 */
+  sortNo?: number | null
+  /** 出库工单:输注计划总次数 */
+  totalCourseCount?: number | null
+  infusionTime?: string
 }
 
 export type ProductionWorkOrderDetail = ProductionWorkOrderItem & {
@@ -59,6 +68,8 @@ export function fetchProductionWorkOrderList(params?: {
   workOrderType?: string
   createTimeStart?: string
   createTimeEnd?: string
+  infusionTimeStart?: string
+  infusionTimeEnd?: string
 }) {
   return request.get<{
     list: ProductionWorkOrderItem[]

+ 8 - 0
web-admin/src/api/qc-work-order.ts

@@ -84,6 +84,12 @@ export type QcWorkOrder = {
   applicationNo?: string
   applicationName?: string
   channelName?: string
+  /** 出库工单:患者/用户姓名 */
+  patientName?: string
+  /** 出库工单:当前为第几次输注计划 */
+  sortNo?: number | null
+  /** 出库工单:输注计划总次数 */
+  totalCourseCount?: number | null
   infusionTime?: string
 }
 
@@ -126,6 +132,8 @@ export function fetchQcWorkOrderList(params?: {
   bizType?: string
   createTimeStart?: string
   createTimeEnd?: string
+  infusionTimeStart?: string
+  infusionTimeEnd?: string
 }) {
   return request.get<PageList<QcWorkOrder>>({
     url: '/web/admin/qc-work-orders',

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/008c2c2e8ed63fd16d970c51613ca92c809beb8a9122066efcdac68870df139f.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/0baebf5301228b4cf583b82f343705cb23dc82efdefc002ec7632583c628ff49.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/1616dcb645c7804452121a05b3f0d7ae0da2c5e32faacc2ab91f172bc079de40.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/3037500e5ddec59e7665b45ec876c836845c14804de1d40ff2d8af1b7c3bef5f.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/3393fb35d5053a87679927dd66e85de1072f3458383eecc4e43e0eb965cd8544.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "label": "ChannelServiceOrderTab.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1"}, {"id": "channel_channelserviceordertab_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1281"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "target": "channel_channelserviceordertab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1281", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "target": "channel_channelserviceordertab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1561", "weight": 1.0}], "raw_calls": [{"caller_nid": "channel_channelserviceordertab_if", "callee": "handleChannelSearch", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1282"}, {"caller_nid": "channel_channelserviceordertab_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1562"}]}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/46195c37337d139b577c474741918a78c2fd5bd24671d650a0c7cf44739b641d.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_infusionscheduletab_vue", "label": "InfusionScheduleTab.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue", "source_location": "L1"}, {"id": "pilot_application_infusionscheduletab_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue", "source_location": "L581"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_pilot_application_infusionscheduletab_vue", "target": "pilot_application_infusionscheduletab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue", "source_location": "L581", "weight": 1.0}], "raw_calls": []}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/5799d4c47c7d97432fc923e3af804098a7219b82825875b35de848afff54474b.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "label": "ChannelServiceOrderTab.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1"}, {"id": "channel_channelserviceordertab_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1280"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "target": "channel_channelserviceordertab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1280", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_channelserviceordertab_vue", "target": "channel_channelserviceordertab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1560", "weight": 1.0}], "raw_calls": [{"caller_nid": "channel_channelserviceordertab_if", "callee": "handleChannelSearch", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1281"}, {"caller_nid": "channel_channelserviceordertab_if", "callee": "error", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue", "source_location": "L1561"}]}

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/66123bb7e450062acafbfc41919fe7eeb763f2057b3cae5208ba8b63689a7e2c.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/6c7fcfe0582ddec0006a3a1dae383a75ce05dcc0c12586f1a5b05b2c347e74de.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/797a3d629722af7e6ed25e1c2fce999313fc5121ae54c8045b00294e11a2d32f.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/ada2b9c3d52a727100298f5daa6a051b488a2003d6d194a043a0c17f2c640593.json


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/b7de30d2ebc33ebf24898d0c1142460ecf7a4bfc05f679a815fcebbf7bdb30d1.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/bb280d7b09bce620ca659c06c0f6c3849f3f121c605684b6bf1d66e006be04c9.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_productiontasktab_vue", "label": "ProductionTaskTab.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue", "source_location": "L1"}, {"id": "production_work_order_productiontasktab_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue", "source_location": "L391"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_productiontasktab_vue", "target": "production_work_order_productiontasktab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue", "source_location": "L391", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_productiontasktab_vue", "target": "production_work_order_productiontasktab_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue", "source_location": "L538", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_productiontasktab_if", "callee": "applicationStatusTagType", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/ProductionTaskTab.vue", "source_location": "L392"}]}

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
web-admin/src/graphify-out/cache/ast/d9b16a6044f207e63d8cd9a10d306fa43e73bb75b40f24b019e40a17c31320c8.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/ef2cc77c90f0280a93b8aedb3bc1feda69c2a0879db0f124e8a84b6e78e09ba7.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/effad9351ebe81b878dd1ccfa6ef88feb7c452f4b69f9bae1a8d44e867a03fcb.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 5 - 1
web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue

@@ -117,7 +117,7 @@
             align="right"
           >
             <template #default="{ row }">
-              <span v-if="row.orderType === 'yfk'" class="amount-text">
+              <span v-if="row.orderType === 'yfk' || row.orderType === 'gmcp'" class="amount-text">
                 ¥{{ formatAmount(row.currentBalance) }}
               </span>
               <span v-else>-</span>
@@ -133,6 +133,9 @@
               <span v-if="row.orderType === 'gmcp'" class="amount-text">
                 ¥{{ formatAmount(row.purchaseTotalAmount) }}
               </span>
+              <span v-else-if="row.orderType === 'yfk'" class="amount-text">
+                ¥{{ formatAmount(row.totalRechargeAmount) }}
+              </span>
               <span v-else>-</span>
             </template>
           </el-table-column>
@@ -1152,6 +1155,7 @@
         title: '金额信息',
         fields: [
           { label: '购买总额(元)', value: `¥${formatAmount(d.purchaseTotalAmount)}` },
+          { label: '当前余额(元)', value: `¥${formatAmount(d.currentBalance)}` },
           { label: '渠道协议', value: viewAgreementLabel.value || d.channelAgreementId }
         ]
       })

+ 531 - 3
web-admin/src/views/pilot-service/checkup-management/CheckupMarkCheckedDialog.vue

@@ -27,8 +27,16 @@
       </div>
 
       <div class="section-card" style="margin-bottom: 16px">
-        <div class="section-title section-title--mint-bar">
+        <div class="section-title section-title--mint-bar section-title--with-action">
           <h3>实际项目</h3>
+          <div class="checkup-item-actions">
+            <el-button size="small" type="primary" @click="handleOpenProjectSelector">
+              <el-icon><Plus /></el-icon>添加单项
+            </el-button>
+            <el-button size="small" @click="handleOpenPackageSelector">
+              <el-icon><Suitcase /></el-icon>从套餐添加
+            </el-button>
+          </div>
         </div>
         <el-alert
           v-if="duplicateNames.length"
@@ -154,6 +162,278 @@
       </el-button>
     </template>
   </el-dialog>
+
+  <!-- 单项:选择体检项目 -->
+  <el-dialog
+    v-model="projectSelectorVisible"
+    title="选择体检项目"
+    width="960px"
+    class="dialog-form-layout checkup-picker-dialog"
+    align-center
+    append-to-body
+    destroy-on-close
+    :close-on-click-modal="false"
+  >
+    <div class="list-toolbar checkup-picker-toolbar">
+      <div class="list-toolbar-row">
+        <div class="list-toolbar-selected">
+          已选 <span class="sel-num">{{ projectSelectorSelected.length }}</span> 项
+        </div>
+        <el-input
+          v-model="projectSelectorKeyword"
+          placeholder="项目名称模糊检索"
+          clearable
+          class="checkup-picker-search"
+        />
+      </div>
+    </div>
+    <div class="list-body list-wrap checkup-picker-list-wrap">
+      <el-table
+        :data="filteredProjectOptions"
+        v-loading="projectLoading"
+        class="list-table"
+        stripe
+        row-key="id"
+        max-height="620"
+        @selection-change="(rows) => (projectSelectorSelected = rows)"
+      >
+        <el-table-column
+          type="selection"
+          min-width="55"
+          align="center"
+          header-align="center"
+          :selectable="(row) => !isProjectAlreadyInActual(row)"
+        />
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="68"
+          align="center"
+          header-align="center"
+        />
+        <el-table-column
+          prop="projectName"
+          label="项目名称"
+          min-width="180"
+          align="center"
+          header-align="center"
+        />
+        <el-table-column
+          label="项目费用(元)"
+          min-width="120"
+          align="center"
+          header-align="center"
+        >
+          <template #default="{ row }">{{
+            row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
+          }}</template>
+        </el-table-column>
+        <el-table-column label="状态" min-width="90" align="center" header-align="center">
+          <template #default="{ row }">
+            <el-tag :type="projectStatusTagType(row.status)" size="small">{{
+              projectStatusLabel(row.status)
+            }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="remark"
+          label="备注"
+          min-width="140"
+          align="center"
+          header-align="center"
+          show-overflow-tooltip
+        />
+      </el-table>
+    </div>
+    <template #footer>
+      <el-button class="btn-ghost" @click="projectSelectorVisible = false">
+        <el-icon><Close /></el-icon>取消
+      </el-button>
+      <el-button class="btn-main" type="primary" @click="confirmProjectSelect">
+        <el-icon><Check /></el-icon>确认添加({{ projectSelectorSelected.length }})
+      </el-button>
+    </template>
+  </el-dialog>
+
+  <!-- 套餐:选择体检套餐 -->
+  <el-dialog
+    v-model="packageSelectorVisible"
+    title="选择体检套餐"
+    width="1080px"
+    class="dialog-form-layout checkup-picker-dialog"
+    align-center
+    append-to-body
+    destroy-on-close
+    :close-on-click-modal="false"
+  >
+    <div class="list-toolbar checkup-picker-toolbar">
+      <div class="list-toolbar-row">
+        <div class="list-toolbar-selected">
+          已选 <span class="sel-num">{{ packageSelectorSelected.length }}</span> 项
+        </div>
+        <el-input
+          v-model="packageSelectorKeyword"
+          placeholder="套餐名称模糊检索"
+          clearable
+          class="checkup-picker-search"
+        />
+      </div>
+    </div>
+    <div class="list-body list-wrap checkup-picker-list-wrap">
+      <el-table
+        :data="filteredPackageOptions"
+        v-loading="packageLoading"
+        class="list-table"
+        stripe
+        row-key="id"
+        max-height="620"
+        @selection-change="(rows) => (packageSelectorSelected = rows)"
+      >
+        <el-table-column type="selection" min-width="55" align="center" header-align="center" />
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="68"
+          align="center"
+          header-align="center"
+        />
+        <el-table-column
+          prop="packageName"
+          label="套餐名称"
+          min-width="140"
+          align="center"
+          header-align="center"
+        />
+        <el-table-column
+          prop="packageCode"
+          label="套餐编码"
+          min-width="120"
+          align="center"
+          header-align="center"
+        />
+        <el-table-column
+          prop="itemCount"
+          label="项目数"
+          min-width="80"
+          align="center"
+          header-align="center"
+        >
+          <template #default="{ row }">{{ row.itemCount ?? '—' }}</template>
+        </el-table-column>
+        <el-table-column
+          label="合计费用(元)"
+          min-width="120"
+          align="center"
+          header-align="center"
+        >
+          <template #default="{ row }">{{
+            row.totalFee != null ? formatMoneyDisplay(row.totalFee) : '—'
+          }}</template>
+        </el-table-column>
+        <el-table-column label="状态" min-width="90" align="center" header-align="center">
+          <template #default="{ row }">
+            <el-tag :type="projectStatusTagType(row.status)" size="small">{{
+              projectStatusLabel(row.status)
+            }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="remark"
+          label="备注"
+          min-width="120"
+          align="center"
+          header-align="center"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          label="操作"
+          min-width="100"
+          fixed="right"
+          align="center"
+          header-align="center"
+        >
+          <template #default="{ row }">
+            <el-button class="btn-op-item" @click="handlePackageView(row)">
+              <el-icon><View /></el-icon>查看
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <template #footer>
+      <el-button class="btn-ghost" @click="packageSelectorVisible = false">
+        <el-icon><Close /></el-icon>取消
+      </el-button>
+      <el-button
+        class="btn-main"
+        type="primary"
+        :loading="packageConfirmLoading"
+        @click="confirmPackageSelect"
+      >
+        <el-icon><Check /></el-icon>确认添加({{ packageSelectorSelected.length }})
+      </el-button>
+    </template>
+  </el-dialog>
+
+  <!-- 套餐项目明细(查看) -->
+  <el-dialog
+    v-model="packageViewVisible"
+    :title="packageViewTitle"
+    width="900px"
+    class="dialog-form-layout"
+    align-center
+    append-to-body
+    destroy-on-close
+    :close-on-click-modal="false"
+  >
+    <el-table
+      :data="packageViewItems"
+      v-loading="packageViewLoading"
+      class="list-table"
+      stripe
+      max-height="420"
+    >
+      <el-table-column
+        type="index"
+        label="序号"
+        min-width="68"
+        align="center"
+        header-align="center"
+      />
+      <el-table-column
+        prop="projectName"
+        label="项目名称"
+        min-width="180"
+        align="center"
+        header-align="center"
+      />
+      <el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
+        <template #default="{ row }">{{
+          row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
+        }}</template>
+      </el-table-column>
+      <el-table-column label="状态" min-width="90" align="center" header-align="center">
+        <template #default="{ row }">
+          <el-tag :type="projectStatusTagType(row.status)" size="small">{{
+            projectStatusLabel(row.status)
+          }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="remark"
+        label="备注"
+        min-width="140"
+        align="center"
+        header-align="center"
+        show-overflow-tooltip
+      />
+    </el-table>
+    <template #footer>
+      <el-button class="btn-ghost" @click="packageViewVisible = false">
+        <el-icon><Close /></el-icon>关闭
+      </el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
@@ -166,12 +446,18 @@
     Delete,
     Download,
     Document,
-    Loading
+    Loading,
+    Check,
+    Suitcase,
+    View
   } from '@element-plus/icons-vue'
   import {
     fetchPilotCheckupDetail,
     fetchPilotCheckupReserveItems,
     fetchPilotCheckupActualItems,
+    fetchCheckupInstitutionProjects,
+    fetchCheckupInstitutionPackages,
+    fetchCheckupInstitutionPackageDetail,
     savePilotCheckup,
     savePilotCheckupActualItems,
     markPilotCheckupChecked
@@ -185,7 +471,9 @@
     parseProjectFee,
     dedupeCheckupActualItems,
     findDuplicateActualItemNames,
-    mapReserveRowsToActualItems
+    mapReserveRowsToActualItems,
+    appendCheckupActualItems,
+    actualItemExists
   } from './checkup-reserve-items'
 
   const props = defineProps<{
@@ -202,6 +490,157 @@
   const checkupRemark = ref('')
   const reportFiles = ref<any[]>([])
   const reportUploadRef = ref()
+  const checkupInstitutionId = ref('')
+
+  function parseList(res: unknown): any[] {
+    return Array.isArray(res) ? res : (res as any)?.list || []
+  }
+
+  function projectStatusLabel(status: string | undefined) {
+    return status === '1' ? '正常' : '停用'
+  }
+
+  function projectStatusTagType(status: string | undefined) {
+    return status === '1' ? 'success' : 'info'
+  }
+
+  const projectOptions = ref<any[]>([])
+  const packageOptions = ref<any[]>([])
+  const projectLoading = ref(false)
+  const packageLoading = ref(false)
+
+  const loadInstitutionCatalog = async (instId: string) => {
+    projectLoading.value = true
+    packageLoading.value = true
+    try {
+      const [projRes, pkgRes] = await Promise.all([
+        fetchCheckupInstitutionProjects(instId),
+        fetchCheckupInstitutionPackages(instId)
+      ])
+      projectOptions.value = parseList(projRes)
+      packageOptions.value = parseList(pkgRes)
+    } catch {
+      projectOptions.value = []
+      packageOptions.value = []
+    } finally {
+      projectLoading.value = false
+      packageLoading.value = false
+    }
+  }
+
+  const requireInstitution = () => {
+    if (!checkupInstitutionId.value) {
+      ElMessage.warning('该记录未关联体检机构,请先在「编辑体检信息」中选择机构')
+      return false
+    }
+    return true
+  }
+
+  const projectSelectorVisible = ref(false)
+  const projectSelectorKeyword = ref('')
+  const projectSelectorSelected = ref<any[]>([])
+
+  const filteredProjectOptions = computed(() => {
+    const kw = projectSelectorKeyword.value.trim().toLowerCase()
+    if (!kw) return projectOptions.value
+    return projectOptions.value.filter((row) => (row.projectName || '').toLowerCase().includes(kw))
+  })
+
+  const handleOpenProjectSelector = async () => {
+    if (!requireInstitution()) return
+    projectSelectorKeyword.value = ''
+    projectSelectorSelected.value = []
+    if (projectOptions.value.length === 0) {
+      await loadInstitutionCatalog(checkupInstitutionId.value)
+    }
+    projectSelectorVisible.value = true
+  }
+
+  const isProjectAlreadyInActual = (row: any) =>
+    actualItemExists(actualItems.value, { projectName: row.projectName || '' })
+
+  const confirmProjectSelect = () => {
+    if (projectSelectorSelected.value.length === 0) {
+      ElMessage.warning('请至少选择一个项目')
+      return
+    }
+    const added = appendCheckupActualItems(actualItems.value, projectSelectorSelected.value)
+    projectSelectorVisible.value = false
+    if (added > 0) {
+      ElMessage.success(`已添加 ${added} 个项目`)
+    } else {
+      ElMessage.info('所选项目均已存在,未重复添加')
+    }
+  }
+
+  const packageSelectorVisible = ref(false)
+  const packageSelectorKeyword = ref('')
+  const packageSelectorSelected = ref<any[]>([])
+  const packageConfirmLoading = ref(false)
+
+  const filteredPackageOptions = computed(() => {
+    const kw = packageSelectorKeyword.value.trim().toLowerCase()
+    if (!kw) return packageOptions.value
+    return packageOptions.value.filter((row) => (row.packageName || '').toLowerCase().includes(kw))
+  })
+
+  const handleOpenPackageSelector = async () => {
+    if (!requireInstitution()) return
+    packageSelectorKeyword.value = ''
+    packageSelectorSelected.value = []
+    if (packageOptions.value.length === 0) {
+      await loadInstitutionCatalog(checkupInstitutionId.value)
+    }
+    packageSelectorVisible.value = true
+  }
+
+  const confirmPackageSelect = async () => {
+    if (packageSelectorSelected.value.length === 0) {
+      ElMessage.warning('请至少选择一个套餐')
+      return
+    }
+    packageConfirmLoading.value = true
+    try {
+      const allItems: any[] = []
+      for (const pkg of packageSelectorSelected.value) {
+        const detail: any = await fetchCheckupInstitutionPackageDetail(pkg.id)
+        const items = detail?.items ?? detail?.detailItems ?? []
+        allItems.push(...items)
+      }
+      const added = appendCheckupActualItems(actualItems.value, allItems)
+      packageSelectorVisible.value = false
+      if (added > 0) {
+        ElMessage.success(`已从套餐添加 ${added} 个项目`)
+      } else {
+        ElMessage.info('套餐中的项目均已存在,未重复添加')
+      }
+    } catch {
+      ElMessage.error('加载套餐项目失败')
+    } finally {
+      packageConfirmLoading.value = false
+    }
+  }
+
+  const packageViewVisible = ref(false)
+  const packageViewTitle = ref('')
+  const packageViewItems = ref<any[]>([])
+  const packageViewLoading = ref(false)
+
+  const handlePackageView = async (row: any) => {
+    packageViewTitle.value = `套餐项目 — ${row.packageName || ''}`
+    packageViewItems.value = []
+    packageViewVisible.value = true
+    packageViewLoading.value = true
+    try {
+      const detail: any = await fetchCheckupInstitutionPackageDetail(row.id)
+      packageViewItems.value = detail?.items ?? detail?.detailItems ?? []
+    } catch {
+      ElMessage.error('加载套餐明细失败')
+      packageViewVisible.value = false
+    } finally {
+      packageViewLoading.value = false
+    }
+  }
 
   const actualTotal = computed(() =>
     actualItems.value.reduce((s, i) => s + (Number(i.projectFee) || 0), 0)
@@ -258,6 +697,12 @@
       checkupRemark.value = detail.checkupRemark || ''
       actualCheckupTime.value = detail.actualCheckupTime || formatNowDatetime()
       reportFiles.value = parseAttachmentUrls(detail.reportAttachments || '')
+      checkupInstitutionId.value = detail.checkupInstitutionId || ''
+      projectOptions.value = []
+      packageOptions.value = []
+      if (checkupInstitutionId.value) {
+        await loadInstitutionCatalog(checkupInstitutionId.value)
+      }
 
       const reserveList = Array.isArray(reserveRes) ? reserveRes : (reserveRes?.list ?? [])
       const actualList = Array.isArray(actualRes) ? actualRes : (actualRes?.list ?? [])
@@ -388,6 +833,9 @@
         actualCheckupTime.value = ''
         checkupRemark.value = ''
         reportFiles.value = []
+        checkupInstitutionId.value = ''
+        projectOptions.value = []
+        packageOptions.value = []
         loadData()
       }
     }
@@ -401,6 +849,20 @@
     }
   }
 
+  .section-title--with-action {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .checkup-item-actions {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
   .section-title--mint-bar {
     margin-bottom: 12px;
 
@@ -429,6 +891,72 @@
     margin-bottom: 12px;
   }
 
+  :deep(.checkup-picker-dialog.el-dialog.dialog-form-layout) {
+    .el-dialog__body {
+      min-height: min(680px, calc(100vh - 160px));
+      padding-top: 16px;
+    }
+  }
+
+  .checkup-picker-toolbar {
+    margin-bottom: 0;
+    background: var(--mint-table-head, #f8fafc);
+    border: 1px solid var(--mint-border, #e2e8f0);
+    border-bottom: none;
+    border-radius: var(--mint-radius, 10px) var(--mint-radius, 10px) 0 0;
+
+    .list-toolbar-row {
+      padding: 8px 16px;
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: 8px;
+    }
+
+    .list-toolbar-selected {
+      display: inline-flex;
+      align-items: center;
+      min-height: 36px;
+      padding: 0 10px 0 0;
+      margin-right: 4px;
+      border-right: 1px solid var(--mint-border, #e2e8f0);
+      font-size: 12px;
+      color: var(--mint-text-muted, #64748b);
+      font-weight: 600;
+    }
+
+    .sel-num {
+      color: var(--mint-primary-dark, #059669);
+      font-weight: 700;
+      margin: 0 2px;
+    }
+
+    .checkup-picker-search {
+      width: 280px;
+    }
+  }
+
+  .checkup-picker-list-wrap.list-body.list-wrap {
+    border: 1px solid var(--mint-border, #e2e8f0);
+    border-top: 1px solid var(--mint-border, #e2e8f0);
+    border-radius: 0 0 var(--mint-radius, 10px) var(--mint-radius, 10px);
+    overflow: hidden;
+    background: #fff;
+
+    :deep(.el-table) {
+      --el-table-header-bg-color: var(--mint-table-head, #f8fafc);
+      --el-table-border-color: var(--mint-border, #e2e8f0);
+      --el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
+      font-size: 14px;
+    }
+
+    :deep(.el-table th.el-table__cell) {
+      font-size: 12px;
+      font-weight: 600;
+      color: var(--mint-text-muted, #64748b);
+    }
+  }
+
   .checkup-inner-list-wrap.list-body.list-wrap {
     border: 1px solid var(--mint-border, #e2e8f0);
     border-radius: var(--mint-radius, 10px);

+ 23 - 0
web-admin/src/views/pilot-service/checkup-management/checkup-reserve-items.ts

@@ -148,6 +148,29 @@ export function findDuplicateActualItemNames(items: CheckupActualItem[]): string
   return [...dups]
 }
 
+export function actualItemExists(
+  target: CheckupActualItem[],
+  candidate: { projectName: string }
+): boolean {
+  return target.some((item) => isSameActualItem(item, candidate))
+}
+
+/** 追加实际项目(按名称去重,保留首次出现项) */
+export function appendCheckupActualItems(target: CheckupActualItem[], projects: any[]): number {
+  let added = 0
+  for (const proj of projects) {
+    const projectName = (proj.projectName || '').trim()
+    if (!projectName) continue
+    if (actualItemExists(target, { projectName })) continue
+    target.push({
+      projectName,
+      projectFee: parseProjectFee(proj.projectFee)
+    })
+    added++
+  }
+  return added
+}
+
 export function mapReserveRowsToActualItems(rows: any[]): CheckupActualItem[] {
   return dedupeCheckupActualItems(
     rows.map((r) => ({

+ 1 - 1
web-admin/src/views/pilot-service/pilot-application/InfusionScheduleTab.vue

@@ -226,7 +226,7 @@
           </el-table-column>
           <el-table-column
             label="操作"
-            min-width="380"
+            min-width="480"
             fixed="right"
             align="center"
             header-align="center"

+ 76 - 12
web-admin/src/views/production/production-work-order/ProductionTaskTab.vue

@@ -7,7 +7,7 @@
             <el-form-item label="关键词">
               <el-input
                 v-model="searchForm.keyword"
-                placeholder="任务号/所属工单号/协议号/样本编码"
+                placeholder="任务号/所属工单号/协议号/申请单号/样本编码"
                 clearable
                 @keyup.enter="handleSearch"
               />
@@ -100,8 +100,8 @@
           <el-table-column prop="receptionSource" label="来源类型" min-width="110" align="center">
             <template #default="{ row }">
               <span v-if="!showReceptionSourceType(row)">—</span>
-              <el-tag v-else :type="receptionSourceTagType(row.receptionSource)" size="small">
-                {{ row.receptionSourceLabel || receptionSourceLabel(row.receptionSource) }}
+              <el-tag v-else :type="receptionSourceTypeTag(row)" size="small">
+                {{ receptionSourceTypeLabel(row) }}
               </el-tag>
             </template>
           </el-table-column>
@@ -116,14 +116,16 @@
                 :type="
                   protocolStatusTagTypeBySource(
                     String(row.protocolStatus || ''),
-                    row.receptionSource || 'cell_storage'
+                    row.receptionSource || 'cell_storage',
+                    row.orderSource
                   )
                 "
                 size="small"
                 >{{
                   protocolStatusLabelBySource(
                     String(row.protocolStatus || ''),
-                    row.receptionSource || 'cell_storage'
+                    row.receptionSource || 'cell_storage',
+                    row.orderSource
                   )
                 }}</el-tag
               >
@@ -338,7 +340,11 @@
     donationStatusLabel,
     donationStatusTagType,
     receptionSourceLabel,
-    receptionSourceTagType
+    receptionSourceTagType,
+    applicationStatusLabel,
+    applicationStatusTagType,
+    bizTypeLabel,
+    bizTypeTagType
   } from '@/utils/status-tag-registry'
   import type { TagType } from '@/utils/status-tag-registry'
   import TaskExecutionDialog from './TaskExecutionDialog.vue'
@@ -351,26 +357,84 @@
     productionTaskTypeTag
   } from '@/utils/production-labels'
 
-  /** 根据来源类型返回协议状态标签 */
-  function protocolStatusLabelBySource(status: string, source: string): string {
+  /** 根据来源类型返回协议状态标签(出库任务按申请单状态展示) */
+  function isOutboundBizSource(row: { orderSource?: string }) {
+    return row.orderSource === 'outbound'
+  }
+
+  function isApplicationBizSource(source: string, orderSource?: string) {
+    return (
+      orderSource === 'outbound' ||
+      source === 'pilot_service' ||
+      source === 'clinical_application'
+    )
+  }
+
+  function protocolStatusLabelBySource(
+    status: string,
+    source: string,
+    orderSource?: string
+  ): string {
+    if (isApplicationBizSource(source, orderSource)) {
+      return applicationStatusLabel(status)
+    }
     return source === 'research_donation'
       ? donationStatusLabel(status)
       : protocolStatusLabel(status)
   }
-  function protocolStatusTagTypeBySource(status: string, source: string): TagType {
+
+  function protocolStatusTagTypeBySource(
+    status: string,
+    source: string,
+    orderSource?: string
+  ): TagType {
+    if (isApplicationBizSource(source, orderSource)) {
+      return applicationStatusTagType(status) as TagType
+    }
     return source === 'research_donation'
       ? donationStatusTagType(status)
       : protocolStatusTagType(status)
   }
 
+  function receptionSourceTypeLabel(row: {
+    orderSource?: string
+    bizType?: string
+    bizTypeLabel?: string
+    receptionSource?: string
+    receptionSourceLabel?: string
+  }) {
+    if (isOutboundBizSource(row)) {
+      return row.bizTypeLabel || bizTypeLabel(row.bizType || row.receptionSource || '')
+    }
+    return row.receptionSourceLabel || receptionSourceLabel(row.receptionSource || '')
+  }
+
+  function receptionSourceTypeTag(row: {
+    orderSource?: string
+    bizType?: string
+    receptionSource?: string
+  }) {
+    if (isOutboundBizSource(row)) {
+      return bizTypeTagType(row.bizType || row.receptionSource || '')
+    }
+    return receptionSourceTagType(row.receptionSource || '')
+  }
+
   /** 临时工单无关联协议/样本时,空字段展示为 — */
   function shouldShowTempEmptyField(row: { orderSource?: string }, value?: string | null) {
     return row.orderSource === 'temporary' && !String(value || '').trim()
   }
 
-  /** 入库任务展示工单来源类型(细胞存储 / 科研捐赠) */
-  function showReceptionSourceType(row: { orderSource?: string; receptionSource?: string }) {
-    if (row.orderSource === 'temporary' || row.orderSource === 'outbound') return false
+  /** 入库任务展示工单来源类型;出库任务展示业务类型 */
+  function showReceptionSourceType(row: {
+    orderSource?: string
+    receptionSource?: string
+    bizType?: string
+  }) {
+    if (row.orderSource === 'temporary') return false
+    if (row.orderSource === 'outbound') {
+      return !!(row.bizType || row.receptionSource)
+    }
     const src = (row.receptionSource || '').trim()
     return src === 'cell_storage' || src === 'research_donation'
   }

+ 58 - 4
web-admin/src/views/production/production-work-order/index.vue

@@ -64,7 +64,7 @@
                 <el-option label="已作废" value="yzf" />
               </el-select>
             </el-form-item>
-            <el-form-item label="创建时间">
+            <el-form-item :label="filterTimeLabel">
               <el-date-picker
                 v-model="createTimeRange"
                 type="daterange"
@@ -77,7 +77,7 @@
             </el-form-item>
           </div>
           <div class="filter-actions-row">
-            <span class="filter-hint">默认展示近 30 天内创建的工单。</span>
+            <span class="filter-hint">{{ filterTimeHint }}</span>
             <div class="filter-actions-end">
               <el-button class="btn-ghost" @click="handleReset"
                 ><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -181,6 +181,31 @@
           >
             <template #default="{ row }">{{ row.applicationNo || '—' }}</template>
           </el-table-column>
+          <el-table-column
+            v-if="activeTypeTab === 'outbound'"
+            prop="patientName"
+            label="用户名称"
+            min-width="100"
+            align="center"
+          >
+            <template #default="{ row }">{{ row.patientName || '—' }}</template>
+          </el-table-column>
+          <el-table-column
+            v-if="activeTypeTab === 'outbound'"
+            label="第几次"
+            min-width="88"
+            align="center"
+          >
+            <template #default="{ row }">{{ formatOutboundScheduleSortNo(row) }}</template>
+          </el-table-column>
+          <el-table-column
+            v-if="activeTypeTab === 'outbound'"
+            label="总次数"
+            min-width="88"
+            align="center"
+          >
+            <template #default="{ row }">{{ formatOutboundScheduleTotalCount(row) }}</template>
+          </el-table-column>
           <el-table-column
             v-if="activeTypeTab === 'outbound'"
             prop="infusionTime"
@@ -658,6 +683,18 @@
   function formatTempEmptyField(row: { workOrderType?: string }, value?: string | null) {
     return shouldShowTempEmptyField(row, value) ? '—' : String(value || '')
   }
+
+  function formatOutboundScheduleSortNo(row: Record<string, unknown>) {
+    const n = row.sortNo ?? row.orderNum
+    if (n == null || n === '' || Number(n) <= 0) return '—'
+    return `第${Number(n)}次`
+  }
+
+  function formatOutboundScheduleTotalCount(row: Record<string, unknown>) {
+    const n = row.totalCourseCount
+    if (n == null || n === '' || Number(n) <= 0) return '—'
+    return `共${Number(n)}次`
+  }
   import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
@@ -684,6 +721,14 @@
     if (activeTypeTab.value === 'outbound') return '工单号/申请单号'
     return '工单号/协议号/样本编码'
   })
+  const filterTimeLabel = computed(() =>
+    activeTypeTab.value === 'outbound' ? '输注时间' : '创建时间'
+  )
+  const filterTimeHint = computed(() =>
+    activeTypeTab.value === 'outbound'
+      ? '默认展示近 30 天内输注的工单。'
+      : '默认展示近 30 天内创建的工单。'
+  )
   const addMode = ref<'inbound' | 'temporary'>('inbound')
 
   // ── 新增工单 ──────────────────────────────────────────
@@ -903,14 +948,23 @@
   async function loadData() {
     loading.value = true
     try {
+      const timeParams =
+        activeTypeTab.value === 'outbound'
+          ? {
+              infusionTimeStart: createTimeRange.value?.[0] || undefined,
+              infusionTimeEnd: createTimeRange.value?.[1] || undefined
+            }
+          : {
+              createTimeStart: createTimeRange.value?.[0] || undefined,
+              createTimeEnd: createTimeRange.value?.[1] || undefined
+            }
       const res = await fetchProductionWorkOrderList({
         page: pagination.page,
         pageSize: pagination.pageSize,
         keyword: searchForm.keyword || undefined,
         workOrderStatus: searchForm.workOrderStatus || undefined,
         workOrderType: activeTypeTab.value,
-        createTimeStart: createTimeRange.value?.[0] || undefined,
-        createTimeEnd: createTimeRange.value?.[1] || undefined
+        ...timeParams
       })
       tableData.value = res.list || []
       pagination.total = res.total ?? 0

+ 58 - 4
web-admin/src/views/quality-inspection/qc-work-order/index.vue

@@ -80,7 +80,7 @@
                 <el-option label="临床应用" value="clinical_application" />
               </el-select>
             </el-form-item>
-            <el-form-item label="创建时间">
+            <el-form-item :label="filterTimeLabel">
               <el-date-picker
                 v-model="createTimeRange"
                 type="daterange"
@@ -93,7 +93,7 @@
             </el-form-item>
           </div>
           <div class="filter-actions-row">
-            <span class="filter-hint">默认展示近 30 天内创建的工单。</span>
+            <span class="filter-hint">{{ filterTimeHint }}</span>
             <div class="filter-actions-end">
               <el-button class="btn-ghost" @click="handleReset"
                 ><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -167,6 +167,31 @@
             align="center"
             show-overflow-tooltip
           />
+          <el-table-column
+            v-if="showOutboundCols"
+            prop="patientName"
+            label="用户名称"
+            min-width="100"
+            align="center"
+          >
+            <template #default="{ row }">{{ row.patientName || '—' }}</template>
+          </el-table-column>
+          <el-table-column
+            v-if="showOutboundCols"
+            label="第几次"
+            min-width="88"
+            align="center"
+          >
+            <template #default="{ row }">{{ formatOutboundScheduleSortNo(row) }}</template>
+          </el-table-column>
+          <el-table-column
+            v-if="showOutboundCols"
+            label="总次数"
+            min-width="88"
+            align="center"
+          >
+            <template #default="{ row }">{{ formatOutboundScheduleTotalCount(row) }}</template>
+          </el-table-column>
           <el-table-column
             v-if="showOutboundCols"
             prop="applicationName"
@@ -774,6 +799,18 @@
   function formatTempEmptyField(row: QcWorkOrder, value?: string | null) {
     return shouldShowTempEmptyField(row, value) ? '—' : String(value || '')
   }
+
+  function formatOutboundScheduleSortNo(row: Record<string, unknown>) {
+    const n = row.sortNo ?? row.orderNum
+    if (n == null || n === '' || Number(n) <= 0) return '—'
+    return `第${Number(n)}次`
+  }
+
+  function formatOutboundScheduleTotalCount(row: Record<string, unknown>) {
+    const n = row.totalCourseCount
+    if (n == null || n === '' || Number(n) <= 0) return '—'
+    return `共${Number(n)}次`
+  }
   import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
@@ -792,6 +829,14 @@
   const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary' && activeTypeTab.value !== 'outbound')
   const showTempProductCols = computed(() => activeTypeTab.value !== 'inbound' && activeTypeTab.value !== 'outbound')
   const showOutboundCols = computed(() => activeTypeTab.value === 'outbound')
+  const filterTimeLabel = computed(() =>
+    activeTypeTab.value === 'outbound' ? '输注时间' : '创建时间'
+  )
+  const filterTimeHint = computed(() =>
+    activeTypeTab.value === 'outbound'
+      ? '默认展示近 30 天内输注的工单。'
+      : '默认展示近 30 天内创建的工单。'
+  )
   const addMode = ref<'inbound' | 'temporary'>('inbound')
   const editingId = ref('')
   const editingWorkOrderNo = ref('')
@@ -1521,6 +1566,16 @@
   async function loadData() {
     loading.value = true
     try {
+      const timeParams =
+        activeTypeTab.value === 'outbound'
+          ? {
+              infusionTimeStart: createTimeRange.value?.[0] || undefined,
+              infusionTimeEnd: createTimeRange.value?.[1] || undefined
+            }
+          : {
+              createTimeStart: createTimeRange.value?.[0] || undefined,
+              createTimeEnd: createTimeRange.value?.[1] || undefined
+            }
       const res = await fetchQcWorkOrderList({
         page: pagination.page,
         pageSize: pagination.pageSize,
@@ -1528,8 +1583,7 @@
         workOrderStatus: searchForm.workOrderStatus || undefined,
         workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
         bizType: searchForm.bizType || undefined,
-        createTimeStart: createTimeRange.value?.[0] || undefined,
-        createTimeEnd: createTimeRange.value?.[1] || undefined
+        ...timeParams
       })
       tableData.value = res.list || []
       pagination.total = res.total ?? 0

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.