zlwu 4 miesięcy temu
rodzic
commit
c90f72fd34

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-23897
+71753

+ 0 - 1
backend/app/api/pl/transport.py

@@ -417,7 +417,6 @@ def arrange_transport(
             freight_amount=freight_amount or 0,
             freight_type=payload.freight_type,
             has_invoice=payload.has_invoice,
-            qr_expire_date=payload.qr_expire_date,
             transport_status="draft",
             approval_status="none",
             remark=payload.remark,

+ 1 - 0
backend/graphify-out/.graphify_labels.json

@@ -110,6 +110,7 @@
   "108": "Community 108",
   "109": "Community 109",
   "110": "Community 110",
+  "111": "Community 111",
   "113": "Community 113",
   "114": "Community 114",
   "115": "Community 115",

+ 27 - 22
backend/graphify-out/GRAPH_REPORT.md

@@ -1,16 +1,16 @@
 # Graph Report - backend  (2026-06-21)
 
 ## Corpus Check
-- 191 files · ~131,118 words
+- 191 files · ~131,117 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 3860 nodes · 29584 edges · 135 communities (131 shown, 4 thin omitted)
+- 3860 nodes · 29584 edges · 136 communities (132 shown, 4 thin omitted)
 - Extraction: 48% EXTRACTED · 52% INFERRED · 0% AMBIGUOUS · INFERRED: 15299 edges (avg confidence: 0.5)
 - Token cost: 0 input · 0 output
 
 ## Graph Freshness
-- Built from commit: `29960970`
+- Built from commit: `cedb7ee6`
 - Run `git rev-parse HEAD` and compare to check if the graph is stale.
 - Run `graphify update .` after code changes (no API cost).
 
@@ -126,6 +126,7 @@
 - [[_COMMUNITY_Community 108|Community 108]]
 - [[_COMMUNITY_Community 109|Community 109]]
 - [[_COMMUNITY_Community 110|Community 110]]
+- [[_COMMUNITY_Community 111|Community 111]]
 - [[_COMMUNITY_Community 122|Community 122]]
 - [[_COMMUNITY_Community 123|Community 123]]
 - [[_COMMUNITY_Community 128|Community 128]]
@@ -166,7 +167,7 @@
 - `int` --uses--> `LocalStorage`  [INFERRED]
   scripts/run_db_auto_generate_plans.py → app/core/storage.py
 
-## Communities (135 total, 4 thin omitted)
+## Communities (136 total, 4 thin omitted)
 
 ### Community 0 - "Community 0"
 Cohesion: 0.23
@@ -205,8 +206,8 @@ Cohesion: 0.23
 Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.03
-Nodes (201): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract (+193 more)
+Cohesion: 0.04
+Nodes (194): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract (+186 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.24
@@ -321,16 +322,16 @@ Cohesion: 0.35
 Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
 
 ### Community 38 - "Community 38"
-Cohesion: 0.41
-Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
+Cohesion: 0.33
+Nodes (15): BxUserContext, int, Session, str, create_workflow(), delete_workflow(), get_workflow(), get_workflow_by_biz_type() (+7 more)
 
 ### Community 39 - "Community 39"
 Cohesion: 0.18
 Nodes (9): 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, 报销(bx)Pydantic 模型 — 响应协议与 bx 原系统保持一致, bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。 (+1 more)
 
 ### Community 40 - "Community 40"
-Cohesion: 0.20
-Nodes (26): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+18 more)
+Cohesion: 0.13
+Nodes (34): Any, bool, Decimal, int, Session, str, create_from_quote(), _quote_products_to_contract_products() (+26 more)
 
 ### Community 41 - "Community 41"
 Cohesion: 0.24
@@ -341,8 +342,8 @@ Cohesion: 0.08
 Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
 
 ### Community 43 - "Community 43"
-Cohesion: 0.08
-Nodes (51): Any, str, Any, str, Any, int, str, Any (+43 more)
+Cohesion: 0.07
+Nodes (53): Any, str, Any, str, Any, int, str, bool (+45 more)
 
 ### Community 44 - "Community 44"
 Cohesion: 0.25
@@ -437,8 +438,8 @@ Cohesion: 0.38
 Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
 
 ### Community 67 - "Community 67"
-Cohesion: 0.11
-Nodes (28): bool, Redis, str, bool, bool, Any, bool, Session (+20 more)
+Cohesion: 0.27
+Nodes (15): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, 简易 User-Agent 解析(替代 Hutool UserAgentUtil) (+7 more)
 
 ### Community 68 - "Community 68"
 Cohesion: 0.12
@@ -521,8 +522,8 @@ Cohesion: 0.50
 Nodes (3): PurchasePlanBase, PurchasePlanCreate, PurchasePlanUpdate
 
 ### Community 88 - "Community 88"
-Cohesion: 0.16
-Nodes (27): BxUserContext, int, Session, str, bool, BxUserContext, int, Session (+19 more)
+Cohesion: 0.17
+Nodes (25): BxUserContext, Session, str, UploadFile, bool, BxUserContext, int, Session (+17 more)
 
 ### Community 89 - "Community 89"
 Cohesion: 0.26
@@ -541,16 +542,16 @@ Cohesion: 0.40
 Nodes (3): AdvanceApplyCreate, AdvanceApplyUpdate, _coerce_optional_id()
 
 ### Community 93 - "Community 93"
-Cohesion: 0.28
-Nodes (42): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+34 more)
+Cohesion: 0.30
+Nodes (41): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+33 more)
 
 ### Community 94 - "Community 94"
 Cohesion: 0.27
 Nodes (18): Any, bool, int, Session, str, create_clue_follow(), delete_clue_follow(), get_clue_follow() (+10 more)
 
 ### Community 95 - "Community 95"
-Cohesion: 0.28
-Nodes (13): int, Session, str, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+5 more)
+Cohesion: 0.23
+Nodes (14): int, Session, str, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+6 more)
 
 ### Community 96 - "Community 96"
 Cohesion: 0.47
@@ -600,6 +601,10 @@ Nodes (10): SysOss, int, Session, str, SysOss, create_oss(), create_oss(), get_o
 Cohesion: 0.30
 Nodes (9): Any, bool, int, M, Session, str, BxCrudService, 报销模块 Service 基类 — 租户隔离 + UUID + 软删除。 (+1 more)
 
+### Community 111 - "Community 111"
+Cohesion: 0.35
+Nodes (10): bool, Redis, str, _claims_from_auth(), _get_redis_client(), _is_crypto_api(), _mark_nonce_once(), _nonce_key() (+2 more)
+
 ### Community 122 - "Community 122"
 Cohesion: 0.22
 Nodes (9): generate_product_no(), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001) (+1 more)
@@ -666,9 +671,9 @@ _Questions this graph is uniquely positioned to answer:_
 
 - **Why does `Session` connect `Community 3` to `Community 2`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 23`, `Community 24`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 40`, `Community 41`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 49`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 62`, `Community 63`, `Community 64`, `Community 66`, `Community 67`, `Community 68`, `Community 69`, `Community 71`, `Community 73`, `Community 78`, `Community 79`, `Community 80`, `Community 81`, `Community 83`, `Community 84`, `Community 85`, `Community 86`, `Community 88`, `Community 89`, `Community 90`, `Community 93`, `Community 94`, `Community 95`, `Community 96`, `Community 97`, `Community 99`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 107`, `Community 109`, `Community 122`, `Community 123`, `Community 133`, `Community 135`, `Community 136`, `Community 137`, `Community 143`?**
   _High betweenness centrality (0.226) - this node is a cross-community bridge._
-- **Why does `BaseModel` connect `Community 12` to `Community 128`, `Community 1`, `Community 2`, `Community 0`, `Community 4`, `Community 134`, `Community 6`, `Community 9`, `Community 138`, `Community 11`, `Community 10`, `Community 15`, `Community 16`, `Community 146`, `Community 20`, `Community 21`, `Community 23`, `Community 30`, `Community 41`, `Community 49`, `Community 50`, `Community 53`, `Community 56`, `Community 61`, `Community 62`, `Community 65`, `Community 74`, `Community 77`, `Community 87`, `Community 88`, `Community 92`, `Community 93`, `Community 96`, `Community 98`, `Community 100`, `Community 103`, `Community 104`, `Community 105`, `Community 108`?**
+- **Why does `BaseModel` connect `Community 12` to `Community 128`, `Community 1`, `Community 2`, `Community 0`, `Community 4`, `Community 134`, `Community 6`, `Community 9`, `Community 138`, `Community 11`, `Community 10`, `Community 15`, `Community 16`, `Community 146`, `Community 20`, `Community 21`, `Community 23`, `Community 30`, `Community 38`, `Community 41`, `Community 49`, `Community 50`, `Community 53`, `Community 56`, `Community 61`, `Community 62`, `Community 65`, `Community 74`, `Community 77`, `Community 87`, `Community 92`, `Community 93`, `Community 96`, `Community 98`, `Community 100`, `Community 103`, `Community 104`, `Community 105`, `Community 108`?**
   _High betweenness centrality (0.061) - this node is a cross-community bridge._
-- **Why does `datetime` connect `Community 48` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 134`, `Community 7`, `Community 6`, `Community 9`, `Community 10`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 146`, `Community 19`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 43`, `Community 44`, `Community 45`, `Community 47`, `Community 49`, `Community 50`, `Community 52`, `Community 53`, `Community 59`, `Community 60`, `Community 62`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 73`, `Community 78`, `Community 80`, `Community 81`, `Community 85`, `Community 86`, `Community 89`, `Community 90`, `Community 93`, `Community 94`, `Community 97`, `Community 103`, `Community 105`?**
+- **Why does `datetime` connect `Community 48` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 134`, `Community 7`, `Community 6`, `Community 9`, `Community 10`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 146`, `Community 19`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 43`, `Community 44`, `Community 45`, `Community 47`, `Community 49`, `Community 50`, `Community 52`, `Community 53`, `Community 59`, `Community 60`, `Community 62`, `Community 63`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 73`, `Community 78`, `Community 80`, `Community 81`, `Community 85`, `Community 86`, `Community 89`, `Community 90`, `Community 93`, `Community 94`, `Community 95`, `Community 97`, `Community 103`, `Community 105`?**
   _High betweenness centrality (0.039) - this node is a cross-community bridge._
 - **Are the 251 inferred relationships involving `str` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
   _`str` has 251 INFERRED edges - model-reasoned connections that need verification._

Plik diff jest za duży
+ 0 - 0
backend/graphify-out/cache/ast/31705b1faa94c9c07b85aa13e3c54759745931f76b1f3e41ef0576231dcbdfdf.json


Plik diff jest za duży
+ 0 - 0
backend/graphify-out/cache/stat-index.json


Plik diff jest za duży
+ 1 - 1
backend/graphify-out/graph.html


+ 132 - 132
backend/graphify-out/graph.json

@@ -1424,7 +1424,7 @@
       "source_file": "app/core/wechat_pay.py",
       "source_location": "L93",
       "id": "core_wechat_pay_wechatpayclient_make_request",
-      "community": 67,
+      "community": 43,
       "norm_label": "._make_request()"
     },
     {
@@ -1433,7 +1433,7 @@
       "source_file": "app/core/wechat_pay.py",
       "source_location": "L93",
       "id": "app_core_wechat_pay_py_bool",
-      "community": 67,
+      "community": 43,
       "norm_label": "bool"
     },
     {
@@ -2468,7 +2468,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L72",
       "id": "core_storage_basestorage_delete_file",
-      "community": 67,
+      "community": 43,
       "norm_label": ".delete_file()"
     },
     {
@@ -2477,7 +2477,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L72",
       "id": "app_core_storage_py_bool",
-      "community": 67,
+      "community": 43,
       "norm_label": "bool"
     },
     {
@@ -2576,7 +2576,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L130",
       "id": "core_storage_localstorage_delete_file",
-      "community": 67,
+      "community": 43,
       "norm_label": ".delete_file()"
     },
     {
@@ -2639,7 +2639,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L193",
       "id": "core_storage_qiniustorage_delete_file",
-      "community": 67,
+      "community": 43,
       "norm_label": ".delete_file()"
     },
     {
@@ -2702,7 +2702,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L247",
       "id": "core_storage_ossstorage_delete_file",
-      "community": 67,
+      "community": 43,
       "norm_label": ".delete_file()"
     },
     {
@@ -2765,7 +2765,7 @@
       "source_file": "app/core/storage.py",
       "source_location": "L313",
       "id": "core_storage_miniostorage_delete_file",
-      "community": 67,
+      "community": 43,
       "norm_label": ".delete_file()"
     },
     {
@@ -3404,7 +3404,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L1",
       "id": "app_core_api_crypto_middleware_py",
-      "community": 67,
+      "community": 111,
       "norm_label": "api_crypto_middleware.py"
     },
     {
@@ -3413,7 +3413,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L28",
       "id": "core_api_crypto_middleware_nonce_key",
-      "community": 67,
+      "community": 111,
       "norm_label": "_nonce_key()"
     },
     {
@@ -3422,7 +3422,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L28",
       "id": "app_core_api_crypto_middleware_py_str",
-      "community": 67,
+      "community": 111,
       "norm_label": "str"
     },
     {
@@ -3431,7 +3431,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L32",
       "id": "core_api_crypto_middleware_should_skip_path",
-      "community": 67,
+      "community": 111,
       "norm_label": "_should_skip_path()"
     },
     {
@@ -3440,7 +3440,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L32",
       "id": "app_core_api_crypto_middleware_py_bool",
-      "community": 67,
+      "community": 111,
       "norm_label": "bool"
     },
     {
@@ -3449,7 +3449,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L37",
       "id": "core_api_crypto_middleware_is_crypto_api",
-      "community": 67,
+      "community": 111,
       "norm_label": "_is_crypto_api()"
     },
     {
@@ -3458,7 +3458,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L43",
       "id": "core_api_crypto_middleware_claims_from_auth",
-      "community": 67,
+      "community": 111,
       "norm_label": "_claims_from_auth()"
     },
     {
@@ -3467,7 +3467,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L61",
       "id": "core_api_crypto_middleware_get_redis_client",
-      "community": 67,
+      "community": 111,
       "norm_label": "_get_redis_client()"
     },
     {
@@ -3485,7 +3485,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L78",
       "id": "core_api_crypto_middleware_mark_nonce_once",
-      "community": 67,
+      "community": 111,
       "norm_label": "_mark_nonce_once()"
     },
     {
@@ -3557,7 +3557,7 @@
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L38",
       "id": "core_api_crypto_middleware_rationale_38",
-      "community": 67,
+      "community": 111,
       "norm_label": "\u4ec5\u5bf9\u8fd0\u8425\u7aef\u4e0e\u5c0f\u7a0b\u5e8f\u4e1a\u52a1 api \u5e94\u7528\u7edf\u4e00\u5bc6\u6587\u534f\u8bae\u3002"
     },
     {
@@ -5186,7 +5186,7 @@
       "source_file": "app/models/db_models.py",
       "source_location": "L1",
       "id": "app_models_db_models_py",
-      "community": 93,
+      "community": 95,
       "norm_label": "db_models.py"
     },
     {
@@ -5231,7 +5231,7 @@
       "source_file": "app/models/db_models.py",
       "source_location": "L1",
       "id": "models_db_models_rationale_1",
-      "community": 93,
+      "community": 95,
       "norm_label": "\u5730\u78c5(db)\u6a21\u5757\u6570\u636e\u6a21\u578b \u2014 \u5bf9\u9f50 docs/sql/db_tables.sql"
     },
     {
@@ -7373,7 +7373,7 @@
       "source_file": "app/schemas/bx/workflow.py",
       "source_location": "L55",
       "id": "bx_workflow_workflowdefout",
-      "community": 88,
+      "community": 38,
       "norm_label": "workflowdefout"
     },
     {
@@ -17183,7 +17183,7 @@
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1609",
       "id": "crm_contract_quote_products_to_contract_products",
-      "community": 9,
+      "community": 40,
       "norm_label": "_quote_products_to_contract_products()"
     },
     {
@@ -17192,7 +17192,7 @@
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1637",
       "id": "crm_contract_create_from_quote",
-      "community": 9,
+      "community": 40,
       "norm_label": "create_from_quote()"
     },
     {
@@ -17552,7 +17552,7 @@
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1646",
       "id": "crm_contract_rationale_1646",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id"
     },
     {
@@ -22221,7 +22221,7 @@
       "label": "get_transport()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L491",
+      "source_location": "L490",
       "id": "pl_transport_get_transport",
       "community": 103,
       "norm_label": "get_transport()"
@@ -22230,7 +22230,7 @@
       "label": "QrCodeResponse",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L506",
+      "source_location": "L505",
       "id": "pl_transport_qrcoderesponse",
       "community": 103,
       "norm_label": "qrcoderesponse"
@@ -22239,7 +22239,7 @@
       "label": "generate_qrcode()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L516",
+      "source_location": "L515",
       "id": "pl_transport_generate_qrcode",
       "community": 103,
       "norm_label": "generate_qrcode()"
@@ -22248,7 +22248,7 @@
       "label": "transport_scan_feedback()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L549",
+      "source_location": "L548",
       "id": "pl_transport_transport_scan_feedback",
       "community": 103,
       "norm_label": "transport_scan_feedback()"
@@ -22257,7 +22257,7 @@
       "label": "TransportFeedback",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L549",
+      "source_location": "L548",
       "id": "transportfeedback",
       "community": 103,
       "norm_label": "transportfeedback"
@@ -22266,7 +22266,7 @@
       "label": "submit_approval()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L590",
+      "source_location": "L589",
       "id": "pl_transport_submit_approval",
       "community": 103,
       "norm_label": "submit_approval()"
@@ -22275,7 +22275,7 @@
       "label": "ApprovalSubmit",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L590",
+      "source_location": "L589",
       "id": "approvalsubmit",
       "community": 103,
       "norm_label": "approvalsubmit"
@@ -22284,7 +22284,7 @@
       "label": "process_approval()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L635",
+      "source_location": "L634",
       "id": "pl_transport_process_approval",
       "community": 103,
       "norm_label": "process_approval()"
@@ -22293,7 +22293,7 @@
       "label": "ApprovalRequest",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L635",
+      "source_location": "L634",
       "id": "approvalrequest",
       "community": 103,
       "norm_label": "approvalrequest"
@@ -22302,7 +22302,7 @@
       "label": "ApprovalRecordRead",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L635",
+      "source_location": "L634",
       "id": "approvalrecordread",
       "community": 103,
       "norm_label": "approvalrecordread"
@@ -22311,7 +22311,7 @@
       "label": "get_transport_approvals()",
       "file_type": "code",
       "source_file": "app/api/pl/transport.py",
-      "source_location": "L697",
+      "source_location": "L696",
       "id": "pl_transport_get_transport_approvals",
       "community": 103,
       "norm_label": "get_transport_approvals()"
@@ -23609,7 +23609,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L1",
       "id": "app_api_bx_attachment_py",
-      "community": 38,
+      "community": 88,
       "norm_label": "attachment.py"
     },
     {
@@ -23618,7 +23618,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L24",
       "id": "bx_attachment_tenant",
-      "community": 38,
+      "community": 88,
       "norm_label": "_tenant()"
     },
     {
@@ -23627,7 +23627,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L24",
       "id": "app_api_bx_attachment_py_session",
-      "community": 38,
+      "community": 88,
       "norm_label": "session"
     },
     {
@@ -23636,7 +23636,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L24",
       "id": "app_api_bx_attachment_py_bxusercontext",
-      "community": 38,
+      "community": 88,
       "norm_label": "bxusercontext"
     },
     {
@@ -23645,7 +23645,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L24",
       "id": "app_api_bx_attachment_py_str",
-      "community": 38,
+      "community": 88,
       "norm_label": "str"
     },
     {
@@ -23654,7 +23654,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L28",
       "id": "bx_attachment_parse_id_list",
-      "community": 38,
+      "community": 88,
       "norm_label": "_parse_id_list()"
     },
     {
@@ -23663,7 +23663,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L33",
       "id": "bx_attachment_upload_file",
-      "community": 38,
+      "community": 88,
       "norm_label": "upload_file()"
     },
     {
@@ -23672,7 +23672,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L33",
       "id": "app_api_bx_attachment_py_uploadfile",
-      "community": 38,
+      "community": 88,
       "norm_label": "uploadfile"
     },
     {
@@ -23681,7 +23681,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L67",
       "id": "bx_attachment_download_file",
-      "community": 38,
+      "community": 88,
       "norm_label": "download_file()"
     },
     {
@@ -23690,7 +23690,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L89",
       "id": "bx_attachment_delete_attachment",
-      "community": 38,
+      "community": 88,
       "norm_label": "delete_attachment()"
     },
     {
@@ -23699,7 +23699,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L103",
       "id": "bx_attachment_preview_attachment",
-      "community": 38,
+      "community": 88,
       "norm_label": "preview_attachment()"
     },
     {
@@ -23708,7 +23708,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L127",
       "id": "bx_attachment_list_by_biz",
-      "community": 38,
+      "community": 88,
       "norm_label": "list_by_biz()"
     },
     {
@@ -23717,7 +23717,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L140",
       "id": "bx_attachment_bind_biz",
-      "community": 38,
+      "community": 88,
       "norm_label": "bind_biz()"
     },
     {
@@ -23726,7 +23726,7 @@
       "source_file": "app/api/bx/attachment.py",
       "source_location": "L108",
       "id": "bx_attachment_rationale_108",
-      "community": 38,
+      "community": 88,
       "norm_label": "\u8fd4\u56de\u56fe\u7247/\u9644\u4ef6\u9884\u89c8 url(minio \u9884\u7b7e\u540d\u6216\u672c\u5730\u516c\u5f00\u5730\u5740,\u5bf9\u9f50\u95e8\u6237 storage \u80fd\u529b)\u3002"
     },
     {
@@ -23978,7 +23978,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L1",
       "id": "app_api_bx_workflow_py",
-      "community": 88,
+      "community": 38,
       "norm_label": "workflow.py"
     },
     {
@@ -23987,7 +23987,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L22",
       "id": "bx_workflow_list_workflows",
-      "community": 88,
+      "community": 38,
       "norm_label": "list_workflows()"
     },
     {
@@ -23996,7 +23996,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L22",
       "id": "app_api_bx_workflow_py_int",
-      "community": 88,
+      "community": 38,
       "norm_label": "int"
     },
     {
@@ -24005,7 +24005,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L22",
       "id": "app_api_bx_workflow_py_session",
-      "community": 88,
+      "community": 38,
       "norm_label": "session"
     },
     {
@@ -24014,7 +24014,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L22",
       "id": "app_api_bx_workflow_py_bxusercontext",
-      "community": 88,
+      "community": 38,
       "norm_label": "bxusercontext"
     },
     {
@@ -24023,7 +24023,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L36",
       "id": "bx_workflow_get_workflow_by_biz_type",
-      "community": 88,
+      "community": 38,
       "norm_label": "get_workflow_by_biz_type()"
     },
     {
@@ -24032,7 +24032,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L36",
       "id": "app_api_bx_workflow_py_str",
-      "community": 88,
+      "community": 38,
       "norm_label": "str"
     },
     {
@@ -24041,7 +24041,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L48",
       "id": "bx_workflow_get_workflow",
-      "community": 88,
+      "community": 38,
       "norm_label": "get_workflow()"
     },
     {
@@ -24050,7 +24050,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L61",
       "id": "bx_workflow_create_workflow",
-      "community": 88,
+      "community": 38,
       "norm_label": "create_workflow()"
     },
     {
@@ -24059,7 +24059,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L61",
       "id": "workflowdefcreate",
-      "community": 88,
+      "community": 38,
       "norm_label": "workflowdefcreate"
     },
     {
@@ -24068,7 +24068,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L77",
       "id": "bx_workflow_update_workflow",
-      "community": 88,
+      "community": 38,
       "norm_label": "update_workflow()"
     },
     {
@@ -24077,7 +24077,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L77",
       "id": "workflowdefupdate",
-      "community": 88,
+      "community": 38,
       "norm_label": "workflowdefupdate"
     },
     {
@@ -24086,7 +24086,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L97",
       "id": "bx_workflow_save_workflow_nodes",
-      "community": 88,
+      "community": 38,
       "norm_label": "save_workflow_nodes()"
     },
     {
@@ -24095,7 +24095,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L97",
       "id": "workflownodecreate",
-      "community": 88,
+      "community": 38,
       "norm_label": "workflownodecreate"
     },
     {
@@ -24104,7 +24104,7 @@
       "source_file": "app/api/bx/workflow.py",
       "source_location": "L119",
       "id": "bx_workflow_delete_workflow",
-      "community": 88,
+      "community": 38,
       "norm_label": "delete_workflow()"
     },
     {
@@ -30097,7 +30097,7 @@
       "file_type": "code",
       "source_file": "scripts/migrate_dibang_data.py",
       "source_location": "L391",
-      "community": 67,
+      "community": 9,
       "norm_label": "bool",
       "id": "bool"
     },
@@ -31969,7 +31969,7 @@
       "file_type": "rationale",
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1648",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id",
       "id": "crm_contract_rationale_1648"
     },
@@ -32194,7 +32194,7 @@
       "file_type": "rationale",
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1641",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id",
       "id": "crm_contract_rationale_1641"
     },
@@ -32419,7 +32419,7 @@
       "file_type": "rationale",
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1616",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id",
       "id": "crm_contract_rationale_1616"
     },
@@ -32725,7 +32725,7 @@
       "file_type": "rationale",
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1492",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id",
       "id": "crm_contract_rationale_1492"
     },
@@ -32896,7 +32896,7 @@
       "file_type": "rationale",
       "source_file": "app/crud/crm/contract.py",
       "source_location": "L1421",
-      "community": 9,
+      "community": 40,
       "norm_label": "java: createfromquote \u2014 \u8fd4\u56de\u5408\u540c id",
       "id": "crm_contract_rationale_1421"
     },
@@ -34822,7 +34822,7 @@
       "file_type": "code",
       "source_file": "app/core/api_crypto_middleware.py",
       "source_location": "L61",
-      "community": 67,
+      "community": 111,
       "norm_label": "redis",
       "id": "app_core_api_crypto_middleware_py_redis"
     },
@@ -62964,9 +62964,9 @@
       "source_file": "app/services/bx/reimburse.py",
       "source_location": "L26",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "app_services_bx_reimburse_py",
-      "target": "utils_bx_codegen_format_reimburse_title",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_format_reimburse_title"
     },
     {
       "relation": "calls",
@@ -63062,9 +63062,9 @@
       "source_file": "app/services/bx/reimburse.py",
       "source_location": "L26",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "app_services_bx_reimburse_py",
-      "target": "utils_bx_codegen_is_auto_reimburse_title",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_is_auto_reimburse_title"
     },
     {
       "relation": "calls",
@@ -63127,9 +63127,9 @@
       "source_file": "app/services/bx/reimburse.py",
       "source_location": "L26",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "app_services_bx_reimburse_py",
-      "target": "utils_bx_codegen_allocate_reimburse_title_seq",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_allocate_reimburse_title_seq"
     },
     {
       "relation": "calls",
@@ -63158,9 +63158,9 @@
       "source_file": "app/utils/bx_codegen.py",
       "source_location": "L86",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "utils_bx_codegen_rationale_86",
-      "target": "utils_bx_codegen_allocate_reimburse_title_seq",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_allocate_reimburse_title_seq"
     },
     {
       "relation": "calls",
@@ -63255,9 +63255,9 @@
       "source_file": "app/utils/bx_codegen.py",
       "source_location": "L109",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "utils_bx_codegen_rationale_109",
-      "target": "utils_bx_codegen_generate_title",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_generate_title"
     },
     {
       "relation": "imports",
@@ -63297,9 +63297,9 @@
       "source_file": "app/utils/bx_codegen.py",
       "source_location": "L123",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "utils_bx_codegen_rationale_123",
-      "target": "utils_bx_codegen_resolve_reimburse_title",
-      "confidence_score": 1.0
+      "target": "utils_bx_codegen_resolve_reimburse_title"
     },
     {
       "relation": "re_exports",
@@ -64346,9 +64346,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehiclecreate",
-      "target": "models_pl_models_plcarrier",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plcarrier"
     },
     {
       "relation": "uses",
@@ -64376,9 +64376,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleread",
-      "target": "models_pl_models_plcarrier",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plcarrier"
     },
     {
       "relation": "uses",
@@ -64386,9 +64386,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleupdate",
-      "target": "models_pl_models_plcarrier",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plcarrier"
     },
     {
       "relation": "inherits",
@@ -66123,9 +66123,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehiclecreate",
-      "target": "models_pl_models_plvehicle",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicle"
     },
     {
       "relation": "uses",
@@ -66153,9 +66153,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleread",
-      "target": "models_pl_models_plvehicle",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicle"
     },
     {
       "relation": "uses",
@@ -66163,9 +66163,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleupdate",
-      "target": "models_pl_models_plvehicle",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicle"
     },
     {
       "relation": "imports",
@@ -66822,9 +66822,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehiclecreate",
-      "target": "models_pl_models_plpersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plpersonnel"
     },
     {
       "relation": "uses",
@@ -66852,9 +66852,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleread",
-      "target": "models_pl_models_plpersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plpersonnel"
     },
     {
       "relation": "uses",
@@ -66862,9 +66862,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleupdate",
-      "target": "models_pl_models_plpersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plpersonnel"
     },
     {
       "relation": "imports",
@@ -68116,9 +68116,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehiclecreate",
-      "target": "models_pl_models_plvehiclepersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehiclepersonnel"
     },
     {
       "relation": "uses",
@@ -68146,9 +68146,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleread",
-      "target": "models_pl_models_plvehiclepersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehiclepersonnel"
     },
     {
       "relation": "uses",
@@ -68156,9 +68156,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleupdate",
-      "target": "models_pl_models_plvehiclepersonnel",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehiclepersonnel"
     },
     {
       "relation": "imports",
@@ -68429,9 +68429,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehiclecreate",
-      "target": "models_pl_models_plvehicledriver",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicledriver"
     },
     {
       "relation": "uses",
@@ -68459,9 +68459,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleread",
-      "target": "models_pl_models_plvehicledriver",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicledriver"
     },
     {
       "relation": "uses",
@@ -68469,9 +68469,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L12",
       "weight": 0.8,
+      "confidence_score": 0.5,
       "source": "vehicleupdate",
-      "target": "models_pl_models_plvehicledriver",
-      "confidence_score": 0.5
+      "target": "models_pl_models_plvehicledriver"
     },
     {
       "relation": "imports",
@@ -304244,9 +304244,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L175",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "pl_base_data_vehicle_to_read",
-      "target": "vehicleread",
-      "confidence_score": 1.0
+      "target": "vehicleread"
     },
     {
       "relation": "calls",
@@ -304299,9 +304299,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L206",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "pl_base_data_list_vehicles",
-      "target": "vehicleread",
-      "confidence_score": 1.0
+      "target": "vehicleread"
     },
     {
       "relation": "uses",
@@ -304569,9 +304569,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L247",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "pl_base_data_create_vehicle",
-      "target": "vehiclecreate",
-      "confidence_score": 1.0
+      "target": "vehiclecreate"
     },
     {
       "relation": "uses",
@@ -304656,9 +304656,9 @@
       "source_file": "app/api/pl/base_data.py",
       "source_location": "L323",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "pl_base_data_update_vehicle",
-      "target": "vehicleupdate",
-      "confidence_score": 1.0
+      "target": "vehicleupdate"
     },
     {
       "relation": "uses",
@@ -320141,9 +320141,9 @@
       "source_file": "app/api/db/inventory.py",
       "source_location": "L366",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "db_inventory_rationale_366",
-      "target": "db_inventory_get_inventory",
-      "confidence_score": 1.0
+      "target": "db_inventory_get_inventory"
     },
     {
       "relation": "rationale_for",
@@ -324726,9 +324726,9 @@
       "source_file": "app/services/pl_dibang_bridge_service.py",
       "source_location": "L216",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "services_pl_dibang_bridge_service_rationale_216",
-      "target": "services_pl_dibang_bridge_service_revoke_regeneratable_dibang_for_transport",
-      "confidence_score": 1.0
+      "target": "services_pl_dibang_bridge_service_revoke_regeneratable_dibang_for_transport"
     },
     {
       "relation": "rationale_for",
@@ -328206,9 +328206,9 @@
       "source_file": "app/services/bx/reimburse.py",
       "source_location": "L298",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "bx_reimburse_rationale_298",
-      "target": "bx_reimburse_reimburseservice_latest_approval_per_node",
-      "confidence_score": 1.0
+      "target": "bx_reimburse_reimburseservice_latest_approval_per_node"
     },
     {
       "relation": "calls",
@@ -328312,9 +328312,9 @@
       "source_file": "app/services/bx/reimburse.py",
       "source_location": "L359",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "bx_reimburse_rationale_359",
-      "target": "bx_reimburse_reimburseservice_get_approval_history",
-      "confidence_score": 1.0
+      "target": "bx_reimburse_reimburseservice_get_approval_history"
     },
     {
       "relation": "references",
@@ -337014,9 +337014,9 @@
       "source_file": "scripts/migrate_dibang_data.py",
       "source_location": "L146",
       "weight": 1.0,
+      "confidence_score": 1.0,
       "source": "scripts_migrate_dibang_data_rationale_146",
-      "target": "scripts_migrate_dibang_data_legacy_id_to_str",
-      "confidence_score": 1.0
+      "target": "scripts_migrate_dibang_data_legacy_id_to_str"
     },
     {
       "relation": "rationale_for",
@@ -341518,5 +341518,5 @@
     }
   ],
   "hyperedges": [],
-  "built_at_commit": "299609706ced6b7d1b8c4ab0a0bfc56364fb1985"
+  "built_at_commit": "cedb7ee681d7559b48234bc2aadf5b1b1075699e"
 }

+ 362 - 0
docs/superpowers/specs/2026-06-21-pl-arrange-transport-refine-design.md

@@ -0,0 +1,362 @@
+# 运输计划 · 安排/调整运输弹窗重构 — 设计文档
+
+> **日期**: 2026-06-21  
+> **状态**: 待评审  
+> **前置**: `docs/运输计划管理-需求说明文档.md`、`docs/superpowers/specs/2026-06-20-pl-arrange-dibang-design.md`、`docs/superpowers/specs/2026-06-20-pl-plan-form-dialog-design.md`  
+> **界面模式**: P06 复杂业务列表 + 弹窗表单;安排弹窗上区编辑、下区只读对照(延续 2026-06-20 布局)
+
+## 目标
+
+重构 `#/pl/plan`「安排运输 / 调整运输」弹窗,使上区成为**完整的运输执行信息编辑区**(车辆、物流节点、运费),下区保留计划只读对照;并支持**多次安排/调整**,在再次提交时**强制作废可撤销的历史地磅联动单据**、**轮换司机免登录链接**;若历史地磅单无法自动作废则**阻断整次调整**。
+
+---
+
+## 已确认决策(2026-06-21)
+
+| # | 决策 | 说明 |
+|---|------|------|
+| D7 | **允许多次安排** | `transportStatus=arranged` 且未归档、未作废的计划,可再次打开弹窗调整 |
+| D8 | **再次安排必清理历史地磅** | 只要该运输单存在 `pl_transport_db_link` 历史记录,再次安排时**必须先**调用 `revoke_regeneratable_dibang_for_transport`,**与本次是否勾选「生成地磅计划」无关** |
+| D9 | **再次安排轮换司机链接** | 再次安排时**强制生成新** `driver_access_token`;旧 URL 失效(404),成功态提示专员重新发送 |
+| D10 | **地磅无法自动作废则阻断** | 与计划作废、现有 revoke 一致:出入库单已执行或已关联有效磅单 → **整次调整运输失败**,提示先去地磅管理处理 |
+| D11 | **运输单原地更新** | 只要 `plan.transport_id` 存在且运输单未 `cancelled`,**始终更新原 `pl_transport`**,禁止新建孤儿运输单 |
+| D12 | **上区字段边界** | 可编辑:车辆 / 物流节点 / 运费;安排特有保留:**运输介质(只读)**、**备注**、**是否生成地磅计划** |
+| D13 | **下区完整只读对照** | 下区**完整复用** `PlPlanViewTabs`,三 Tab(计划概况 / **运输信息** / 合同信息)与「查看」弹窗**同一展示模式**;有历史数据则正常展示,不做弱化或裁剪 |
+
+### 对 2026-06-20 设计的修订
+
+| 原决策 | 修订 |
+|--------|------|
+| D319「重新安排保留原 token」 | **废止**,改为 D9 轮换 token |
+| 上区「起终点」两字段 | 改为与计划表单一致的**完整物流节点**(取货/送货地址、时间、注意事项) |
+| revoke 仅在勾选地磅时触发 | 改为 D8:**再次安排且存在历史 link 时必 revoke** |
+
+---
+
+## 非目标(本期不做)
+
+- 运输 N:N 拼车、一车多计划合并地磅
+- 自动作废**地磅月计划** `db_purchase_plan`(仍只软删出入库单 + link)
+- 自动作废已关联的**地磅单** `db_weighing_slip`(不可作废时走 D10 阻断)
+- 司机链接过期日期 `qr_expire_date` 策略变更
+- 已归档 / 已作废计划的调整入口
+
+---
+
+## 一、业务语义
+
+### 1.1 首次安排 vs 再次调整
+
+| 维度 | 首次安排 | 再次调整 |
+|------|---------|---------|
+| 入口文案 | 安排运输 | 调整运输 |
+| 计划状态 | `transportStatus=pending` | `transportStatus=arranged` |
+| 运输单 | 创建 `pl_transport`(若尚无) | 更新原 `pl_transport`(D11) |
+| 司机链接 | 生成 token | **轮换** token(D9) |
+| 历史地磅 link | 无 | 存在则 **先 revoke**(D8) |
+| 地磅重建 | 勾选则 `sync_dibang` | revoke 后,勾选则 `sync_dibang`;不勾选则仅作废不建 |
+
+### 1.2 再次调整判定(后端 `is_rearrange`)
+
+满足任一即视为再次调整:
+
+- `plan.transport_status == "arranged"`,或
+- `plan.transport_id` 非空且关联的 `pl_transport.del_flag=0` 且 `transport_status != "cancelled"`
+
+---
+
+## 二、安排/调整弹窗(`PlPlanArrangeDialog`)
+
+### 2.1 布局
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ 安排运输 · {planNo}  /  调整运输 · {planNo}            [×]   │
+├──────────────────────────────────────────────────────────────┤
+│ ▲ 上区:运输安排表单(max-height ~42vh,可滚动)              │
+│   [车辆信息]  车辆 / 司机 / 承运商 / PlVehicleSummaryStrip   │
+│   [物流节点]  取货信息 ↔ 送货信息(对齐 PlPlanFormDialog)    │
+│   [运费信息]  公里数 / 计费方式 / 量 / 单价 / 金额 / 开票    │
+│   [安排项]    运输介质(只读) / 备注 / □生成地磅计划           │
+├──────────────────────────────────────────────────────────────┤
+│ ▼ 下区:计划单查看(flex:1,min-height 280px)               │
+│   [计划概况] [运输信息] [合同信息]  ← 与「查看」弹窗同模式     │
+└──────────────────────────────────────────────────────────────┘
+│ 取消 │ 确认安排/确认调整 │(成功后)复制链接 / 简述 / 磅单二维码 │
+```
+
+**尺寸与规范**:延续 `dialog-form-layout`、`align-center`、`width: min(1680px, calc(100vw - 80px))`、无 backdrop blur;表单 `label-position="top"`。
+
+### 2.2 上区字段清单
+
+#### 车辆信息(可编辑)
+
+| 字段 | 来源 | 规则 |
+|------|------|------|
+| 选择车辆 | `formData.vehicleId` | 必填;变更时加载车辆人员、默认司机 |
+| 选择司机 | `formData.driverId` | 必填;来自所选车辆绑定司机 |
+| 运输公司 | `formData.carrierId` | 自有车辆只读「安庆市山峡化工有限公司」 |
+
+复用 `PlVehicleSummaryStrip` + `PlVehicleViewDialog`(与计划表单一致)。
+
+#### 物流节点(可编辑,写入 plan + transport)
+
+对齐 `PlPlanFormDialog` 取货/送货结构:
+
+| 区块 | 字段 | 写入 |
+|------|------|------|
+| 取货 | 取货地址、取货时间、注意事项 | `pl_plan.pickup_*`;`pl_transport.start_point` ← 取货地址 |
+| 送货 | 送货地址、送达时间、注意事项 | `pl_plan.destination_*`;`pl_transport.end_point` ← 送货地址 |
+
+**删除**上区独立的「运输起点 / 运输终点」两栏(由物流节点替代)。
+
+#### 运费信息(可编辑,补全 UI)
+
+| 字段 | 规则 |
+|------|------|
+| 是否有运输费用 | 跟随计划 `hasFreight`;为 0 时隐藏运费明细(只读展示「无运费」) |
+| 运输公里数 | `distanceKm` |
+| 运费计算方式 | `by_weight` / `by_trip` |
+| 运输量、运输单价 | 按重量时展示;复用 `usePlFreightCalc` |
+| 运费金额 | 可手调;按趟必填 |
+| 是否开票 | `hasInvoice` |
+
+#### 安排特有(保留)
+
+| 字段 | 规则 |
+|------|------|
+| 运输介质 | 只读,由产品行拼接(现有逻辑) |
+| 备注 | 写入 `pl_transport.remark` |
+| 是否生成地磅计划 | 仅 inbound/outbound;直销单 disabled |
+
+### 2.3 提交前确认(再次调整)
+
+| 条件 | 确认文案要点 |
+|------|-------------|
+| 存在历史 `pl_transport_db_link` | 将作废此前自动生成的出入库单;已执行/已关联磅单需先在地磅处理 |
+| 历史 link + 本次不勾地磅 | 补充:作废后**不再**新建地磅单 |
+| 历史 link + 本次勾地磅 | 作废后按**当前计划产品**重新生成 |
+| 再次调整(任意) | 司机链接将更新,旧链接失效 |
+
+### 2.4 成功态
+
+- 不立即关弹窗
+- 展示**新**司机链接 +「复制链接」
+- 若 `is_rearrange`:额外提示「司机链接已更新,旧链接已失效,请重新发送给司机」
+- 「复制计划简述」「复制磅单二维码」逻辑不变(二维码仅本次勾地磅且有 `inventoryRecords` 时)
+
+### 2.5 列表入口(`#/pl/plan`)
+
+```typescript
+function canArrangeTransport(row: Plan) {
+  return (
+    (row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
+    row.archiveStatus !== 'archived' &&
+    row.followStatus !== 'cancelled'
+  )
+}
+```
+
+按钮文案:`pending` →「安排运输」;`arranged` →「调整运输」。
+
+### 2.6 下区「计划单查看」(与查看弹窗对齐,D13)
+
+下区继续嵌入 `PlPlanViewTabs`(`embedded` 模式),**三 Tab 齐全**,展示逻辑与 `PlPlanViewDialog` 打开的查看弹窗**完全一致**,不得删减区块。
+
+**运输信息 Tab 须完整包含**(与查看弹窗相同):
+
+| 区块 | 内容 |
+|------|------|
+| 车辆与承运 | `PlVehicleSummaryStrip`、承运商 |
+| 物流节点 | 取货信息 ↔ 送货信息(`ViewKvTable` + 流向箭头) |
+| 运费信息 | 是否有运费、公里数、计费方式、运输量/单价、运费金额、是否开票 |
+| 司机回传信息 | 回传时间、数量、备注、附件 |
+| 地磅单信息 | 关联出入库单、磅单明细(含历史已生成数据) |
+
+**上下区关系**:
+
+- **上区** = 本次待提交的编辑态(改什么在这里改)
+- **下区** = 当前库内数据的只读快照(含历史安排、回传、地磅结果;再次调整前对照用)
+- 上区编辑与下区展示字段可以重叠,**下区不因上区可编辑而弱化**;安排/调整成功后调用 `tabsRef.reload()` 刷新下区,使历史数据与提交结果一致
+
+**默认 Tab**:打开弹窗时默认「计划概况」;已安排计划再次打开时可默认「运输信息」(实现时可选,非强制)。
+
+---
+
+## 三、后端 API(`POST /api/transport/order/arrange/{plan_id}`)
+
+### 3.1 编排顺序(同一 DB 事务)
+
+```
+1. 校验计划、车辆、司机、危化品证书(现有)
+2. is_rearrange = 判定(§1.2)
+3. 若 is_rearrange 且存在 pl_transport_db_link(del_flag=0):
+     revoke_regeneratable_dibang_for_transport()  → 失败则 rollback + 400(D10)
+4. 获取/更新 pl_transport(D11:始终更新原单,不新建孤儿)
+5. 同步 plan 物流字段 + transport 执行字段 + 运费冗余
+6. 若 is_rearrange:轮换 driver_access_token(D9)
+   否则:_ensure_driver_access_token(仅首次生成)
+7. 若 payload.generate_dibang_plan:sync_dibang_for_transport(重建)
+8. plan.transport_status = 'arranged'
+9. commit → TransportArrangeResult
+```
+
+### 3.2 `TransportArrange` 请求体扩展
+
+在现有字段基础上,arrange 接口需能接收并写回计划物流/运费(或通过 arrange 内聚更新 plan 表):
+
+| 字段 | 说明 |
+|------|------|
+| `pickupAddress` / `pickupTime` / `pickupNotes` | 取货信息 |
+| `destinationAddress` / `deliveryTime` / `destinationNotes` | 送货信息 |
+| `distanceKm` / `freightType` / `freightPrice` / `freightAmount` / `hasInvoice` | 运费(现有部分已有) |
+| `generateDibangPlan` | 是否重建地磅联动 |
+
+`startPoint` / `endPoint` 可保留为兼容别名(服务端映射为取货/送货地址),但前端不再单独暴露。
+
+### 3.3 司机 token 轮换
+
+```python
+def _rotate_driver_access_token(transport: PlTransport) -> str:
+    token = secrets.token_urlsafe(32)[:64]
+    transport.driver_access_token = token
+    return token
+```
+
+- 再次调整:调用 `_rotate_driver_access_token`
+- 首次安排:token 为空则生成,否则保留(仅首次)
+
+公共 GET `/{token}`:token 不存在或已轮换 → **404**(现有查询逻辑已满足)。
+
+### 3.4 地磅 revoke 触发变更
+
+**现状**:`revoke_regeneratable_dibang_for_transport` 仅在 `sync_dibang_for_transport` 内部调用。
+
+**变更**:
+
+- `arrange_transport` 在步骤 3 **独立调用** revoke(当 is_rearrange 且存在 link)
+- `sync_dibang_for_transport` **移除**内部重复的 revoke 调用(避免双次 revoke),或保留幂等(推荐移除重复,单一入口)
+
+revoke 规则不变(`pl_dibang_bridge_service.py`):
+
+- 可作废状态:`待入库`、`待出库`、`待三方填写`、`待处理`
+- 阻断:其他状态、或已关联未作废磅单
+
+### 3.5 运输单更新(D11)
+
+删除「仅 `transport_status==draft` 才更新,否则新建」分支。
+
+```python
+if existing_transport and existing_transport.transport_status != "cancelled":
+    transport = existing_transport
+    # 更新全部字段
+else:
+    # 仅当 plan 尚无 transport_id 时创建
+```
+
+---
+
+## 四、数据流
+
+```mermaid
+sequenceDiagram
+  participant UI as PlPlanArrangeDialog
+  participant API as arrange_transport
+  participant PL as pl_plan + pl_transport
+  participant BR as pl_dibang_bridge
+
+  UI->>API: POST arrange(物流/运费/车辆 + generateDibangPlan)
+  API->>API: is_rearrange?
+
+  alt is_rearrange 且存在历史 link
+    API->>BR: revoke_regeneratable_dibang_for_transport
+    alt 不可作废
+      BR-->>API: ValueError
+      API-->>UI: 400 阻断
+    end
+  end
+
+  API->>PL: 更新 plan 物流 + transport 执行信息
+  alt is_rearrange
+    API->>PL: 轮换 driver_access_token
+  else 首次
+    API->>PL: 确保 token
+  end
+
+  alt generateDibangPlan
+    API->>BR: sync_dibang_for_transport
+  end
+
+  API-->>UI: driverUrl + inventoryRecords
+```
+
+---
+
+## 五、前端文件清单
+
+| 文件 | 动作 |
+|------|------|
+| `components/business/pl-plan-arrange-dialog/index.vue` | 上区补全物流/运费;删除起终点;再次调整确认与成功提示 |
+| `components/business/pl-plan-form-dialog/index.vue` | **参考**物流/运费区块结构(可抽公共子组件,非必须) |
+| `components/business/pl-plan-view-tabs/index.vue` | **无结构性改动**;安排弹窗与查看弹窗共用,保证运输信息 Tab 完整展示 |
+| `views/pl/plan/index.vue` | `canArrangeTransport` + 按钮文案 |
+| `views/pl/plan/detail.vue` | 同上(若有安排入口) |
+| `api/pl/index.ts` | 扩展 `TransportArrange` 类型 |
+| `backend/app/api/pl/transport.py` | arrange 编排、token 轮换、transport 原地更新 |
+| `backend/app/schemas/pl/transport.py` | `TransportArrange` 扩展 plan 物流字段 |
+| `backend/app/services/pl_dibang_bridge_service.py` | revoke 调用点调整(避免重复) |
+
+---
+
+## 六、安全与约束
+
+| 项 | 规则 |
+|----|------|
+| 旧司机链接 | 轮换后立即失效;不在列表 API 暴露 token |
+| 地磅 | 不可自动作废时整单阻断,不允许「只改车辆不动地磅」的双轨数据 |
+| 租户 | 所有读写带 `tenant_no` |
+| 事务 | revoke、transport 更新、sync_dibang 同一事务 |
+
+---
+
+## 七、验收标准
+
+| # | 场景 | 预期 |
+|---|------|------|
+| V1 | 待安排计划首次安排 | 创建 transport + 司机链接;可选地磅 |
+| V2 | 已安排计划打开弹窗 | 显示「调整运输」;上区预填当前车辆/物流/运费 |
+| V3 | 再次调整(曾勾地磅,本次仍勾) | 旧出入库单作废 → 按当前产品重建 |
+| V4 | 再次调整(曾勾地磅,本次不勾) | 旧出入库单作废 → 不新建 |
+| V5 | 再次调整后访问旧司机链接 | 404 |
+| V6 | 再次调整成功后 | 仅新链接可打开司机页 |
+| V7 | 地磅单已执行/已关联磅单 | 调整运输 400 阻断,文案与作废计划一致 |
+| V8 | 上区物流/运费可编辑并提交 | plan + transport 字段一致;复制简述含新路线 |
+| V9 | 仅改车辆不勾地磅 | 仍轮换链接;有历史 link 仍作废地磅单 |
+| V10 | 直销单 | 地磅勾选 disabled;仍可调整车辆/物流/运费/链接 |
+| V11 | 已安排计划打开弹窗下区 | 「运输信息」Tab 与「查看」弹窗一致,历史车辆/物流/运费/地磅/回传均正常展示 |
+| V12 | 调整运输提交成功后 | 下区 `PlPlanViewTabs` reload,运输信息与地磅/链接状态更新为最新 |
+
+---
+
+## 八、实施分期
+
+| Phase | 内容 |
+|-------|------|
+| **P1** | 后端:D11 原地更新 + D8/D9/D10 arrange 编排 + schema 扩展 |
+| **P2** | 前端:列表入口 + 上区物流/运费 UI + 下区保持完整 `PlPlanViewTabs` + 确认/成功提示 |
+| **P3** | 联调验收 V1–V12 |
+
+---
+
+## 九、参考路径
+
+| 说明 | 路径 |
+|------|------|
+| 现安排弹窗 | `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue` |
+| 查看弹窗(下区对齐基准) | `web-admin/src/components/business/pl-plan-view-dialog/index.vue` |
+| 下区三 Tab 共用组件 | `web-admin/src/components/business/pl-plan-view-tabs/index.vue` |
+| 计划表单物流/运费 | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` |
+| arrange API | `backend/app/api/pl/transport.py` |
+| 地磅 revoke/sync | `backend/app/services/pl_dibang_bridge_service.py` |
+| 计划作废(revoke 参考) | `backend/app/api/pl/plan.py` → `cancel_plan` |
+| 2026-06-20 基线设计 | `docs/superpowers/specs/2026-06-20-pl-arrange-dibang-design.md` |

+ 30 - 20
graphify-out/manifest.json

@@ -280,7 +280,7 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
-    "mtime": 1782010658.6295862,
+    "mtime": 1782050635.5785413,
     "ast_hash": "6553f54ca1fd19086c170095f402a628",
     "semantic_hash": ""
   },
@@ -395,7 +395,7 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_product.py": {
-    "mtime": 1782010658.6296172,
+    "mtime": 1782050635.5785413,
     "ast_hash": "d2e1538af2d36ecc9daf28a3b135bd0f",
     "semantic_hash": ""
   },
@@ -475,7 +475,7 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue_statistics.py": {
-    "mtime": 1782010658.6296113,
+    "mtime": 1782050635.5792494,
     "ast_hash": "9a026c628764467e4d1398900105dc78",
     "semantic_hash": ""
   },
@@ -495,7 +495,7 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py": {
-    "mtime": 1782010658.6296005,
+    "mtime": 1782050635.5790546,
     "ast_hash": "3cd90455687d8cc24d4b664d1fd5eaa2",
     "semantic_hash": ""
   },
@@ -5870,8 +5870,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py": {
-    "mtime": 1781958790.7459605,
-    "ast_hash": "c8a214703c5dbe51ddf01c55c4a40397",
+    "mtime": 1782050677.6775932,
+    "ast_hash": "dbaf1df18942f192c7c23518cebe1d9f",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/__init__.py": {
@@ -5895,8 +5895,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/index.ts": {
-    "mtime": 1781960000.1835015,
-    "ast_hash": "debd561cfaa4d2117bfd62c9bd29ccd4",
+    "mtime": 1782018145.470547,
+    "ast_hash": "0b35e56483060da5661a965f5ac43fba",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/transport/index.vue": {
@@ -6030,8 +6030,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue": {
-    "mtime": 1781959941.9666793,
-    "ast_hash": "2b3653e2492fa20bc9af7b1091813c9f",
+    "mtime": 1782050212.6489468,
+    "ast_hash": "4fa234ddbfe09a37c54df2d0abe00b92",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py": {
@@ -6045,13 +6045,13 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlContractToPlan.ts": {
-    "mtime": 1781959932.9813125,
-    "ast_hash": "b00e61df86d1f28ac7738185efe36b57",
+    "mtime": 1782046239.4682035,
+    "ast_hash": "6de0714fdb3ddd841509ccf3515bdb79",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue": {
-    "mtime": 1781959936.1473446,
-    "ast_hash": "940afd1e164a75be5886ae7a948745ce",
+    "mtime": 1782046239.2915168,
+    "ast_hash": "8321224653dba2e3a5f0e2e075387b7d",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/contract-selector/index.vue": {
@@ -6070,8 +6070,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlToxicProductCertCheck.ts": {
-    "mtime": 1781941278.9839284,
-    "ast_hash": "c62de039298b81ae80f78135ca1921cb",
+    "mtime": 1782018156.3833735,
+    "ast_hash": "801f5ac81edc62ff8ca92c3f2f0f8df6",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-cert-upload-dialog/index.vue": {
@@ -6115,8 +6115,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlPlanViewDetail.ts": {
-    "mtime": 1781962634.3591664,
-    "ast_hash": "6cc72d47784070d888c772547cabbb90",
+    "mtime": 1782049784.3807657,
+    "ast_hash": "6d9a515343c43f4762bec3326f852756",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/http/public-pl.ts": {
@@ -6125,8 +6125,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-view-tabs/index.vue": {
-    "mtime": 1782010690.7052264,
-    "ast_hash": "7fe38e5293c98d4e28d6de38c0cafba9",
+    "mtime": 1782050211.0717344,
+    "ast_hash": "4f19c0bbbd9957d9aac1c3f2d5694ba7",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue": {
@@ -6223,5 +6223,15 @@
     "mtime": 1782011029.8922439,
     "ast_hash": "f9b109c6169262ef0513614b73fe2377",
     "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-summary-strip/index.vue": {
+    "mtime": 1782050205.511665,
+    "ast_hash": "64680f39bb9b78c400343cbd50ed5c39",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-view-dialog/index.vue": {
+    "mtime": 1782020327.6240473,
+    "ast_hash": "0dc66e4f525735e1e07c715ccbcfa013",
+    "semantic_hash": ""
   }
 }

+ 19 - 0
web-admin/graphify-out/.graphify_labels.json

@@ -433,10 +433,12 @@
   "431": "Community 431",
   "432": "Community 432",
   "433": "Community 433",
+  "434": "Community 434",
   "435": "Community 435",
   "436": "Community 436",
   "437": "Community 437",
   "438": "Community 438",
+  "439": "Community 439",
   "440": "Community 440",
   "441": "Community 441",
   "442": "Community 442",
@@ -552,6 +554,7 @@
   "552": "Community 552",
   "553": "Community 553",
   "554": "Community 554",
+  "555": "Community 555",
   "556": "Community 556",
   "557": "Community 557",
   "558": "Community 558",
@@ -607,11 +610,13 @@
   "608": "Community 608",
   "609": "Community 609",
   "610": "Community 610",
+  "611": "Community 611",
   "612": "Community 612",
   "613": "Community 613",
   "614": "Community 614",
   "615": "Community 615",
   "616": "Community 616",
+  "617": "Community 617",
   "618": "Community 618",
   "619": "Community 619",
   "620": "Community 620",
@@ -624,6 +629,8 @@
   "627": "Community 627",
   "628": "Community 628",
   "629": "Community 629",
+  "630": "Community 630",
+  "631": "Community 631",
   "632": "Community 632",
   "633": "Community 633",
   "634": "Community 634",
@@ -660,23 +667,35 @@
   "665": "Community 665",
   "666": "Community 666",
   "667": "Community 667",
+  "668": "Community 668",
   "669": "Community 669",
+  "670": "Community 670",
   "671": "Community 671",
   "672": "Community 672",
+  "673": "Community 673",
   "674": "Community 674",
+  "675": "Community 675",
   "676": "Community 676",
+  "677": "Community 677",
   "678": "Community 678",
   "679": "Community 679",
   "680": "Community 680",
   "681": "Community 681",
+  "682": "Community 682",
+  "683": "Community 683",
   "684": "Community 684",
   "685": "Community 685",
   "686": "Community 686",
+  "687": "Community 687",
   "688": "Community 688",
+  "689": "Community 689",
   "690": "Community 690",
   "691": "Community 691",
   "692": "Community 692",
+  "693": "Community 693",
   "694": "Community 694",
+  "695": "Community 695",
+  "696": "Community 696",
   "700": "Community 700",
   "704": "Community 704"
 }

Plik diff jest za duży
+ 229 - 215
web-admin/graphify-out/GRAPH_REPORT.md


Plik diff jest za duży
+ 112 - 112
web-admin/graphify-out/graph.json


+ 1 - 0
web-admin/src/api/pl/index.ts

@@ -232,6 +232,7 @@ export interface PlanProduct {
   deliveryMethod?: string
   quantity?: number
   actualQty?: number
+  taxRate?: number
   taxPrice?: number
   taxAmount?: number
   taxSubtotal?: number

+ 6 - 2
web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue

@@ -15,7 +15,9 @@
         </el-table-column>
         <el-table-column prop="categoryName" label="产品分类" min-width="110" show-overflow-tooltip />
         <el-table-column label="税率(%)" min-width="90" align="center">
-          <template #default="{ row }">{{ row.taxRate ?? 0 }}%</template>
+          <template #default="{ row }">
+            {{ row.taxRate != null ? `${row.taxRate}%` : '—' }}
+          </template>
         </el-table-column>
         <el-table-column label="单位" min-width="80" align="center">
           <template #default="{ row }">
@@ -35,7 +37,7 @@
             <span v-else>{{ row.quantity ?? '—' }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="实际数量" min-width="110" align="right">
+        <el-table-column v-if="showActualQty" label="实际数量" min-width="110" align="right">
           <template #default="{ row }">
             <el-input-number
               v-if="editableQty"
@@ -94,6 +96,8 @@ defineProps<{
   showDelete?: boolean
   editableRemark?: boolean
   editableQty?: boolean
+  /** 新增/编辑弹窗默认不展示实际数量 */
+  showActualQty?: boolean
 }>()
 
 const emit = defineEmits<{

+ 195 - 78
web-admin/src/components/business/pl-plan-form-dialog/index.vue

@@ -51,54 +51,26 @@
       <section class="pl-plan-form-section">
         <div class="pl-plan-form-section-head">
           <h3 class="pl-plan-form-section-title">合同信息</h3>
-          <el-button class="btn-main" type="primary" @click="contractSelectorVisible = true">
-            <el-icon><Search /></el-icon>选择合同
-          </el-button>
+          <div class="pl-plan-form-section-actions">
+            <el-button
+              v-if="contractSnapshot"
+              class="btn-ghost"
+              @click="handleViewContract"
+            >
+              <el-icon><View /></el-icon>查看合同
+            </el-button>
+            <el-button class="btn-main" type="primary" @click="contractSelectorVisible = true">
+              <el-icon><Search /></el-icon>选择合同
+            </el-button>
+          </div>
         </div>
 
-        <p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">请选择 CRM 合同,系统将自动带入供方、需方与产品信息</p>
+        <p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">请选择 CRM 合同,系统将自动带入合同与产品信息</p>
 
         <template v-else>
-          <dl class="pl-plan-kv-grid">
-            <div class="pl-plan-kv"><dt>合同编号</dt><dd>{{ contractSnapshot.contractNo || '—' }}</dd></div>
-            <div class="pl-plan-kv"><dt>合同名称</dt><dd>{{ contractSnapshot.contractName || '—' }}</dd></div>
-            <div class="pl-plan-kv"><dt>签订日期</dt><dd>{{ contractSnapshot.signDate || '—' }}</dd></div>
-            <div class="pl-plan-kv"><dt>合同有效期</dt><dd>{{ contractSnapshot.startDate || '—' }} ~ {{ contractSnapshot.endDate || '—' }}</dd></div>
-            <div class="pl-plan-kv"><dt>结算方式</dt><dd>
-              <DictTag
-                v-if="contractSnapshot.settlementMethod"
-                dict-type="crm_settlement_method"
-                :value="contractSnapshot.settlementMethod"
-              />
-              <span v-else>—</span>
-            </dd></div>
-            <div class="pl-plan-kv"><dt>合同金额</dt><dd>{{ formatMoneyDisplay(contractSnapshot.amount) }}</dd></div>
-          </dl>
-
-          <div class="pl-plan-party-row">
-            <div class="pl-plan-party-block">
-              <h4 class="pl-plan-party-title">供方信息</h4>
-              <dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
-                <div class="pl-plan-kv"><dt>名称</dt><dd>{{ contractSnapshot.supplierName || contractSnapshot.partyA || '—' }}</dd></div>
-                <div class="pl-plan-kv"><dt>联系人</dt><dd>{{ contractSnapshot.partyAContact || '—' }}</dd></div>
-                <div class="pl-plan-kv"><dt>电话</dt><dd>{{ contractSnapshot.partyAMobile || '—' }}</dd></div>
-                <div class="pl-plan-kv pl-plan-kv-span"><dt>地址</dt><dd>{{ contractSnapshot.partyAAddress || '—' }}</dd></div>
-              </dl>
-            </div>
-
-            <div class="pl-plan-flow-bridge" aria-hidden="true">
-              <el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
-            </div>
-
-            <div class="pl-plan-party-block">
-              <h4 class="pl-plan-party-title">需方信息</h4>
-              <dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
-                <div class="pl-plan-kv"><dt>名称</dt><dd>{{ contractSnapshot.customerName || contractSnapshot.partyB || '—' }}</dd></div>
-                <div class="pl-plan-kv"><dt>联系人</dt><dd>{{ contractSnapshot.partyBContact || contractSnapshot.contactName || '—' }}</dd></div>
-                <div class="pl-plan-kv"><dt>电话</dt><dd>{{ contractSnapshot.partyBMobile || contractSnapshot.contactMobile || '—' }}</dd></div>
-                <div class="pl-plan-kv pl-plan-kv-span"><dt>地址</dt><dd>{{ contractSnapshot.partyBAddress || '—' }}</dd></div>
-              </dl>
-            </div>
+          <div class="pl-plan-contract-kv-rows">
+            <ViewKvTable :fields="contractBasicRow1Fields" mode="triple" />
+            <ViewKvTable :fields="contractBasicRow2Fields" mode="triple" />
           </div>
 
           <h4 class="pl-plan-subtitle">合同产品预览</h4>
@@ -141,20 +113,19 @@
             </el-form-item>
           </div>
 
-          <div v-if="vehicleBundle" class="pl-plan-vehicle-detail">
-            <PlVehicleViewPanel
-              collapsible
-              :default-expanded="false"
-              :detail="vehicleBundle.detail"
-              :personnel-list="vehicleBundle.personnelList"
-            />
-          </div>
+          <PlVehicleSummaryStrip
+            v-if="vehicleBundle"
+            :detail="vehicleBundle.detail"
+            :personnel-list="vehicleBundle.personnelList"
+            :carrier-name="summaryCarrierName"
+            @view="vehicleViewVisible = true"
+          />
 
           <div class="pl-plan-logistics-row">
             <div class="pl-plan-logistics-block">
               <h4 class="pl-plan-subtitle">取货信息</h4>
-              <div class="form-grid-2">
-                <el-form-item label="取货地址" class="form-col-span-2">
+              <div class="form-grid-3">
+                <el-form-item label="取货地址">
                   <el-input v-model="formData.pickupAddress" placeholder="取货地址" />
                 </el-form-item>
                 <el-form-item label="取货时间">
@@ -173,13 +144,30 @@
             </div>
 
             <div class="pl-plan-flow-bridge" aria-hidden="true">
-              <el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
+              <span class="pl-plan-flow-bridge__label">运输</span>
+              <span class="pl-plan-flow-bridge__arrow">
+                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                  <path
+                    d="M5 12h11.5"
+                    stroke="currentColor"
+                    stroke-width="2.75"
+                    stroke-linecap="round"
+                  />
+                  <path
+                    d="M13.5 8.5 18.5 12l-5 3.5"
+                    stroke="currentColor"
+                    stroke-width="2.75"
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                  />
+                </svg>
+              </span>
             </div>
 
             <div class="pl-plan-logistics-block">
               <h4 class="pl-plan-subtitle">送货信息</h4>
-              <div class="form-grid-2">
-                <el-form-item label="送货地址" class="form-col-span-2">
+              <div class="form-grid-3">
+                <el-form-item label="送货地址">
                   <el-input v-model="formData.destinationAddress" placeholder="送货地址" />
                 </el-form-item>
                 <el-form-item label="送达时间">
@@ -271,17 +259,25 @@
     </template>
 
     <ContractSelector v-model:visible="contractSelectorVisible" @confirm="handleContractConfirm" />
+    <ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
+    <PlVehicleViewDialog
+      v-model:visible="vehicleViewVisible"
+      :vehicle-id="formData.vehicleId"
+      :bundle="vehicleBundle"
+    />
   </el-dialog>
 </template>
 
 <script setup lang="ts">
-import { Check, Close, Right, Search } from '@element-plus/icons-vue'
+import { Check, Close, Search, View } from '@element-plus/icons-vue'
 import { getContract } from '@/api/crm/contract'
 import type { ContractVO } from '@/api/crm/contract/types'
 import { carrierApi, planApi, transportApi, vehicleApi, type Carrier, type Plan, type PlanProduct, type PlanUpdatePayload, type Vehicle, type VehiclePersonnelRow } from '@/api/pl'
 import ContractSelector from '@/components/business/contract-selector/index.vue'
-import DictTag from '@/components/DictTag/index.vue'
-import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
+import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
+import type { ViewField } from '@/components/business/system-view-dialog/types'
+import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
+import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
 import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
 import {
   applyContractToPlanForm,
@@ -294,6 +290,10 @@ import { formatMoneyDisplay } from '@/utils/money-format'
 import { useUserStore } from '@/store/modules/user'
 import { useDictStore } from '@/store/modules/dict'
 
+const ContractDetailDialog = defineAsyncComponent(
+  () => import('@/views/crm/contract/components/ContractDetailDialog.vue')
+)
+
 const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
 
 const props = defineProps<{
@@ -343,6 +343,9 @@ const innerVisible = computed({
 const submitLoading = ref(false)
 const detailLoading = ref(false)
 const contractSelectorVisible = ref(false)
+const contractDetailDialogActive = ref(false)
+const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
+const vehicleViewVisible = ref(false)
 const vehicleLoading = ref(false)
 const basicFormRef = ref()
 const contractSnapshot = ref<ContractVO | null>(null)
@@ -413,6 +416,52 @@ const isSelfVehicle = computed(
     selectedVehicle.value?.vehicleType === 'self' || vehicleBundle.value?.detail?.vehicleType === 'self'
 )
 
+const summaryCarrierName = computed(() => {
+  if (isSelfVehicle.value) return SELF_CARRIER_NAME
+  const carrier = carrierList.value.find(item => item.id === formData.carrierId)
+  if (carrier?.carrierName) return carrier.carrierName
+  return vehicleBundle.value?.detail?.carrierName || ''
+})
+
+function buildContractBasicFields(contract: ContractVO): ViewField[] {
+  const settlementMethod = contract.settlementMethod
+  return [
+    { label: '合同编号', value: contract.contractNo, long: false },
+    { label: '合同名称', value: contract.contractName, long: false },
+    { label: '签订日期', value: contract.signDate, long: false },
+    {
+      label: '合同有效期',
+      value:
+        contract.startDate && contract.endDate
+          ? `${contract.startDate} ~ ${contract.endDate}`
+          : contract.startDate || contract.endDate || '—',
+      long: false
+    },
+    {
+      label: '结算方式',
+      value: settlementMethod ? dictStore.getLabel('crm_settlement_method', settlementMethod) : '—',
+      tagType: settlementMethod ? dictStore.getTagType('crm_settlement_method', settlementMethod) : undefined,
+      long: false
+    },
+    {
+      label: '合同金额(元)',
+      value: formatMoneyDisplay(contract.amount),
+      money: true,
+      long: false
+    }
+  ]
+}
+
+const contractBasicFieldRows = computed(() => {
+  const contract = contractSnapshot.value
+  if (!contract) return { row1: [] as ViewField[], row2: [] as ViewField[] }
+  const fields = buildContractBasicFields(contract)
+  return { row1: fields.slice(0, 3), row2: fields.slice(3, 6) }
+})
+
+const contractBasicRow1Fields = computed(() => contractBasicFieldRows.value.row1)
+const contractBasicRow2Fields = computed(() => contractBasicFieldRows.value.row2)
+
 function resolveSelfCarrierId() {
   const matched = carrierList.value.find(item => item.carrierName === SELF_CARRIER_NAME)
   formData.carrierId = matched?.id || ''
@@ -456,9 +505,7 @@ function mapPlanProductToFormRow(item: PlanProduct, index: number): PlanFormProd
     categoryName: item.categoryName,
     unit: item.unit || '吨',
     quantity: Number(item.quantity || item.inboundEstimateQty || item.outboundEstimateQty || 0),
-    actualQty: Number(
-      item.actualQty ?? item.quantity ?? item.inboundEstimateQty ?? item.outboundEstimateQty ?? 0
-    ),
+    actualQty: 0,
     packageSpec: item.packageSpec,
     deliveryMethod: item.deliveryMethod,
     taxRate: undefined,
@@ -484,7 +531,7 @@ function mapProductsToSubmitPayload() {
     packageSpec: item.packageSpec,
     deliveryMethod: item.deliveryMethod,
     quantity: item.quantity,
-    actualQty: item.actualQty,
+    actualQty: 0,
     taxPrice: item.taxPrice,
     taxAmount: item.taxAmount,
     taxSubtotal: item.taxSubtotal,
@@ -777,6 +824,18 @@ async function loadMasterData() {
   }
 }
 
+async function handleViewContract() {
+  const contractId = formData.contractId || contractSnapshot.value?.id
+  if (!contractId) {
+    ElMessage.warning('请先选择合同')
+    return
+  }
+
+  contractDetailDialogActive.value = true
+  await nextTick()
+  contractDetailRef.value?.open(contractId)
+}
+
 async function handleContractConfirm(contract: ContractVO) {
   const mapped = applyContractToPlanForm(contract)
   contractSnapshot.value = contract
@@ -908,6 +967,13 @@ async function handleSubmit() {
   margin-bottom: 12px;
 }
 
+.pl-plan-form-section-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
 .pl-plan-form-section-title {
   margin: 0 0 12px;
   padding-left: 10px;
@@ -936,6 +1002,25 @@ async function handleSubmit() {
   color: #1e293b;
 }
 
+.pl-plan-contract-kv-rows {
+  :deep(.view-kv-table-wrap) {
+    border-radius: 0;
+  }
+
+  :deep(.view-kv-table-wrap:first-child) {
+    border-radius: 8px 8px 0 0;
+  }
+
+  :deep(.view-kv-table-wrap:last-child) {
+    border-radius: 0 0 8px 8px;
+    border-top: none;
+  }
+
+  :deep(.view-kv-table-wrap:only-child) {
+    border-radius: 8px;
+  }
+}
+
 .pl-plan-kv-grid {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -959,7 +1044,7 @@ async function handleSubmit() {
   display: flex;
   align-items: stretch;
   gap: 0;
-  margin-top: 12px;
+  margin-top: 8px;
 }
 
 .pl-plan-party-row .pl-plan-party-block,
@@ -969,17 +1054,44 @@ async function handleSubmit() {
 }
 
 .pl-plan-flow-bridge {
-  flex: 0 0 72px;
+  flex: 0 0 88px;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
+  gap: 10px;
   padding: 0 8px;
 }
 
+.pl-plan-flow-bridge__label {
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--primary-dark, #059669);
+  letter-spacing: 0.06em;
+  padding: 4px 10px;
+  border-radius: 999px;
+  background: #ecfdf5;
+  border: 1px solid #a7f3d0;
+  line-height: 1.2;
+}
+
 .pl-plan-flow-bridge__arrow {
-  font-size: 28px;
-  color: #10b981;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 44px;
+  height: 44px;
+  border-radius: 50%;
+  color: var(--primary-dark, #059669);
+  background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
+  border: 1px solid #a7f3d0;
+  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.14);
+
+  svg {
+    width: 22px;
+    height: 22px;
+    display: block;
+  }
 }
 
 .pl-plan-kv-grid-party {
@@ -987,10 +1099,22 @@ async function handleSubmit() {
 }
 
 .pl-plan-logistics-block {
-  padding: 12px;
+  padding: 10px 12px;
   border: 1px solid #e2e8f0;
   border-radius: 8px;
   background: #f8fafc;
+
+  .pl-plan-subtitle {
+    margin: 0 0 8px;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+  }
+
+  .form-grid-3 {
+    gap: 8px 12px;
+  }
 }
 
 .pl-plan-party-block {
@@ -1007,14 +1131,6 @@ async function handleSubmit() {
   color: #1e293b;
 }
 
-.pl-plan-vehicle-detail {
-  margin: 12px 0;
-  padding: 12px;
-  border: 1px dashed #cbd5e1;
-  border-radius: 8px;
-  background: #f8fafc;
-}
-
 @media (max-width: 1200px) {
   .pl-plan-kv-grid,
   .pl-plan-kv-grid-party {
@@ -1031,7 +1147,8 @@ async function handleSubmit() {
   .pl-plan-flow-bridge {
     flex: 0 0 auto;
     flex-direction: row;
-    padding: 12px 0;
+    gap: 12px;
+    padding: 8px 0;
   }
 
   .pl-plan-flow-bridge__arrow {

+ 339 - 248
web-admin/src/components/business/pl-plan-view-tabs/index.vue

@@ -5,34 +5,13 @@
       <el-tab-pane label="计划概况" name="overview">
         <section class="pl-plan-form-section">
           <h3 class="pl-plan-form-section-title">基础信息</h3>
-          <dl class="pl-plan-kv-grid">
-            <div class="pl-plan-kv">
-              <dt>计划编号</dt>
-              <dd>{{ plan?.planNo || '—' }}</dd>
-            </div>
-            <div class="pl-plan-kv">
-              <dt>计划日期</dt>
-              <dd>{{ plan?.planDate || '—' }}</dd>
-            </div>
-            <div class="pl-plan-kv">
-              <dt>归属公司</dt>
-              <dd>{{ getPlPlanCompanyLabel(plan?.belongsCompany) }}</dd>
-            </div>
-            <div class="pl-plan-kv">
-              <dt>单据类型</dt>
-              <dd>{{ getPlPlanTypeLabel(plan?.planType || '') }}</dd>
-            </div>
-            <div class="pl-plan-kv">
-              <dt>跟进人</dt>
-              <dd>{{ plan?.followUserName || '—' }}</dd>
-            </div>
-          </dl>
+          <ViewKvTable :fields="overviewBasicViewFields" mode="quad" />
         </section>
 
         <section class="pl-plan-form-section">
           <h3 class="pl-plan-form-section-title">产品信息</h3>
           <p v-if="!productRows.length && !loading" class="pl-plan-form-empty-hint">暂无产品明细</p>
-          <PlPlanFormProductTable v-else :products="productRows" />
+          <PlPlanFormProductTable v-else :products="productRows" show-actual-qty />
         </section>
 
         <section class="pl-plan-form-section">
@@ -81,131 +60,61 @@
 
       <el-tab-pane label="运输信息" name="transport">
         <section class="pl-plan-form-section">
-          <h3 class="pl-plan-form-section-title">运输信息</h3>
-          <dl class="pl-plan-kv-grid">
-            <div class="pl-plan-kv">
-              <dt>车牌号</dt>
-              <dd>{{ plan?.licensePlate || transport?.licensePlate || '—' }}</dd>
-            </div>
-            <div class="pl-plan-kv">
-              <dt>运输公司</dt>
-              <dd>{{ displayCarrierName || '—' }}</dd>
-            </div>
-          </dl>
-
-          <div v-if="vehicleBundle" class="pl-plan-vehicle-detail">
-            <PlVehicleViewPanel
-              collapsible
-              :default-expanded="false"
-              :detail="vehicleBundle.detail"
-              :personnel-list="vehicleBundle.personnelList"
-            />
-          </div>
-
-          <div class="pl-plan-logistics-row">
-            <div class="pl-plan-logistics-block">
-              <h4 class="pl-plan-subtitle">取货信息</h4>
-              <dl class="pl-plan-kv-grid pl-plan-kv-grid-2">
-                <div class="pl-plan-kv pl-plan-kv-span">
-                  <dt>取货地址</dt>
-                  <dd>{{ plan?.pickupAddress || '—' }}</dd>
-                </div>
-                <div class="pl-plan-kv">
-                  <dt>取货时间</dt>
-                  <dd>{{ plan?.pickupTime || '—' }}</dd>
-                </div>
-                <div class="pl-plan-kv">
-                  <dt>注意事项</dt>
-                  <dd>{{ plan?.pickupNotes || '—' }}</dd>
-                </div>
-              </dl>
+          <h3 class="pl-plan-form-section-title">车辆与承运</h3>
+          <PlVehicleSummaryStrip
+            v-if="vehicleBundle"
+            :detail="vehicleBundle.detail"
+            :personnel-list="vehicleBundle.personnelList"
+            :carrier-name="summaryCarrierName"
+            @view="vehicleViewVisible = true"
+          />
+          <p v-else-if="!loading" class="pl-plan-form-empty-hint">暂无车辆信息</p>
+
+          <h4 class="pl-plan-subtitle">物流节点</h4>
+          <div class="parties-row">
+            <div class="party-panel">
+              <div class="party-panel__title">取货信息</div>
+              <ViewKvTable :fields="pickupViewFields" mode="quad" />
             </div>
 
-            <div class="pl-plan-flow-bridge" aria-hidden="true">
-              <el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
+            <div class="party-flow-bridge" aria-hidden="true">
+              <span class="party-flow-bridge__label">运输</span>
+              <span class="party-flow-bridge__arrow">
+                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                  <path
+                    d="M5 12h11.5"
+                    stroke="currentColor"
+                    stroke-width="2.75"
+                    stroke-linecap="round"
+                  />
+                  <path
+                    d="M13.5 8.5 18.5 12l-5 3.5"
+                    stroke="currentColor"
+                    stroke-width="2.75"
+                    stroke-linecap="round"
+                    stroke-linejoin="round"
+                  />
+                </svg>
+              </span>
             </div>
 
-            <div class="pl-plan-logistics-block">
-              <h4 class="pl-plan-subtitle">送货信息</h4>
-              <dl class="pl-plan-kv-grid pl-plan-kv-grid-2">
-                <div class="pl-plan-kv pl-plan-kv-span">
-                  <dt>送货地址</dt>
-                  <dd>{{ plan?.destinationAddress || plan?.destinationName || '—' }}</dd>
-                </div>
-                <div class="pl-plan-kv">
-                  <dt>送达时间</dt>
-                  <dd>{{ plan?.deliveryTime || '—' }}</dd>
-                </div>
-                <div class="pl-plan-kv">
-                  <dt>注意事项</dt>
-                  <dd>{{ plan?.destinationNotes || '—' }}</dd>
-                </div>
-              </dl>
+            <div class="party-panel">
+              <div class="party-panel__title">送货信息</div>
+              <ViewKvTable :fields="deliveryViewFields" mode="quad" />
             </div>
           </div>
 
           <h4 class="pl-plan-subtitle">运费信息</h4>
-          <dl class="pl-plan-kv-grid">
-            <div class="pl-plan-kv">
-              <dt>是否有运输费用</dt>
-              <dd>{{ getPlYesNoLabel(plan?.hasFreight) }}</dd>
-            </div>
-            <template v-if="plan?.hasFreight === 1">
-              <div class="pl-plan-kv">
-                <dt>运输公里数</dt>
-                <dd>{{ displayDistanceKm }}</dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>运费计算方式</dt>
-                <dd>{{ getPlFreightTypeLabel(displayFreightType) }}</dd>
-              </div>
-              <div v-if="displayFreightType === 'by_weight'" class="pl-plan-kv">
-                <dt>运输量(吨)</dt>
-                <dd>{{ displayTransportQty }}</dd>
-              </div>
-              <div v-if="displayFreightType === 'by_weight'" class="pl-plan-kv">
-                <dt>运输单价(元/吨)</dt>
-                <dd class="money-display">{{ displayFreightPrice }}</dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>运费金额(元)</dt>
-                <dd class="money-display">{{ displayFreightAmount }}</dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>是否开票</dt>
-                <dd>{{ getPlYesNoLabel(transport?.hasInvoice) }}</dd>
-              </div>
-            </template>
-          </dl>
+          <ViewKvTable :fields="freightViewFields" mode="quad" />
 
           <h4 class="pl-plan-subtitle">司机回传信息</h4>
           <p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
           <template v-else>
-            <dl class="pl-plan-kv-grid">
-              <div class="pl-plan-kv">
-                <dt>回传时间</dt>
-                <dd>{{ displayFeedbackTime }}</dd>
-              </div>
-              <div v-if="transport?.feedbackQty != null" class="pl-plan-kv">
-                <dt>回传数量</dt>
-                <dd>{{ transport?.feedbackQty }}</dd>
-              </div>
-              <div class="pl-plan-kv pl-plan-kv-span">
-                <dt>回传备注</dt>
-                <dd class="pl-plan-view-remark">{{ transport?.feedbackRemark || '—' }}</dd>
-              </div>
-              <div class="pl-plan-kv pl-plan-kv-span">
-                <dt>回传附件</dt>
-                <dd>
-                  <AttachmentUploadField
-                    v-if="feedbackImageCsv"
-                    :model-value="feedbackImageCsv"
-                    disabled
-                  />
-                  <span v-else>—</span>
-                </dd>
-              </div>
-            </dl>
+            <ViewKvTable :fields="driverFeedbackViewFields" mode="quad" />
+            <div v-if="feedbackImageCsv" class="pl-plan-feedback-attachments">
+              <span class="pl-plan-feedback-attachments__label">回传附件</span>
+              <AttachmentUploadField :model-value="feedbackImageCsv" disabled />
+            </div>
           </template>
 
           <h4 class="pl-plan-subtitle">地磅单信息</h4>
@@ -364,122 +273,85 @@
 
       <el-tab-pane label="合同信息" name="contract">
         <section class="pl-plan-form-section">
-          <h3 class="pl-plan-form-section-title">合同信息</h3>
           <p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">暂无关联合同信息</p>
           <template v-else>
-            <dl class="pl-plan-kv-grid">
-              <div class="pl-plan-kv">
-                <dt>合同编号</dt>
-                <dd>{{ contractSnapshot.contractNo || '—' }}</dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>合同名称</dt>
-                <dd>{{ contractSnapshot.contractName || '—' }}</dd>
+            <div class="pl-plan-form-section-head">
+              <h3 class="pl-plan-form-section-title">基本信息</h3>
+              <div class="pl-plan-form-section-actions">
+                <el-button class="btn-ghost" @click="handleViewContract">
+                  <el-icon><View /></el-icon>查看合同
+                </el-button>
               </div>
-              <div class="pl-plan-kv">
-                <dt>签订日期</dt>
-                <dd>{{ contractSnapshot.signDate || '—' }}</dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>合同有效期</dt>
-                <dd
-                  >{{ contractSnapshot.startDate || '—' }} ~
-                  {{ contractSnapshot.endDate || '—' }}</dd
-                >
-              </div>
-              <div class="pl-plan-kv">
-                <dt>结算方式</dt>
-                <dd>
-                  <DictTag
-                    v-if="contractSnapshot.settlementMethod"
-                    dict-type="crm_settlement_method"
-                    :value="contractSnapshot.settlementMethod"
-                  />
-                  <span v-else>—</span>
-                </dd>
-              </div>
-              <div class="pl-plan-kv">
-                <dt>合同金额</dt>
-                <dd class="money-display">{{ formatMoneyDisplay(contractSnapshot.amount) }}</dd>
-              </div>
-            </dl>
+            </div>
+            <ViewKvTable :fields="contractBasicViewFields" mode="triple" />
 
-            <div class="pl-plan-party-row">
-              <div class="pl-plan-party-block">
-                <h4 class="pl-plan-party-title">供方信息</h4>
-                <dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
-                  <div class="pl-plan-kv">
-                    <dt>名称</dt>
-                    <dd>{{ contractSnapshot.supplierName || contractSnapshot.partyA || '—' }}</dd>
-                  </div>
-                  <div class="pl-plan-kv">
-                    <dt>联系人</dt>
-                    <dd>{{ contractSnapshot.partyAContact || '—' }}</dd>
-                  </div>
-                  <div class="pl-plan-kv">
-                    <dt>电话</dt>
-                    <dd>{{ contractSnapshot.partyAMobile || '—' }}</dd>
-                  </div>
-                  <div class="pl-plan-kv pl-plan-kv-span">
-                    <dt>地址</dt>
-                    <dd>{{ contractSnapshot.partyAAddress || '—' }}</dd>
-                  </div>
-                </dl>
+            <h4 class="pl-plan-subtitle">供需双方</h4>
+            <div class="parties-row">
+              <div class="party-panel">
+                <div class="party-panel__title">供方信息</div>
+                <ViewKvTable :fields="contractSupplierViewFields" mode="dual" />
               </div>
 
-              <div class="pl-plan-flow-bridge" aria-hidden="true">
-                <el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
+              <div class="party-flow-bridge" aria-hidden="true">
+                <span class="party-flow-bridge__label">供货</span>
+                <span class="party-flow-bridge__arrow">
+                  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                    <path
+                      d="M5 12h11.5"
+                      stroke="currentColor"
+                      stroke-width="2.75"
+                      stroke-linecap="round"
+                    />
+                    <path
+                      d="M13.5 8.5 18.5 12l-5 3.5"
+                      stroke="currentColor"
+                      stroke-width="2.75"
+                      stroke-linecap="round"
+                      stroke-linejoin="round"
+                    />
+                  </svg>
+                </span>
               </div>
 
-              <div class="pl-plan-party-block">
-                <h4 class="pl-plan-party-title">需方信息</h4>
-                <dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
-                  <div class="pl-plan-kv">
-                    <dt>名称</dt>
-                    <dd>{{ contractSnapshot.customerName || contractSnapshot.partyB || '—' }}</dd>
-                  </div>
-                  <div class="pl-plan-kv">
-                    <dt>联系人</dt>
-                    <dd>{{
-                      contractSnapshot.partyBContact || contractSnapshot.contactName || '—'
-                    }}</dd>
-                  </div>
-                  <div class="pl-plan-kv">
-                    <dt>电话</dt>
-                    <dd>{{
-                      contractSnapshot.partyBMobile || contractSnapshot.contactMobile || '—'
-                    }}</dd>
-                  </div>
-                  <div class="pl-plan-kv pl-plan-kv-span">
-                    <dt>地址</dt>
-                    <dd>{{ contractSnapshot.partyBAddress || '—' }}</dd>
-                  </div>
-                </dl>
+              <div class="party-panel">
+                <div class="party-panel__title">需方信息</div>
+                <ViewKvTable :fields="contractBuyerViewFields" mode="dual" />
               </div>
             </div>
 
             <h4 class="pl-plan-subtitle">合同产品预览</h4>
-            <PlPlanFormProductTable :products="productRows" />
+            <PlPlanFormProductTable :products="contractProductRows" />
           </template>
         </section>
       </el-tab-pane>
     </el-tabs>
+
+    <ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
+    <PlVehicleViewDialog
+      v-model:visible="vehicleViewVisible"
+      :vehicle-id="plan?.vehicleId"
+      :bundle="vehicleBundle"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-  import { Right } from '@element-plus/icons-vue'
+  import { View } from '@element-plus/icons-vue'
   import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
   import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
-  import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
-  import DictTag from '@/components/DictTag/index.vue'
+  import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
+  import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
+  import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
+  import type { ViewField } from '@/components/business/system-view-dialog/types'
   import {
     businessTypeTagType,
     inventoryStatusTagType
   } from '@/components/business/db/db-related-view'
-  import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
+  import type { ContractVO } from '@/api/crm/contract/types'
+  import { mapContractProducts, type PlanFormProductRow } from '@/composables/usePlContractToPlan'
   import { mapPlanProductToFormRow, usePlPlanViewDetail } from '@/composables/usePlPlanViewDetail'
   import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
+  import { useDictStore } from '@/store/modules/dict'
   import { formatDateTime } from '@/utils/format'
   import {
     getPlFreightTypeLabel,
@@ -491,8 +363,14 @@
   import { formatMoneyDisplay } from '@/utils/money-format'
   import type { Transport } from '@/api/pl'
 
+  const ContractDetailDialog = defineAsyncComponent(
+    () => import('@/views/crm/contract/components/ContractDetailDialog.vue')
+  )
+
   const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
 
+  const dictStore = useDictStore()
+
   const props = withDefaults(
     defineProps<{
       planId: string
@@ -502,6 +380,9 @@
   )
 
   const activeTab = ref('overview')
+  const contractDetailDialogActive = ref(false)
+  const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
+  const vehicleViewVisible = ref(false)
   const {
     loading,
     plan,
@@ -565,12 +446,151 @@
     return amount ? formatMoneyDisplay(Number(amount)) : '—'
   })
 
-  const productRows = computed<PlanFormProductRow[]>(() =>
-    products.value.map((item, index) => mapPlanProductToFormRow(item, index))
+  function mergeTaxRatesFromContract(
+    rows: PlanFormProductRow[],
+    contract: ContractVO | null
+  ): PlanFormProductRow[] {
+    if (!contract?.products?.length) return rows
+    const rateByProductId = new Map<string, number>()
+    for (const item of contract.products) {
+      const key = String(item.productId ?? item.id ?? '')
+      if (key && item.taxRate != null) rateByProductId.set(key, Number(item.taxRate))
+    }
+    return rows.map((row) => ({
+      ...row,
+      taxRate: row.taxRate ?? rateByProductId.get(row.productId)
+    }))
+  }
+
+  const overviewBasicViewFields = computed<ViewField[]>(() => [
+    { label: '计划编号', value: plan.value?.planNo },
+    { label: '计划日期', value: plan.value?.planDate },
+    { label: '归属公司', value: getPlPlanCompanyLabel(plan.value?.belongsCompany) },
+    { label: '单据类型', value: getPlPlanTypeLabel(plan.value?.planType || '') },
+    { label: '跟进人', value: plan.value?.followUserName }
+  ])
+
+  const summaryCarrierName = computed(() => displayCarrierName.value)
+
+  const pickupViewFields = computed<ViewField[]>(() => [
+    { label: '取货地址', value: plan.value?.pickupAddress, span: 'full' },
+    { label: '取货时间', value: plan.value?.pickupTime },
+    { label: '注意事项', value: plan.value?.pickupNotes, span: 'full' }
+  ])
+
+  const deliveryViewFields = computed<ViewField[]>(() => [
+    {
+      label: '送货地址',
+      value: plan.value?.destinationAddress || plan.value?.destinationName,
+      span: 'full'
+    },
+    { label: '送达时间', value: plan.value?.deliveryTime },
+    { label: '注意事项', value: plan.value?.destinationNotes, span: 'full' }
+  ])
+
+  const freightViewFields = computed<ViewField[]>(() => {
+    const fields: ViewField[] = [
+      { label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) }
+    ]
+    if (plan.value?.hasFreight !== 1) return fields
+
+    fields.push(
+      { label: '运输公里数', value: displayDistanceKm.value },
+      { label: '运费计算方式', value: getPlFreightTypeLabel(displayFreightType.value) }
+    )
+    if (displayFreightType.value === 'by_weight') {
+      fields.push(
+        { label: '运输量(吨)', value: displayTransportQty.value },
+        { label: '运输单价(元/吨)', value: displayFreightPrice.value, money: true }
+      )
+    }
+    fields.push(
+      { label: '运费金额(元)', value: displayFreightAmount.value, money: true },
+      { label: '是否开票', value: getPlYesNoLabel(transport.value?.hasInvoice) }
+    )
+    return fields
+  })
+
+  const driverFeedbackViewFields = computed<ViewField[]>(() => {
+    const fields: ViewField[] = [{ label: '回传时间', value: displayFeedbackTime.value }]
+    if (transport.value?.feedbackQty != null) {
+      fields.push({ label: '回传数量', value: transport.value.feedbackQty })
+    }
+    fields.push({ label: '回传备注', value: transport.value?.feedbackRemark, span: 'full' })
+    return fields
+  })
+
+  const contractBasicViewFields = computed<ViewField[]>(() => {
+    const c = contractSnapshot.value
+    if (!c) return []
+    const settlementLabel = c.settlementMethod
+      ? dictStore.getLabel('crm_settlement_method', c.settlementMethod)
+      : '—'
+    return [
+      { label: '合同编号', value: c.contractNo, long: false },
+      { label: '合同名称', value: c.contractName, long: false },
+      { label: '签订日期', value: c.signDate },
+      {
+        label: '合同有效期',
+        value: c.startDate && c.endDate ? `${c.startDate} ~ ${c.endDate}` : '—',
+        long: false
+      },
+      { label: '结算方式', value: settlementLabel },
+      { label: '合同金额(元)', value: formatMoneyDisplay(c.amount), money: true }
+    ]
+  })
+
+  const contractSupplierViewFields = computed<ViewField[]>(() => {
+    const c = contractSnapshot.value
+    if (!c) return []
+    return [
+      { label: '单位名称(章)', value: c.partyA },
+      { label: '单位地址', value: c.partyAAddress },
+      { label: '法定代表人', value: c.partyALegalPerson },
+      { label: '代理人', value: c.partyAAgent },
+      { label: '电话/传真', value: c.partyAPhoneFax },
+      { label: '开户银行', value: c.partyABank },
+      { label: '账号', value: c.partyAAccount }
+    ]
+  })
+
+  const contractBuyerViewFields = computed<ViewField[]>(() => {
+    const c = contractSnapshot.value
+    if (!c) return []
+    return [
+      { label: '单位名称(章)', value: c.partyB },
+      { label: '单位地址', value: c.partyBAddress },
+      { label: '法定代表人', value: c.partyBLegalPerson },
+      { label: '代理人', value: c.partyBAgent },
+      { label: '电话/传真', value: c.partyBPhoneFax },
+      { label: '开户银行', value: c.partyBBank },
+      { label: '账号', value: c.partyBAccount }
+    ]
+  })
+
+  const contractProductRows = computed<PlanFormProductRow[]>(() =>
+    mapContractProducts(contractSnapshot.value?.products)
   )
 
+  const productRows = computed<PlanFormProductRow[]>(() => {
+    const rows = products.value.map((item, index) => mapPlanProductToFormRow(item, index))
+    return mergeTaxRatesFromContract(rows, contractSnapshot.value)
+  })
+
   const hazardProducts = computed(() => products.value.filter((item) => isHazardProduct(item)))
 
+  async function handleViewContract() {
+    const contractId = contractSnapshot.value?.id || plan.value?.contractId
+    if (!contractId) {
+      ElMessage.warning('暂无关联合同')
+      return
+    }
+
+    contractDetailDialogActive.value = true
+    await nextTick()
+    contractDetailRef.value?.open(contractId)
+  }
+
   watch(
     () => props.planId,
     async (planId) => {
@@ -622,6 +642,21 @@
     }
   }
 
+  .pl-plan-form-section-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+
+  .pl-plan-form-section-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
   .pl-plan-form-section-title {
     margin: 0 0 12px;
     padding-left: 10px;
@@ -632,6 +667,10 @@
     line-height: 1;
   }
 
+  .pl-plan-form-section-head .pl-plan-form-section-title {
+    margin-bottom: 0;
+  }
+
   .pl-plan-subtitle {
     margin: 16px 0 10px;
     font-size: 13px;
@@ -687,54 +726,106 @@
     grid-column: 1 / -1;
   }
 
-  .pl-plan-party-row,
-  .pl-plan-logistics-row {
+  .parties-row {
     display: flex;
     align-items: stretch;
     gap: 0;
-    margin-top: 12px;
+    margin-top: 4px;
+    min-height: 0;
   }
 
-  .pl-plan-party-row .pl-plan-party-block,
-  .pl-plan-logistics-row .pl-plan-logistics-block {
+  .party-panel {
     flex: 1 1 0;
     min-width: 0;
+    padding: 16px;
+    border: 1px solid var(--border, #d9e8e2);
+    border-radius: 10px;
+    background: #fff;
   }
 
-  .pl-plan-flow-bridge {
-    flex: 0 0 72px;
+  .party-panel__title {
+    margin-bottom: 12px;
+    padding-left: 10px;
+    font-size: 14px;
+    font-weight: 600;
+    color: var(--text, #1e293b);
+    line-height: 1.5;
+    border-left: 3px solid var(--primary, #10b981);
+  }
+
+  .party-flow-bridge {
+    flex: 0 0 88px;
     display: flex;
+    flex-direction: column;
     align-items: center;
     justify-content: center;
+    gap: 10px;
     padding: 0 8px;
   }
 
-  .pl-plan-flow-bridge__arrow {
-    font-size: 28px;
-    color: #10b981;
+  .party-flow-bridge__label {
+    font-size: 12px;
+    font-weight: 600;
+    color: var(--primary-dark, #059669);
+    letter-spacing: 0.06em;
+    padding: 4px 10px;
+    border-radius: 999px;
+    background: #ecfdf5;
+    border: 1px solid #a7f3d0;
+    line-height: 1.2;
   }
 
-  .pl-plan-kv-grid-party {
-    grid-template-columns: repeat(4, minmax(0, 1fr));
+  .party-flow-bridge__arrow {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 44px;
+    height: 44px;
+    border-radius: 50%;
+    color: var(--primary-dark, #059669);
+    background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
+    border: 1px solid #a7f3d0;
+    box-shadow: 0 2px 8px rgba(16, 185, 129, 0.14);
+
+    svg {
+      width: 22px;
+      height: 22px;
+      display: block;
+    }
+  }
+
+  @media (max-width: 1100px) {
+    .parties-row {
+      flex-direction: column;
+      min-height: 0;
+    }
+
+    .party-flow-bridge {
+      flex: 0 0 auto;
+      flex-direction: row;
+      gap: 12px;
+      padding: 12px 0;
+    }
+
+    .party-flow-bridge__arrow {
+      transform: rotate(90deg);
+    }
   }
 
-  .pl-plan-logistics-block,
-  .pl-plan-party-block {
+  .pl-plan-feedback-attachments {
+    margin-top: 12px;
     padding: 12px;
     border: 1px solid #e2e8f0;
     border-radius: 8px;
     background: #f8fafc;
   }
 
-  .pl-plan-party-title {
-    margin: 0 0 10px;
+  .pl-plan-feedback-attachments__label {
+    display: block;
+    margin-bottom: 8px;
     font-size: 13px;
-    font-weight: 600;
-    color: #1e293b;
-  }
-
-  .pl-plan-vehicle-detail {
-    margin-top: 12px;
+    font-weight: 500;
+    color: #64748b;
   }
 
   .pl-plan-cert-block {

+ 155 - 0
web-admin/src/components/business/pl-vehicle-summary-strip/index.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="pl-vehicle-summary-strip">
+    <div class="pl-vehicle-summary-strip__content">
+      <div class="pl-vehicle-summary-group">
+        <span class="pl-vehicle-summary-label">车辆</span>
+        <span class="pl-vehicle-summary-value pl-vehicle-summary-value--strong">
+          {{ detail.licensePlate || '—' }}
+        </span>
+        <el-tag :type="getPlVehicleTypeTagType(detail.vehicleType)" size="small">
+          {{ getPlVehicleTypeLabel(detail.vehicleType) }}
+        </el-tag>
+        <span class="pl-vehicle-summary-value">{{ formatLoadCapacity(detail.loadCapacity) }}(载重)</span>
+        <span v-if="displayCarrierName" class="pl-vehicle-summary-value">
+          {{ displayCarrierName }}(承运商)
+        </span>
+      </div>
+
+      <span class="pl-vehicle-summary-divider" aria-hidden="true" />
+
+      <div class="pl-vehicle-summary-group">
+        <span class="pl-vehicle-summary-label">人员</span>
+        <span v-if="!personnelList.length" class="pl-vehicle-summary-value muted">暂无关联人员</span>
+        <template v-else>
+          <span
+            v-for="(person, index) in personnelList"
+            :key="person.personnelId || `${person.personnelName}-${index}`"
+            class="pl-vehicle-summary-person"
+          >
+            <el-tag :type="getRoleTagType(person.role)" size="small">{{ getRoleLabel(person.role) }}</el-tag>
+            <span class="pl-vehicle-summary-value">{{ person.personnelName || '—' }}</span>
+            <span v-if="person.phoneNo" class="pl-vehicle-summary-value muted">{{ person.phoneNo }}</span>
+          </span>
+        </template>
+      </div>
+    </div>
+
+    <el-button link type="primary" class="pl-vehicle-summary-view-btn" @click="emit('view')">
+      <el-icon><View /></el-icon>查看
+    </el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { View } from '@element-plus/icons-vue'
+import type { VehicleDetail } from '@/api/pl'
+import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
+import { getPlVehicleTypeLabel, getPlVehicleTypeTagType } from '@/utils/pl-display'
+
+const props = withDefaults(
+  defineProps<{
+    detail: VehicleDetail
+    personnelList?: VehiclePersonnelViewRow[]
+    /** 展示用承运商名称,优先于 detail.carrierName */
+    carrierName?: string
+  }>(),
+  {
+    personnelList: () => []
+  }
+)
+
+const displayCarrierName = computed(
+  () => props.carrierName || props.detail.carrierName || ''
+)
+
+function formatLoadCapacity(value?: number | string | null) {
+  if (value == null || value === '') return '— 吨'
+  const num = Number(value)
+  if (Number.isNaN(num)) return `${value} 吨`
+  return `${num.toFixed(2)} 吨`
+}
+
+const emit = defineEmits<{
+  view: []
+}>()
+
+function getRoleLabel(role: string) {
+  const map: Record<string, string> = {
+    driver: '司机',
+    escort: '押运员'
+  }
+  return map[role] || role
+}
+
+function getRoleTagType(role: string) {
+  return role === 'driver' ? 'primary' : 'warning'
+}
+</script>
+
+<style scoped lang="scss">
+.pl-vehicle-summary-strip {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-top: 12px;
+  padding: 10px 12px;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  background: #f8fafc;
+}
+
+.pl-vehicle-summary-strip__content {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px 14px;
+  flex: 1;
+  min-width: 0;
+}
+
+.pl-vehicle-summary-group {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  min-width: 0;
+}
+
+.pl-vehicle-summary-label {
+  font-size: 12px;
+  font-weight: 600;
+  color: #64748b;
+}
+
+.pl-vehicle-summary-value {
+  font-size: 13px;
+  color: #1e293b;
+
+  &.muted {
+    color: #64748b;
+  }
+
+  &--strong {
+    font-weight: 600;
+  }
+}
+
+.pl-vehicle-summary-person {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.pl-vehicle-summary-divider {
+  width: 1px;
+  height: 18px;
+  background: #cbd5e1;
+  flex-shrink: 0;
+}
+
+.pl-vehicle-summary-view-btn {
+  flex-shrink: 0;
+  margin: 0;
+  padding: 0 4px;
+}
+</style>

+ 138 - 0
web-admin/src/components/business/pl-vehicle-view-dialog/index.vue

@@ -0,0 +1,138 @@
+<!-- eslint-disable vue/no-v-model-argument -->
+<template>
+  <SystemViewDialog
+    v-model:visible="innerVisible"
+    :title="detail?.licensePlate || '车辆详情'"
+    :icon="Van"
+    :meta="heroMeta"
+    :sections="viewSections"
+    :loading="loading"
+    width="1040px"
+    modal-class="order-module-dialog-mask"
+    append-to-body
+  >
+    <template #vehicleDetailTabs>
+      <PlVehicleCertPersonnelTabs
+        :cert-list="detail?.certList || []"
+        :personnel-list="personnelList"
+      />
+    </template>
+  </SystemViewDialog>
+</template>
+
+<script setup lang="ts">
+import { Van } from '@element-plus/icons-vue'
+import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
+import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
+import SystemViewDialog, {
+  type ViewSection
+} from '@/components/business/system-view-dialog/index.vue'
+import {
+  loadVehicleViewBundle,
+  type VehicleViewBundle
+} from '@/composables/usePlVehicleViewDetail'
+import type { VehicleDetail } from '@/api/pl'
+import { formatDateTime } from '@/utils/format'
+import {
+  getPlVehicleStatusLabel,
+  getPlVehicleStatusTagType,
+  getPlVehicleTypeLabel,
+  getPlVehicleTypeTagType
+} from '@/utils/pl-display'
+
+const props = defineProps<{
+  visible: boolean
+  vehicleId?: string
+  bundle?: VehicleViewBundle | null
+}>()
+
+const emit = defineEmits<{
+  'update:visible': [value: boolean]
+}>()
+
+const innerVisible = computed({
+  get: () => props.visible,
+  set: value => emit('update:visible', value)
+})
+
+const loading = ref(false)
+const detail = ref<VehicleDetail | null>(null)
+const personnelList = ref<VehiclePersonnelViewRow[]>([])
+
+const heroMeta = computed(() => {
+  if (!detail.value) return ''
+  const parts = [
+    getPlVehicleTypeLabel(detail.value.vehicleType),
+    getPlVehicleStatusLabel(detail.value.vehicleStatus),
+    detail.value.carrierName || undefined
+  ].filter(Boolean)
+  return parts.join(' · ')
+})
+
+const viewSections = computed<ViewSection[]>(() => {
+  const d = detail.value
+  if (!d) return []
+
+  return [
+    {
+      title: '车辆信息',
+      fieldLayout: 'table',
+      tableMode: 'quad',
+      fields: [
+        { label: '车牌号', value: d.licensePlate },
+        {
+          label: '车辆类型',
+          value: getPlVehicleTypeLabel(d.vehicleType),
+          tagType: getPlVehicleTypeTagType(d.vehicleType)
+        },
+        { label: '承运商', value: d.carrierName || '—' },
+        { label: '载重(吨)', value: d.loadCapacity ?? '—' },
+        {
+          label: '车辆状态',
+          value: getPlVehicleStatusLabel(d.vehicleStatus),
+          tagType: getPlVehicleStatusTagType(d.vehicleStatus),
+          long: false
+        },
+        { label: '创建时间', value: formatDateTime(d.createTime), long: false },
+        { label: '关联人员摘要', value: d.personnelSummary || '—', long: false },
+        { label: '备注', value: d.remark || '—', long: false }
+      ],
+      slot: 'vehicleDetailTabs'
+    }
+  ]
+})
+
+watch(
+  () => [props.visible, props.vehicleId, props.bundle] as const,
+  async ([visible, vehicleId, bundle]) => {
+    if (!visible) {
+      detail.value = null
+      personnelList.value = []
+      return
+    }
+
+    if (bundle) {
+      detail.value = bundle.detail
+      personnelList.value = bundle.personnelList
+      return
+    }
+
+    if (!vehicleId) return
+
+    loading.value = true
+    detail.value = null
+    personnelList.value = []
+    try {
+      const loaded = await loadVehicleViewBundle(vehicleId)
+      detail.value = loaded.detail
+      personnelList.value = loaded.personnelList
+    } catch {
+      innerVisible.value = false
+      ElMessage.error('加载车辆详情失败')
+    } finally {
+      loading.value = false
+    }
+  },
+  { immediate: true }
+)
+</script>

+ 1 - 1
web-admin/src/composables/usePlContractToPlan.ts

@@ -48,7 +48,7 @@ export function mapContractProductToPlanRow(item: ContractProductForm, index: nu
     categoryName: item.categoryName,
     unit: item.unit || '吨',
     quantity: Number(item.quantity || 0),
-    actualQty: Number(item.quantity || 0),
+    actualQty: 0,
     packageSpec: item.packageSpec,
     deliveryMethod: item.deliveryMethod,
     taxRate: item.taxRate != null ? Number(item.taxRate) : undefined,

+ 25 - 4
web-admin/src/composables/usePlPlanViewDetail.ts

@@ -38,6 +38,7 @@ export function mapPlanProductToFormRow(item: PlanProduct, index: number): PlanF
     ),
     packageSpec: item.packageSpec,
     deliveryMethod: item.deliveryMethod,
+    taxRate: item.taxRate != null ? Number(item.taxRate) : undefined,
     taxPrice: item.taxPrice ?? item.inboundPrice ?? item.outboundPrice,
     taxSubtotal: item.taxSubtotal ?? item.inboundAmount ?? item.outboundAmount,
     taxAmount: item.taxAmount,
@@ -72,16 +73,26 @@ export function buildContractSnapshotFromPlan(data: Plan, rows: PlanFormProductR
     unreceivedAmount: 0,
     invoiceAmount: 0,
     uninvoiceAmount: 0,
-    partyA: data.partyA || '',
+    partyA: data.partyA || data.supplierName || '',
     partyAAddress: '',
     partyAContact: '',
     partyAMobile: '',
+    partyALegalPerson: '',
+    partyAAgent: '',
+    partyAPhoneFax: '',
+    partyABank: '',
+    partyAAccount: '',
     supplierName: data.supplierName,
     settlementMethod: data.settlementMethod,
-    partyB: data.partyB || '',
-    partyBAddress: data.destinationAddress || '',
+    partyB: data.partyB || data.customerName || '',
+    partyBAddress: '',
     partyBContact: data.contactName || '',
     partyBMobile: data.contactPhone || '',
+    partyBLegalPerson: '',
+    partyBAgent: '',
+    partyBPhoneFax: '',
+    partyBBank: '',
+    partyBAccount: '',
     paymentTerms: '',
     deliveryTerms: '',
     contractTerms: '',
@@ -93,6 +104,16 @@ export function buildContractSnapshotFromPlan(data: Plan, rows: PlanFormProductR
   }
 }
 
+function normalizeContractSnapshot(raw: unknown): ContractVO | null {
+  if (!raw || typeof raw !== 'object') return null
+  const payload = raw as Record<string, unknown>
+  const nested = payload.data
+  if (nested && typeof nested === 'object') {
+    return nested as ContractVO
+  }
+  return raw as ContractVO
+}
+
 export function usePlPlanViewDetail() {
   const loading = ref(false)
   const plan = ref<Plan | null>(null)
@@ -155,7 +176,7 @@ export function usePlPlanViewDetail() {
       if (data?.contractId) {
         try {
           const contractRes = await getContract(data.contractId)
-          contractSnapshot.value = (contractRes.data ?? contractRes) as ContractVO
+          contractSnapshot.value = normalizeContractSnapshot(contractRes.data ?? contractRes)
         } catch {
           contractSnapshot.value = buildContractSnapshotFromPlan(data, rows)
         }

+ 11 - 6
web-admin/src/composables/usePlToxicProductCertCheck.ts

@@ -55,24 +55,29 @@ export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduc
     ...new Set(
       products
         .filter(product => {
+          if (!product.productId) return false
           const flags = resolveProductHazardFlags(product)
-          return !flags.isToxic && !flags.isExplosive && product.productId
+          const needsHazard = !flags.isToxic && !flags.isExplosive
+          const needsTaxRate = product.taxRate == null
+          return needsHazard || needsTaxRate
         })
         .map(product => product.productId)
     )
   ]
   if (!needLookupIds.length) return products
 
-  const hazardMap = new Map<string, { isToxic: number; isExplosive: number }>()
+  const hazardMap = new Map<string, { isToxic: number; isExplosive: number; taxRate?: number }>()
   await Promise.all(
     needLookupIds.map(async productId => {
       try {
         const res = await fetchProductDetail({ id: productId })
         const data = (res as { data?: Record<string, unknown> }).data ?? res
         const row = data as Record<string, unknown>
+        const rawTaxRate = row.taxRate ?? row.tax_rate
         hazardMap.set(productId, {
           isToxic: Number(row.isToxic ?? row.is_toxic ?? 0),
-          isExplosive: Number(row.isExplosive ?? row.is_explosive ?? 0)
+          isExplosive: Number(row.isExplosive ?? row.is_explosive ?? 0),
+          taxRate: rawTaxRate != null && rawTaxRate !== '' ? Number(rawTaxRate) : undefined
         })
       } catch {
         // 单条产品查询失败时跳过
@@ -82,13 +87,13 @@ export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduc
 
   return products.map(product => {
     const flags = resolveProductHazardFlags(product)
-    if (flags.isToxic || flags.isExplosive) return product
     const fromCrm = hazardMap.get(product.productId)
     if (!fromCrm) return product
     return {
       ...product,
-      isToxic: fromCrm.isToxic,
-      isExplosive: fromCrm.isExplosive
+      isToxic: flags.isToxic ? product.isToxic : fromCrm.isToxic,
+      isExplosive: flags.isExplosive ? product.isExplosive : fromCrm.isExplosive,
+      taxRate: product.taxRate ?? fromCrm.taxRate
     }
   })
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików