zlwu 5 месяцев назад
Родитель
Сommit
74f751d0d1
70 измененных файлов с 2847 добавлено и 1770 удалено
  1. 1 1
      backend/app.pid
  2. 3 2
      backend/app/api/public.py
  3. 39 15
      backend/app/api/qc.py
  4. 1 0
      backend/app/core/config.py
  5. 55 2
      backend/app/crud/collection_protocol.py
  6. 75 2
      backend/app/crud/qc_material_usage.py
  7. 33 8
      backend/app/crud/qc_task.py
  8. 6 1
      backend/app/crud/qc_task_result.py
  9. 0 0
      backend/app/graphify-out/cache/ast/2226d87f9379fddba24b8c49746c754a58779ef285b0503f826f8b342c09e3ce.json
  10. 0 0
      backend/app/graphify-out/cache/ast/3f06a30671b16ed0de9ce845021b11e658a6f4259d3ddbd35f57780f44c6a916.json
  11. 0 0
      backend/app/graphify-out/cache/ast/44b5b988acb6740b2ffce97a6d210134efa349e12ab736ae6bb45d4cb097e97f.json
  12. 0 0
      backend/app/graphify-out/cache/ast/4a4de840a7b2a2449b36ae3a61c26fd6c5231822a3457f81aec8f515cf748a03.json
  13. 0 0
      backend/app/graphify-out/cache/ast/6b08dd8cdec0b8b42794930b9d2998ec46bf793c2444dc89b7ecb131102b6ffc.json
  14. 0 0
      backend/app/graphify-out/cache/ast/6fe2fc067d3d4d04f84e67e35fbfb2d8c25e8873753e2775ddff7c0cbc06ffc5.json
  15. 0 0
      backend/app/graphify-out/cache/ast/75d7e14fc2a1fec8bc214036f69abd15de81b5a9aecf3d775c30feb5ca67815e.json
  16. 0 0
      backend/app/graphify-out/cache/ast/a63c2519460a97de1968d0c6092894d0065f4b7a5e1ee0529d40441746b795c4.json
  17. 0 0
      backend/app/graphify-out/cache/ast/ad372661ba5b6508cee5809e144dff1348c8f419590415135941eb606d82d239.json
  18. 0 0
      backend/app/graphify-out/cache/ast/ae4a4325463230a190e80a357bbe4497f1a2df79eff653fd7a9c88fd7e303a69.json
  19. 0 0
      backend/app/graphify-out/cache/ast/e714ec8ea68584f4f1ff6d1a62df71095d30d095cf52bbae4283250c612999a1.json
  20. 0 0
      backend/app/graphify-out/cache/ast/edf5403c4985d59d27139afdaf52561ddad929fc7644bc94575d6dc83e5ec654.json
  21. 476 483
      backend/graphify-out/.graphify_analysis.json
  22. 208 145
      backend/graphify-out/.graphify_extract.json
  23. 117 107
      backend/graphify-out/GRAPH_REPORT.md
  24. 1 1
      backend/graphify-out/graph.html
  25. 112 112
      backend/graphify-out/graph.json
  26. 10 10
      backend/graphify-out/manifest.json
  27. 473 423
      web-admin/graphify-out/.graphify_analysis.json
  28. 259 187
      web-admin/graphify-out/.graphify_extract.json
  29. 107 76
      web-admin/graphify-out/GRAPH_REPORT.md
  30. 1 1
      web-admin/graphify-out/graph.html
  31. 20 20
      web-admin/graphify-out/manifest.json
  32. 15 0
      web-admin/src/api/qc-task-result.ts
  33. 131 1
      web-admin/src/components/business/qc-task-view-dialog/index.vue
  34. 0 0
      web-admin/src/graphify-out/cache/ast/273e16a51782d2cd6a364377402732b7e9353b2db246ea2eccc72a74f4209636.json
  35. 0 0
      web-admin/src/graphify-out/cache/ast/278fad47026f3072c36587172b595bf144f9b305f071e6ea820518586ca6a291.json
  36. 1 0
      web-admin/src/graphify-out/cache/ast/35c597f77786048e1ed2b83ce3c2fce02eb7b9bcf0ca2a2a753bc5aa34e6fc49.json
  37. 0 0
      web-admin/src/graphify-out/cache/ast/3c936f17d2e9e6a496a180668be0088b02c3a8e4bc93a451da3f5f53597565dc.json
  38. 1 0
      web-admin/src/graphify-out/cache/ast/45bb6457ff46884d1c84043887ced74898bf6aca7e7cecc741d8e67e8ac9ecc0.json
  39. 1 0
      web-admin/src/graphify-out/cache/ast/4d0021279b628c8e8407dff298aeb74de09df32213b35391cac6ef290579972c.json
  40. 1 0
      web-admin/src/graphify-out/cache/ast/614e479889d658f0ae9e8b74f43f22378d9a0771a88db33705b67bc4172a754a.json
  41. 1 0
      web-admin/src/graphify-out/cache/ast/689e25a2585afdec9cede7cb77c204dd4b4ca739f1c1fd53deb4a8e6e53f4bdc.json
  42. 0 0
      web-admin/src/graphify-out/cache/ast/68f205b6c471934393e0139fec46f051d57b323be3ae1023782475871fa5253a.json
  43. 1 0
      web-admin/src/graphify-out/cache/ast/8c77bcf0f4d4117fc27a8deb964812a58ed789953516af3997428b741b224de7.json
  44. 0 0
      web-admin/src/graphify-out/cache/ast/8daffc5ab89a54f68c2a40d38d1f0bd09db5f49d4974bd62e63f61af18dc07fe.json
  45. 1 0
      web-admin/src/graphify-out/cache/ast/8e46f182c660116d657599ca62a7bf900153be3b0bc902dc43fdddd31205a9aa.json
  46. 1 0
      web-admin/src/graphify-out/cache/ast/975c601c2a97e683859e5078ec31b380293af33d045adaff968261fda88b62df.json
  47. 0 0
      web-admin/src/graphify-out/cache/ast/a8a13284b6a072f61c69f707ea73fadac43e12c97001cf6616cbfa6105c48b1d.json
  48. 0 0
      web-admin/src/graphify-out/cache/ast/a9f185d9f67ea5ae01fd38466642afe42917a7ad2e3f1191e632d61eb554fe49.json
  49. 1 0
      web-admin/src/graphify-out/cache/ast/ac72f263ef537f925af299e24475bad3c351c90e9a7aad305d9192e5256b3636.json
  50. 0 0
      web-admin/src/graphify-out/cache/ast/cbfac312b94f4f714a80bee4a42027e89f6c7e7ca10119fa7a78d572fb349751.json
  51. 0 0
      web-admin/src/graphify-out/cache/ast/d21a1e1ddef3a368c4465ebd05199d8d317bc72d3d00495d72503f1fbd133772.json
  52. 0 0
      web-admin/src/graphify-out/cache/ast/d9257a0ae4d03614cff1182692347326af46b6a26fec228af189de874a258a5d.json
  53. 0 0
      web-admin/src/graphify-out/cache/ast/dc66a42c4eefebf31d144902854cc75093280a941fa4e3613f1ce4279defc08c.json
  54. 1 0
      web-admin/src/graphify-out/cache/ast/df0a12a66e18852dd41936d06e10e6152371da28a1f37cf8b94ea761c074f385.json
  55. 1 0
      web-admin/src/graphify-out/cache/ast/e2a66704830573344d2e8ec2dd906d8f9ea30a387cfeb25643178ddeb312ec3e.json
  56. 1 0
      web-admin/src/graphify-out/cache/ast/eb0f8318029c50c119055081742af0398eac63fc7684ba8573f388deb8aee5d5.json
  57. 0 0
      web-admin/src/graphify-out/cache/ast/f72481c9a45f8c6b7c90d49500e50f77b5222e7fc22fbc1c04b7f02d7e89b505.json
  58. 8 2
      web-admin/src/store/modules/user.ts
  59. 91 14
      web-admin/src/utils/http/index.ts
  60. 1 0
      web-admin/src/views/cell-storage/payment-info/index.vue
  61. 2 1
      web-admin/src/views/cell-storage/protocol-audit/index.vue
  62. 1 1
      web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue
  63. 4 0
      web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue
  64. 3 1
      web-admin/src/views/cell-storage/protocol/index.vue
  65. 3 2
      web-admin/src/views/cell-storage/storage-cert/index.vue
  66. 1 1
      web-admin/src/views/quality-inspection/lab-test-item/index.vue
  67. 349 65
      web-admin/src/views/quality-inspection/qc-task/index.vue
  68. 39 0
      web-admin/src/views/quality-inspection/sample-reception/index.vue
  69. 189 85
      web-admin/src/views/quality-inspection/third-party-company/index.vue
  70. 1 1
      web-admin/src/views/research-donation/list/DonationFormDialog.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-82671
+37832

+ 3 - 2
backend/app/api/public.py

@@ -21,6 +21,7 @@ from app.core.auth import (
     store_session,
 )
 from app.core.response import success_response, error_response
+from app.core.config import settings
 from app.models.models import SysUser
 from app.crud.user import user_crud
 from app.core.utils import generate_uuid
@@ -123,7 +124,7 @@ async def web_login(
         {
             "token": access_token,
             "refreshToken": refresh_token,
-            "expiresIn": 7200,
+            "expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
             "userInfo": _build_user_info_payload(user, web_role),
         }
     )
@@ -184,7 +185,7 @@ async def refresh_token(body: dict, db: Session = Depends(get_db)):
             {
                 "token": new_access_token,
                 "refreshToken": new_refresh_token,
-                "expiresIn": 7200,
+                "expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
             }
         )
     except HTTPException:

+ 39 - 15
backend/app/api/qc.py

@@ -1,7 +1,7 @@
 """质量检测 — 第三方/实验室主数据、质检工单、质检任务 API"""
 from typing import Any, Dict, List, Optional
 
-from fastapi import APIRouter, Depends, Query
+from fastapi import APIRouter, Depends, HTTPException, Query
 from pydantic import BaseModel, ConfigDict, Field
 from sqlalchemy.orm import Session
 
@@ -733,6 +733,26 @@ async def get_task_results(
         return error_response(str(e))
 
 
+@router.get("/qc-tasks/{task_id}/material-usages")
+async def get_task_material_usages(
+    task_id: str,
+    db: Session = Depends(get_db),
+    user: dict = Depends(require_menu_code("biz:qcTask:list")),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
+        if not t:
+            return error_response("任务不存在")
+        return success_response(
+            mu_svc.list_usages_by_task(db, task_id, tenant_no, user_info=user)
+        )
+    except PermissionError as e:
+        return error_response(str(e), code=403)
+    except Exception as e:
+        return error_response(str(e))
+
+
 @router.post("/qc-tasks/{task_id}/complete-with-result")
 async def complete_task_with_result(
     task_id: str,
@@ -748,26 +768,16 @@ async def complete_task_with_result(
             return error_response("任务不存在")
         was_done = (t.task_status or "").strip() == "ywc"
         if body.results:
-            result_svc.save_results_for_task(db, task_id, body.results, tenant_no, user_id)
+            result_svc.save_results_for_task(
+                db, task_id, body.results, tenant_no, user_id, commit=False,
+            )
         concl = (body.conclusion or "").strip()
         if concl not in ("tg", "btg"):
             return error_response("请选择检测结论(通过/不通过)")
         if body.third_party_attachments is not None:
             task_svc.update_task_attachments(
                 db, task_id, attachments=body.third_party_attachments,
-                tenant_no=tenant_no, user_id=user_id,
-            )
-        # 物料扣减(可选,仅当传了 materialUsages 时)
-        if body.material_usages:
-            task_obj_pre = task_svc.get_task(db, task_id, tenant_no=tenant_no)
-            task_no = task_obj_pre.task_no if task_obj_pre else ""
-            mu_svc.deduct_for_task(
-                db,
-                task_id,
-                task_no=task_no,
-                material_usages=body.material_usages,
-                tenant_no=tenant_no,
-                user_id=user_id,
+                tenant_no=tenant_no, user_id=user_id, commit=False,
             )
         obj = task_svc.apply_task_result(
             db,
@@ -776,16 +786,30 @@ async def complete_task_with_result(
             user_id=user_id,
             conclusion=concl,
             remark=body.remark,
+            material_usages=body.material_usages,
+            commit=False,
         )
+        db.commit()
+        db.refresh(obj)
+        if (obj.task_status or "").strip() == "ywc":
+            wo_svc.try_complete_work_order(db, obj.work_order_id, tenant_no)
         return success_response(
             task_svc._task_to_dict(obj, work_order_no="", db=db, tenant_no=tenant_no),
             message="已保存" if was_done else "已完成",
         )
+    except HTTPException as e:
+        db.rollback()
+        detail = e.detail
+        msg = detail if isinstance(detail, str) else str(detail)
+        return error_response(msg, code=e.status_code)
     except PermissionError as e:
+        db.rollback()
         return error_response(str(e), code=403)
     except ValueError as e:
+        db.rollback()
         return error_response(str(e))
     except Exception as e:
+        db.rollback()
         return error_response(str(e))
 
 

+ 1 - 0
backend/app/core/config.py

@@ -133,6 +133,7 @@ class Settings(BaseSettings):
         "/openapi.json,"
         "/api/web/auth/login-rsa-public-key,"
         "/api/web/auth/login,"
+        "/api/web/auth/refresh-token,"
         "/api/web/admin/attachment/upload,"
         "/api/web/admin/products/upload-image,"
         "/api/web/admin/categories/upload-image,"

+ 55 - 2
backend/app/crud/collection_protocol.py

@@ -733,10 +733,20 @@ def list_collection_protocols(
         kw = (keyword or "").strip()
         if kw:
             like = f"%{kw}%"
+            party_q = db.query(BizProtocolPartyCustomer.collection_protocol_id).filter(
+                BizProtocolPartyCustomer.del_flag == 0,
+                or_(
+                    BizProtocolPartyCustomer.mother_name.like(like),
+                    BizProtocolPartyCustomer.mother_id_card.like(like),
+                    BizProtocolPartyCustomer.mother_phone.like(like),
+                ),
+            )
+            if tenant_no:
+                party_q = party_q.filter(BizProtocolPartyCustomer.tenant_no == tenant_no)
             q0 = q0.filter(
                 or_(
                     BizCollectionProtocol.protocol_no.like(like),
-                    BizCollectionProtocol.remark.like(like),
+                    BizCollectionProtocol.id.in_(party_q),
                 )
             )
         if user_info:
@@ -960,6 +970,9 @@ def get_protocol_detail_bundle(
         samples_out.append(item)
     out["samples"] = samples_out
     out["storageCert"] = _storage_cert_to_dict(sc) if sc else None
+    ft = get_fullmoon_todo(db, protocol_id, tenant_no)
+    delivery_dt = pc.newborn_delivery_time if pc else None
+    out["fullmoonTodo"] = _fullmoon_todo_detail_dict(ft, newborn_delivery_time=delivery_dt)
     out["attachments"] = urls_to_attachment_list(p.protocol_attachments, db, tenant_no)
     if pay:
         detail_rows = list_payment_details(db, pay.id, tenant_no)
@@ -1539,8 +1552,11 @@ def list_payment_details_page(
     total = q.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
+    due_col = BizProtocolPaymentDetail.installment_due_date
+    # 缴费信息 Tab:待缴费/缴费确认中/退费确认中 → 应缴日期正序;已缴费/已退费 → 倒序
+    due_order = due_col.desc() if pay_status in ("yjf", "ytf") else due_col.asc()
     rows = (
-        q.order_by(BizProtocolPaymentDetail.installment_due_date.asc())
+        q.order_by(due_order)
         .offset((page - 1) * page_size)
         .limit(page_size)
         .all()
@@ -2494,6 +2510,43 @@ def upsert_storage_cert(
 # ---------------------------------------------------------------------------
 
 
+def _fullmoon_todo_empty_dict() -> Dict[str, Any]:
+    return {
+        "todoStatus": "0",
+        "handleDate": "",
+        "handleRemark": "",
+        "insuranceAttachments": "",
+        "insuranceCompanyId": "",
+        "insuranceCompanyName": "",
+        "insuranceType": "",
+        "insurancePolicyNo": "",
+        "insuranceEffectiveDate": "",
+        "insuranceExpiryDate": "",
+        "insuranceSubscribed": "0",
+        "insuranceFee": None,
+        "fullmoonDate": "",
+        "isFullmoonPassed": False,
+    }
+
+
+def _fullmoon_todo_detail_dict(
+    ft: Optional[BizProtocolFullmoonTodo],
+    *,
+    newborn_delivery_time: Optional[datetime] = None,
+) -> Dict[str, Any]:
+    """协议详情 bundle 用:满月待办字段 + 由分娩日推算的满月日期。"""
+    base = _fullmoon_todo_to_dict(ft) if ft else dict(_fullmoon_todo_empty_dict())
+    fullmoon_date_str = ""
+    is_fullmoon_passed = False
+    if newborn_delivery_time:
+        fullmoon_date = newborn_delivery_time + timedelta(days=30)
+        fullmoon_date_str = fullmoon_date.strftime("%Y-%m-%d")
+        is_fullmoon_passed = fullmoon_date <= datetime.now()
+    base["fullmoonDate"] = fullmoon_date_str
+    base["isFullmoonPassed"] = is_fullmoon_passed
+    return base
+
+
 def _fullmoon_todo_to_dict(ft: BizProtocolFullmoonTodo) -> Dict[str, Any]:
     return {
         "id": ft.id,

+ 75 - 2
backend/app/crud/qc_material_usage.py

@@ -145,6 +145,76 @@ def list_usages(
     ], int(total)
 
 
+def list_usages_by_task(
+    db: Session,
+    task_id: str,
+    tenant_no: str,
+    *,
+    user_info=None,
+) -> List[Dict[str, Any]]:
+    """查询质检任务关联的物料使用记录(task_complete)。"""
+    q = db.query(BizQcMaterialUsage).filter(
+        BizQcMaterialUsage.tenant_no == tenant_no,
+        BizQcMaterialUsage.del_flag == 0,
+        BizQcMaterialUsage.qc_task_id == task_id,
+        BizQcMaterialUsage.change_type == "use",
+        BizQcMaterialUsage.source_type == "task_complete",
+    )
+    if user_info:
+        from app.core.data_scope import apply_data_scope
+
+        q = apply_data_scope(q, BizQcMaterialUsage, user_info, db, tenant_no)
+    rows = q.order_by(BizQcMaterialUsage.create_time.asc()).all()
+    return [_row_to_dict(x) for x in rows]
+
+
+def revert_task_material_usages(
+    db: Session,
+    task_id: str,
+    tenant_no: str,
+    user_id: Optional[str] = None,
+) -> None:
+    """撤销任务此前 task_complete 扣减,便于重新填报物料使用。"""
+    rows = (
+        db.query(BizQcMaterialUsage)
+        .filter(
+            BizQcMaterialUsage.qc_task_id == task_id,
+            BizQcMaterialUsage.tenant_no == tenant_no,
+            BizQcMaterialUsage.del_flag == 0,
+            BizQcMaterialUsage.source_type == "task_complete",
+            BizQcMaterialUsage.change_type == "use",
+        )
+        .all()
+    )
+    if not rows:
+        return
+    now = datetime.now()
+    for r in rows:
+        restore_qty = abs(r.change_qty) if r.change_qty is not None else Decimal("0")
+        if restore_qty <= 0:
+            r.del_flag = 1
+            r.update_by = user_id
+            r.update_time = now
+            continue
+        material = (
+            db.query(BizQcMaterial)
+            .with_for_update()
+            .filter(
+                BizQcMaterial.id == r.material_id,
+                BizQcMaterial.tenant_no == tenant_no,
+                BizQcMaterial.del_flag == 0,
+            )
+            .first()
+        )
+        if material:
+            material.stock_qty = (material.stock_qty or Decimal("0")) + restore_qty
+            material.update_by = user_id
+            material.update_time = now
+        r.del_flag = 1
+        r.update_by = user_id
+        r.update_time = now
+
+
 def deduct_for_task(
     db: Session,
     task_id: str,
@@ -155,8 +225,9 @@ def deduct_for_task(
     dept_id: str = "",
 ) -> None:
     """扣减物料库存并生成变动记录。调用方控制事务,本函数不 commit。"""
+    revert_task_material_usages(db, task_id, tenant_no, user_id=user_id)
     for item in material_usages:
-        mid = item.get("materialId") or ""
+        mid = (item.get("materialId") or item.get("material_id") or "").strip()
         qty = item.get("qty")
         if not mid:
             raise HTTPException(status_code=400, detail="物料ID不能为空")
@@ -191,6 +262,8 @@ def deduct_for_task(
         material.update_by = user_id
         material.update_time = datetime.now()
 
+        usage_remark = str(item.get("remark") or "").strip()[:512]
+
         usage = BizQcMaterialUsage(
             id=generate_uuid(),
             material_id=mid,
@@ -203,7 +276,7 @@ def deduct_for_task(
             qc_task_no=task_no,
             source_type="task_complete",
             tenant_no=tenant_no,
-            remark="",
+            remark=usage_remark,
             create_by=user_id,
             create_dept_id=dept_id,
             create_time=datetime.now(),

+ 33 - 8
backend/app/crud/qc_task.py

@@ -371,6 +371,7 @@ def complete_task(
     user_id: Optional[str],
     conclusion: Optional[str] = None,
     remark: Optional[str] = None,
+    commit: bool = True,
 ) -> BizQcTask:
     t = get_task(db, row_id, tenant_no=tenant_no)
     if not t:
@@ -395,11 +396,14 @@ def complete_task(
     t.update_by = user_id
     t.update_time = datetime.now()
     db.add(t)
-    db.commit()
-    db.refresh(t)
-    from app.crud import qc_work_order as wo_svc
+    if commit:
+        db.commit()
+        db.refresh(t)
+        from app.crud import qc_work_order as wo_svc
 
-    wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no)
+        wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no)
+    else:
+        db.flush()
     return t
 
 
@@ -411,22 +415,38 @@ def apply_task_result(
     user_id: Optional[str],
     conclusion: str,
     remark: Optional[str] = None,
+    material_usages: Optional[List[Dict[str, Any]]] = None,
+    commit: bool = True,
 ) -> BizQcTask:
     """保存检测结果与结论;任务未完成时顺带标记为已完成。"""
+    from app.crud import qc_material_usage as mu_svc
+
     t = get_task(db, row_id, tenant_no=tenant_no)
     if not t:
         raise ValueError("任务不存在")
     concl = (conclusion or "").strip()
     if concl not in ("tg", "btg"):
         raise ValueError("请选择检测结论(通过/不通过)")
+    if material_usages is not None:
+        mu_svc.deduct_for_task(
+            db,
+            row_id,
+            task_no=t.task_no or "",
+            material_usages=material_usages,
+            tenant_no=tenant_no,
+            user_id=user_id,
+        )
     if (t.task_status or "").strip() == "ywc":
         t.conclusion = concl
         _set_task_remark(t, remark if remark is not None else "")
         t.update_by = user_id
         t.update_time = datetime.now()
         db.add(t)
-        db.commit()
-        db.refresh(t)
+        if commit:
+            db.commit()
+            db.refresh(t)
+        else:
+            db.flush()
         return t
     return complete_task(
         db,
@@ -435,6 +455,7 @@ def apply_task_result(
         user_id=user_id,
         conclusion=concl,
         remark=remark,
+        commit=commit,
     )
 
 
@@ -445,6 +466,7 @@ def update_task_attachments(
     attachments: str,
     tenant_no: str,
     user_id: Optional[str],
+    commit: bool = True,
 ) -> BizQcTask:
     t = get_task(db, row_id, tenant_no=tenant_no)
     if not t:
@@ -459,6 +481,9 @@ def update_task_attachments(
     t.update_by = user_id
     t.update_time = datetime.now()
     db.add(t)
-    db.commit()
-    db.refresh(t)
+    if commit:
+        db.commit()
+        db.refresh(t)
+    else:
+        db.flush()
     return t

+ 6 - 1
backend/app/crud/qc_task_result.py

@@ -113,6 +113,8 @@ def save_results_for_task(
     results: List[Dict[str, Any]],
     tenant_no: str,
     user_id: Optional[str] = None,
+    *,
+    commit: bool = True,
 ) -> List[Dict[str, Any]]:
     task = get_task(db, task_id, tenant_no=tenant_no)
     if not task:
@@ -148,5 +150,8 @@ def save_results_for_task(
         )
         db.add(obj)
 
-    db.commit()
+    if commit:
+        db.commit()
+    else:
+        db.flush()
     return list_results_by_task(db, task_id, tenant_no)

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/2226d87f9379fddba24b8c49746c754a58779ef285b0503f826f8b342c09e3ce.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/3f06a30671b16ed0de9ce845021b11e658a6f4259d3ddbd35f57780f44c6a916.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/44b5b988acb6740b2ffce97a6d210134efa349e12ab736ae6bb45d4cb097e97f.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/4a4de840a7b2a2449b36ae3a61c26fd6c5231822a3457f81aec8f515cf748a03.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/6b08dd8cdec0b8b42794930b9d2998ec46bf793c2444dc89b7ecb131102b6ffc.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/6fe2fc067d3d4d04f84e67e35fbfb2d8c25e8873753e2775ddff7c0cbc06ffc5.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/75d7e14fc2a1fec8bc214036f69abd15de81b5a9aecf3d775c30feb5ca67815e.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/a63c2519460a97de1968d0c6092894d0065f4b7a5e1ee0529d40441746b795c4.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/ad372661ba5b6508cee5809e144dff1348c8f419590415135941eb606d82d239.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/ae4a4325463230a190e80a357bbe4497f1a2df79eff653fd7a9c88fd7e303a69.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/e714ec8ea68584f4f1ff6d1a62df71095d30d095cf52bbae4283250c612999a1.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
backend/app/graphify-out/cache/ast/edf5403c4985d59d27139afdaf52561ddad929fc7644bc94575d6dc83e5ec654.json


Разница между файлами не показана из-за своего большого размера
+ 476 - 483
backend/graphify-out/.graphify_analysis.json


Разница между файлами не показана из-за своего большого размера
+ 208 - 145
backend/graphify-out/.graphify_extract.json


+ 117 - 107
backend/graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - app  (2026-05-21)
 
 ## Corpus Check
-- 135 files · ~117,218 words
+- 135 files · ~117,792 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 2365 nodes · 9204 edges · 50 communities detected
-- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5326 edges (avg confidence: 0.64)
+- 2375 nodes · 9260 edges · 52 communities detected
+- Extraction: 42% EXTRACTED · 58% INFERRED · 0% AMBIGUOUS · INFERRED: 5364 edges (avg confidence: 0.64)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -54,21 +54,23 @@
 - [[_COMMUNITY_Community 41|Community 41]]
 - [[_COMMUNITY_Community 42|Community 42]]
 - [[_COMMUNITY_Community 43|Community 43]]
-- [[_COMMUNITY_Community 46|Community 46]]
-- [[_COMMUNITY_Community 47|Community 47]]
+- [[_COMMUNITY_Community 44|Community 44]]
+- [[_COMMUNITY_Community 45|Community 45]]
 - [[_COMMUNITY_Community 48|Community 48]]
 - [[_COMMUNITY_Community 49|Community 49]]
 - [[_COMMUNITY_Community 50|Community 50]]
 - [[_COMMUNITY_Community 51|Community 51]]
+- [[_COMMUNITY_Community 52|Community 52]]
+- [[_COMMUNITY_Community 53|Community 53]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `success_response()` - 375 edges
-2. `error_response()` - 301 edges
-3. `SysUser` - 214 edges
-4. `BizQcMaterialUsage` - 195 edges
+1. `success_response()` - 376 edges
+2. `error_response()` - 302 edges
+3. `SysUser` - 217 edges
+4. `BizQcMaterialUsage` - 197 edges
 5. `BizMaterialStockLog` - 193 edges
 6. `BizQcTaskResult` - 193 edges
-7. `BizQcMaterial` - 191 edges
+7. `BizQcMaterial` - 193 edges
 8. `BizMaterial` - 190 edges
 9. `BizQcItemParam` - 188 edges
 10. `generate_uuid()` - 129 edges
@@ -88,64 +90,64 @@
 ## Communities
 
 ### Community 0 - "Community 0"
-Cohesion: 0.01
-Nodes (393): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+385 more)
+Cohesion: 0.02
+Nodes (423): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+415 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.03
-Nodes (375): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+367 more)
+Cohesion: 0.01
+Nodes (368): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+360 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.06
-Nodes (255): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+247 more)
+Nodes (196): InsuranceCompanyCreateBody, InsuranceCompanyInvoiceCreateBody, InsuranceCompanyInvoiceUpdateBody, InsuranceCompanyUpdateBody, InsuranceProjectPriceCreateBody, InsuranceProjectPriceUpdateBody, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), Base (+188 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.01
-Nodes (224): list_collection_regions(), generate_uuid(), delete(), get_multi_with_filters(), create_cell_product(), delete_cell_product(), get_cell_product(), list_cell_products() (+216 more)
+Cohesion: 0.03
+Nodes (135): generate_uuid(), create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists() (+127 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.05
-Nodes (92): _persist_protocol_sub_resources(), list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), _add_months_yyyymmdd(), align_fq_payment_details_payload() (+84 more)
+Cohesion: 0.03
+Nodes (123): list_collection_regions(), _persist_protocol_sub_resources(), list_ethnics_for_select(), list_access_logs(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes() (+115 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.05
-Nodes (83): _format_attachment_file_size(), urls_to_attachment_list(), complete_production_work_order(), create_production_work_order_from_reception(), _fmt_dt(), _generate_work_order_no(), get_production_work_order(), list_production_work_orders() (+75 more)
+Cohesion: 0.09
+Nodes (125): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+117 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.06
-Nodes (59): _business_type_label(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data(), get_instance_detail(), list_completed_tasks(), list_my_instances() (+51 more)
+Cohesion: 0.05
+Nodes (83): _format_attachment_file_size(), get_protocol_nos_used_in_protocols(), urls_to_attachment_list(), complete_production_work_order(), create_production_work_order_from_reception(), _fmt_dt(), _generate_work_order_no(), get_production_work_order() (+75 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.05
-Nodes (59): list_roles_for_select(), _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)。 (+51 more)
+Cohesion: 0.04
+Nodes (80): activate_channel_agreement(), cell_product_price_code_exists(), cell_product_spec_code_exists(), cell_product_type_code_exists(), _channel_agreement_price_exists(), _channel_agreement_price_item_key(), checkup_package_code_exists(), create_cell_product_price() (+72 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.07
-Nodes (66): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约), StorageFacilityCreateBody (+58 more)
+Cohesion: 0.06
+Nodes (61): _business_type_label(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data(), get_instance_detail(), list_completed_tasks(), list_my_instances() (+53 more)
 
 ### Community 9 - "Community 9"
 Cohesion: 0.05
-Nodes (26): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+18 more)
+Nodes (59): list_roles_for_select(), _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)。 (+51 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.05
-Nodes (42): generate_id(), camel_to_snake(), CRUDBase, get_model_attr(), HistoryCRUD, get_protocol_nos_used_in_protocols(), create_dict_type(), dict_type_code_exists() (+34 more)
+Cohesion: 0.07
+Nodes (66): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slots(), 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+58 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.09
-Nodes (56): AuditSubmitBody, batch_delete_research_donations(), BatchDeleteBody, change_donation_status(), convert_from_storage(), convert_to_storage(), ConvertFromStorageBody, create_research_donation() (+48 more)
+Cohesion: 0.05
+Nodes (26): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+18 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.43
-Nodes (45): ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody, OutputTypeCreateBody, OutputTypeUpdateBody, PackItemInput, ProcessCategoryCreateBody (+37 more)
+Cohesion: 0.06
+Nodes (35): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json(), derive_key(), encrypt_json(), gen_nonce_hex() (+27 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.08
-Nodes (36): BaseHTTPMiddleware, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。  与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。      Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+28 more)
+Cohesion: 0.43
+Nodes (45): ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody, OutputTypeCreateBody, OutputTypeUpdateBody, PackItemInput, ProcessCategoryCreateBody (+37 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.16
-Nodes (32): _create_insurance_company(), _create_insurance_company_invoice(), _create_insurance_project_price(), delete_insurance_company(), delete_insurance_company_invoice(), delete_insurance_project_price(), _fmt_dt(), _generate_uuid() (+24 more)
+Cohesion: 0.14
+Nodes (35): list_research_donation_audits(), _audit_to_dict(), batch_delete_donations(), _collection_to_dict(), convert_to_storage(), _create_collection(), create_donation(), _create_party() (+27 more)
 
 ### Community 15 - "Community 15"
 Cohesion: 0.12
@@ -156,177 +158,185 @@ 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 17 - "Community 17"
-Cohesion: 0.22
-Nodes (18): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+10 more)
-
-### Community 18 - "Community 18"
-Cohesion: 0.15
-Nodes (16): 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() (+8 more)
-
-### Community 19 - "Community 19"
 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)
 
-### Community 20 - "Community 20"
+### Community 18 - "Community 18"
 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 19 - "Community 19"
 Cohesion: 0.23
 Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
 
-### Community 23 - "Community 23"
-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 24 - "Community 24"
+### Community 20 - "Community 20"
 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"
+### Community 21 - "Community 21"
 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 26 - "Community 26"
+### Community 22 - "Community 22"
 Cohesion: 0.31
 Nodes (8): consumable_code_exists(), create_consumable(), get_consumable(), list_consumables(), 耗材 CRUD(biz_consumable), _row_to_dict(), soft_delete_consumable(), update_consumable()
 
-### Community 27 - "Community 27"
+### Community 23 - "Community 23"
+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 24 - "Community 24"
+Cohesion: 0.36
+Nodes (9): deduct_for_task(), _display_remark(), list_usages(), list_usages_by_task(), _parse_receiver_purpose_from_remark(), _resolve_receiver_purpose(), revert_task_material_usages(), _row_to_dict() (+1 more)
+
+### Community 25 - "Community 25"
 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 28 - "Community 28"
+### Community 26 - "Community 26"
 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 27 - "Community 27"
+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 28 - "Community 28"
+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 29 - "Community 29"
 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()
+Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
 
 ### Community 30 - "Community 30"
 Cohesion: 0.31
-Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
+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 31 - "Community 31"
 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()
+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 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()
+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"
 Cohesion: 0.36
-Nodes (7): create_reagent(), get_reagent(), list_reagents(), reagent_code_exists(), _row_to_dict(), soft_delete_reagent(), update_reagent()
+Nodes (7): create_dict_type(), dict_type_code_exists(), get_dict_type(), list_dict_types(), _row_to_dict(), soft_delete_dict_type(), update_dict_type()
 
 ### Community 34 - "Community 34"
 Cohesion: 0.36
-Nodes (8): _display_remark(), list_usages(), _parse_receiver_purpose_from_remark(), 兼容旧数据:领用人/用途曾拼接在 remark 中。, 手动出库时备注仅展示用户填写内容,不含领用人/用途拼接段。, _resolve_receiver_purpose(), _row_to_dict(), _user_nick_name_map()
+Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
 
 ### Community 35 - "Community 35"
-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()
+Cohesion: 0.36
+Nodes (7): create_reagent(), get_reagent(), list_reagents(), reagent_code_exists(), _row_to_dict(), soft_delete_reagent(), update_reagent()
 
 ### Community 36 - "Community 36"
+Cohesion: 0.31
+Nodes (7): create_cell_product(), delete_cell_product(), get_cell_product(), list_cell_products(), 细胞产品 CRUD(biz_cell_product), _row_to_dict(), update_cell_product()
+
+### Community 37 - "Community 37"
 Cohesion: 0.36
 Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
 
-### Community 37 - "Community 37"
+### Community 38 - "Community 38"
+Cohesion: 0.36
+Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
+
+### Community 39 - "Community 39"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 38 - "Community 38"
+### Community 40 - "Community 40"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 39 - "Community 39"
+### Community 41 - "Community 41"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 40 - "Community 40"
+### Community 42 - "Community 42"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 41 - "Community 41"
+### Community 43 - "Community 43"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)
 
-### Community 42 - "Community 42"
+### Community 44 - "Community 44"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 43 - "Community 43"
+### Community 45 - "Community 45"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 46 - "Community 46"
+### Community 48 - "Community 48"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 47 - "Community 47"
+### Community 49 - "Community 49"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 48 - "Community 48"
+### Community 50 - "Community 50"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 49 - "Community 49"
+### Community 51 - "Community 51"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 50 - "Community 50"
+### Community 52 - "Community 52"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 51 - "Community 51"
+### Community 53 - "Community 53"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
 - **89 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+84 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 37`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 39`** (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 40`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 42`** (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 41`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
+- **Thin community `Community 43`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 44`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 43`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 45`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 48`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 47`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 49`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 48`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 50`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 49`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 51`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 50`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 52`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 53`** (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 `generate_uuid()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 11`, `Community 13`, `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 35`, `Community 36`?**
-  _High betweenness centrality (0.105) - this node is a cross-community bridge._
-- **Why does `SysUser` connect `Community 0` to `Community 2`, `Community 34`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 19`, `Community 20`?**
-  _High betweenness centrality (0.099) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 1` to `Community 0`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 41`, `Community 11`, `Community 14`?**
+- **Why does `generate_uuid()` connect `Community 3` to `Community 0`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `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`?**
+  _High betweenness centrality (0.096) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 1` to `Community 2`, `Community 3`, `Community 5`, `Community 8`, `Community 9`, `Community 10`, `Community 13`, `Community 17`, `Community 18`?**
+  _High betweenness centrality (0.095) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 4`, `Community 8`, `Community 9`, `Community 10`, `Community 43`, `Community 14`?**
   _High betweenness centrality (0.075) - this node is a cross-community bridge._
-- **Are the 374 inferred relationships involving `success_response()` (e.g. with `web_login_rsa_public_key()` and `web_login()`) actually correct?**
-  _`success_response()` has 374 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 347 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
-  _`str` has 347 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 300 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `web_login()`) actually correct?**
-  _`error_response()` has 300 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 212 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
-  _`SysUser` has 212 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 375 inferred relationships involving `success_response()` (e.g. with `web_login_rsa_public_key()` and `web_login()`) actually correct?**
+  _`success_response()` has 375 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 348 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
+  _`str` has 348 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 301 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `web_login()`) actually correct?**
+  _`error_response()` has 301 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 215 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
+  _`SysUser` has 215 INFERRED edges - model-reasoned connections that need verification._

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
backend/graphify-out/graph.html


Разница между файлами не показана из-за своего большого размера
+ 112 - 112
backend/graphify-out/graph.json


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

@@ -5,7 +5,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/auth.py": 1779245791.6811426,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto.py": 1778578258.039314,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/login_crypto.py": 1778578258.041774,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1778775532.8838744,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/core/config.py": 1779313263.9269438,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_pay.py": 1778578258.0461762,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/api_request_log_middleware.py": 1778578258.039998,
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/database.py": 1779242347.136184,
@@ -59,10 +59,10 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review.py": 1778578258.098316,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category.py": 1778578258.0941947,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/leave.py": 1778666088.7680702,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779265700.504972,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779331765.6469367,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_route.py": 1779265696.2771301,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_code.py": 1779279460.690872,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1779265777.8667352,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1779331731.6720955,
   "/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,
@@ -77,8 +77,8 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779265679.474413,
   "/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": 1779265718.524432,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1779265708.6464283,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_usage.py": 1779284725.8672473,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1779329214.3583212,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_usage.py": 1779332036.2049248,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/clean_room.py": 1778775238.2993922,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/__init__.py": 1778605691.3816392,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/express_company.py": 1779265659.3453207,
@@ -96,8 +96,8 @@
   "/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": 1779265686.8066864,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1779265693.0041955,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779265757.2435377,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1779309113.0447981,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779310971.8304002,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1778733031.447253,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/workflow.py": 1779265993.677624,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception.py": 1779265759.1550276,
@@ -113,9 +113,9 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/leave.py": 1779265839.500242,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/output_type.py": 1779265703.7363207,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/exhibition_appointment.py": 1778801075.5211918,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1779009064.3735678,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1779313409.0685387,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1779283386.9305117,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779283997.3485882,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779332040.5250597,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1779265813.1605325,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1779280842.8447173,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1779230551.0712175,
@@ -124,7 +124,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/admin.py": 1779265690.8423955,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation.py": 1779266149.3047824,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/ethnic.py": 1779266243.9511197,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_service.py": 1779265798.3244395,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_service.py": 1779310968.9855928,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/workflow.py": 1779266232.4568832,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1779304425.1104686,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1779281097.279193,

Разница между файлами не показана из-за своего большого размера
+ 473 - 423
web-admin/graphify-out/.graphify_analysis.json


Разница между файлами не показана из-за своего большого размера
+ 259 - 187
web-admin/graphify-out/.graphify_extract.json


+ 107 - 76
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
 # Graph Report - src  (2026-05-21)
 
 ## Corpus Check
-- Large corpus: 575 files · ~291,906 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 575 files · ~293,920 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 1387 nodes · 1729 edges · 35 communities detected
-- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 379 edges (avg confidence: 0.8)
+- 1396 nodes · 1745 edges · 40 communities detected
+- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 383 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -34,7 +34,7 @@
 - [[_COMMUNITY_Community 22|Community 22]]
 - [[_COMMUNITY_Community 23|Community 23]]
 - [[_COMMUNITY_Community 24|Community 24]]
-- [[_COMMUNITY_Community 26|Community 26]]
+- [[_COMMUNITY_Community 25|Community 25]]
 - [[_COMMUNITY_Community 27|Community 27]]
 - [[_COMMUNITY_Community 29|Community 29]]
 - [[_COMMUNITY_Community 30|Community 30]]
@@ -43,12 +43,17 @@
 - [[_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 42|Community 42]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `get()` - 151 edges
+1. `get()` - 152 edges
 2. `WebSocketClient` - 26 edges
-3. `push()` - 17 edges
+3. `push()` - 18 edges
 4. `withFullscreenLoading()` - 16 edges
 5. `MenuProcessor` - 16 edges
 6. `VersionManager` - 15 edges
@@ -62,122 +67,122 @@
   views/system/site/index.vue → utils/fullscreen-loading.ts
 - `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
   views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
-- `handleBatchDeleteHospital()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
-- `fetchProcessDefList()` --calls--> `get()`  [INFERRED]
-  api/workflow.ts → views/production/process-route/index.vue
-- `fetchVersionList()` --calls--> `get()`  [INFERRED]
-  api/workflow.ts → views/production/process-route/index.vue
+- `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()`  [INFERRED]
+  utils/http/index.ts → api/auth.ts
+- `fetchLeaveList()` --calls--> `get()`  [INFERRED]
+  api/leave.ts → views/production/process-route/index.vue
+- `fetchLeaveDetail()` --calls--> `get()`  [INFERRED]
+  api/leave.ts → views/production/process-route/index.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (111): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+103 more)
+Nodes (75): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+67 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.02
-Nodes (6): fetchDeleteProjectSystemItem(), fetchGetRoleList(), fetchNotDeletedRegions(), fetchRegionList(), loadRoleList(), if()
-
-### Community 2 - "Community 2"
 Cohesion: 0.03
-Nodes (55): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+47 more)
+Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
 
-### Community 3 - "Community 3"
+### Community 2 - "Community 2"
 Cohesion: 0.04
 Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
 
+### Community 3 - "Community 3"
+Cohesion: 0.06
+Nodes (11): fetchGetLatestUpgradeLog(), getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager (+3 more)
+
 ### Community 4 - "Community 4"
-Cohesion: 0.05
-Nodes (24): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), fetchAttachmentDownloadUrl(), collectIds(), postorder(), preorder() (+16 more)
+Cohesion: 0.04
+Nodes (18): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllSopDocuments(), fetchConsumableList() (+10 more)
 
 ### Community 5 - "Community 5"
 Cohesion: 0.05
-Nodes (31): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), handleMenuJump() (+23 more)
+Nodes (32): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+24 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.06
-Nodes (10): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), TableCache, themeAnimation() (+2 more)
+Cohesion: 0.05
+Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.09
-Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+Nodes (5): handleReset(), handleSearch(), WebSocketClient, TableCache, emit()
 
 ### Community 8 - "Community 8"
 Cohesion: 0.09
-Nodes (28): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+20 more)
+Nodes (31): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+23 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.13
-Nodes (4): handleReset(), handleSearch(), WebSocketClient, emit()
+Cohesion: 0.05
+Nodes (9): fetchCellProductDetail(), fetchCellProductList(), fetchLeaveDetail(), fetchLeaveList(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchResearchDonationAuditList(), fetchResearchDonationDetail() (+1 more)
 
 ### Community 10 - "Community 10"
-Cohesion: 0.1
-Nodes (23): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchUpdateAccessLog(), fetchUpdateCleanRoom() (+15 more)
+Cohesion: 0.07
+Nodes (13): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+5 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.1
-Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
+Cohesion: 0.08
+Nodes (10): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), themeAnimation() (+2 more)
 
 ### Community 12 - "Community 12"
 Cohesion: 0.1
-Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
+Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.1
-Nodes (7): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
+Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
 
 ### Community 14 - "Community 14"
 Cohesion: 0.1
-Nodes (19): fetchCreateSite(), fetchDeleteSite(), fetchDictDataOptions(), fetchSiteList(), fetchUpdateSite(), fetchUserList(), remoteSearchUsers(), handleAdd() (+11 more)
+Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.12
-Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
+Cohesion: 0.1
+Nodes (18): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+10 more)
 
 ### Community 16 - "Community 16"
 Cohesion: 0.12
-Nodes (19): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchRoleOptions(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd() (+11 more)
+Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.19
-Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
+Cohesion: 0.14
+Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
 
 ### Community 18 - "Community 18"
+Cohesion: 0.11
+Nodes (8): fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchRefundConfirmedList(), fetchRefundConfirmList()
+
+### Community 19 - "Community 19"
 Cohesion: 0.12
 Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
 
-### Community 19 - "Community 19"
+### Community 20 - "Community 20"
+Cohesion: 0.16
+Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
+
+### Community 21 - "Community 21"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 20 - "Community 20"
+### Community 22 - "Community 22"
 Cohesion: 0.2
 Nodes (2): ComponentLoader, RouteTransformer
 
-### Community 21 - "Community 21"
-Cohesion: 0.18
-Nodes (9): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+1 more)
-
-### Community 22 - "Community 22"
+### Community 23 - "Community 23"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 23 - "Community 23"
+### Community 24 - "Community 24"
 Cohesion: 0.17
 Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
 
-### Community 24 - "Community 24"
+### Community 25 - "Community 25"
 Cohesion: 0.14
 Nodes (2): fetchProductionTaskDetail(), fetchProductionTaskList()
 
-### Community 26 - "Community 26"
-Cohesion: 0.17
-Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
-
 ### Community 27 - "Community 27"
-Cohesion: 0.2
-Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
+Cohesion: 0.18
+Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
 
 ### Community 29 - "Community 29"
 Cohesion: 0.39
@@ -196,54 +201,80 @@ Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
 ### Community 33 - "Community 33"
+Cohesion: 0.33
+Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
+
+### Community 34 - "Community 34"
+Cohesion: 0.33
+Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
+
+### Community 35 - "Community 35"
+Cohesion: 0.33
+Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
+
+### Community 36 - "Community 36"
+Cohesion: 0.33
+Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
+
+### Community 37 - "Community 37"
+Cohesion: 0.53
+Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
+
+### Community 38 - "Community 38"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 34 - "Community 34"
+### Community 39 - "Community 39"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 35 - "Community 35"
+### Community 40 - "Community 40"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 37 - "Community 37"
+### Community 42 - "Community 42"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
 ## Knowledge Gaps
-- **Thin community `Community 20`** (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 24`** (14 nodes): `fetchAddTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
+- **Thin community `Community 22`** (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 27`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
+- **Thin community `Community 25`** (14 nodes): `fetchAddTaskStep()`, `fetchCreateProductionTask()`, `fetchDeleteTaskStep()`, `fetchProductionTaskDetail()`, `fetchProductionTaskList()`, `fetchReplaceStepConsumables()`, `fetchReplaceStepDataCollection()`, `fetchReplaceStepReagents()`, `fetchSetTaskInputs()`, `fetchSetTaskOutputs()`, `fetchUpdateProductionTask()`, `fetchUpdateProductionTaskStatus()`, `fetchUpdateTaskStep()`, `production-task.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 - **Thin community `Community 30`** (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 31`** (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 33`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 33`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 34`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 35`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 36`** (6 nodes): `fetchCompleteQcTask()`, `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`** (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 34`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 39`** (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 35`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 40`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 42`** (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 5`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 23`, `Community 24`, `Community 26`, `Community 27`?**
-  _High betweenness centrality (0.250) - this node is a cross-community bridge._
-- **Why does `push()` connect `Community 5` to `Community 1`, `Community 3`, `Community 6`, `Community 9`, `Community 11`, `Community 17`?**
-  _High betweenness centrality (0.089) - this node is a cross-community bridge._
-- **Why does `fetchGetLatestUpgradeLog()` connect `Community 17` to `Community 0`?**
-  _High betweenness centrality (0.044) - this node is a cross-community bridge._
-- **Are the 150 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
-  _`get()` has 150 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 16 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
-  _`push()` has 16 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 24`, `Community 25`, `Community 27`, `Community 33`, `Community 34`, `Community 35`, `Community 36`?**
+  _High betweenness centrality (0.256) - this node is a cross-community bridge._
+- **Why does `push()` connect `Community 5` to `Community 0`, `Community 2`, `Community 3`, `Community 7`, `Community 11`, `Community 14`?**
+  _High betweenness centrality (0.086) - this node is a cross-community bridge._
+- **Why does `fetchGetLatestUpgradeLog()` connect `Community 3` to `Community 0`?**
+  _High betweenness centrality (0.043) - this node is a cross-community bridge._
+- **Are the 151 inferred relationships involving `get()` (e.g. with `buildTreePathLabelFromFlat()` and `alignFqPaymentDetailsForDisplay()`) actually correct?**
+  _`get()` has 151 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 17 inferred relationships involving `push()` (e.g. with `fetchAllPagedRecords()` and `calcInstallmentDueAmounts()`) actually correct?**
+  _`push()` has 17 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 15 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
   _`withFullscreenLoading()` has 15 INFERRED edges - model-reasoned connections that need verification._
 - **Should `Community 0` be split into smaller, more focused modules?**

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
web-admin/graphify-out/graph.html


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

@@ -17,8 +17,8 @@
   "/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": 1779310456.9084861,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779306613.6203473,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779328937.243264,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779313245.9581928,
   "/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,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -85,7 +85,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/table/tableUtils.ts": 1778582068.0454955,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/http/status.ts": 1778582068.036632,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/http/crypto.ts": 1778582068.0353796,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/http/index.ts": 1778910222.6539366,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/http/index.ts": 1779312712.0018501,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/http/error.ts": 1778582068.0356607,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/socket/index.ts": 1778582068.0397933,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/router/sidebar-menu-filter.ts": 1778582068.0391986,
@@ -169,7 +169,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-cutter-img/index.vue": 1778582067.9817953,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/index.vue": 1779184108.8553443,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/finance-attachments.ts": 1778987672.644368,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779203366.0093758,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779332634.772821,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue": 1779254253.4663467,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1778582866.2188683,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1778582067.9494708,
@@ -195,12 +195,12 @@
   "/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/workflow.ts": 1778729873.096252,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-work-order.ts": 1779207884.934813,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task-result.ts": 1779184105.1763976,
+  "/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": 1779244706.1724706,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/chinaRegion.ts": 1778582067.8620915,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/business.ts": 1779159587.6033742,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1779281097.8289247,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts": 1779310973.2465198,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/ethnic.ts": 1778929297.3919344,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-lab-test-item.ts": 1779017838.8231559,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1779207897.9650705,
@@ -217,15 +217,15 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779180968.908451,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1778910234.3222458,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779269573.509064,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779253420.0480523,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779329214.96505,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue": 1779161483.857836,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779255586.96183,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1779328834.3196502,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue": 1778938314.528914,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779200506.6351457,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1779327539.5577006,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue": 1778978838.5026703,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1779247774.782579,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1779269672.430675,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/index.vue": 1779245911.567694,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue": 1779313518.430975,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue": 1779313530.5053055,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/index.vue": 1779329216.7304003,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue": 1779286049.8894517,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/fullmoon-todo/index.vue": 1779268810.2184024,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-confirm/index.vue": 1779269651.5116343,
@@ -239,7 +239,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1779227504.1804457,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationAuditDialog.vue": 1779112125.8042517,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1779255542.4968102,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779199512.4395914,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1779327540.322163,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue": 1779161484.445245,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/dashboard/console/index.vue": 1779227504.1947322,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/tenant/index.vue": 1779257191.2809517,
@@ -280,18 +280,18 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1778582068.070888,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779269560.7371643,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779269589.0865154,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779306022.697412,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779331328.5864854,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779284232.1223023,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779255980.3303447,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779269565.6523979,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779328630.8453777,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779332615.9118567,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779285902.4727366,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779305140.1530182,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779330250.9402645,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/500/index.vue": 1778582068.0705533,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/403/index.vue": 1778582068.0697916,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/404/index.vue": 1778582068.0701847,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/cell-product-type/index.vue": 1779307919.8891723,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/index.vue": 1779269678.415951,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue": 1779269549.2034976,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/index.vue": 1779311045.8670757,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue": 1779311419.51521,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/index.vue": 1779269591.7394044,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/ChannelViewDialog.vue": 1779310694.8957999,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/cell-product-price/index.vue": 1779309928.4076538,
@@ -309,7 +309,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/modules/table.ts": 1779257771.098434,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/modules/menu.ts": 1779240273.3067045,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/modules/setting.ts": 1778582068.0225098,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/modules/user.ts": 1778582068.0230925,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/store/modules/user.ts": 1779313361.6982958,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/routesAlias.ts": 1778582068.0211735,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/index.ts": 1779232037.727516,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/RouteTransformer.ts": 1778582068.0132053,

+ 15 - 0
web-admin/src/api/qc-task-result.ts

@@ -15,6 +15,20 @@ export function fetchQcTaskResults(taskId: string) {
   })
 }
 
+/** 任务已保存的物料使用(用于填写结果弹窗回显) */
+export type QcTaskMaterialUsageItem = {
+  materialId: string
+  materialName: string
+  changeQty: number
+  remark?: string
+}
+
+export function fetchQcTaskMaterialUsages(taskId: string) {
+  return request.get<QcTaskMaterialUsageItem[]>({
+    url: `/web/admin/qc-tasks/${taskId}/material-usages`
+  })
+}
+
 export function fetchCompleteTaskWithResult(
   taskId: string,
   data: {
@@ -22,6 +36,7 @@ export function fetchCompleteTaskWithResult(
     results: QcTaskResultItem[]
     remark?: string
     thirdPartyAttachments?: string
+    materialUsages?: Array<{ materialId: string; qty: number; remark?: string }>
   }
 ) {
   return request.post({

+ 131 - 1
web-admin/src/components/business/qc-task-view-dialog/index.vue

@@ -28,6 +28,49 @@
       </div>
     </template>
     <template #append>
+      <div
+        v-if="viewDetail?.taskType !== 'dsfjc'"
+        class="view-section qc-task-view-material-usages"
+      >
+        <h3>物料使用</h3>
+        <el-table
+          v-if="viewMaterialUsages.length"
+          :data="viewMaterialUsages"
+          stripe
+          class="list-table inner-table qc-task-view-material-table"
+        >
+          <el-table-column
+            type="index"
+            label="序号"
+            width="56"
+            align="center"
+            header-align="center"
+            :index="(index) => index + 1"
+          />
+          <el-table-column
+            prop="materialName"
+            label="物料名称"
+            min-width="160"
+            align="center"
+            header-align="center"
+          />
+          <el-table-column label="规格型号" min-width="120" align="center" header-align="center">
+            <template #default="{ row }">{{ row.specModel || '—' }}</template>
+          </el-table-column>
+          <el-table-column label="用量" min-width="100" align="center" header-align="center">
+            <template #default="{ row }">
+              {{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="单位" width="72" align="center" header-align="center">
+            <template #default="{ row }">{{ row.unit || '—' }}</template>
+          </el-table-column>
+          <el-table-column label="备注" min-width="140" align="center" header-align="center">
+            <template #default="{ row }">{{ row.remark || '—' }}</template>
+          </el-table-column>
+        </el-table>
+        <p v-else class="view-material-empty">暂无物料使用记录</p>
+      </div>
       <div
         v-if="viewDetail?.attachmentFiles?.length"
         class="view-section qc-task-view-attachments"
@@ -50,7 +93,12 @@
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
   import { fetchQcTaskDetail, type QcTaskDetail } from '@/api/qc-task'
-  import { fetchQcTaskResults } from '@/api/qc-task-result'
+  import {
+    fetchQcTaskResults,
+    fetchQcTaskMaterialUsages,
+    type QcTaskMaterialUsageItem
+  } from '@/api/qc-task-result'
+  import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
   import {
     taskTypeLabel,
     taskStatusLabel,
@@ -77,15 +125,60 @@
     set: (val) => emit('update:visible', val)
   })
 
+  type ViewMaterialUsageRow = {
+    materialId: string
+    materialName: string
+    specModel: string
+    unit: string
+    qty: number | undefined
+    remark: string
+  }
+
   const loading = ref(false)
   const viewDetail = ref<QcTaskDetail | null>(null)
+  const viewMaterialUsages = ref<ViewMaterialUsageRow[]>([])
   const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
 
+  function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): ViewMaterialUsageRow[] {
+    const merged = new Map<string, ViewMaterialUsageRow>()
+    for (const u of items || []) {
+      const mid = (u.materialId || '').trim()
+      if (!mid) continue
+      const qtyRaw = Math.abs(Number(u.changeQty) || 0)
+      const prev = merged.get(mid)
+      if (prev) {
+        prev.qty = (Number(prev.qty) || 0) + qtyRaw
+      } else {
+        merged.set(mid, {
+          materialId: mid,
+          materialName: u.materialName || '',
+          specModel: '',
+          unit: '',
+          qty: qtyRaw > 0 ? qtyRaw : undefined,
+          remark: u.remark || ''
+        })
+      }
+    }
+    return Array.from(merged.values())
+  }
+
+  function enrichMaterialUsageRows(rows: ViewMaterialUsageRow[], options: QcMaterialOption[]) {
+    for (const row of rows) {
+      const opt = options.find((o) => o.id === row.materialId)
+      if (opt) {
+        row.materialName = opt.materialName || row.materialName
+        row.specModel = opt.specModel || ''
+        row.unit = opt.unit || ''
+      }
+    }
+  }
+
   watch(
     () => [props.visible, props.taskId] as const,
     async ([vis, id]) => {
       if (!vis || !id) {
         viewDetail.value = null
+        viewMaterialUsages.value = []
         return
       }
       loading.value = true
@@ -97,9 +190,24 @@
         } catch {
           ;(detail as QcTaskDetail).results = []
         }
+        viewMaterialUsages.value = []
+        if (detail.taskType !== 'dsfjc') {
+          try {
+            const [savedUsages, opts] = await Promise.all([
+              fetchQcTaskMaterialUsages(id).catch(() => [] as QcTaskMaterialUsageItem[]),
+              fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
+            ])
+            const rows = materialRowsFromApi(savedUsages || [])
+            enrichMaterialUsageRows(rows, opts || [])
+            viewMaterialUsages.value = rows
+          } catch {
+            viewMaterialUsages.value = []
+          }
+        }
         viewDetail.value = detail
       } catch {
         viewDetail.value = null
+        viewMaterialUsages.value = []
       } finally {
         loading.value = false
       }
@@ -286,6 +394,7 @@
     }
   }
 
+  .qc-task-view-material-usages,
   .qc-task-view-attachments {
     padding: 12px 18px;
     border-bottom: none;
@@ -300,4 +409,25 @@
       line-height: 1;
     }
   }
+
+  .qc-task-view-material-table {
+    width: 100%;
+
+    :deep(.el-table__header .cell),
+    :deep(.el-table__body .cell) {
+      font-size: 12px;
+      line-height: 1.4;
+    }
+
+    :deep(.el-table__header th.el-table__cell),
+    :deep(.el-table__body td.el-table__cell) {
+      padding: 6px 0;
+    }
+  }
+
+  .view-material-empty {
+    margin: 0;
+    font-size: 12px;
+    color: #64748b;
+  }
 </style>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/273e16a51782d2cd6a364377402732b7e9353b2db246ea2eccc72a74f4209636.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/278fad47026f3072c36587172b595bf144f9b305f071e6ea820518586ca6a291.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "label": "ProtocolViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310"}, {"id": "protocol_protocolviewdialog_handleclosed", "label": "handleClosed()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1627"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_dash", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_handleclosed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1627", "weight": 1.0}], "raw_calls": [{"caller_nid": "protocol_protocolviewdialog_dash", "callee": "unitName", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310"}]}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/3c936f17d2e9e6a496a180668be0088b02c3a8e4bc93a451da3f5f53597565dc.json


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

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

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_lab_test_item_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_third_party_company_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

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

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/68f205b6c471934393e0139fec46f051d57b323be3ae1023782475871fa5253a.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_checkup_institution_checkuppackagepanel_vue", "label": "CheckupPackagePanel.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/8daffc5ab89a54f68c2a40d38d1f0bd09db5f49d4974bd62e63f61af18dc07fe.json


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

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

+ 1 - 0
web-admin/src/graphify-out/cache/ast/975c601c2a97e683859e5078ec31b380293af33d045adaff968261fda88b62df.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": []}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/a8a13284b6a072f61c69f707ea73fadac43e12c97001cf6616cbfa6105c48b1d.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/a9f185d9f67ea5ae01fd38466642afe42917a7ad2e3f1191e632d61eb554fe49.json


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

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/cbfac312b94f4f714a80bee4a42027e89f6c7e7ca10119fa7a78d572fb349751.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/d21a1e1ddef3a368c4465ebd05199d8d317bc72d3d00495d72503f1fbd133772.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/d9257a0ae4d03614cff1182692347326af46b6a26fec228af189de874a258a5d.json


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/dc66a42c4eefebf31d144902854cc75093280a941fa4e3613f1ce4279defc08c.json


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

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

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

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

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

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
web-admin/src/graphify-out/cache/ast/f72481c9a45f8c6b7c90d49500e50f77b5222e7fc22fbc1c04b7f02d7e89b505.json


+ 8 - 2
web-admin/src/store/modules/user.ts

@@ -22,6 +22,7 @@ import { setPageTitle } from '@/utils/router'
 import { resetRouterState } from '@/router/guards/beforeEach'
 import { useMenuStore } from './menu'
 import { StorageConfig } from '@/utils/storage/storage-config'
+import { fetchLogout } from '@/api/auth'
 
 /**
  * 用户状态管理
@@ -101,9 +102,14 @@ export const useUserStore = defineStore(
 
     /**
      * 退出登录
-     * 清空所有用户相关状态并跳转到登录页
+     * 通知后端撤销 Redis 会话,然后清空所有用户相关状态并跳转到登录页
      */
-    const logOut = () => {
+    const logOut = async () => {
+      // 先通知后端撤销 Redis 会话(不等待结果,失败也不阻塞退出)
+      if (accessToken.value) {
+        fetchLogout().catch(() => {})
+      }
+
       // 保存当前用户 ID,用于下次登录时判断是否为同一用户
       const currentUserId = info.value.userId
       if (currentUserId) {

+ 91 - 14
web-admin/src/utils/http/index.ts

@@ -5,7 +5,7 @@
  * ## 主要功能
  *
  * - 请求/响应拦截器(自动添加 Token、统一错误处理)
- * - 401 未授权自动登出(带防抖机制)
+ * - 401 未授权自动刷新 Token(并发安全,刷新失败后登出)
  * - 请求失败自动重试(可配置)
  * - 统一的成功/错误消息提示
  * - 支持 GET/POST/PUT/DELETE 等常用方法
@@ -21,6 +21,7 @@ import { HttpError, handleError, showError, showSuccess } from './error'
 import { $t } from '@/locales'
 import { BaseResponse } from '@/types'
 import { decryptJson, deriveKey, encryptJson, randomNonce } from './crypto'
+import { fetchRefreshToken } from '@/api/auth'
 
 /** 请求配置常量 */
 const REQUEST_TIMEOUT = 60000
@@ -34,10 +35,32 @@ const UNAUTHORIZED_DEBOUNCE_TIME = 3000
 let isUnauthorizedErrorShown = false
 let unauthorizedTimer: NodeJS.Timeout | null = null
 
+/** Token 刷新状态(并发安全) */
+let isRefreshing = false
+let pendingRequests: Array<{
+  resolve: (token: string) => void
+  reject: (error: unknown) => void
+}> = []
+
+function subscribeTokenRefresh(resolve: (token: string) => void, reject: (error: unknown) => void) {
+  pendingRequests.push({ resolve, reject })
+}
+
+function onRefreshed(newToken: string) {
+  pendingRequests.forEach(({ resolve }) => resolve(newToken))
+  pendingRequests = []
+}
+
+function onRefreshFailed(error: unknown) {
+  pendingRequests.forEach(({ reject }) => reject(error))
+  pendingRequests = []
+}
+
 /** 扩展 AxiosRequestConfig */
 interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
   showErrorMessage?: boolean
   showSuccessMessage?: boolean
+  _retry?: boolean
 }
 
 const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env
@@ -148,11 +171,15 @@ axiosInstance.interceptors.response.use(
     const code = resData?.code
     const msg = resData?.msg || resData?.message
     if (code === ApiStatus.success) return response
-    if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
+    if (code === ApiStatus.unauthorized) {
+      return handleUnauthorizedResponse(response.config, msg)
+    }
     throw createHttpError(msg || $t('httpMsg.requestFailed'), code)
   },
-  (error) => {
-    if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError()
+  async (error) => {
+    if (error.response?.status === ApiStatus.unauthorized) {
+      return handleUnauthorizedResponse(error.response.config, undefined)
+    }
     return Promise.reject(handleError(error))
   }
 )
@@ -162,21 +189,71 @@ function createHttpError(message: string, code: number) {
   return new HttpError(message, code)
 }
 
-/** 处理401错误(带防抖) */
-function handleUnauthorizedError(message?: string): never {
-  const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+/**
+ * 处理 401 响应:尝试刷新 token,成功则重试原请求,失败则登出
+ */
+async function handleUnauthorizedResponse(
+  config: InternalAxiosRequestConfig,
+  message?: string
+): Promise<AxiosResponse> {
+  const userStore = useUserStore()
+  const storedRefreshToken = userStore.refreshToken
 
-  if (!isUnauthorizedErrorShown) {
-    isUnauthorizedErrorShown = true
-    logOut()
+  if (!storedRefreshToken) {
+    triggerLogoutWithDebounce(message)
+    throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+  }
 
-    unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
+  if ((config as ExtendedAxiosRequestConfig)._retry) {
+    triggerLogoutWithDebounce(message)
+    throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+  }
 
-    showError(error, true)
-    throw error
+  if (isRefreshing) {
+    return new Promise<AxiosResponse>((resolve, reject) => {
+      subscribeTokenRefresh(
+        (newToken: string) => {
+          config.headers.set('Authorization', `Bearer ${newToken}`)
+          ;(config as ExtendedAxiosRequestConfig)._retry = true
+          resolve(axiosInstance.request(config))
+        },
+        (error: unknown) => {
+          reject(createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized))
+        }
+      )
+    })
   }
 
-  throw error
+  isRefreshing = true
+  ;(config as ExtendedAxiosRequestConfig)._retry = true
+
+  try {
+    const result = await fetchRefreshToken(storedRefreshToken)
+    const newAccessToken = result.token
+    const newRefreshToken = result.refreshToken
+    userStore.setToken(newAccessToken, newRefreshToken)
+
+    onRefreshed(newAccessToken)
+
+    config.headers.set('Authorization', `Bearer ${newAccessToken}`)
+    return await axiosInstance.request(config)
+  } catch (refreshError) {
+    onRefreshFailed(refreshError)
+    triggerLogoutWithDebounce(message)
+    throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+  } finally {
+    isRefreshing = false
+  }
+}
+
+/** 带 3 秒防抖的登出提示 */
+function triggerLogoutWithDebounce(message?: string) {
+  if (isUnauthorizedErrorShown) return
+  isUnauthorizedErrorShown = true
+  const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+  logOut()
+  unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
+  showError(error, true)
 }
 
 /** 重置401防抖状态 */

+ 1 - 0
web-admin/src/views/cell-storage/payment-info/index.vue

@@ -404,6 +404,7 @@
         :list-row="viewRow"
         show-audit-history
         show-storage-cert
+        show-fullmoon-insurance
       />
     </section>
   </div>

+ 2 - 1
web-admin/src/views/cell-storage/protocol-audit/index.vue

@@ -16,7 +16,7 @@
         <el-form :model="searchForm" class="filter-form" @submit.prevent>
           <div class="filter-grid">
             <el-form-item label="关键词">
-              <el-input v-model="searchForm.keyword" placeholder="协议号/备注" clearable />
+              <el-input v-model="searchForm.keyword" placeholder="协议号/姓名/身份证号/电话" clearable />
             </el-form-item>
             <el-form-item label="协议状态">
               <el-select
@@ -290,6 +290,7 @@
         :list-row="viewRow"
         show-audit-history
         show-storage-cert
+        show-fullmoon-insurance
       />
     </section>
   </div>

+ 1 - 1
web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue

@@ -3,7 +3,7 @@
       <el-dialog
         :model-value="modelValue"
         :title="dialogTitle"
-        width="1200px"
+        width="1380px"
         class="dialog-form-layout"
         align-center
         destroy-on-close

+ 4 - 0
web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue

@@ -366,6 +366,10 @@
                           }}
                         </div>
                       </div>
+                      <div class="data-item">
+                        <div class="data-label">体积(ml)</div>
+                        <div class="data-value">{{ formatMetric(row.sampleReception.volumeMl) }}</div>
+                      </div>
                       <div class="data-item">
                         <div class="data-label">净重(g)</div>
                         <div class="data-value">{{ formatMetric(row.sampleReception.netWeightG) }}</div>

+ 3 - 1
web-admin/src/views/cell-storage/protocol/index.vue

@@ -16,7 +16,7 @@
         <el-form :model="searchForm" class="filter-form" @submit.prevent>
           <div class="filter-grid">
             <el-form-item label="关键词">
-              <el-input v-model="searchForm.keyword" placeholder="协议号/备注" clearable />
+              <el-input v-model="searchForm.keyword" placeholder="协议号/姓名/身份证号/电话" clearable />
             </el-form-item>
             <el-form-item label="协议状态">
               <el-select
@@ -255,6 +255,8 @@
         :protocol-id="viewProtocolId"
         :list-row="viewRow"
         show-audit-history
+        show-storage-cert
+        show-fullmoon-insurance
       />
     </section>
   </div>

+ 3 - 2
web-admin/src/views/cell-storage/storage-cert/index.vue

@@ -245,13 +245,14 @@
         </div>
       </el-dialog>
 
-      <!-- 查看弹窗(与采集协议单录入一致) -->
+      <!-- 查看弹窗(与满月待办管理等一致:审核+储存证+保险) -->
       <ProtocolViewDialog
         v-model="viewVisible"
         :protocol-id="viewProtocolId"
         :list-row="viewRow"
         show-audit-history
         show-storage-cert
+        show-fullmoon-insurance
       />
     </section>
   </div>
@@ -327,7 +328,7 @@
   const previewType = ref<'image' | 'video'>('image')
 
   // ---------------------------------------------------------------------------
-  // 查看弹窗(与采集协议单录入一致)
+  // 查看弹窗(与满月待办管理等一致)
   // ---------------------------------------------------------------------------
   const viewVisible = ref(false)
   const viewProtocolId = ref<string | null>(null)

+ 1 - 1
web-admin/src/views/quality-inspection/lab-test-item/index.vue

@@ -82,7 +82,7 @@
         </div>
       </div>
 
-      <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
+      <el-dialog v-model="dialogVisible" :title="dialogTitle" width="840px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
         <el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="biz-dialog-form">
           <div class="form-grid-2">
             <el-form-item label="项目编码" prop="itemCode">

+ 349 - 65
web-admin/src/views/quality-inspection/qc-task/index.vue

@@ -145,8 +145,8 @@
       <el-dialog
         v-model="resultVisible"
         title="填写检测结果"
-        width="720px"
-        class="dialog-form-layout"
+        :width="resultDialogWidth"
+        class="dialog-form-layout qc-task-result-dialog"
         align-center
         destroy-on-close
         :close-on-click-modal="false"
@@ -160,7 +160,7 @@
             >
               该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
             </div>
-            <div class="form-grid-2">
+            <div class="form-grid-3">
               <el-form-item
                 v-for="(param, idx) in resultParams"
                 :key="param.paramId || param.paramCode || idx"
@@ -178,42 +178,85 @@
               </el-form-item>
             </div>
           </div>
-          <div class="form-section material-usage-section">
-            <div class="section-title">物料使用(选填)</div>
-            <div v-for="(mu, idx) in materialUsages" :key="idx" class="material-usage-row">
-              <el-form-item label="物料" class="mu-field-material">
-                <el-select
-                  v-model="mu.materialId"
-                  filterable
-                  placeholder="选择物料"
-                  style="width: 100%"
-                  @change="(v: string) => onMaterialSelect(mu, v)"
-                >
-                  <el-option
-                    v-for="opt in materialOptions"
-                    :key="opt.id"
-                    :label="opt.label"
-                    :value="opt.id"
-                  />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="使用数量" class="mu-field-qty">
-                <el-input-number
-                  v-model="mu.qty"
-                  :min="0.0001"
-                  :precision="4"
-                  :step="1"
-                  style="width: 100%"
-                  placeholder="数量"
-                />
-              </el-form-item>
-              <el-button class="btn-ghost mu-remove-btn" @click="materialUsages.splice(idx, 1)">
-                <el-icon><Delete /></el-icon>
+          <div v-if="resultTaskType !== 'dsfjc'" class="form-section material-usage-section">
+            <div class="section-title material-usage-head">
+              <div class="section-title-left"><h3>物料使用(选填)</h3></div>
+              <el-button class="btn-main" type="primary" size="small" @click="addMaterialRow">
+                <el-icon><Plus /></el-icon>添加物料
               </el-button>
             </div>
-            <el-button class="btn-ghost" size="small" @click="materialUsages.push({ materialId: '', materialName: '', qty: undefined })">
-              <el-icon><Plus /></el-icon>添加物料
-            </el-button>
+            <el-table
+              v-if="materialUsages.length"
+              :data="materialUsages"
+              :row-key="(row) => row._rowKey"
+              stripe
+              class="list-table inner-table material-usage-table"
+              max-height="280"
+            >
+              <el-table-column
+                type="index"
+                label="序号"
+                width="56"
+                align="center"
+                header-align="center"
+                :index="(index) => index + 1"
+              />
+              <el-table-column label="物料名称" min-width="160" align="center" header-align="center">
+                <template #default="{ row, $index }">
+                  <el-select
+                    v-model="row.materialId"
+                    filterable
+                    placeholder="搜索并选择物料"
+                    class="inner-cell-input"
+                    @change="(v) => onMaterialSelect($index, v)"
+                  >
+                    <el-option
+                      v-for="opt in materialOptions"
+                      :key="opt.id"
+                      :label="opt.label"
+                      :value="opt.id"
+                      :disabled="isMaterialOptionDisabled(opt.id, $index)"
+                    />
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="规格型号" min-width="120" align="center" header-align="center">
+                <template #default="{ row }">{{ row.specModel || '—' }}</template>
+              </el-table-column>
+              <el-table-column label="用量" min-width="120" align="center" header-align="center">
+                <template #default="{ row }">
+                  <el-input-number
+                    v-model="row.qty"
+                    :min="0.0001"
+                    :precision="4"
+                    :step="1"
+                    controls-position="right"
+                    placeholder="用量"
+                    class="inner-cell-input"
+                  />
+                </template>
+              </el-table-column>
+              <el-table-column label="单位" width="72" align="center" header-align="center">
+                <template #default="{ row }">{{ row.unit || '—' }}</template>
+              </el-table-column>
+              <el-table-column label="备注" min-width="140" align="center" header-align="center">
+                <template #default="{ row }">
+                  <el-input v-model="row.remark" placeholder="选填" clearable class="inner-cell-input" />
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="96" align="center" header-align="center" fixed="right">
+                <template #default="{ $index }">
+                  <el-button link type="danger" size="small" @click="removeMaterialRow($index)">
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-empty
+              v-else
+              description="暂无物料记录,可点击上方按钮添加"
+              :image-size="56"
+            />
           </div>
           <div class="form-section result-conclusion-section">
             <div class="section-title">检测结论</div>
@@ -267,6 +310,49 @@
           </div>
         </template>
         <template #append>
+          <div
+            v-if="viewDetail?.taskType !== 'dsfjc'"
+            class="view-section qc-task-view-material-usages"
+          >
+            <h3>物料使用</h3>
+            <el-table
+              v-if="viewMaterialUsages.length"
+              :data="viewMaterialUsages"
+              stripe
+              class="list-table inner-table qc-task-view-material-table"
+            >
+              <el-table-column
+                type="index"
+                label="序号"
+                width="56"
+                align="center"
+                header-align="center"
+                :index="(index) => index + 1"
+              />
+              <el-table-column
+                prop="materialName"
+                label="物料名称"
+                min-width="160"
+                align="center"
+                header-align="center"
+              />
+              <el-table-column label="规格型号" min-width="120" align="center" header-align="center">
+                <template #default="{ row }">{{ row.specModel || '—' }}</template>
+              </el-table-column>
+              <el-table-column label="用量" min-width="100" align="center" header-align="center">
+                <template #default="{ row }">
+                  {{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
+                </template>
+              </el-table-column>
+              <el-table-column label="单位" width="72" align="center" header-align="center">
+                <template #default="{ row }">{{ row.unit || '—' }}</template>
+              </el-table-column>
+              <el-table-column label="备注" min-width="140" align="center" header-align="center">
+                <template #default="{ row }">{{ row.remark || '—' }}</template>
+              </el-table-column>
+            </el-table>
+            <p v-else class="view-material-empty">暂无物料使用记录</p>
+          </div>
           <div
             v-if="viewDetail?.attachmentFiles?.length"
             class="view-section qc-task-view-attachments"
@@ -287,7 +373,7 @@
 <script setup lang="ts">
   import { ref, reactive, computed, onMounted } from 'vue'
   import { ElMessage } from 'element-plus'
-  import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen } from '@element-plus/icons-vue'
+  import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen, Plus } from '@element-plus/icons-vue'
   import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
   import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
@@ -303,7 +389,13 @@
     type QcTask,
     type QcTaskDetail
   } from '@/api/qc-task'
-  import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
+  import {
+    fetchQcTaskResults,
+    fetchQcTaskMaterialUsages,
+    fetchCompleteTaskWithResult,
+    type QcTaskResultItem,
+    type QcTaskMaterialUsageItem
+  } from '@/api/qc-task-result'
   import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
   import {
     taskTypeLabel,
@@ -333,8 +425,28 @@
   const resultRemark = ref('')
   const resultConclusion = ref('')
   const resultAttachments = ref<FinanceAttachmentItem[]>([])
+  type MaterialUsageRow = {
+    _rowKey: string
+    materialId: string
+    materialName: string
+    specModel: string
+    unit: string
+    qty: number | undefined
+    remark: string
+  }
+
+  let materialUsageRowSeq = 0
+  function nextMaterialRowKey() {
+    materialUsageRowSeq += 1
+    return `mu_${Date.now()}_${materialUsageRowSeq}`
+  }
+
   const materialOptions = ref<QcMaterialOption[]>([])
-  const materialUsages = ref<Array<{ materialId: string; materialName: string; qty: number | undefined }>>([])
+  const materialUsages = ref<MaterialUsageRow[]>([])
+
+  const resultDialogWidth = computed(() =>
+    resultTaskType.value === 'sysz' ? '970px' : '720px'
+  )
 
   function canFillResult(row: QcTask) {
     if (row.taskType === 'sysz') {
@@ -358,13 +470,111 @@
     resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
   )
 
-  function onMaterialSelect(mu: { materialId: string; materialName: string; qty: number | undefined }, id: string) {
+  function clearMaterialRow(row: MaterialUsageRow) {
+    row.materialId = ''
+    row.materialName = ''
+    row.specModel = ''
+    row.unit = ''
+  }
+
+  function isMaterialOptionDisabled(materialId: string, currentIndex: number) {
+    if (!materialId) return false
+    return materialUsages.value.some((r, i) => i !== currentIndex && r.materialId === materialId)
+  }
+
+  function isDuplicateMaterial(materialId: string, excludeIndex: number) {
+    if (!materialId) return false
+    return materialUsages.value.some((r, i) => i !== excludeIndex && r.materialId === materialId)
+  }
+
+  function parseMaterialQty(qty: number | string | undefined): number | null {
+    if (qty === undefined || qty === null || qty === '') return null
+    const n = typeof qty === 'number' ? qty : Number(String(qty).trim())
+    if (!Number.isFinite(n) || n <= 0) return null
+    return n
+  }
+
+  function onMaterialSelect(index: number, id: string) {
+    const row = materialUsages.value[index]
+    if (!row) return
+    if (!id) {
+      if (parseMaterialQty(row.qty) == null) {
+        removeMaterialRow(index)
+      } else {
+        clearMaterialRow(row)
+      }
+      return
+    }
+    if (isDuplicateMaterial(id, index)) {
+      ElMessage.warning('该物料已在列表中,请勿重复添加')
+      clearMaterialRow(row)
+      return
+    }
     const opt = materialOptions.value.find((o) => o.id === id)
-    mu.materialName = opt ? opt.materialName : ''
+    row.materialId = id
+    row.materialName = opt?.materialName || ''
+    row.specModel = opt?.specModel || ''
+    row.unit = opt?.unit || ''
+  }
+
+  function addMaterialRow() {
+    materialUsages.value.push({
+      _rowKey: nextMaterialRowKey(),
+      materialId: '',
+      materialName: '',
+      specModel: '',
+      unit: '',
+      qty: undefined,
+      remark: ''
+    })
+  }
+
+  function removeMaterialRow(index: number) {
+    materialUsages.value = materialUsages.value.filter((_, i) => i !== index)
+  }
+
+  /** 合并同物料多条变动记录(历史重复扣减时),并生成稳定行 key */
+  function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): MaterialUsageRow[] {
+    const merged = new Map<string, MaterialUsageRow>()
+    for (const u of items || []) {
+      const mid = (u.materialId || '').trim()
+      if (!mid) continue
+      const qtyRaw = Math.abs(Number(u.changeQty) || 0)
+      const prev = merged.get(mid)
+      if (prev) {
+        prev.qty = (Number(prev.qty) || 0) + qtyRaw
+      } else {
+        merged.set(mid, {
+          _rowKey: nextMaterialRowKey(),
+          materialId: mid,
+          materialName: u.materialName || '',
+          specModel: '',
+          unit: '',
+          qty: qtyRaw > 0 ? qtyRaw : undefined,
+          remark: u.remark || ''
+        })
+      }
+    }
+    return Array.from(merged.values())
+  }
+
+  function enrichMaterialUsageRows(
+    rows: MaterialUsageRow[],
+    options: QcMaterialOption[] = materialOptions.value
+  ) {
+    for (const row of rows) {
+      const opt = options.find((o) => o.id === row.materialId)
+      if (opt) {
+        row.materialName = opt.materialName || row.materialName
+        row.specModel = opt.specModel || ''
+        row.unit = opt.unit || ''
+      }
+    }
   }
 
   const viewVisible = ref(false)
   const viewDetail = ref<QcTaskDetail | null>(null)
+  const viewMaterialUsages = ref<MaterialUsageRow[]>([])
   const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
 
   function apiFilesToFinanceItems(
@@ -603,6 +813,20 @@
     } catch {
       ;(detail as any).results = []
     }
+    viewMaterialUsages.value = []
+    if (row.taskType !== 'dsfjc') {
+      try {
+        const [savedUsages, opts] = await Promise.all([
+          fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[]),
+          fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
+        ])
+        const rows = materialRowsFromApi(savedUsages || [])
+        enrichMaterialUsageRows(rows, opts || [])
+        viewMaterialUsages.value = rows
+      } catch {
+        viewMaterialUsages.value = []
+      }
+    }
     viewDetail.value = detail
     viewVisible.value = true
   }
@@ -623,6 +847,7 @@
     resultConclusion.value = ''
     resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
     materialUsages.value = []
+    materialOptions.value = []
     resultLoading.value = true
     try {
       const detail = await fetchQcTaskDetail(row.id)
@@ -632,16 +857,21 @@
       resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
       const results = await fetchQcTaskResults(row.id)
       resultParams.value = normalizeResultParams(results || [])
+      if (row.taskType !== 'dsfjc') {
+        const [opts, savedUsages] = await Promise.all([
+          fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[]),
+          fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[])
+        ])
+        materialOptions.value = opts || []
+        materialUsages.value = materialRowsFromApi(savedUsages || [])
+        enrichMaterialUsageRows(materialUsages.value)
+      }
     } catch {
       resultParams.value = []
+      materialUsages.value = []
     } finally {
       resultLoading.value = false
     }
-    try {
-      materialOptions.value = await fetchQcMaterialOptions()
-    } catch {
-      materialOptions.value = []
-    }
     resultVisible.value = true
   }
 
@@ -658,17 +888,33 @@
       ElMessage.warning('请上传第三方检测材料')
       return
     }
+    if (resultTaskType.value !== 'dsfjc') {
+      materialUsages.value = materialUsages.value.filter((mu) => !!mu.materialId)
+      const seenMaterialIds = new Set<string>()
+      for (const mu of materialUsages.value) {
+        if (!mu.materialId) continue
+        if (parseMaterialQty(mu.qty) == null) {
+          ElMessage.warning('请为已选物料填写有效用量')
+          return
+        }
+        if (seenMaterialIds.has(mu.materialId)) {
+          ElMessage.warning('物料列表中存在重复物料,请检查')
+          return
+        }
+        seenMaterialIds.add(mu.materialId)
+      }
+    }
     resultLoading.value = true
     try {
-      const validMaterialUsages = materialUsages.value.filter(
-        (mu) => mu.materialId && mu.qty && mu.qty > 0
-      )
+      const validMaterialUsages = materialUsages.value
+        .map((mu) => ({ ...mu, qtyNum: parseMaterialQty(mu.qty) }))
+        .filter((mu) => mu.materialId && mu.qtyNum != null)
       const data: {
         conclusion: string
         results: QcTaskResultItem[]
         remark?: string
         thirdPartyAttachments?: string
-        materialUsages?: Array<{ materialId: string; qty: number }>
+        materialUsages?: Array<{ materialId: string; qty: number; remark?: string }>
       } = {
         conclusion: resultConclusion.value,
         remark: resultRemark.value,
@@ -680,10 +926,11 @@
           }))
       }
       data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
-      if (validMaterialUsages.length > 0) {
+      if (resultTaskType.value !== 'dsfjc') {
         data.materialUsages = validMaterialUsages.map((mu) => ({
           materialId: mu.materialId,
-          qty: mu.qty!,
+          qty: mu.qtyNum!,
+          remark: mu.remark?.trim() || undefined
         }))
       }
       await fetchCompleteTaskWithResult(resultTaskId.value, data)
@@ -719,25 +966,51 @@
   .material-usage-section {
     margin-top: 12px;
 
-    .material-usage-row {
+    .material-usage-head {
       display: flex;
-      align-items: flex-start;
-      gap: 12px;
-      margin-bottom: 12px;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 14px;
+      padding-bottom: 10px;
+      border-bottom: 1px solid #ebeef5;
+      border-left: none;
+      padding-left: 0;
+    }
 
-      .mu-field-material {
-        flex: 3;
-      }
+    .section-title-left {
+      display: flex;
+      align-items: center;
+      min-width: 0;
 
-      .mu-field-qty {
-        flex: 1.2;
+      &::before {
+        content: '';
+        width: 3px;
+        height: 18px;
+        background: #10b981;
+        border-radius: 2px;
+        margin-right: 10px;
+        flex-shrink: 0;
       }
 
-      .mu-remove-btn {
-        margin-top: 28px;
-        padding: 8px;
+      h3 {
+        margin: 0;
+        font-size: 15px;
+        font-weight: 600;
+        color: #1e293b;
       }
     }
+
+    .material-usage-table {
+      width: 100%;
+    }
+
+    .inner-cell-input {
+      width: 100%;
+    }
+
+    :deep(.inner-cell-input.el-input-number) {
+      width: 100%;
+    }
   }
 
   .result-conclusion-section {
@@ -779,6 +1052,7 @@
     }
   }
 
+  .qc-task-view-material-usages,
   .qc-task-view-attachments {
     padding: 18px 22px;
     border-bottom: none;
@@ -794,6 +1068,16 @@
     }
   }
 
+  .qc-task-view-material-table {
+    width: 100%;
+  }
+
+  .view-material-empty {
+    margin: 0;
+    font-size: 13px;
+    color: #64748b;
+  }
+
   .view-reception-attachments {
     margin-top: 4px;
 

+ 39 - 0
web-admin/src/views/quality-inspection/sample-reception/index.vue

@@ -89,6 +89,7 @@
           v-loading="loading"
           stripe
           row-key="protocolSampleId"
+          :row-class-name="sampleReceptionRowClassName"
           @row-dblclick="handleRowDblClick"
           @selection-change="handlePendingSelectionChange"
         >
@@ -679,6 +680,35 @@
     return Number.isFinite(n) ? String(n) : '-'
   }
 
+  function parseEtaLocalDate(eta: unknown): Date | null {
+    const s = String(eta || '').trim()
+    if (!s) return null
+    const normalized = s.replace('T', ' ').slice(0, 19)
+    const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})/)
+    if (!m) return null
+    const y = Number(m[1])
+    const mo = Number(m[2]) - 1
+    const d = Number(m[3])
+    const dt = new Date(y, mo, d)
+    if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null
+    return dt
+  }
+
+  function startOfLocalDay(d: Date) {
+    return new Date(d.getFullYear(), d.getMonth(), d.getDate())
+  }
+
+  function isEtaTodayOrEarlier(etaTime: unknown) {
+    const etaDay = parseEtaLocalDate(etaTime)
+    if (!etaDay) return false
+    return etaDay.getTime() <= startOfLocalDay(new Date()).getTime()
+  }
+
+  function sampleReceptionRowClassName({ row }: { row: Record<string, unknown> }) {
+    if (activeTab.value !== 'pending') return ''
+    return isEtaTodayOrEarlier(row.etaTime) ? 'row-eta-overdue' : ''
+  }
+
   function apiFilesToFinanceItems(
     files?: Array<{ id?: string; fileUrl?: string; fileName?: string; fileType?: string }>
   ): FinanceAttachmentItem[] {
@@ -1236,6 +1266,15 @@
     }
   }
 
+  .list-table :deep(tr.row-eta-overdue > td.el-table__cell) {
+    background-color: #fee2e2 !important;
+    color: #991b1b;
+  }
+
+  .list-table :deep(tr.row-eta-overdue:hover > td.el-table__cell) {
+    background-color: #fecaca !important;
+  }
+
   .sample-reception-dialog .check-list-view,
   .batch-receive-dialog .check-list-view {
     pointer-events: none;

+ 189 - 85
web-admin/src/views/quality-inspection/third-party-company/index.vue

@@ -126,7 +126,9 @@
               </el-form-item>
             </div>
             <div class="form-section">
-              <div class="section-title">开票信息</div>
+              <div class="section-title section-title--static">
+                <div class="section-title-left"><h3>开票信息</h3></div>
+              </div>
               <div class="form-grid-2">
                 <el-form-item label="发票抬头">
                   <el-input v-model="companyForm.invoiceTitle" placeholder="与营业执照一致" />
@@ -167,8 +169,8 @@
       <el-dialog
         v-model="itemDialogVisible"
         :title="itemDialogTitle"
-        width="880px"
-        class="dialog-form-layout"
+        width="980px"
+        class="dialog-form-layout third-party-item-config-dialog"
         align-center
         destroy-on-close
         :close-on-click-modal="false"
@@ -194,7 +196,7 @@
             v-loading="itemLoading"
             stripe
             row-key="id"
-            max-height="360"
+            max-height="460"
             @selection-change="onItemSelectionChange"
           >
             <el-table-column type="selection" width="55" align="center" />
@@ -233,77 +235,130 @@
         <el-dialog
           v-model="itemFormVisible"
           :title="itemFormTitle"
-          width="640px"
+          width="840px"
           class="dialog-form-layout"
           align-center
           append-to-body
           destroy-on-close
           :close-on-click-modal="false"
         >
-          <el-form ref="itemFormRef" :model="itemForm" :rules="itemFormRules" label-position="top" class="biz-dialog-form">
+          <el-form
+            ref="itemFormRef"
+            :model="itemForm"
+            :rules="itemFormRules"
+            label-position="top"
+            class="biz-dialog-form item-dialog-form"
+            scroll-to-error
+            :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
+          >
+            <div class="form-section">
+              <div class="section-title">
+                <div class="section-title-left"><h3>基本信息</h3></div>
+              </div>
+              <div class="form-grid-2">
+                <el-form-item label="项目编码" prop="itemCode">
+                  <el-input v-model="itemForm.itemCode" :disabled="itemIsEdit" />
+                </el-form-item>
+                <el-form-item label="项目名称" prop="itemName">
+                  <el-input v-model="itemForm.itemName" />
+                </el-form-item>
+                <el-form-item label="费用(元)" prop="feeAmount">
+                  <el-input-number v-model="itemForm.feeAmount" :min="0" :precision="2" style="width: 100%" />
+                </el-form-item>
+                <el-form-item label="状态" prop="status">
+                  <el-radio-group v-model="itemForm.status">
+                    <el-radio value="1">启用</el-radio>
+                    <el-radio value="0">停用</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+              </div>
+            </div>
+
+            <div class="form-section param-config-section">
+              <div class="section-title">
+                <div class="section-title-left"><h3>参数配置</h3></div>
+                <el-button class="btn-main" type="primary" size="small" @click="addItemParam">
+                  <el-icon><Plus /></el-icon>添加参数
+                </el-button>
+              </div>
+              <el-table
+                v-if="itemParams.length > 0"
+                :data="itemParams"
+                stripe
+                class="list-table inner-table"
+                max-height="280"
+              >
+                <el-table-column
+                  type="index"
+                  label="序号"
+                  width="56"
+                  align="center"
+                  header-align="center"
+                  :index="(i) => i + 1"
+                />
+                <el-table-column label="参数名称" min-width="140" align="center" header-align="center">
+                  <template #default="{ row }">
+                    <el-input v-model="row.paramName" placeholder="参数名称" class="inner-cell-input" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="参数编码" min-width="120" align="center" header-align="center">
+                  <template #default="{ row }">
+                    <el-input v-model="row.paramCode" placeholder="参数编码" class="inner-cell-input" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="单位" min-width="120" align="center" header-align="center">
+                  <template #default="{ row }">
+                    <el-select
+                      v-model="row.paramUnit"
+                      placeholder="单位"
+                      clearable
+                      filterable
+                      remote
+                      reserve-keyword
+                      class="inner-cell-select"
+                      :loading="unitOptionsLoading"
+                      :remote-method="searchUnitOptions"
+                    >
+                      <el-option
+                        v-for="item in unitOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                      />
+                    </el-select>
+                  </template>
+                </el-table-column>
+                <el-table-column label="类型" width="110" align="center" header-align="center">
+                  <template #default="{ row }">
+                    <el-select v-model="row.paramType" class="inner-cell-select">
+                      <el-option label="文本" value="text" />
+                      <el-option label="数字" value="number" />
+                    </el-select>
+                  </template>
+                </el-table-column>
+                <el-table-column label="必填" width="72" align="center" header-align="center">
+                  <template #default="{ row }">
+                    <el-switch v-model="row.requiredFlag" active-value="1" inactive-value="0" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="操作" width="80" align="center" header-align="center">
+                  <template #default="{ $index }">
+                    <el-button link type="danger" size="small" @click="removeItemParam($index)">删除</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+              <el-empty
+                v-else
+                description="暂无参数,点击上方按钮添加"
+                :image-size="48"
+              />
+            </div>
+
             <div class="form-grid-2">
-              <el-form-item label="项目编码" prop="itemCode">
-                <el-input v-model="itemForm.itemCode" :disabled="itemIsEdit" />
-              </el-form-item>
-              <el-form-item label="项目名称" prop="itemName">
-                <el-input v-model="itemForm.itemName" />
-              </el-form-item>
-              <el-form-item label="费用(元)" prop="feeAmount">
-                <el-input-number v-model="itemForm.feeAmount" :min="0" :precision="2" style="width: 100%" />
-              </el-form-item>
-              <el-form-item label="状态" prop="status">
-                <el-radio-group v-model="itemForm.status">
-                  <el-radio value="1">启用</el-radio>
-                  <el-radio value="0">停用</el-radio>
-                </el-radio-group>
-              </el-form-item>
               <el-form-item label="备注" class="form-col-span-2">
                 <el-input v-model="itemForm.remark" type="textarea" :rows="2" />
               </el-form-item>
             </div>
-            <div class="form-section" style="margin-top: 8px">
-              <div class="section-title">参数配置</div>
-              <div class="param-row param-row-header">
-                <span style="flex: 2">参数名称</span>
-                <span style="flex: 1.5">参数编码</span>
-                <span style="flex: 1">单位</span>
-                <span style="flex: 1">类型</span>
-                <span style="width: 40px; text-align: center">必填</span>
-                <span style="width: 32px"></span>
-              </div>
-              <div v-if="itemParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
-                暂无参数,点击下方按钮添加
-              </div>
-              <div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
-                <el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
-                <el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
-                <el-select
-                  v-model="param.paramUnit"
-                  placeholder="单位"
-                  clearable
-                  filterable
-                  remote
-                  reserve-keyword
-                  :loading="unitOptionsLoading"
-                  :remote-method="searchUnitOptions"
-                  style="flex: 1"
-                >
-                  <el-option
-                    v-for="item in unitOptions"
-                    :key="item.value"
-                    :label="item.label"
-                    :value="item.value"
-                  />
-                </el-select>
-                <el-select v-model="param.paramType" style="flex: 1">
-                  <el-option label="文本" value="text" />
-                  <el-option label="数字" value="number" />
-                </el-select>
-                <el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
-                <el-button class="btn-op-item" @click="removeItemParam(idx)"><el-icon><Delete /></el-icon></el-button>
-              </div>
-              <el-button type="primary" plain style="margin-top: 8px" @click="addItemParam"><el-icon><Plus /></el-icon>添加参数</el-button>
-            </div>
           </el-form>
           <template #footer>
             <el-button class="btn-ghost" @click="itemFormVisible = false">
@@ -747,37 +802,86 @@
     padding-top: 0;
   }
 
-  .form-section {
+  .biz-dialog-form .form-section {
     background: #fafafa;
     border-radius: 8px;
-    padding: 20px;
-    margin: 4px 0;
+    padding: 16px 20px;
+    margin: 4px 0 16px;
   }
 
-  .form-section .section-title {
-    font-size: 15px;
-    font-weight: 500;
-    color: #303133;
-    margin-bottom: 16px;
-    padding-bottom: 12px;
-    padding-left: 10px;
+  .biz-dialog-form .section-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 14px;
+    padding-bottom: 10px;
     border-bottom: 1px solid #ebeef5;
-    border-left: 3px solid #10b981;
   }
 
-  .param-row {
+  .biz-dialog-form .section-title--static {
+    justify-content: flex-start;
+  }
+
+  .biz-dialog-form .section-title-left {
     display: flex;
     align-items: center;
-    gap: 8px;
-    margin-bottom: 8px;
+    min-width: 0;
+
+    &::before {
+      content: '';
+      width: 3px;
+      height: 18px;
+      background: #10b981;
+      border-radius: 2px;
+      margin-right: 10px;
+      flex-shrink: 0;
+    }
+
+    h3 {
+      margin: 0;
+      font-size: 15px;
+      font-weight: 600;
+      color: #1e293b;
+    }
   }
 
-  .param-row-header {
-    font-size: 12px;
-    color: #64748b;
-    font-weight: 500;
-    padding-bottom: 4px;
-    border-bottom: 1px solid #e2e8f0;
-    margin-bottom: 10px;
+  .item-dialog-form {
+    .form-section:last-of-type {
+      margin-bottom: 0;
+    }
+
+    .inner-table {
+      width: 100%;
+
+      :deep(td.el-table__cell) {
+        padding-top: 8px;
+        padding-bottom: 8px;
+      }
+
+      :deep(.inner-cell-input) {
+        .el-input__wrapper {
+          min-height: 36px;
+          padding-top: 2px;
+          padding-bottom: 2px;
+        }
+
+        .el-input__inner {
+          height: 32px;
+          line-height: 32px;
+        }
+      }
+    }
+
+    .inner-cell-select {
+      width: 100%;
+    }
+  }
+</style>
+
+<!-- 检测项目配置弹窗 teleport 到 body,尺寸类名需非 scoped -->
+<style lang="scss">
+  .el-dialog.third-party-item-config-dialog.dialog-form-layout {
+    max-height: calc(100vh);
+    min-height: min(740px, calc(100vh - 80px));
   }
 </style>

+ 1 - 1
web-admin/src/views/research-donation/list/DonationFormDialog.vue

@@ -3,7 +3,7 @@
   <el-dialog
     :model-value="modelValue"
     :title="dialogTitle"
-    width="1200px"
+    width="1380px"
     class="dialog-form-layout"
     align-center
     destroy-on-close

Некоторые файлы не были показаны из-за большого количества измененных файлов