zlwu 3 月之前
父節點
當前提交
b8b0446542
共有 22 個文件被更改,包括 1177 次插入 和 1013 次删除
  1. 1 0
      backend/app.pid
  2. 2 5
      backend/app/api/qc.py
  3. 19 5
      backend/app/crud/qc_task.py
  4. 0 0
      backend/app/graphify-out/cache/ast/1272db5a3035c7391561614af9e1e8cea81a7625418d45e2126c6e2ec801c6b7.json
  5. 0 0
      backend/app/graphify-out/cache/ast/d232ec13eef069043d123f6c5d77b92c6a74f08f55d289c37cc70b29f1ad6e60.json
  6. 140 608
      backend/graphify-out/.graphify_analysis.json
  7. 166 125
      backend/graphify-out/.graphify_extract.json
  8. 63 70
      backend/graphify-out/GRAPH_REPORT.md
  9. 1 1
      backend/graphify-out/graph.html
  10. 112 112
      backend/graphify-out/graph.json
  11. 2 2
      backend/graphify-out/manifest.json
  12. 22 0
      backend/tests/test_qc_task_result_conclusion.py
  13. 315 0
      docs/superpowers/plans/2026-07-22-qc-result-optional-conclusion.md
  14. 105 0
      docs/superpowers/specs/2026-07-22-qc-result-optional-conclusion-design.md
  15. 21 12
      web-admin/graphify-out/.graphify_extract.json
  16. 2 2
      web-admin/graphify-out/GRAPH_REPORT.md
  17. 4 4
      web-admin/graphify-out/manifest.json
  18. 0 0
      web-admin/src/graphify-out/cache/ast/08d8296d3725d482d78d74761729934f93dcdb3708456f28a15492b0545cc1a4.json
  19. 0 0
      web-admin/src/graphify-out/cache/ast/63698e9d7eb4da1a63fcf6315366bde4c8fe6d54010932dfb7a7218e6595c825.json
  20. 0 0
      web-admin/src/graphify-out/cache/ast/8f292717400fa58f17c328444acab4b8ac3e1d80fd5fe4ad324564d9a27d62ce.json
  21. 192 63
      web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue
  22. 10 4
      web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue

+ 1 - 0
backend/app.pid

@@ -0,0 +1 @@
+31511

+ 2 - 5
backend/app/api/qc.py

@@ -413,7 +413,7 @@ class TaskResultBody(BaseModel):
 
 class TaskCompleteWithResultBody(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
-    conclusion: str = Field(..., alias="conclusion")
+    conclusion: Optional[str] = Field(None, alias="conclusion")
     results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
     remark: Optional[str] = Field(None, alias="remark")
     third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
@@ -1190,9 +1190,6 @@ async def complete_task_with_result(
             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(400, "请选择检测结论(通过/不通过)")
         if body.third_party_attachments is not None:
             task_svc.update_task_attachments(
                 db, task_id, attachments=body.third_party_attachments,
@@ -1203,7 +1200,7 @@ async def complete_task_with_result(
             task_id,
             tenant_no=tenant_no,
             user_id=user_id,
-            conclusion=concl,
+            conclusion=body.conclusion,
             remark=body.remark,
             material_usages=body.material_usages,
             task_status=body.task_status,

+ 19 - 5
backend/app/crud/qc_task.py

@@ -760,13 +760,29 @@ def complete_task(
 _QC_TASK_RESULT_STATUSES = frozenset({"dzx", "jxz", "ywc"})
 
 
+def resolve_task_result_conclusion(
+    conclusion: Optional[str],
+    target_status: str,
+) -> str:
+    """Normalize conclusion for apply_task_result.
+
+    Returns '' | 'tg' | 'btg'. Empty allowed only when target is dzx/jxz.
+    """
+    concl = (conclusion or "").strip()
+    if concl and concl not in ("tg", "btg"):
+        raise ValueError("检测结论无效")
+    if target_status == "ywc" and concl not in ("tg", "btg"):
+        raise ValueError("请选择检测结论(通过/不通过)")
+    return concl
+
+
 def apply_task_result(
     db: Session,
     row_id: str,
     *,
     tenant_no: str,
     user_id: Optional[str],
-    conclusion: str,
+    conclusion: Optional[str] = None,
     remark: Optional[str] = None,
     material_usages: Optional[List[Dict[str, Any]]] = None,
     task_status: Optional[str] = None,
@@ -779,12 +795,10 @@ def apply_task_result(
     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("请选择检测结论(通过/不通过)")
     target = (task_status or "").strip() or "ywc"
     if target not in _QC_TASK_RESULT_STATUSES:
         raise ValueError("任务状态无效")
+    concl = resolve_task_result_conclusion(conclusion, target)
     _require_dsfjc_attachments(t, for_status=target)
     if material_usages is not None:
         mu_svc.deduct_for_task(
@@ -796,7 +810,7 @@ def apply_task_result(
             user_id=user_id,
         )
     was_done = (t.task_status or "").strip() == "ywc"
-    t.conclusion = concl
+    t.conclusion = concl or None
     _set_task_remark(t, remark if remark is not None else "")
     t.task_status = target
     t.update_by = user_id

File diff suppressed because it is too large
+ 0 - 0
backend/app/graphify-out/cache/ast/1272db5a3035c7391561614af9e1e8cea81a7625418d45e2126c6e2ec801c6b7.json


File diff suppressed because it is too large
+ 0 - 0
backend/app/graphify-out/cache/ast/d232ec13eef069043d123f6c5d77b92c6a74f08f55d289c37cc70b29f1ad6e60.json


File diff suppressed because it is too large
+ 140 - 608
backend/graphify-out/.graphify_analysis.json


File diff suppressed because it is too large
+ 166 - 125
backend/graphify-out/.graphify_extract.json


+ 63 - 70
backend/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
-# Graph Report - app  (2026-07-21)
+# Graph Report - app  (2026-07-22)
 
 ## Corpus Check
-- Large corpus: 225 files · ~280,117 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 225 files · ~280,154 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 4764 nodes · 26310 edges · 39 communities detected
-- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 18051 edges (avg confidence: 0.6)
+- 4766 nodes · 26325 edges · 38 communities detected
+- Extraction: 31% EXTRACTED · 69% INFERRED · 0% AMBIGUOUS · INFERRED: 18063 edges (avg confidence: 0.6)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -41,19 +41,18 @@
 - [[_COMMUNITY_Community 29|Community 29]]
 - [[_COMMUNITY_Community 30|Community 30]]
 - [[_COMMUNITY_Community 31|Community 31]]
-- [[_COMMUNITY_Community 32|Community 32]]
+- [[_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]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `success_response()` - 708 edges
 2. `error_response()` - 587 edges
 3. `SysUser` - 551 edges
-4. `BizQcTaskResult` - 333 edges
+4. `BizQcTaskResult` - 334 edges
 5. `BizProductionMaterial` - 324 edges
 6. `BizProductionMaterialUsage` - 318 edges
 7. `BizQcMaterialUsage` - 316 edges
@@ -77,23 +76,23 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (926): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+918 more)
+Nodes (911): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+903 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.01
-Nodes (746): generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。     须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_cell_sample_type(), get_cell_sample_type() (+738 more)
+Nodes (698): generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。     须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_cell_sample_type(), get_cell_sample_type() (+690 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.01
-Nodes (601): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+593 more)
+Nodes (613): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+605 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.01
-Nodes (339): _normalize_date_bound(), _persist_protocol_sub_resources(), list_ethnics_for_select(), _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no() (+331 more)
+Cohesion: 0.08
+Nodes (373): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Base (+365 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.08
-Nodes (373): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+365 more)
+Cohesion: 0.01
+Nodes (289): _normalize_date_bound(), batch_delete_research_donations(), change_donation_status(), convert_from_storage(), convert_to_storage(), create_research_donation(), delete_research_donation(), _get_current_user_id() (+281 more)
 
 ### Community 5 - "Community 5"
 Cohesion: 0.08
@@ -101,47 +100,47 @@ Nodes (259): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody,
 
 ### Community 6 - "Community 6"
 Cohesion: 0.02
-Nodes (198): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), batch_review(), _dec() (+190 more)
+Nodes (211): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map(), format_protocol_status_label() (+203 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.02
-Nodes (112): get_designer_data(), _error_json(), 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware, ApiCryptoError (+104 more)
+Nodes (182): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), create_cell_product() (+174 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.04
-Nodes (141): 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), create_app(), create_group(), create_role(), _create_version(), delete_app() (+133 more)
+Cohesion: 0.02
+Nodes (165): list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), ApiRequestLogCRUD, delete(), get_multi_with_filters() (+157 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.03
-Nodes (116): auto_create_payment(), get_protocol_nos_used_in_protocols(), _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict(), _handover_view_dict() (+108 more)
+Cohesion: 0.02
+Nodes (112): get_designer_data(), _error_json(), lifespan(), 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+104 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.04
-Nodes (116): build_void_type_label_map(), format_protocol_status_label(), resolve_void_type_label(), join_equipment_ids(), parse_equipment_ids(), 工艺/步骤设备多选:逗号分隔 ID 与名称快照解析, 校验设备列表,返回 (equipment_ids 逗号串, equipment_name 逗号串快照)。, resolve_equipment_list_snapshot() (+108 more)
+Nodes (104): 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), create_app(), _create_version(), delete_app(), delete_group(), _dumps() (+96 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.05
-Nodes (104): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+96 more)
+Cohesion: 0.06
+Nodes (101): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+93 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.45
-Nodes (69): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+61 more)
+Cohesion: 0.05
+Nodes (72): complete_application(), create_basic_experiment_sale_schedules(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd(), _generate_application_no(), get_application() (+64 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.06
-Nodes (66): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes() (+58 more)
+Cohesion: 0.45
+Nodes (69): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+61 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.06
-Nodes (49): _jwt_user_payload(), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, refresh_token(), web_login(), web_login_rsa_public_key(), lifespan(), create_access_token() (+41 more)
+Cohesion: 0.07
+Nodes (47): _jwt_user_payload(), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, refresh_token(), web_login(), web_login_rsa_public_key(), create_access_token(), create_refresh_token() (+39 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.12
-Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
+Cohesion: 0.11
+Nodes (35): batch_create_sterilize_logs(), batch_create_wash_logs(), batch_discard_garments(), batch_soft_delete_garments(), _count_map(), create_garment(), create_sterilize_log(), create_wash_log() (+27 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.19
-Nodes (27): _amount_for_row(), _batch_map(), deduct_for_task(), _deduct_task_usage_legacy_batch(), _display_remark(), _fmt_date_val(), _linked_qc_task_no_map(), list_linked_task_usages() (+19 more)
+Cohesion: 0.12
+Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
 
 ### Community 17 - "Community 17"
 Cohesion: 0.21
@@ -168,17 +167,17 @@ Cohesion: 0.24
 Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
 
 ### Community 23 - "Community 23"
+Cohesion: 0.31
+Nodes (10): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。, _row_to_dict() (+2 more)
+
+### Community 24 - "Community 24"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 24 - "Community 24"
+### Community 25 - "Community 25"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 25 - "Community 25"
-Cohesion: 0.6
-Nodes (5): _dec(), _fmt_dt(), list_stock_logs(), _row_to_dict(), _user_nick_name_map()
-
 ### Community 26 - "Community 26"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
@@ -188,88 +187,82 @@ Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
 ### Community 28 - "Community 28"
-Cohesion: 0.5
-Nodes (2): delete(), get_multi_with_filters()
-
-### Community 29 - "Community 29"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)
 
-### Community 30 - "Community 30"
+### Community 29 - "Community 29"
 Cohesion: 1.0
 Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
 
-### Community 31 - "Community 31"
+### Community 30 - "Community 30"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 32 - "Community 32"
+### Community 31 - "Community 31"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 35 - "Community 35"
+### Community 34 - "Community 34"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 36 - "Community 36"
+### Community 35 - "Community 35"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 37 - "Community 37"
+### Community 36 - "Community 36"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 38 - "Community 38"
+### Community 37 - "Community 37"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 39 - "Community 39"
+### Community 38 - "Community 38"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 40 - "Community 40"
+### Community 39 - "Community 39"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
 - **134 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+129 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 23`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 24`** (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 27`** (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 28`** (4 nodes): `delete()`, `get()`, `get_multi_with_filters()`, `api_request_log.py`
-  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 29`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
+- **Thin community `Community 28`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 30`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
+- **Thin community `Community 29`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 31`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 30`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 32`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 31`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 35`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 34`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 36`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 35`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 36`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 38`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 37`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 39`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 38`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 40`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 39`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `SysUser` connect `Community 2` to `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 14`, `Community 21`, `Community 22`?**
-  _High betweenness centrality (0.139) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 3`, `Community 7`, `Community 11`, `Community 13`, `Community 14`, `Community 29`?**
-  _High betweenness centrality (0.045) - this node is a cross-community bridge._
-- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`?**
-  _High betweenness centrality (0.037) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 21`, `Community 22`?**
+  _High betweenness centrality (0.123) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 4`, `Community 7`, `Community 8`, `Community 9`, `Community 14`, `Community 28`?**
+  _High betweenness centrality (0.051) - this node is a cross-community bridge._
+- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`?**
+  _High betweenness centrality (0.042) - this node is a cross-community bridge._
 - **Are the 780 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
   _`str` has 780 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 707 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**

File diff suppressed because it is too large
+ 1 - 1
backend/graphify-out/graph.html


File diff suppressed because it is too large
+ 112 - 112
backend/graphify-out/graph.json


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

@@ -77,7 +77,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/leave.py": 1778666088.7680702,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_release_review.py": 1784176201.2823155,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/equipment.py": 1782717312.9816003,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1784621210.8993871,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1784687513.367388,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_route.py": 1782717483.0699635,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_code.py": 1782709076.5173395,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1782379630.9640372,
@@ -180,7 +180,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1782122639.96012,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1782894693.6582327,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1782404982.2474263,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1784180424.8120596,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1784687516.1028,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1782718998.90251,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1783920067.0560462,

+ 22 - 0
backend/tests/test_qc_task_result_conclusion.py

@@ -0,0 +1,22 @@
+import pytest
+from app.crud.qc_task import resolve_task_result_conclusion
+
+
+def test_empty_ok_for_jxz():
+    assert resolve_task_result_conclusion("", "jxz") == ""
+    assert resolve_task_result_conclusion(None, "dzx") == ""
+
+
+def test_tg_btg_ok_for_any_status():
+    assert resolve_task_result_conclusion("tg", "ywc") == "tg"
+    assert resolve_task_result_conclusion("btg", "jxz") == "btg"
+
+
+def test_empty_rejected_for_ywc():
+    with pytest.raises(ValueError, match="请选择检测结论"):
+        resolve_task_result_conclusion("", "ywc")
+
+
+def test_invalid_code_rejected():
+    with pytest.raises(ValueError, match="检测结论无效"):
+        resolve_task_result_conclusion("xyz", "jxz")

+ 315 - 0
docs/superpowers/plans/2026-07-22-qc-result-optional-conclusion.md

@@ -0,0 +1,315 @@
+# QC Result Optional Conclusion Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Allow「填写检测结果」to save without a pass/fail conclusion when task status is 待执行/进行中 (shows as 待结论); keep conclusion required when status is 已完成.
+
+**Architecture:** Empty string (or null) continues to mean「待结论」in display helpers. Add a pure backend normalizer for conclusion×status rules; wire it into `apply_task_result` and the API. Frontend adds a third radio「待结论」, disables it for `ywc`, stops defaulting to `tg`, and only validates conclusion when saving as completed.
+
+**Tech Stack:** FastAPI / SQLAlchemy (Python 3.9), Vue 3 + Element Plus, pytest
+
+**Spec:** `docs/superpowers/specs/2026-07-22-qc-result-optional-conclusion-design.md`
+
+---
+
+## File map
+
+| File | Responsibility |
+|------|----------------|
+| `backend/app/crud/qc_task.py` | Pure `resolve_task_result_conclusion` + `apply_task_result` uses it |
+| `backend/tests/test_qc_task_result_conclusion.py` | Unit tests for the pure helper |
+| `backend/app/api/qc.py` | Optional body field; defer validation to service / match new rules |
+| `web-admin/.../QcWorkOrderTaskSubList.vue` | Dialog radios, defaults, client validation |
+
+---
+
+### Task 1: Backend pure helper + unit tests
+
+**Files:**
+- Modify: `backend/app/crud/qc_task.py` (add helper near `_QC_TASK_RESULT_STATUSES`)
+- Create: `backend/tests/test_qc_task_result_conclusion.py`
+
+- [ ] **Step 1: Write failing tests**
+
+```python
+# backend/tests/test_qc_task_result_conclusion.py
+import pytest
+from app.crud.qc_task import resolve_task_result_conclusion
+
+
+def test_empty_ok_for_jxz():
+    assert resolve_task_result_conclusion("", "jxz") == ""
+    assert resolve_task_result_conclusion(None, "dzx") == ""
+
+
+def test_tg_btg_ok_for_any_status():
+    assert resolve_task_result_conclusion("tg", "ywc") == "tg"
+    assert resolve_task_result_conclusion("btg", "jxz") == "btg"
+
+
+def test_empty_rejected_for_ywc():
+    with pytest.raises(ValueError, match="请选择检测结论"):
+        resolve_task_result_conclusion("", "ywc")
+
+
+def test_invalid_code_rejected():
+    with pytest.raises(ValueError, match="检测结论无效"):
+        resolve_task_result_conclusion("xyz", "jxz")
+```
+
+- [ ] **Step 2: Run tests — expect FAIL (import / not defined)**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_qc_task_result_conclusion.py -v
+```
+
+Expected: FAIL (function missing)
+
+- [ ] **Step 3: Implement helper in `qc_task.py`**
+
+Place after `_QC_TASK_RESULT_STATUSES = frozenset({"dzx", "jxz", "ywc"})`:
+
+```python
+def resolve_task_result_conclusion(
+    conclusion: Optional[str],
+    target_status: str,
+) -> str:
+    """Normalize conclusion for apply_task_result.
+
+    Returns '' | 'tg' | 'btg'. Empty allowed only when target is dzx/jxz.
+    """
+    concl = (conclusion or "").strip()
+    if concl and concl not in ("tg", "btg"):
+        raise ValueError("检测结论无效")
+    if target_status == "ywc" and concl not in ("tg", "btg"):
+        raise ValueError("请选择检测结论(通过/不通过)")
+    return concl
+```
+
+Ensure `Optional` is already imported from `typing` in that file.
+
+- [ ] **Step 4: Run tests — expect PASS**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_qc_task_result_conclusion.py -v
+```
+
+Expected: 4 passed
+
+- [ ] **Step 5: Commit** (only if user asked to commit in this session; otherwise leave unstaged and continue)
+
+```bash
+git add backend/app/crud/qc_task.py backend/tests/test_qc_task_result_conclusion.py
+git commit -m "$(cat <<'EOF'
+feat(qc): allow empty task conclusion when status is not completed
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Wire helper into `apply_task_result` + API
+
+**Files:**
+- Modify: `backend/app/crud/qc_task.py` — `apply_task_result`
+- Modify: `backend/app/api/qc.py` — `TaskCompleteWithResultBody` + `complete-with-result` handler
+
+- [ ] **Step 1: Update `apply_task_result` validation block**
+
+Replace the current early conclusion check:
+
+```python
+    concl = (conclusion or "").strip()
+    if concl not in ("tg", "btg"):
+        raise ValueError("请选择检测结论(通过/不通过)")
+    target = (task_status or "").strip() or "ywc"
+    if target not in _QC_TASK_RESULT_STATUSES:
+        raise ValueError("任务状态无效")
+```
+
+With:
+
+```python
+    target = (task_status or "").strip() or "ywc"
+    if target not in _QC_TASK_RESULT_STATUSES:
+        raise ValueError("任务状态无效")
+    concl = resolve_task_result_conclusion(conclusion, target)
+```
+
+Keep `t.conclusion = concl` (empty string clears to pending). Optionally use `t.conclusion = concl or None` if surrounding code prefers NULL; both display as 待结论 — prefer `concl` as `''` then assign `t.conclusion = concl or None` for cleaner DB nulls:
+
+```python
+    t.conclusion = concl or None
+```
+
+Signature may stay `conclusion: str` or become `Optional[str] = None` — prefer:
+
+```python
+    conclusion: Optional[str] = None,
+```
+
+- [ ] **Step 2: Update API body + handler**
+
+In `TaskCompleteWithResultBody`:
+
+```python
+    conclusion: Optional[str] = Field(None, alias="conclusion")
+```
+
+In the `complete-with-result` handler, **remove** the hard reject:
+
+```python
+        concl = (body.conclusion or "").strip()
+        if concl not in ("tg", "btg"):
+            return error_response(400, "请选择检测结论(通过/不通过)")
+```
+
+Pass `body.conclusion` through to `apply_task_result` (still catch `ValueError` → 400 as today). Keep attachment / results / commit flow unchanged.
+
+- [ ] **Step 3: Re-run unit tests + quick import check**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_qc_task_result_conclusion.py -v
+```
+
+Expected: PASS
+
+- [ ] **Step 4: Commit** (if committing)
+
+```bash
+git add backend/app/crud/qc_task.py backend/app/api/qc.py
+git commit -m "$(cat <<'EOF'
+feat(qc): accept optional conclusion on complete-with-result for in-progress tasks
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Frontend dialog — radio, default, validation
+
+**Files:**
+- Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue`
+
+- [ ] **Step 1: Template — three radios + disable 待结论 when ywc**
+
+Replace the 结论 `el-form-item` block (~320–325) with:
+
+```vue
+                <el-form-item label="结论" :required="resultSaveStatus === 'ywc'">
+                  <el-radio-group v-model="resultConclusion">
+                    <el-radio value="" :disabled="resultSaveStatus === 'ywc'">待结论</el-radio>
+                    <el-radio value="tg">通过</el-radio>
+                    <el-radio value="btg">不通过</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+```
+
+- [ ] **Step 2: `openResult` — default to pending, not 通过**
+
+Change initial assign and detail backfill:
+
+```typescript
+    resultConclusion.value = ''
+```
+
+and:
+
+```typescript
+      const c = (detail.conclusion || '').trim()
+      resultConclusion.value = c === 'tg' || c === 'btg' ? c : ''
+```
+
+Remove both places that set `resultConclusion.value = 'tg'` / `detail.conclusion || 'tg'`.
+
+- [ ] **Step 3: `submitResult` — require conclusion only for ywc**
+
+Replace:
+
+```typescript
+    if (!resultConclusion.value) {
+      ElMessage.warning('请选择检测结论')
+      return
+    }
+```
+
+With:
+
+```typescript
+    if (
+      resultSaveStatus.value === 'ywc' &&
+      resultConclusion.value !== 'tg' &&
+      resultConclusion.value !== 'btg'
+    ) {
+      ElMessage.warning('请选择检测结论')
+      return
+    }
+```
+
+`data.conclusion` remains `resultConclusion.value` (may be `''`).
+
+- [ ] **Step 4: Manual smoke (dev)**
+
+1. 质检工单 / 我的质检任务 → 填写检测结果:三项可见;无结论任务默认「待结论」。
+2. 状态「进行中」+「待结论」→ 保存成功,卡片「待结论」。
+3. 改「已完成」:「待结论」禁用;未改结论直接保存 → 提示请选择检测结论。
+4. 「已完成」+「通过」→ 与改前一致。
+
+- [ ] **Step 5: Commit** (if committing)
+
+```bash
+git add web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): allow pending conclusion when saving QC results in progress
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Verify + graph update
+
+- [ ] **Step 1: Grep sanity**
+
+```bash
+rg -n "请选择检测结论|resultConclusion\.value = 'tg'|detail\.conclusion \|\| 'tg'" \
+  backend/app/api/qc.py \
+  backend/app/crud/qc_task.py \
+  web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue
+```
+
+Expect: no unconditional default-to-`tg`; API no longer hard-rejects empty before service; frontend only warns when `ywc`.
+
+- [ ] **Step 2: Update knowledge graph (backend + frontend touched)**
+
+```bash
+./scripts/update-graph.sh all
+```
+
+- [ ] **Step 3: Confirm checklist against spec §6**
+
+| Spec验收 | Done? |
+|----------|-------|
+| 两页弹窗三项含待结论 | |
+| dzx/jxz + 待结论可保存 | |
+| ywc 禁用待结论且必填 | |
+| 回填 tg/btg;改回待结论后列表正确 | |
+| ywc + tg/btg 行为不变 | |
+
+---
+
+## Self-review
+
+| Spec requirement | Task |
+|------------------|------|
+| 待结论 radio + disable on ywc | T3 |
+| Default empty not tg | T3 |
+| Client validate only when ywc | T3 |
+| Backend empty OK for dzx/jxz | T1–T2 |
+| Backend ywc requires tg/btg | T1–T2 |
+| No new `djl` code / no schema change | (none) |
+| Display helpers unchanged | (none) |
+| Shared component covers both pages | T3 |

+ 105 - 0
docs/superpowers/specs/2026-07-22-qc-result-optional-conclusion-design.md

@@ -0,0 +1,105 @@
+# 填写检测结果:结论可选(待结论)
+
+> 日期:2026-07-22  
+> 状态:已批准(2026-07-22)  
+> 范围:`web-admin`【质检工单】【我的质检任务】共用「填写检测结果」弹窗;后端 `complete-with-result` / `apply_task_result`  
+> 非范围:列表/查看「待结论」展示逻辑(已有);数据库结构;新增结论码 `djl`
+
+---
+
+## 1. 背景与目标
+
+### 1.1 问题
+
+「填写检测结果」弹窗中「结论」仅「通过 / 不通过」,且前后端均强制必填。打开时默认「通过」。无法先保存参数/附件/进行中状态而结论仍为「待结论」。
+
+### 1.2 目标
+
+1. 结论支持显式选择「待结论」(存空),保存后列表/查看仍按现有规则显示「待结论」。
+2. **已完成**(`ywc`)时结论仍必填(仅 `tg` / `btg`);**待执行 / 进行中**(`dzx` / `jxz`)时结论可空。
+3. 【质检工单】与【我的质检任务】共用同一弹窗组件,一次改动覆盖两页。
+
+### 1.3 非目标
+
+- 不引入新结论码(如 `djl`);待结论 = `conclusion` 空字符串 / null。
+- 不改 `qcConclusionDisplayLabel` / Tag 展示语义。
+- 不改短信综合结论等下游推导逻辑(仍要求任务具备 `tg`/`btg` 才纳入有效综合结论)。
+
+---
+
+## 2. 已确认产品决策
+
+| 决策点 | 结论 |
+|--------|------|
+| 空结论与任务状态 | **B**:仅 `dzx` / `jxz` 允许空结论;`ywc` 结论必填 |
+| UI 形态 | **A**:第三项 radio「待结论」,值为空;选 `ywc` 时禁用该项 |
+| 空值表示 | 方案 1:空字符串,与现有展示 helper 一致 |
+| 打开默认 | 无既有 `tg`/`btg` 时默认「待结论」,不再默认「通过」 |
+| 切到已完成且仍为待结论 | 不自动改成通过;提交时提示必选结论 |
+
+---
+
+## 3. 行为规则
+
+| 任务状态(保存目标) | 结论可选值 | 校验 |
+|----------------------|------------|------|
+| `dzx` 待执行 | 待结论 / 通过 / 不通过 | 允许 `conclusion` 为空 |
+| `jxz` 进行中 | 同上 | 允许 `conclusion` 为空 |
+| `ywc` 已完成 | 仅通过 / 不通过(「待结论」radio 禁用) | 必须为 `tg` 或 `btg` |
+
+落库:
+
+- 选「通过」→ `conclusion = 'tg'`
+- 选「不通过」→ `conclusion = 'btg'`
+- 选「待结论」→ `conclusion` 置空(`''` / null,与现有空结论语义一致)
+
+列表/查看:无 `tg`/`btg` 时继续用 `qcConclusionDisplayLabel` → 「待结论」。
+
+---
+
+## 4. 前端改动
+
+**文件**:`web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue`(两入口共用)
+
+1. 「结论」radio 增加「待结论」(`value=""` 或等价空绑定)。
+2. `resultSaveStatus === 'ywc'` 时禁用「待结论」选项。
+3. `openResult`:`resultConclusion` 回填 `detail.conclusion`;若无有效 `tg`/`btg`,设为 `''`(待结论),**不要**再默认 `'tg'`。
+4. `submitResult`:仅当 `resultSaveStatus === 'ywc'` 且结论非 `tg`/`btg` 时提示「请选择检测结论」;`dzx`/`jxz` 允许空结论提交。
+5. 请求体 `conclusion` 可为空字符串;类型仍可为 `string`。
+
+API 封装 `fetchCompleteTaskWithResult` 若有严格非空类型,允许 `conclusion: string`(含 `''`)即可。
+
+---
+
+## 5. 后端改动
+
+**文件**:
+
+- `backend/app/api/qc.py` — `TaskCompleteWithResultBody`、`complete-with-result` 处理
+- `backend/app/crud/qc_task.py` — `apply_task_result`
+
+规则:
+
+1. `conclusion` 改为可选(或允许空串);规范化后仅接受 `''` / `tg` / `btg`。
+2. 目标 `task_status`(默认仍可为 `ywc`)为 `ywc` 时:空结论 → `400`「请选择检测结论(通过/不通过)」。
+3. 目标为 `dzx` / `jxz` 时:允许空结论,并将 `t.conclusion` 置空。
+4. 非空且非 `tg`/`btg` → 无效结论错误(与现有一致)。
+
+`complete_task` 等其它路径若已有「有结论才写入」逻辑,保持最小改动;本需求以 `apply_task_result` / `complete-with-result` 为主路径。
+
+---
+
+## 6. 验收要点
+
+1. 【质检工单】与【我的质检任务】打开「填写检测结果」:结论可见三项,含「待结论」。
+2. 状态选「待执行」或「进行中」,结论选「待结论」,可保存;卡片结论 Tag 为「待结论」。
+3. 状态选「已完成」时「待结论」禁用;若结论仍空,提交失败并提示选结论。
+4. 已有通过/不通过的任务再打开,正确回填;清空为待结论并保存为进行中后,列表显示待结论。
+5. 已完成 + 通过/不通过 行为与改前一致。
+
+---
+
+## 7. 风险与说明
+
+- 工单自动完成(`try_complete_work_order`)仍依赖任务进入 `ywc`;空结论任务不能以「待结论」完成,与决策 B 一致。
+- 短信/综合质检结论等下游仍只认 `tg`/`btg`,待结论任务不计入「全部通过」。

+ 21 - 12
web-admin/graphify-out/.graphify_extract.json

@@ -17169,21 +17169,21 @@
       "label": "isCurrentUserAssignee()",
       "file_type": "code",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L635"
+      "source_location": "L636"
     },
     {
       "id": "qc_work_order_qcworkordertasksublist_nextmaterialrowkey",
       "label": "nextMaterialRowKey()",
       "file_type": "code",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L665"
+      "source_location": "L666"
     },
     {
       "id": "qc_work_order_qcworkordertasksublist_if",
       "label": "if()",
       "file_type": "code",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L927"
+      "source_location": "L928"
     },
     {
       "id": "views_quality_inspection_qc_work_order_qcworkordertabbar_vue",
@@ -51794,7 +51794,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L635",
+      "source_location": "L636",
       "weight": 1.0
     },
     {
@@ -51803,7 +51803,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L665",
+      "source_location": "L666",
       "weight": 1.0
     },
     {
@@ -51812,7 +51812,7 @@
       "relation": "contains",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue",
-      "source_location": "L927",
+      "source_location": "L928",
       "weight": 1.0
     },
     {
@@ -51968,13 +51968,22 @@
       "source_location": "L216",
       "weight": 1.0
     },
+    {
+      "source": "views_quality_inspection_qc_work_order_qcworkorderprotocolpaymentpanel_vue",
+      "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_datetime_display",
+      "relation": "imports_from",
+      "confidence": "EXTRACTED",
+      "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
+      "source_location": "L236",
+      "weight": 1.0
+    },
     {
       "source": "views_quality_inspection_qc_work_order_qcworkorderprotocolpaymentpanel_vue",
       "target": "vue",
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L177",
+      "source_location": "L237",
       "weight": 1.0
     },
     {
@@ -51983,7 +51992,7 @@
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L178",
+      "source_location": "L238",
       "weight": 1.0
     },
     {
@@ -51992,7 +52001,7 @@
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L179",
+      "source_location": "L239",
       "weight": 1.0
     },
     {
@@ -52001,7 +52010,7 @@
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L180",
+      "source_location": "L240",
       "weight": 1.0
     },
     {
@@ -52010,7 +52019,7 @@
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L185",
+      "source_location": "L245",
       "weight": 1.0
     },
     {
@@ -52019,7 +52028,7 @@
       "relation": "imports_from",
       "confidence": "EXTRACTED",
       "source_file": "views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue",
-      "source_location": "L186",
+      "source_location": "L246",
       "weight": 1.0
     },
     {

+ 2 - 2
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,7 +1,7 @@
-# Graph Report - src  (2026-07-21)
+# Graph Report - src  (2026-07-22)
 
 ## Corpus Check
-- Large corpus: 883 files · ~589,917 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 883 files · ~590,189 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
 - 3051 nodes · 4278 edges · 63 communities detected

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

@@ -17,8 +17,8 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1781272556.3597305,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1781272556.3601494,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1781272556.3606477,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1784618977.1386993,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1784618895.7650168,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1784686266.7518713,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1784686242.3599403,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1781272556.364766,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1782394569.8806834,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1783921717.4015942,
@@ -496,11 +496,11 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderSelectCheckbox.vue": 1783322131.5377831,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderBatchResultTabs.vue": 1782893854.2285423,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcUnfinishedBadge.vue": 1782893854.1479301,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue": 1784621207.3381128,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue": 1784687555.0892544,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTabBar.vue": 1782893854.0314786,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderBatchSubList.vue": 1782893854.0134735,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderCardList.vue": 1783392683.9513938,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue": 1783330023.9447763,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue": 1784686704.7647197,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/tabs/OutboundPilotTab.vue": 1782893854.159883,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/tabs/InboundCellStorageTab.vue": 1782893854.1357596,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/tabs/QcWorkOrderTabPanel.vue": 1783392684.8612459,

File diff suppressed because it is too large
+ 0 - 0
web-admin/src/graphify-out/cache/ast/08d8296d3725d482d78d74761729934f93dcdb3708456f28a15492b0545cc1a4.json


File diff suppressed because it is too large
+ 0 - 0
web-admin/src/graphify-out/cache/ast/63698e9d7eb4da1a63fcf6315366bde4c8fe6d54010932dfb7a7218e6595c825.json


File diff suppressed because it is too large
+ 0 - 0
web-admin/src/graphify-out/cache/ast/8f292717400fa58f17c328444acab4b8ac3e1d80fd5fe4ad324564d9a27d62ce.json


+ 192 - 63
web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue

@@ -8,86 +8,145 @@
     }"
   >
     <template v-if="summary">
-      <!-- 列表卡片:单行摘要 + 横向排列的多条明细 -->
+      <!-- 列表卡片:默认折叠为各期状态(单行),展开后为完整摘要 + 明细 -->
       <template v-if="compact">
-        <div class="qc-protocol-payment__compact-summary">
+        <!-- 折叠:标题 + 各期状态 + 箭头,同一行 -->
+        <button
+          v-if="!feeExpanded"
+          type="button"
+          class="qc-protocol-payment__toggle qc-protocol-payment__toggle--collapsed"
+          :aria-expanded="false"
+          @click="feeExpanded = true"
+        >
           <span v-if="!embedded" class="qc-protocol-payment__title">协议费用</span>
-          <el-tag v-if="summary.payModeLabel" :type="payModeTagType" size="small">
-            {{ summary.payModeLabel }}
-          </el-tag>
-          <span class="qc-protocol-payment__compact-item">
-            合同 {{ formatAmount(summary.contractAmount) }}
-          </span>
-          <span class="qc-protocol-payment__compact-item">
-            已收 {{ formatAmount(summary.receivedAmount) }}
-          </span>
-          <span class="qc-protocol-payment__compact-item">
-            待收 {{ formatAmount(summary.pendingAmount) }}
-          </span>
-          <template v-if="isLumpSum && primaryDetail">
-            <span
-              v-if="primaryDetail.installmentDueDate"
-              class="qc-protocol-payment__compact-item"
-            >
-              应缴 {{ primaryDetail.installmentDueDate }}
-            </span>
+          <div class="qc-protocol-payment__collapsed-status">
+            <template v-if="collapsedStatusRows.length">
+              <div
+                v-for="(row, index) in collapsedStatusRows"
+                :key="row.installmentNo ?? index"
+                class="qc-protocol-payment__status-chip"
+              >
+                <span
+                  v-if="isInstallment && row.installmentLabel"
+                  class="qc-protocol-payment__status-chip-label"
+                >
+                  {{ row.installmentLabel }}
+                </span>
+                <el-tag :type="payStatusTagType(row.payStatus)" size="small">
+                  {{ row.payStatusLabel || payStatusLabel(row.payStatus || '') }}
+                </el-tag>
+              </div>
+            </template>
             <el-tag
-              v-if="hideOverallPayStatus && primaryDetail.payStatus"
-              :type="payStatusTagType(primaryDetail.payStatus)"
+              v-else-if="summary.overallPayStatusLabel"
+              :type="overallStatusTagType"
               size="small"
             >
-              {{ primaryDetail.payStatusLabel || payStatusLabel(primaryDetail.payStatus || '') }}
+              {{ summary.overallPayStatusLabel }}
             </el-tag>
-          </template>
-          <span v-if="summary.statusHint" class="qc-protocol-payment__compact-hint">
-            {{ summary.statusHint }}
-          </span>
-          <el-tag
-            v-if="!hideOverallPayStatus && summary.overallPayStatusLabel"
-            :type="overallStatusTagType"
-            size="small"
-            class="qc-protocol-payment__compact-status"
-          >
-            {{ summary.overallPayStatusLabel }}
-          </el-tag>
-        </div>
-        <div v-if="showCompactDetailChips" class="qc-protocol-payment__compact-details">
-          <div
-            v-for="(row, index) in detailRows"
-            :key="row.installmentNo ?? index"
-            class="qc-protocol-payment__detail-chip"
-            :class="{
-              'qc-protocol-payment__detail-chip--head-only': hideInstallmentDetailAmounts && isInstallment,
-              'qc-protocol-payment__detail-chip--inline': isLumpSum
-            }"
+            <span v-else class="qc-protocol-payment__empty">暂无缴费状态</span>
+          </div>
+          <el-icon class="qc-protocol-payment__toggle-icon" :size="14">
+            <ArrowDown />
+          </el-icon>
+        </button>
+
+        <!-- 展开:标题行 + 完整摘要 + 明细 -->
+        <template v-else>
+          <button
+            type="button"
+            class="qc-protocol-payment__toggle"
+            :aria-expanded="true"
+            @click="feeExpanded = false"
           >
-            <div class="qc-protocol-payment__detail-chip-head">
-              <span v-if="isInstallment && row.installmentLabel" class="qc-protocol-payment__detail-chip-label">
-                {{ row.installmentLabel }}
-              </span>
+            <span v-if="!embedded" class="qc-protocol-payment__title">协议费用</span>
+            <span v-else class="qc-protocol-payment__toggle-spacer" />
+            <el-icon class="qc-protocol-payment__toggle-icon" :size="14">
+              <ArrowUp />
+            </el-icon>
+          </button>
+          <div class="qc-protocol-payment__compact-summary">
+            <el-tag v-if="summary.payModeLabel" :type="payModeTagType" size="small">
+              {{ summary.payModeLabel }}
+            </el-tag>
+            <span class="qc-protocol-payment__compact-item">
+              合同 {{ formatAmount(summary.contractAmount) }}
+            </span>
+            <span class="qc-protocol-payment__compact-item">
+              已收 {{ formatAmount(summary.receivedAmount) }}
+            </span>
+            <span class="qc-protocol-payment__compact-item">
+              待收 {{ formatAmount(summary.pendingAmount) }}
+            </span>
+            <template v-if="isLumpSum && primaryDetail">
               <span
-                v-if="isInstallment && row.installmentDueDate"
-                class="qc-protocol-payment__detail-chip-date"
+                v-if="primaryDetail.installmentDueDate"
+                class="qc-protocol-payment__compact-item"
               >
-                {{ row.installmentDueDate }}
+                应缴 {{ primaryDetail.installmentDueDate }}
               </span>
               <el-tag
-                :type="payStatusTagType(row.payStatus)"
+                v-if="hideOverallPayStatus && primaryDetail.payStatus"
+                :type="payStatusTagType(primaryDetail.payStatus)"
                 size="small"
-                class="qc-protocol-payment__detail-chip-tag"
               >
-                {{ row.payStatusLabel || payStatusLabel(row.payStatus || '') }}
+                {{ primaryDetail.payStatusLabel || payStatusLabel(primaryDetail.payStatus || '') }}
               </el-tag>
-            </div>
+            </template>
+            <span v-if="summary.statusHint" class="qc-protocol-payment__compact-hint">
+              {{ summary.statusHint }}
+            </span>
+            <el-tag
+              v-if="!hideOverallPayStatus && summary.overallPayStatusLabel"
+              :type="overallStatusTagType"
+              size="small"
+              class="qc-protocol-payment__compact-status"
+            >
+              {{ summary.overallPayStatusLabel }}
+            </el-tag>
+          </div>
+          <div v-if="showCompactDetailChips" class="qc-protocol-payment__compact-details">
             <div
-              v-if="!(hideInstallmentDetailAmounts && isInstallment)"
-              class="qc-protocol-payment__detail-chip-body"
+              v-for="(row, index) in detailRows"
+              :key="row.installmentNo ?? index"
+              class="qc-protocol-payment__detail-chip"
+              :class="{
+                'qc-protocol-payment__detail-chip--head-only':
+                  hideInstallmentDetailAmounts && isInstallment,
+                'qc-protocol-payment__detail-chip--inline': isLumpSum
+              }"
             >
-              <span>应缴 {{ formatAmount(row.dueAmount) }}</span>
-              <span>实收 {{ formatAmount(row.transferAmount) }}</span>
+              <div class="qc-protocol-payment__detail-chip-head">
+                <span
+                  v-if="isInstallment && row.installmentLabel"
+                  class="qc-protocol-payment__detail-chip-label"
+                >
+                  {{ row.installmentLabel }}
+                </span>
+                <span
+                  v-if="isInstallment && row.installmentDueDate"
+                  class="qc-protocol-payment__detail-chip-date"
+                >
+                  {{ row.installmentDueDate }}
+                </span>
+                <el-tag
+                  :type="payStatusTagType(row.payStatus)"
+                  size="small"
+                  class="qc-protocol-payment__detail-chip-tag"
+                >
+                  {{ row.payStatusLabel || payStatusLabel(row.payStatus || '') }}
+                </el-tag>
+              </div>
+              <div
+                v-if="!(hideInstallmentDetailAmounts && isInstallment)"
+                class="qc-protocol-payment__detail-chip-body"
+              >
+                <span>应缴 {{ formatAmount(row.dueAmount) }}</span>
+                <span>实收 {{ formatAmount(row.transferAmount) }}</span>
+              </div>
             </div>
           </div>
-        </div>
+        </template>
       </template>
 
       <!-- 查看弹窗:汇总指标 + 明细表 -->
@@ -173,8 +232,9 @@
 </template>
 
 <script setup lang="ts">
+  import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
   import { formatDisplayOrDash } from '@/utils/datetime-display'
-  import { computed } from 'vue'
+  import { computed, ref } from 'vue'
   import type { QcProtocolPaymentSummary } from '@/api/qc-work-order'
   import { formatMoneyDisplay } from '@/utils/money-format'
   import {
@@ -199,6 +259,9 @@
     hideInstallmentDetailAmounts?: boolean
   }>()
 
+  /** 列表卡片协议费用默认折叠 */
+  const feeExpanded = ref(false)
+
   const isInstallment = computed(() => isInstallmentPayMode(props.summary?.payMode || ''))
 
   const isLumpSum = computed(() => props.summary?.payMode === 'dj')
@@ -207,6 +270,9 @@
 
   const primaryDetail = computed(() => detailRows.value[0] ?? null)
 
+  /** 折叠态:有明细则按期展示状态;无明细则回退整体状态 */
+  const collapsedStatusRows = computed(() => detailRows.value)
+
   /** 趸交仅 1 条明细时信息已并入摘要行,不再占满整行的明细区 */
   const showCompactDetailChips = computed(() => {
     if (!detailRows.value.length) return false
@@ -246,6 +312,7 @@
   }
 
   .qc-protocol-payment__title {
+    flex-shrink: 0;
     font-size: 13px;
     font-weight: 600;
     color: #065f46;
@@ -299,12 +366,74 @@
     box-sizing: border-box;
   }
 
+  .qc-protocol-payment__toggle {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    width: 100%;
+    margin: 0;
+    padding: 0;
+    border: none;
+    background: transparent;
+    cursor: pointer;
+    text-align: left;
+    font: inherit;
+    color: inherit;
+
+    &:hover .qc-protocol-payment__title,
+    &:hover .qc-protocol-payment__toggle-icon {
+      color: var(--el-color-primary, #10b981);
+    }
+
+    &--collapsed {
+      flex-wrap: nowrap;
+      gap: 8px 12px;
+      min-width: 0;
+    }
+  }
+
+  .qc-protocol-payment__toggle-spacer {
+    flex: 1;
+  }
+
+  .qc-protocol-payment__toggle-icon {
+    margin-left: auto;
+    flex-shrink: 0;
+    color: #065f46;
+    transition: color 0.15s ease;
+  }
+
+  .qc-protocol-payment__collapsed-status {
+    display: flex;
+    flex-wrap: nowrap;
+    align-items: center;
+    gap: 8px 14px;
+    min-width: 0;
+    flex: 1;
+    overflow-x: auto;
+  }
+
+  .qc-protocol-payment__status-chip {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+    gap: 6px;
+  }
+
+  .qc-protocol-payment__status-chip-label {
+    font-size: 12px;
+    font-weight: 600;
+    color: #334155;
+    white-space: nowrap;
+  }
+
   .qc-protocol-payment__compact-summary {
     display: flex;
     flex-wrap: wrap;
     align-items: center;
     gap: 6px 14px;
     width: 100%;
+    margin-top: 8px;
     font-size: 12px;
     line-height: 1.4;
   }

+ 10 - 4
web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue

@@ -317,8 +317,9 @@
                     <el-radio value="ywc">已完成</el-radio>
                   </el-radio-group>
                 </el-form-item>
-                <el-form-item label="结论" required>
+                <el-form-item label="结论" :required="resultSaveStatus === 'ywc'">
                   <el-radio-group v-model="resultConclusion">
+                    <el-radio value="" :disabled="resultSaveStatus === 'ywc'">待结论</el-radio>
                     <el-radio value="tg">通过</el-radio>
                     <el-radio value="btg">不通过</el-radio>
                   </el-radio-group>
@@ -1274,7 +1275,7 @@
     resultTaskId.value = row.id
     resultTaskType.value = row.taskType
     resultRemark.value = row.remark || ''
-    resultConclusion.value = 'tg'
+    resultConclusion.value = ''
     resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
     materialUsages.value = []
     requisitionOptions.value = []
@@ -1284,7 +1285,8 @@
       resultTaskStatus.value = detail.taskStatus || row.taskStatus
       resultSaveStatus.value = normalizeResultSaveStatus(detail.taskStatus || row.taskStatus)
       resultRemark.value = detail.remark || row.remark || ''
-      resultConclusion.value = detail.conclusion || 'tg'
+      const c = (detail.conclusion || '').trim()
+      resultConclusion.value = c === 'tg' || c === 'btg' ? c : ''
       resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
       const results = await fetchQcTaskResults(row.id)
       resultParams.value = normalizeResultParams(results || [])
@@ -1318,7 +1320,11 @@
       ElMessage.warning('请上传检测材料')
       return
     }
-    if (!resultConclusion.value) {
+    if (
+      resultSaveStatus.value === 'ywc' &&
+      resultConclusion.value !== 'tg' &&
+      resultConclusion.value !== 'btg'
+    ) {
       ElMessage.warning('请选择检测结论')
       return
     }

Some files were not shown because too many files changed in this diff