zlwu пре 3 месеци
родитељ
комит
b4a49183db
33 измењених фајлова са 2603 додато и 827 уклоњено
  1. 1 1
      backend/app.pid
  2. 4 1
      backend/app/api/pl/__init__.py
  3. 8 0
      backend/app/api/pl/plan.py
  4. 4 0
      backend/app/api/pl/public_transport.py
  5. 6 0
      backend/app/api/pl/transport.py
  6. 28 0
      backend/app/models/pl_models.py
  7. 13 4
      backend/app/schemas/pl/plan.py
  8. 11 2
      backend/app/schemas/pl/transport.py
  9. 3 13
      backend/graphify-out/.graphify_labels.json
  10. 179 229
      backend/graphify-out/GRAPH_REPORT.md
  11. 0 0
      backend/graphify-out/cache/stat-index.json
  12. 112 112
      backend/graphify-out/graph.json
  13. 36 0
      docs/sql/shanxia2.0.sql
  14. 80 35
      graphify-out/manifest.json
  15. 3 8
      web-admin/graphify-out/.graphify_labels.json
  16. 237 235
      web-admin/graphify-out/GRAPH_REPORT.md
  17. 112 112
      web-admin/graphify-out/graph.json
  18. 58 0
      web-admin/src/api/pl/index.ts
  19. 4 0
      web-admin/src/api/pl/public-transport.ts
  20. 427 0
      web-admin/src/components/business/amap-address-picker/index.vue
  21. 89 0
      web-admin/src/components/business/pl-address-with-library/index.vue
  22. 83 57
      web-admin/src/components/business/pl-plan-archive-dialog/index.vue
  23. 33 2
      web-admin/src/components/business/pl-plan-arrange-dialog/index.vue
  24. 34 7
      web-admin/src/components/business/pl-plan-form-dialog/index.vue
  25. 1 1
      web-admin/src/components/business/pl-plan-view-tabs/index.vue
  26. 238 0
      web-admin/src/components/business/pl-transport-address-select-dialog/index.vue
  27. 39 8
      web-admin/src/composables/usePlPlanArrangeActions.ts
  28. 9 0
      web-admin/src/env.d.ts
  29. 7 0
      web-admin/src/router/modules/pl.ts
  30. 168 0
      web-admin/src/utils/amap/loader.ts
  31. 20 0
      web-admin/src/utils/amap/navigation.ts
  32. 497 0
      web-admin/src/views/pl/transport-address/index.vue
  33. 59 0
      web-admin/src/views/public/pl/transport/index.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-8708
+8001

+ 4 - 1
backend/app/api/pl/__init__.py

@@ -2,13 +2,16 @@
 from fastapi import APIRouter
 
 from app.api.pl import plan, transport, freight, inventory
-from app.api.pl import base_data, vehicle_recharge, vehicle_maintenance, vehicle_urea
+from app.api.pl import base_data, vehicle_recharge, vehicle_maintenance, vehicle_urea, transport_address
 
 pl_router = APIRouter(prefix="/transport", tags=["运输计划管理"])
 
 # 基础数据(承运商、车辆、司机)
 pl_router.include_router(base_data.router)
 
+# 运输通用地址库
+pl_router.include_router(transport_address.router)
+
 # 计划管理
 pl_router.include_router(plan.router)
 

+ 8 - 0
backend/app/api/pl/plan.py

@@ -303,9 +303,13 @@ def create_plan(
         settlement_method=payload.settlement_method,
         has_freight=payload.has_freight,
         pickup_address=payload.pickup_address,
+        pickup_lng=payload.pickup_lng,
+        pickup_lat=payload.pickup_lat,
         pickup_time=payload.pickup_time,
         pickup_notes=payload.pickup_notes,
         destination_address=payload.destination_address,
+        destination_lng=payload.destination_lng,
+        destination_lat=payload.destination_lat,
         delivery_time=payload.delivery_time,
         destination_notes=payload.destination_notes,
         follow_user_id=payload.follow_user_id or user.id,
@@ -380,9 +384,13 @@ def create_plan_full(
         settlement_method=payload.settlement_method,
         has_freight=payload.has_freight,
         pickup_address=payload.pickup_address,
+        pickup_lng=payload.pickup_lng,
+        pickup_lat=payload.pickup_lat,
         pickup_time=payload.pickup_time,
         pickup_notes=payload.pickup_notes,
         destination_address=payload.destination_address,
+        destination_lng=payload.destination_lng,
+        destination_lat=payload.destination_lat,
         delivery_time=payload.delivery_time,
         destination_notes=payload.destination_notes,
         total_amount=calc_plan_total_amount(payload.products or []),

+ 4 - 0
backend/app/api/pl/public_transport.py

@@ -113,9 +113,13 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
         plan_date=_format_plan_date(plan.plan_date if plan else None),
         plan_type=plan.plan_type if plan else "",
         pickup_address=plan.pickup_address if plan else transport.start_point,
+        pickup_lng=plan.pickup_lng if plan else None,
+        pickup_lat=plan.pickup_lat if plan else None,
         destination_address=(
             (plan.destination_address or plan.destination_name) if plan else transport.end_point
         ),
+        destination_lng=plan.destination_lng if plan else None,
+        destination_lat=plan.destination_lat if plan else None,
         license_plate=transport.license_plate,
         driver_name=transport.driver_name,
         driver_phone=transport.driver_phone,

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

@@ -246,6 +246,9 @@ def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange)
         plan.pickup_address = payload.pickup_address
     elif payload.start_point is not None:
         plan.pickup_address = payload.start_point
+    if "pickup_lng" in payload.model_fields_set or "pickup_lat" in payload.model_fields_set:
+        plan.pickup_lng = payload.pickup_lng
+        plan.pickup_lat = payload.pickup_lat
     if payload.pickup_time is not None:
         plan.pickup_time = payload.pickup_time
     if payload.pickup_notes is not None:
@@ -254,6 +257,9 @@ def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange)
         plan.destination_address = payload.destination_address
     elif payload.end_point is not None:
         plan.destination_address = payload.end_point
+    if "destination_lng" in payload.model_fields_set or "destination_lat" in payload.model_fields_set:
+        plan.destination_lng = payload.destination_lng
+        plan.destination_lat = payload.destination_lat
     if payload.delivery_time is not None:
         plan.delivery_time = payload.delivery_time
     if payload.destination_notes is not None:

+ 28 - 0
backend/app/models/pl_models.py

@@ -102,6 +102,30 @@ class PlPersonnel(Base):
     del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
 
 
+class PlTransportAddress(Base):
+    """运输通用地址库 pl_transport_address"""
+
+    __tablename__ = "pl_transport_address"
+
+    id = Column(String(64), primary_key=True, comment="主键ID")
+    address_name = Column(String(200), nullable=False, comment="地址名称")
+    full_address = Column(String(500), nullable=False, comment="详细地址")
+    lng = Column(DECIMAL(10, 6), nullable=False, comment="经度")
+    lat = Column(DECIMAL(10, 6), nullable=False, comment="纬度")
+    contact_name = Column(String(100), nullable=True, comment="联系人")
+    contact_phone = Column(String(32), nullable=True, comment="联系电话")
+    status = Column(CHAR(1), nullable=False, default="1", comment="状态 1-启用 0-停用")
+    tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
+    order_num = Column(Integer, nullable=True, comment="排序号")
+    remark = Column(String(512), nullable=True, comment="备注")
+    create_by = Column(String(64), nullable=True, comment="创建人id")
+    create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
+    create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
+    update_by = Column(String(64), nullable=True, comment="更新人id")
+    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
+    del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
+
+
 class PlCert(Base):
     """证书表 pl_cert"""
 
@@ -248,9 +272,13 @@ class PlPlan(Base):
     # 运输信息
     has_freight = Column(SmallInteger, nullable=False, default=0, comment="是否有运输费用 0-否 1-是")
     pickup_address = Column(String(500), nullable=True, comment="取货地址")
+    pickup_lng = Column(DECIMAL(10, 6), nullable=True, comment="取货经度")
+    pickup_lat = Column(DECIMAL(10, 6), nullable=True, comment="取货纬度")
     pickup_time = Column(DateTime, nullable=True, comment="取货时间")
     pickup_notes = Column(String(500), nullable=True, comment="取货注意事项")
     destination_address = Column(String(500), nullable=True, comment="目的地地址")
+    destination_lng = Column(DECIMAL(10, 6), nullable=True, comment="送货经度")
+    destination_lat = Column(DECIMAL(10, 6), nullable=True, comment="送货纬度")
     delivery_time = Column(DateTime, nullable=True, comment="送达时间")
     destination_notes = Column(String(500), nullable=True, comment="目的地注意事项")
     transport_id = Column(String(64), nullable=True, comment="运输ID")

+ 13 - 4
backend/app/schemas/pl/plan.py

@@ -5,6 +5,7 @@ from typing import Optional
 
 from pydantic import BaseModel, Field
 
+from app.schemas.pl.datetime_parse import OptionalPlanDateTime
 from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
 
 # =============================================================================
@@ -105,10 +106,14 @@ class PlanBase(BaseModel):
     settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod", description="结算方式")
     has_freight: int = Field(default=0, alias="hasFreight", description="是否有运输费用")
     pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress", description="取货地址")
-    pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime", description="取货时间")
+    pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度")
+    pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度")
+    pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间")
     pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes", description="取货注意事项")
     destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress", description="目的地地址")
-    delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime", description="送达时间")
+    destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度")
+    destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度")
+    delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间")
     destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes", description="目的地注意事项")
     notify_message: Optional[str] = Field(default=None, alias="notifyMessage", description="通知信息")
     follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID")
@@ -158,10 +163,14 @@ class PlanUpdate(BaseModel):
     settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod")
     has_freight: Optional[int] = Field(default=None, alias="hasFreight")
     pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress")
-    pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
+    pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng")
+    pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat")
+    pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime")
     pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes")
     destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress")
-    delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
+    destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng")
+    destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat")
+    delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime")
     destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes")
     notify_message: Optional[str] = Field(default=None, alias="notifyMessage")
     follow_user_id: Optional[str] = Field(default=None, alias="followUserId")

+ 11 - 2
backend/app/schemas/pl/transport.py

@@ -5,6 +5,7 @@ from typing import Optional
 
 from pydantic import BaseModel, Field
 
+from app.schemas.pl.datetime_parse import OptionalPlanDateTime
 from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
 
 
@@ -48,10 +49,14 @@ class TransportArrange(BaseModel):
     carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID")
     transport_medium: Optional[str] = Field(default=None, alias="transportMedium", description="运输介质")
     pickup_address: Optional[str] = Field(default=None, alias="pickupAddress", description="取货地址")
-    pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime", description="取货时间")
+    pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度")
+    pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度")
+    pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间")
     pickup_notes: Optional[str] = Field(default=None, alias="pickupNotes", description="取货注意事项")
     destination_address: Optional[str] = Field(default=None, alias="destinationAddress", description="送货地址")
-    delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime", description="送达时间")
+    destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度")
+    destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度")
+    delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间")
     destination_notes: Optional[str] = Field(default=None, alias="destinationNotes", description="送货注意事项")
     start_point: Optional[str] = Field(default=None, alias="startPoint", description="运输起点(兼容,等同取货地址)")
     end_point: Optional[str] = Field(default=None, alias="endPoint", description="运输终点(兼容,等同送货地址)")
@@ -191,7 +196,11 @@ class PublicTransportPageRead(BaseModel):
     plan_date: Optional[str] = Field(default=None, alias="planDate")
     plan_type: str = Field(alias="planType")
     pickup_address: Optional[str] = Field(default=None, alias="pickupAddress")
+    pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng")
+    pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat")
     destination_address: Optional[str] = Field(default=None, alias="destinationAddress")
+    destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng")
+    destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat")
     license_plate: Optional[str] = Field(default=None, alias="licensePlate")
     driver_name: Optional[str] = Field(default=None, alias="driverName")
     driver_phone: Optional[str] = Field(default=None, alias="driverPhone")

+ 3 - 13
backend/graphify-out/.graphify_labels.json

@@ -108,10 +108,8 @@
   "106": "Community 106",
   "107": "Community 107",
   "108": "Community 108",
-  "109": "Community 109",
   "110": "Community 110",
   "111": "Community 111",
-  "112": "Community 112",
   "113": "Community 113",
   "114": "Community 114",
   "115": "Community 115",
@@ -121,32 +119,24 @@
   "119": "Community 119",
   "120": "Community 120",
   "121": "Community 121",
-  "122": "Community 122",
-  "123": "Community 123",
-  "124": "Community 124",
   "125": "Community 125",
-  "126": "Community 126",
   "127": "Community 127",
   "128": "Community 128",
-  "129": "Community 129",
   "130": "Community 130",
   "131": "Community 131",
   "132": "Community 132",
-  "133": "Community 133",
   "134": "Community 134",
   "135": "Community 135",
   "136": "Community 136",
-  "137": "Community 137",
   "138": "Community 138",
-  "139": "Community 139",
   "140": "Community 140",
   "141": "Community 141",
   "142": "Community 142",
   "143": "Community 143",
   "144": "Community 144",
   "145": "Community 145",
-  "146": "Community 146",
-  "147": "Community 147",
   "148": "Community 148",
-  "150": "Community 150"
+  "149": "Community 149",
+  "150": "Community 150",
+  "151": "Community 151"
 }

+ 179 - 229
backend/graphify-out/GRAPH_REPORT.md

@@ -1,16 +1,16 @@
 # Graph Report - backend  (2026-07-01)
 
 ## Corpus Check
-- 226 files · ~158,852 words
+- 229 files · ~159,883 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 5033 nodes · 34891 edges · 150 communities (148 shown, 2 thin omitted)
-- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 17194 edges (avg confidence: 0.5)
+- 5097 nodes · 35085 edges · 140 communities (138 shown, 2 thin omitted)
+- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 17242 edges (avg confidence: 0.5)
 - Token cost: 0 input · 0 output
 
 ## Graph Freshness
-- Built from commit: `7c838cfb`
+- Built from commit: `8e70047f`
 - Run `git rev-parse HEAD` and compare to check if the graph is stale.
 - Run `graphify update .` after code changes (no API cost).
 
@@ -124,46 +124,36 @@
 - [[_COMMUNITY_Community 106|Community 106]]
 - [[_COMMUNITY_Community 107|Community 107]]
 - [[_COMMUNITY_Community 108|Community 108]]
-- [[_COMMUNITY_Community 109|Community 109]]
 - [[_COMMUNITY_Community 110|Community 110]]
 - [[_COMMUNITY_Community 111|Community 111]]
-- [[_COMMUNITY_Community 112|Community 112]]
-- [[_COMMUNITY_Community 122|Community 122]]
-- [[_COMMUNITY_Community 123|Community 123]]
-- [[_COMMUNITY_Community 124|Community 124]]
 - [[_COMMUNITY_Community 125|Community 125]]
-- [[_COMMUNITY_Community 126|Community 126]]
 - [[_COMMUNITY_Community 127|Community 127]]
 - [[_COMMUNITY_Community 128|Community 128]]
-- [[_COMMUNITY_Community 129|Community 129]]
 - [[_COMMUNITY_Community 130|Community 130]]
 - [[_COMMUNITY_Community 131|Community 131]]
 - [[_COMMUNITY_Community 132|Community 132]]
-- [[_COMMUNITY_Community 133|Community 133]]
 - [[_COMMUNITY_Community 134|Community 134]]
 - [[_COMMUNITY_Community 135|Community 135]]
 - [[_COMMUNITY_Community 136|Community 136]]
-- [[_COMMUNITY_Community 137|Community 137]]
 - [[_COMMUNITY_Community 138|Community 138]]
-- [[_COMMUNITY_Community 139|Community 139]]
 - [[_COMMUNITY_Community 140|Community 140]]
 - [[_COMMUNITY_Community 141|Community 141]]
 - [[_COMMUNITY_Community 143|Community 143]]
 - [[_COMMUNITY_Community 144|Community 144]]
 - [[_COMMUNITY_Community 145|Community 145]]
-- [[_COMMUNITY_Community 146|Community 146]]
-- [[_COMMUNITY_Community 147|Community 147]]
 - [[_COMMUNITY_Community 148|Community 148]]
+- [[_COMMUNITY_Community 149|Community 149]]
 - [[_COMMUNITY_Community 150|Community 150]]
+- [[_COMMUNITY_Community 151|Community 151]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `Session` - 1123 edges
-2. `BaseModel` - 420 edges
+2. `BaseModel` - 423 edges
 3. `int` - 336 edges
-4. `Any` - 331 edges
-5. `get_tenant_no()` - 327 edges
-6. `SysUser` - 282 edges
-7. `DbUserContext` - 272 edges
+4. `get_tenant_no()` - 334 edges
+5. `Any` - 331 edges
+6. `DbUserContext` - 282 edges
+7. `SysUser` - 282 edges
 8. `success_response()` - 259 edges
 9. `Session` - 257 edges
 10. `bool` - 255 edges
@@ -180,135 +170,135 @@
 - `int` --uses--> `LocalStorage`  [INFERRED]
   scripts/run_db_auto_generate_plans.py → app/core/storage.py
 
-## Communities (150 total, 2 thin omitted)
+## Communities (140 total, 2 thin omitted)
 
 ### Community 0 - "Community 0"
 Cohesion: 0.14
-Nodes (103): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementUpdate (+95 more)
+Nodes (105): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementCreate (+97 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.04
-Nodes (46): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+38 more)
+Cohesion: 0.08
+Nodes (26): 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo (+18 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.35
-Nodes (219): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+211 more)
+Nodes (220): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+212 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.07
-Nodes (171): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+163 more)
+Nodes (170): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+162 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.13
-Nodes (29): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, _apply_list_filters(), create_supplier_info() (+21 more)
+Cohesion: 0.08
+Nodes (41): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水 (+33 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.08
-Nodes (39): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+31 more)
+Cohesion: 0.07
+Nodes (41): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+33 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.14
 Nodes (39): Any, bool, int, Session, str, SysUser, create_user(), get_user() (+31 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.06
-Nodes (33): BaseModel, AttachmentOut, ReimburseApplyCreate, ReimburseApplyUpdate, ReimburseApprovalCreate, _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate (+25 more)
+Cohesion: 0.33
+Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
 
 ### Community 8 - "Community 8"
 Cohesion: 0.35
 Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
 
 ### Community 9 - "Community 9"
-Cohesion: 0.19
-Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
+Cohesion: 0.14
+Nodes (34): Session, str, Session, str, user_info_from_context(), parse_date_str(), plan_to_read(), plan_to_read_with_fill_link() (+26 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.03
-Nodes (151): register_quote_routes(), Any, bool, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), datetime, Decimal, int, Session (+143 more)
+Nodes (185): register_quote_routes(), CrmContractProductPriceTier, Query, Any, bool, int, Session, str (+177 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.15
-Nodes (38): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+30 more)
+Cohesion: 0.16
+Nodes (26): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+18 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.12
-Nodes (84): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+76 more)
+Cohesion: 0.10
+Nodes (92): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+84 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.05
-Nodes (72): BxUserContext, Session, str, UploadFile, Any, bool, bytes, int (+64 more)
+Cohesion: 0.13
+Nodes (8): bool, Path, str, BaseStorage, LocalStorage, OSSStorage, QiniuStorage, Path
 
 ### Community 14 - "Community 14"
 Cohesion: 0.09
-Nodes (55): Any, bool, datetime, int, Session, str, _advance_contract_after_approval(), approve_contract() (+47 more)
+Nodes (58): Any, bool, CrmContract, datetime, int, Query, Session, str (+50 more)
 
 ### Community 15 - "Community 15"
 Cohesion: 0.14
-Nodes (28): str, T, bool, date, datetime, int, Session, str (+20 more)
+Nodes (29): str, T, bool, date, datetime, int, Session, str (+21 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.07
-Nodes (103): Any, CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), Decimal, int, Session (+95 more)
+Cohesion: 0.03
+Nodes (201): register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, date (+193 more)
 
 ### Community 17 - "Community 17"
 Cohesion: 0.18
-Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
+Nodes (55): Any, date, Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal (+47 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.18
-Nodes (19): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+11 more)
+Cohesion: 0.14
+Nodes (31): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+23 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.21
-Nodes (27): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields() (+19 more)
+Cohesion: 0.23
+Nodes (25): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+17 more)
 
 ### Community 20 - "Community 20"
-Cohesion: 0.22
-Nodes (47): BxUserContext, int, Session, str, BxUserContext, int, Session, str (+39 more)
+Cohesion: 0.28
+Nodes (34): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+26 more)
 
 ### Community 21 - "Community 21"
 Cohesion: 0.05
-Nodes (162): bool, DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str (+154 more)
+Nodes (114): DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), Session, str, date, 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), bool (+106 more)
 
 ### Community 22 - "Community 22"
 Cohesion: 0.22
 Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
 
 ### Community 23 - "Community 23"
-Cohesion: 0.23
-Nodes (37): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+29 more)
+Cohesion: 0.24
+Nodes (44): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, int, str (+36 more)
 
 ### Community 24 - "Community 24"
 Cohesion: 0.12
-Nodes (89): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+81 more)
+Nodes (92): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+84 more)
 
 ### Community 25 - "Community 25"
-Cohesion: 0.09
-Nodes (29): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), CarrierAuditReject, CarrierBase, CarrierCreate, CarrierListResponse, CarrierQualificationBase, CarrierRead, CarrierUpdate (+21 more)
+Cohesion: 0.05
+Nodes (126): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, PlTransportSiteLink (+118 more)
 
 ### Community 26 - "Community 26"
-Cohesion: 0.15
-Nodes (23): logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, bool, int, Redis, Request (+15 more)
+Cohesion: 0.20
+Nodes (18): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+10 more)
 
 ### Community 27 - "Community 27"
 Cohesion: 0.11
-Nodes (57): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+49 more)
+Nodes (58): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+50 more)
 
 ### Community 28 - "Community 28"
-Cohesion: 0.33
-Nodes (29): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+21 more)
+Cohesion: 0.14
+Nodes (25): bool, BxUserContext, int, Session, str, Any, bool, int (+17 more)
 
 ### Community 29 - "Community 29"
-Cohesion: 0.08
-Nodes (26): 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo (+18 more)
+Cohesion: 0.28
+Nodes (25): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, _address_to_read(), _build_address_query() (+17 more)
 
 ### Community 30 - "Community 30"
 Cohesion: 0.07
-Nodes (43): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+35 more)
+Nodes (41): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+33 more)
 
 ### Community 31 - "Community 31"
-Cohesion: 0.15
-Nodes (30): Any, bool, int, Session, str, approve_quote(), can_user_approve(), create_approval_records() (+22 more)
+Cohesion: 0.08
+Nodes (26): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+18 more)
 
 ### Community 32 - "Community 32"
 Cohesion: 0.10
@@ -316,15 +306,15 @@ Nodes (99): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate,
 
 ### Community 33 - "Community 33"
 Cohesion: 0.13
-Nodes (56): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+48 more)
+Nodes (54): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+46 more)
 
 ### Community 34 - "Community 34"
-Cohesion: 0.19
-Nodes (34): int, str, generate_random_password(), get_file_size_display(), Engine, int, Path, build_id_maps() (+26 more)
+Cohesion: 0.23
+Nodes (28): Engine, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main() (+20 more)
 
 ### Community 35 - "Community 35"
-Cohesion: 0.04
-Nodes (182): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+174 more)
+Cohesion: 0.03
+Nodes (196): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+188 more)
 
 ### Community 36 - "Community 36"
 Cohesion: 0.19
@@ -339,60 +329,60 @@ Cohesion: 0.13
 Nodes (15): Any, bool, int, str, get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total_amo (+7 more)
 
 ### Community 39 - "Community 39"
-Cohesion: 0.18
-Nodes (22): datetime, date, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), get_current_time(), parse_datetime() (+14 more)
+Cohesion: 0.16
+Nodes (23): datetime, int, str, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), generate_random_password() (+15 more)
 
 ### Community 40 - "Community 40"
 Cohesion: 0.13
 Nodes (37): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+29 more)
 
 ### Community 41 - "Community 41"
-Cohesion: 0.12
-Nodes (41): Any, bool, date, int, Session, str, Session, str (+33 more)
+Cohesion: 0.13
+Nodes (40): Any, bool, date, int, Session, str, Session, str (+32 more)
 
 ### Community 42 - "Community 42"
 Cohesion: 0.08
 Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
 
 ### Community 43 - "Community 43"
-Cohesion: 0.21
-Nodes (16): Any, int, str, BusinessException, InvalidStatusTransitionException, log_api_request(), log_delivery_operation(), log_operation() (+8 more)
+Cohesion: 0.08
+Nodes (48): Any, int, str, float, int, str, Any, int (+40 more)
 
 ### Community 44 - "Community 44"
 Cohesion: 0.21
-Nodes (36): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+28 more)
+Nodes (37): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+29 more)
 
 ### Community 45 - "Community 45"
-Cohesion: 0.36
-Nodes (15): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave(), update_leave() (+7 more)
+Cohesion: 0.27
+Nodes (17): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave(), update_leave() (+9 more)
 
 ### Community 46 - "Community 46"
 Cohesion: 0.15
 Nodes (22): clue_import_template(), customer_import_template(), product_import_template(), str, Any, bool, Session, str (+14 more)
 
 ### Community 47 - "Community 47"
-Cohesion: 0.05
-Nodes (82): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), bool, bool, 运营端角色守卫:admin / super_admin 可访问。, require_admin_or_super_admin(), batch_assign_owner(), batch_submit_approval() (+74 more)
+Cohesion: 0.24
+Nodes (10): Exception, bytes, bytes, BytesIO, convert_heic_to_jpeg(), MinIOStorage, process_image_content(), 文件存储工具模块  支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS ( (+2 more)
 
 ### Community 48 - "Community 48"
-Cohesion: 0.39
-Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
+Cohesion: 0.18
+Nodes (23): BxUserContext, int, Session, str, Any, str, create_workflow(), delete_workflow() (+15 more)
 
 ### Community 49 - "Community 49"
-Cohesion: 0.30
-Nodes (21): Any, bool, CrmOpportunity, datetime, int, Session, str, ensure_crm_create_by_row_in_scope() (+13 more)
+Cohesion: 0.31
+Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
 
 ### Community 50 - "Community 50"
-Cohesion: 0.12
-Nodes (31): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+23 more)
+Cohesion: 0.11
+Nodes (39): CRM 报价单 API 路由 — 挂接到 crm.router, str, CRM 报价单 Pydantic Schemas, AdvanceApprovalCreate, ContractApprovalPassBody, CustomerCheckDuplicateBody, CustomerPoolDetailBody, CustomerPoolListBody (+31 more)
 
 ### Community 51 - "Community 51"
-Cohesion: 0.07
-Nodes (55): int, PlTransport, Session, str, UploadFile, Any, Session, str (+47 more)
+Cohesion: 0.22
+Nodes (18): bytes, int, str, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url() (+10 more)
 
 ### Community 52 - "Community 52"
-Cohesion: 0.10
-Nodes (63): CrmContract, Query, Any, bool, CrmContractProductPriceTier, date, Decimal, int (+55 more)
+Cohesion: 0.16
+Nodes (33): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+25 more)
 
 ### Community 53 - "Community 53"
 Cohesion: 0.24
@@ -403,8 +393,8 @@ Cohesion: 0.22
 Nodes (17): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, Java: statisticsVisits (+9 more)
 
 ### Community 55 - "Community 55"
-Cohesion: 0.20
-Nodes (30): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, barcode_exists(), category_code_exists() (+22 more)
+Cohesion: 0.04
+Nodes (114): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+106 more)
 
 ### Community 56 - "Community 56"
 Cohesion: 0.26
@@ -423,28 +413,28 @@ Cohesion: 0.25
 Nodes (23): Any, bool, int, Session, str, create_role(), get_role(), get_role_data_scope() (+15 more)
 
 ### Community 60 - "Community 60"
-Cohesion: 0.28
-Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
+Cohesion: 0.35
+Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
 
 ### Community 61 - "Community 61"
-Cohesion: 0.12
-Nodes (46): Session, str, bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan (+38 more)
+Cohesion: 0.08
+Nodes (85): int, PlTransport, Session, str, UploadFile, bool, date, DbInboundOutbound (+77 more)
 
 ### Community 62 - "Community 62"
-Cohesion: 0.12
-Nodes (45): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+37 more)
+Cohesion: 0.10
+Nodes (56): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+48 more)
 
 ### Community 63 - "Community 63"
-Cohesion: 0.09
-Nodes (22): generate_auto_product_no(), Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符 (+14 more)
+Cohesion: 0.14
+Nodes (19): Session, str, Session, str, get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, 接口菜单权限守卫。super_admin 直接放行。 (+11 more)
 
 ### Community 64 - "Community 64"
 Cohesion: 0.27
 Nodes (11): Any, bool, str, decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。 (+3 more)
 
 ### Community 65 - "Community 65"
-Cohesion: 0.33
-Nodes (10): Any, int, Session, str, list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 民族码表 sys_ethnic(国标 GB/T 3304,全局) (+2 more)
+Cohesion: 0.39
+Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
 
 ### Community 66 - "Community 66"
 Cohesion: 0.14
@@ -455,20 +445,20 @@ Cohesion: 0.16
 Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
 
 ### Community 68 - "Community 68"
-Cohesion: 0.25
-Nodes (11): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度)      遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+3 more)
+Cohesion: 0.07
+Nodes (61): Any, CertRead, date, int, Session, str, CertRead, Session (+53 more)
 
 ### Community 69 - "Community 69"
-Cohesion: 0.28
-Nodes (25): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+17 more)
+Cohesion: 0.33
+Nodes (22): DbUserContext, float, Session, str, get_db_current_user(), DashboardCargoStat, DashboardPartnerCargoStat, DashboardStats (+14 more)
 
 ### Community 70 - "Community 70"
 Cohesion: 0.29
 Nodes (14): is_valid_dict_type(), load_portal_baseline(), load_sx(), load_zkxw(), main(), map_tag_type(), normalize_row(), parse_insert() (+6 more)
 
 ### Community 71 - "Community 71"
-Cohesion: 0.08
-Nodes (51): BxUserContext, Session, str, Any, str, bool, BxUserContext, int (+43 more)
+Cohesion: 0.25
+Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
 
 ### Community 72 - "Community 72"
 Cohesion: 0.34
@@ -479,36 +469,36 @@ Cohesion: 0.07
 Nodes (100): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+92 more)
 
 ### Community 74 - "Community 74"
-Cohesion: 0.08
-Nodes (26): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+18 more)
+Cohesion: 0.04
+Nodes (46): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+38 more)
 
 ### Community 75 - "Community 75"
-Cohesion: 0.05
-Nodes (124): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+116 more)
+Cohesion: 0.07
+Nodes (95): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+87 more)
 
 ### Community 76 - "Community 76"
 Cohesion: 0.18
 Nodes (10): Any, Request, str, str, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。  与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。      Returns:, 统一解析接口请求载荷(明文 / 密文信封)。 (+2 more)
 
 ### Community 77 - "Community 77"
-Cohesion: 0.34
-Nodes (23): DbUserContext, DbWeighingSlip, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing(), _apply_weighing_product_update(), _apply_weighing_update() (+15 more)
+Cohesion: 0.23
+Nodes (29): DbUserContext, DbWeighingSlip, Session, str, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing() (+21 more)
 
 ### Community 78 - "Community 78"
-Cohesion: 0.13
-Nodes (20): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。      - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收)     - 遍历所有租户,按上月计划模板生 (+12 more)
+Cohesion: 0.19
+Nodes (14): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。      - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收)     - 遍历所有租户,按上月计划模板生 (+6 more)
 
 ### Community 79 - "Community 79"
 Cohesion: 0.36
 Nodes (10): add_scope_before_order(), ensure_import(), patch_base_data(), patch_get_helper(), patch_inventory(), patch_plan(), patch_transport(), str (+2 more)
 
 ### Community 80 - "Community 80"
-Cohesion: 0.05
-Nodes (62): str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+54 more)
+Cohesion: 0.07
+Nodes (46): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+38 more)
 
 ### Community 81 - "Community 81"
-Cohesion: 0.25
-Nodes (8): generate_template_no(), Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}
+Cohesion: 0.41
+Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
 
 ### Community 82 - "Community 82"
 Cohesion: 0.29
@@ -523,28 +513,28 @@ Cohesion: 0.41
 Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
 
 ### Community 85 - "Community 85"
-Cohesion: 0.26
-Nodes (49): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, Session, str (+41 more)
+Cohesion: 0.23
+Nodes (47): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+39 more)
 
 ### Community 86 - "Community 86"
-Cohesion: 0.18
-Nodes (11): float, int, str, 发送订单状态通知                  Args:             openid: 用户openid             order_n, 发送支付成功通知                  Args:             openid: 用户openid             order_n, 发送配送通知                  Args:             openid: 配送员openid             task_no:, 发送配送时效预警                  Args:             openid: 配送员openid             task_n, 发送退款结果通知                  Args:             openid: 用户openid             order_n (+3 more)
+Cohesion: 0.30
+Nodes (13): Any, bool, bytes, int, str, ApiCryptoError, _b64url_decode(), _b64url_encode() (+5 more)
 
 ### Community 87 - "Community 87"
-Cohesion: 0.22
-Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
+Cohesion: 0.19
+Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
 
 ### Community 88 - "Community 88"
-Cohesion: 0.18
-Nodes (11): generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号} (+3 more)
+Cohesion: 0.25
+Nodes (11): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度)      遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+3 more)
 
 ### Community 89 - "Community 89"
-Cohesion: 0.14
-Nodes (14): _build_category_tree(), _collect_category_with_ancestors(), list_category_tree(), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 按 parentId 将扁平分类列表组装为嵌套树, 获取产品分类树(嵌套 children,供树形表格/树选择器使用) (+6 more)
+Cohesion: 0.20
+Nodes (6): int, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(PDF 等 iframe 场景)。, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
 
 ### Community 90 - "Community 90"
-Cohesion: 0.22
-Nodes (18): Any, bool, bytes, int, Request, str, _claims_from_auth(), _format_body_bytes() (+10 more)
+Cohesion: 0.16
+Nodes (26): Any, bool, bytes, int, Request, str, bool, int (+18 more)
 
 ### Community 91 - "Community 91"
 Cohesion: 0.32
@@ -555,8 +545,8 @@ Cohesion: 0.34
 Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
 
 ### Community 93 - "Community 93"
-Cohesion: 0.15
-Nodes (22): int, Session, str, bool, datetime, DbWeighingSlip, Session, str (+14 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 94 - "Community 94"
 Cohesion: 0.18
@@ -571,112 +561,80 @@ Cohesion: 0.34
 Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
 
 ### Community 97 - "Community 97"
-Cohesion: 0.24
-Nodes (27): Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑, int, Session, str, create_pool_rule(), create_rule() (+19 more)
+Cohesion: 0.21
+Nodes (30): pool_rule_toggle(), Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑, int, Session, str, create_pool_rule() (+22 more)
 
 ### Community 98 - "Community 98"
 Cohesion: 0.38
 Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
 
 ### Community 99 - "Community 99"
-Cohesion: 0.21
-Nodes (9): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info() (+1 more)
+Cohesion: 0.07
+Nodes (29): BaseModel, AdvanceApplyOut, AttachmentOut, ReimburseApplyCreate, Config, PageResult, ResponseModel, ContactByCustomerBody (+21 more)
 
 ### Community 100 - "Community 100"
-Cohesion: 0.45
-Nodes (12): bool, date, PlCarrier, str, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved(), carrier_eligible_for_transport() (+4 more)
+Cohesion: 0.20
+Nodes (10): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+2 more)
 
 ### Community 101 - "Community 101"
 Cohesion: 0.40
 Nodes (3): BaseSettings, Config, Settings
 
 ### Community 102 - "Community 102"
-Cohesion: 0.60
-Nodes (10): Any, Session, str, _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics() (+2 more)
+Cohesion: 0.53
+Nodes (11): Any, Session, str, _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics() (+3 more)
 
 ### Community 103 - "Community 103"
-Cohesion: 0.08
-Nodes (158): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, bool (+150 more)
+Cohesion: 0.07
+Nodes (141): bool, PlCarrier, PlVehicle, VehicleCreate, VehicleRead, VehicleUpdate, bool, DbUserContext (+133 more)
 
 ### Community 104 - "Community 104"
-Cohesion: 0.13
-Nodes (38): bool, Redis, Request, str, _T, bool, Redis, decode_token() (+30 more)
+Cohesion: 0.11
+Nodes (49): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, Session, str, _T (+41 more)
 
 ### Community 105 - "Community 105"
 Cohesion: 0.38
 Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
 
 ### Community 106 - "Community 106"
-Cohesion: 0.13
-Nodes (15): patch_plan_product_certs(), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输) (+7 more)
+Cohesion: 0.33
+Nodes (8): Decimal, str, derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
 ### Community 107 - "Community 107"
-Cohesion: 0.22
-Nodes (9): create_from_opportunity(), _opportunity_products_to_contract_products(), Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id (+1 more)
+Cohesion: 0.34
+Nodes (25): BxUserContext, Session, str, ApiResponse, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+17 more)
 
 ### Community 108 - "Community 108"
-Cohesion: 0.44
-Nodes (8): bool, int, Session, str, ApiRequestLogCRUD, SysApiRequestLog, SysApiRequestLogHis, SysApiRequestLog
-
-### Community 109 - "Community 109"
-Cohesion: 0.22
-Nodes (9): Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签 (+1 more)
-
-### Community 111 - "Community 111"
-Cohesion: 0.07
-Nodes (27): Session, Any, Session, str, Session, str, get_current_user(), get_current_user_by_token() (+19 more)
-
-### Community 112 - "Community 112"
-Cohesion: 0.17
-Nodes (12): 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) (+4 more)
-
-### Community 122 - "Community 122"
 Cohesion: 0.40
-Nodes (10): SysOss, int, Session, str, SysOss, create_oss(), create_oss(), get_oss_by_url() (+2 more)
+Nodes (3): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单)
 
-### Community 123 - "Community 123"
-Cohesion: 0.33
-Nodes (8): Decimal, str, derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
-
-### Community 124 - "Community 124"
-Cohesion: 0.28
-Nodes (8): date, datetime, str, _date_to_str(), _dt_to_str(), PagedQuery, PagedResponse, bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
+### Community 111 - "Community 111"
+Cohesion: 0.25
+Nodes (7): Any, Session, str, get_db_current_user_optional(), 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。, 有 Bearer 且为 admin/super_admin 时返回上下文,否则 None(供三方 H5 无登录调用)。, 有 Bearer 且为 admin/super_admin 时返回上下文,否则 None(供三方 H5 无登录调用)。
 
 ### Community 125 - "Community 125"
-Cohesion: 0.23
-Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
-
-### Community 126 - "Community 126"
 Cohesion: 0.22
-Nodes (9): _category_row_to_dict(), list_categories(), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 获取分类(扁平列表,支持名称模糊与状态筛选), 获取分类(扁平列表,支持名称模糊与状态筛选) (+1 more)
+Nodes (11): Any, bool, int, Session, str, generate_id(), camel_to_snake(), CRUDBase (+3 more)
 
 ### Community 127 - "Community 127"
 Cohesion: 0.29
 Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
 
 ### Community 128 - "Community 128"
-Cohesion: 0.20
-Nodes (10): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+2 more)
-
-### Community 129 - "Community 129"
 Cohesion: 0.22
-Nodes (9): generate_barcode(), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001) (+1 more)
+Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
 
 ### Community 130 - "Community 130"
-Cohesion: 0.10
-Nodes (32): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户信息(与 sys_user 字段对应)。 (+24 more)
+Cohesion: 0.08
+Nodes (30): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+22 more)
 
 ### Community 131 - "Community 131"
-Cohesion: 0.11
-Nodes (33): bool, Session, str, _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids() (+25 more)
+Cohesion: 0.08
+Nodes (43): bool, Session, str, Any, int, Session, str, _append_user_scope_conditions() (+35 more)
 
 ### Community 132 - "Community 132"
-Cohesion: 0.40
-Nodes (5): bool, str, get_password_hash(), 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password()
-
-### Community 133 - "Community 133"
-Cohesion: 0.32
-Nodes (5): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, _coerce_optional_id()
+Cohesion: 0.28
+Nodes (8): date, datetime, str, _date_to_str(), _dt_to_str(), PagedQuery, PagedResponse, bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
 
 ### Community 134 - "Community 134"
 Cohesion: 0.38
@@ -687,32 +645,24 @@ Cohesion: 0.25
 Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4. 配置文件, 5. 与 Nginx 配合, 6. 常见问题, shanxia2.0 Backend 生产部署说明
 
 ### Community 136 - "Community 136"
-Cohesion: 0.29
-Nodes (7): _product_row_to_dict(), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名)
-
-### Community 137 - "Community 137"
-Cohesion: 0.13
-Nodes (15): _generate_plan_no(), 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD###### (+7 more)
+Cohesion: 0.50
+Nodes (4): Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), web_login_rsa_public_key()
 
 ### Community 138 - "Community 138"
 Cohesion: 0.39
 Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
 
-### Community 139 - "Community 139"
-Cohesion: 0.20
-Nodes (10): 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。 (+2 more)
-
 ### Community 140 - "Community 140"
 Cohesion: 0.83
 Nodes (3): _add_user_info_kwarg(), patch_file(), str
 
 ### Community 141 - "Community 141"
-Cohesion: 0.29
-Nodes (7): get_third_party_plan_page(), 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。
+Cohesion: 0.25
+Nodes (8): generate_template_no(), Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}
 
 ### Community 143 - "Community 143"
-Cohesion: 0.32
-Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 more)
+Cohesion: 0.18
+Nodes (31): Any, CrmOpportunity, Decimal, int, Session, str, create_from_opportunity(), _opportunity_products_to_contract_products() (+23 more)
 
 ### Community 144 - "Community 144"
 Cohesion: 0.17
@@ -722,37 +672,37 @@ Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日
 Cohesion: 0.33
 Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
 
-### Community 146 - "Community 146"
-Cohesion: 0.47
-Nodes (5): OperLogDetailBody, OperLogListBody, 操作日志(兼容 RuoYi OperLog 视图), OperLogDetailBody, OperLogListBody
-
-### Community 147 - "Community 147"
-Cohesion: 0.18
-Nodes (26): Any, bool, int, Session, str, Session, str, _build_tree() (+18 more)
+### Community 149 - "Community 149"
+Cohesion: 0.13
+Nodes (19): operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), Session, _auto_fill_create_dept_id(), get_db() (+11 more)
 
 ### Community 150 - "Community 150"
 Cohesion: 0.33
 Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
 
+### Community 151 - "Community 151"
+Cohesion: 0.50
+Nodes (4): datetime, object, parse_optional_plan_datetime(), 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
+
 ## Knowledge Gaps
-- **69 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+64 more)
+- **70 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+65 more)
   These have ≤1 connection - possible missing edges or undocumented components.
 - **2 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `Session` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 27`, `Community 28`, `Community 29`, `Community 31`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 41`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 49`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 71`, `Community 73`, `Community 74`, `Community 75`, `Community 77`, `Community 78`, `Community 81`, `Community 85`, `Community 88`, `Community 89`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 102`, `Community 103`, `Community 105`, `Community 107`, `Community 108`, `Community 109`, `Community 111`, `Community 112`, `Community 122`, `Community 125`, `Community 126`, `Community 127`, `Community 128`, `Community 129`, `Community 130`, `Community 131`, `Community 137`, `Community 141`, `Community 143`, `Community 146`, `Community 147`?**
-  _High betweenness centrality (0.204) - this node is a cross-community bridge._
-- **Why does `BaseModel` connect `Community 80` to `Community 0`, `Community 2`, `Community 133`, `Community 134`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 146`, `Community 19`, `Community 148`, `Community 21`, `Community 20`, `Community 23`, `Community 150`, `Community 25`, `Community 24`, `Community 28`, `Community 30`, `Community 32`, `Community 44`, `Community 45`, `Community 47`, `Community 50`, `Community 51`, `Community 62`, `Community 69`, `Community 71`, `Community 73`, `Community 75`, `Community 77`, `Community 85`, `Community 103`, `Community 124`?**
-  _High betweenness centrality (0.067) - this node is a cross-community bridge._
-- **Why does `datetime` connect `Community 39` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 143`, `Community 16`, `Community 17`, `Community 15`, `Community 147`, `Community 148`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 27`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 40`, `Community 41`, `Community 43`, `Community 44`, `Community 47`, `Community 49`, `Community 50`, `Community 51`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 59`, `Community 60`, `Community 61`, `Community 62`, `Community 67`, `Community 68`, `Community 69`, `Community 75`, `Community 77`, `Community 78`, `Community 80`, `Community 84`, `Community 85`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 100`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 108`, `Community 124`, `Community 127`?**
-  _High betweenness centrality (0.058) - this node is a cross-community bridge._
+- **Why does `Session` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 27`, `Community 28`, `Community 31`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 41`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 48`, `Community 49`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 71`, `Community 73`, `Community 74`, `Community 77`, `Community 78`, `Community 81`, `Community 85`, `Community 87`, `Community 90`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 100`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 107`, `Community 111`, `Community 125`, `Community 127`, `Community 130`, `Community 131`, `Community 141`, `Community 143`, `Community 149`?**
+  _High betweenness centrality (0.217) - this node is a cross-community bridge._
+- **Why does `BaseModel` connect `Community 50` to `Community 0`, `Community 2`, `Community 132`, `Community 134`, `Community 7`, `Community 11`, `Community 12`, `Community 15`, `Community 19`, `Community 148`, `Community 20`, `Community 150`, `Community 23`, `Community 149`, `Community 21`, `Community 25`, `Community 24`, `Community 29`, `Community 30`, `Community 32`, `Community 35`, `Community 44`, `Community 45`, `Community 48`, `Community 61`, `Community 62`, `Community 68`, `Community 69`, `Community 71`, `Community 73`, `Community 75`, `Community 77`, `Community 80`, `Community 85`, `Community 87`, `Community 99`, `Community 103`, `Community 107`?**
+  _High betweenness centrality (0.074) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 87` to `Community 130`, `Community 131`, `Community 2`, `Community 3`, `Community 6`, `Community 8`, `Community 10`, `Community 12`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 22`, `Community 23`, `Community 24`, `Community 27`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 39`, `Community 43`, `Community 52`, `Community 56`, `Community 69`, `Community 73`, `Community 90`, `Community 103`, `Community 104`?**
+  _High betweenness centrality (0.057) - 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._
 - **Are the 248 inferred relationships involving `Session` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
   _`Session` has 248 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 140 inferred relationships involving `int` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
   _`int` has 140 INFERRED edges - model-reasoned connections that need verification._
-- **What connects `定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。`, `数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误`, `未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin` to the rest of the system?**
-  _1298 weakly-connected nodes found - possible documentation gaps or missing edges._
+- **Are the 28 inferred relationships involving `get_tenant_no()` (e.g. with `category_tree()` and `contact_create()`) actually correct?**
+  _`get_tenant_no()` has 28 INFERRED edges - model-reasoned connections that need verification._

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
backend/graphify-out/cache/stat-index.json


Разлика између датотеке није приказан због своје велике величине
+ 112 - 112
backend/graphify-out/graph.json


+ 36 - 0
docs/sql/shanxia2.0.sql

@@ -2766,9 +2766,13 @@ CREATE TABLE `pl_plan` (
   `settlement_method` varchar(50) DEFAULT NULL COMMENT '结算方式',
   `has_freight` smallint(6) NOT NULL DEFAULT '0' COMMENT '是否有运输费用 0-否 1-是',
   `pickup_address` varchar(500) DEFAULT NULL COMMENT '取货地址',
+  `pickup_lng` decimal(10,6) DEFAULT NULL COMMENT '取货经度',
+  `pickup_lat` decimal(10,6) DEFAULT NULL COMMENT '取货纬度',
   `pickup_time` datetime DEFAULT NULL COMMENT '取货时间',
   `pickup_notes` varchar(500) DEFAULT NULL COMMENT '取货注意事项',
   `destination_address` varchar(500) DEFAULT NULL COMMENT '目的地地址',
+  `destination_lng` decimal(10,6) DEFAULT NULL COMMENT '送货经度',
+  `destination_lat` decimal(10,6) DEFAULT NULL COMMENT '送货纬度',
   `delivery_time` datetime DEFAULT NULL COMMENT '送达时间',
   `destination_notes` varchar(500) DEFAULT NULL COMMENT '目的地注意事项',
   `transport_id` varchar(64) DEFAULT NULL COMMENT '运输ID',
@@ -3041,6 +3045,38 @@ INSERT INTO `pl_remittance_voucher` (`id`, `settlement_id`, `voucher_no`, `remit
 INSERT INTO `pl_remittance_voucher` (`id`, `settlement_id`, `voucher_no`, `remittance_date`, `remittance_amount`, `voucher_urls`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('8BCB0652B9414F7EAD6EA2A4E752ECF3', '73854CC0B7B64AFE8C983120551420BB', NULL, '2026-06-28', 306.00, '[\"pl/freight-voucher/202606/28/58a088fd1641474c856a3dc71c0397ae.jpeg\"]', 'shanxia2.0', NULL, '123', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 17:04:52', NULL, '2026-06-28 17:04:52', 0);
 COMMIT;
 
+-- ----------------------------
+-- Table structure for pl_transport_address
+-- ----------------------------
+DROP TABLE IF EXISTS `pl_transport_address`;
+CREATE TABLE `pl_transport_address` (
+  `id` varchar(64) NOT NULL COMMENT '主键ID',
+  `address_name` varchar(200) NOT NULL COMMENT '地址名称',
+  `full_address` varchar(500) NOT NULL COMMENT '详细地址',
+  `lng` decimal(10,6) NOT NULL COMMENT '经度',
+  `lat` decimal(10,6) NOT NULL COMMENT '纬度',
+  `contact_name` varchar(100) DEFAULT NULL COMMENT '联系人',
+  `contact_phone` varchar(32) DEFAULT NULL COMMENT '联系电话',
+  `status` char(1) NOT NULL DEFAULT '1' COMMENT '状态 1-启用 0-停用',
+  `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
+  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
+  `remark` varchar(512) DEFAULT NULL COMMENT '备注',
+  `create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
+  `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
+  PRIMARY KEY (`id`),
+  KEY `idx_pl_transport_address_tenant` (`tenant_no`,`del_flag`,`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运输通用地址库';
+
+-- ----------------------------
+-- Records of pl_transport_address
+-- ----------------------------
+BEGIN;
+COMMIT;
+
 -- ----------------------------
 -- Table structure for pl_transport
 -- ----------------------------

+ 80 - 35
graphify-out/manifest.json

@@ -280,7 +280,7 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
-    "mtime": 1782857094.9578602,
+    "mtime": 1782906332.877103,
     "ast_hash": "6553f54ca1fd19086c170095f402a628",
     "semantic_hash": ""
   },
@@ -1725,8 +1725,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/env.d.ts": {
-    "mtime": 1782858528.2320914,
-    "ast_hash": "c9f9ecf49f80dcac1e408db4c1892ef8",
+    "mtime": 1782903782.0621061,
+    "ast_hash": "7490c67ebd4a28d850f0085e15e4eb26",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/shims-loose.d.ts": {
@@ -5835,18 +5835,18 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py": {
-    "mtime": 1782793455.702941,
-    "ast_hash": "6b8c4660d7028d7186267699d5ca3aa1",
+    "mtime": 1782906535.7235444,
+    "ast_hash": "e78c2b5826952efd2979f1a5d1fd93bb",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py": {
-    "mtime": 1782793472.1020458,
-    "ast_hash": "84fc6e5bd61edae63a7e23577d32c20b",
+    "mtime": 1782905365.6902194,
+    "ast_hash": "0f6ab4b0bf6aae653e35c93be4a9bb1c",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py": {
-    "mtime": 1782793487.671617,
-    "ast_hash": "4cf6d81e08cca8d6e92c43856ecf52e2",
+    "mtime": 1782905367.5290751,
+    "ast_hash": "e9b1480cdd11a2adb78247bd3d5b3b80",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py": {
@@ -5865,18 +5865,18 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py": {
-    "mtime": 1782793517.0180697,
-    "ast_hash": "9167954b7475bdb07dad255a9348e6b1",
+    "mtime": 1782904185.1767316,
+    "ast_hash": "0d840b3017a9357fa63428b838b9c5b5",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py": {
-    "mtime": 1782793483.046418,
-    "ast_hash": "229a992f46ae31178bb20aa0834b60d5",
+    "mtime": 1782904202.7464836,
+    "ast_hash": "e75ce6bfce6df56f49e2a0cb4b678217",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/__init__.py": {
-    "mtime": 1782696220.8612864,
-    "ast_hash": "b5a4c54d5c2a73740d5dc29a59c62a89",
+    "mtime": 1782906547.2093558,
+    "ast_hash": "28dc4010ec275301fcb4a013b6855a50",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py": {
@@ -5895,8 +5895,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/index.ts": {
-    "mtime": 1782858537.3636208,
-    "ast_hash": "c3c649ae2c94a034a0c57ee85bf8e4ec",
+    "mtime": 1782906561.7571106,
+    "ast_hash": "0f2965eae3756fb9045da589c832df80",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/transport/index.vue": {
@@ -5930,8 +5930,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/router/modules/pl.ts": {
-    "mtime": 1782858544.556459,
-    "ast_hash": "7d1ddf0f841328644a2c3734e9ff50fe",
+    "mtime": 1782906603.1698864,
+    "ast_hash": "1c1a94c7fdd3e4b1f93bbd77fdcd39c8",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan/arrange.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": 1782902572.0918643,
-    "ast_hash": "591a913bc32fa5d6d35e938609158451",
+    "mtime": 1782907228.400472,
+    "ast_hash": "ffa41946bfeaf3cd8dff3abbf585078c",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py": {
@@ -6100,13 +6100,13 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-arrange-dialog/index.vue": {
-    "mtime": 1782902592.068708,
-    "ast_hash": "3818b8e76ca8c7227fa79f0bbcbaa011",
+    "mtime": 1782906602.6501355,
+    "ast_hash": "e26aedc11d6ef73dae6ea8251ff78cce",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py": {
-    "mtime": 1782790898.570381,
-    "ast_hash": "fbbba3979eed1c05b47d9acc45c0ecb1",
+    "mtime": 1782904189.9898672,
+    "ast_hash": "39ff399ebeb91882700686c192780bb7",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py": {
@@ -6125,8 +6125,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-view-tabs/index.vue": {
-    "mtime": 1782902595.1814783,
-    "ast_hash": "0d2b0fb0b941d63b1ec42c70b4f77483",
+    "mtime": 1783001126.5947132,
+    "ast_hash": "c940f2b9f6c1b2a1678083b65ae38bbc",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue": {
@@ -6135,13 +6135,13 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/public-transport.ts": {
-    "mtime": 1782858537.3784862,
-    "ast_hash": "677f5eaaa05adb17442119694cd3aefc",
+    "mtime": 1782904310.288007,
+    "ast_hash": "42a82e7cc5bf345b537d35f6095c5b34",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/public/pl/transport/index.vue": {
-    "mtime": 1782858542.845052,
-    "ast_hash": "e8f8114d2972c7b1a388c8dd1d7f56b1",
+    "mtime": 1782904322.2179258,
+    "ast_hash": "de73ab2289eba99e7ccaadbbb5ed9390",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/layouts/PublicMobileShell.vue": {
@@ -6150,8 +6150,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlPlanArrangeActions.ts": {
-    "mtime": 1782902595.352905,
-    "ast_hash": "7bc4755be0a912766098ae7bebbc0c8f",
+    "mtime": 1782904305.6774886,
+    "ast_hash": "a640aac6bd737470877b0449125f5812",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/router/portal-menu.ts": {
@@ -6290,8 +6290,8 @@
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-archive-dialog/index.vue": {
-    "mtime": 1782858535.4483042,
-    "ast_hash": "b1015cda352bbe0dea3fa29faac65561",
+    "mtime": 1783001120.0693388,
+    "ast_hash": "b7fa59a57d7261011227b147c250a84a",
     "semantic_hash": ""
   },
   "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan-archive/index.vue": {
@@ -6673,5 +6673,50 @@
     "mtime": 1782865843.497152,
     "ast_hash": "f33b59225e189655c09853759400b048",
     "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/amap/loader.ts": {
+    "mtime": 1782915097.1272,
+    "ast_hash": "2a88741c1b8e29efe4049d40d44db119",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/amap-address-picker/index.vue": {
+    "mtime": 1782904217.7621434,
+    "ast_hash": "06a6495e30b80e357a6a11c7b2b7cca6",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/amap/navigation.ts": {
+    "mtime": 1782904191.971898,
+    "ast_hash": "a1beb46fb9a717580c5bccec73dde5d9",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/datetime_parse.py": {
+    "mtime": 1782905360.7460208,
+    "ast_hash": "999484f6738d95159b3771ac05d57b4f",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport_address.py": {
+    "mtime": 1782906539.7314954,
+    "ast_hash": "9e795527053f9ac5d2f4c4161782000c",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py": {
+    "mtime": 1782906547.114098,
+    "ast_hash": "465480e4c04d089602bd09ee8618bcf3",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-transport-address-select-dialog/index.vue": {
+    "mtime": 1782907507.8495402,
+    "ast_hash": "0f140b53c99c240dc8665e249bba168a",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-address-with-library/index.vue": {
+    "mtime": 1782907231.5989144,
+    "ast_hash": "e3c51808508324c011fe3e069edfc646",
+    "semantic_hash": ""
+  },
+  "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/transport-address/index.vue": {
+    "mtime": 1782906655.9155624,
+    "ast_hash": "4ba8d6395ad9952db3868ff6f6a5df10",
+    "semantic_hash": ""
   }
 }

+ 3 - 8
web-admin/graphify-out/.graphify_labels.json

@@ -703,33 +703,28 @@
   "701": "Community 701",
   "702": "Community 702",
   "703": "Community 703",
+  "704": "Community 704",
   "705": "Community 705",
   "706": "Community 706",
+  "707": "Community 707",
+  "708": "Community 708",
   "709": "Community 709",
   "710": "Community 710",
   "711": "Community 711",
-  "712": "Community 712",
   "713": "Community 713",
   "714": "Community 714",
   "715": "Community 715",
   "717": "Community 717",
   "718": "Community 718",
-  "719": "Community 719",
-  "720": "Community 720",
   "722": "Community 722",
   "723": "Community 723",
-  "724": "Community 724",
   "727": "Community 727",
   "728": "Community 728",
   "729": "Community 729",
   "730": "Community 730",
   "731": "Community 731",
-  "733": "Community 733",
-  "734": "Community 734",
   "735": "Community 735",
   "736": "Community 736",
-  "737": "Community 737",
-  "738": "Community 738",
   "739": "Community 739",
   "741": "Community 741"
 }

Разлика између датотеке није приказан због своје велике величине
+ 237 - 235
web-admin/graphify-out/GRAPH_REPORT.md


Разлика између датотеке није приказан због своје велике величине
+ 112 - 112
web-admin/graphify-out/graph.json


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

@@ -184,6 +184,52 @@ export const personnelApi = {
   delete: (id: string) => request.delete(`/transport/personnel/${id}`)
 }
 
+// 运输通用地址库
+export interface TransportAddress {
+  id: string
+  addressName: string
+  fullAddress: string
+  lng: number
+  lat: number
+  contactName?: string
+  contactPhone?: string
+  status: string
+  remark?: string
+  createTime: string
+  updateTime: string
+}
+
+export interface TransportAddressQuery {
+  q?: string
+  status?: string
+  page?: number
+  pageSize?: number
+}
+
+export interface TransportAddressPayload {
+  addressName: string
+  fullAddress: string
+  lng: number
+  lat: number
+  contactName?: string
+  contactPhone?: string
+  status?: string
+  remark?: string
+}
+
+export const transportAddressApi = {
+  list: (params?: TransportAddressQuery) =>
+    request.get<PlListResponse<TransportAddress>>('/transport/address', { params }),
+  listAll: (params?: { q?: string }) =>
+    request.get<PlListResponse<TransportAddress>>('/transport/address/all', { params }),
+  get: (id: string) => request.get<TransportAddress>(`/transport/address/${id}`),
+  create: (data: TransportAddressPayload) =>
+    request.post<TransportAddress>('/transport/address', data),
+  update: (id: string, data: Partial<TransportAddressPayload>) =>
+    request.put<TransportAddress>(`/transport/address/${id}`, data),
+  delete: (id: string) => request.delete(`/transport/address/${id}`)
+}
+
 // 司机(兼容旧 API,映射至 personnel)
 export interface Driver {
   id: string
@@ -325,9 +371,13 @@ export interface Plan {
   settlementMethod?: string
   hasFreight: number
   pickupAddress?: string
+  pickupLng?: number | null
+  pickupLat?: number | null
   pickupTime?: string
   pickupNotes?: string
   destinationAddress?: string
+  destinationLng?: number | null
+  destinationLat?: number | null
   deliveryTime?: string
   destinationNotes?: string
   vehicleId?: string
@@ -403,9 +453,13 @@ export interface PlanCreateFullPayload {
   settlementMethod?: string
   hasFreight: number
   pickupAddress?: string
+  pickupLng?: number | null
+  pickupLat?: number | null
   pickupTime?: string
   pickupNotes?: string
   destinationAddress?: string
+  destinationLng?: number | null
+  destinationLat?: number | null
   deliveryTime?: string
   destinationNotes?: string
   followUserId?: string
@@ -521,9 +575,13 @@ export interface TransportArrange {
   carrierId?: string
   transportMedium?: string
   pickupAddress?: string
+  pickupLng?: number | null
+  pickupLat?: number | null
   pickupTime?: string
   pickupNotes?: string
   destinationAddress?: string
+  destinationLng?: number | null
+  destinationLat?: number | null
   deliveryTime?: string
   destinationNotes?: string
   distanceKm?: number

+ 4 - 0
web-admin/src/api/pl/public-transport.ts

@@ -13,7 +13,11 @@ export interface PublicTransportPage {
   planDate?: string
   planType: string
   pickupAddress?: string
+  pickupLng?: number | null
+  pickupLat?: number | null
   destinationAddress?: string
+  destinationLng?: number | null
+  destinationLat?: number | null
   licensePlate?: string
   driverName?: string
   driverPhone?: string

+ 427 - 0
web-admin/src/components/business/amap-address-picker/index.vue

@@ -0,0 +1,427 @@
+<template>
+  <div class="amap-address-picker">
+    <el-input
+      :id="inputId"
+      v-model="innerAddress"
+      :placeholder="placeholder"
+      :disabled="disabled"
+      clearable
+      @input="handleAddressInput"
+    >
+      <template #append>
+        <el-button :disabled="disabled" :loading="mapOpening" @click="openMapDialog">
+          <el-icon><Location /></el-icon>地图选点
+        </el-button>
+      </template>
+    </el-input>
+
+    <el-dialog
+      v-model="mapVisible"
+      title="地图选点"
+      width="760px"
+      align-center
+      destroy-on-close
+      append-to-body
+      class="dialog-form-layout amap-address-picker-dialog"
+      modal-class="order-module-dialog-mask"
+      :close-on-click-modal="false"
+      style="backdrop-filter: none"
+      @opened="handleMapDialogOpened"
+      @closed="handleMapDialogClosed"
+    >
+      <div class="amap-address-picker-body">
+        <div class="amap-address-picker-search">
+          <el-input
+            :id="searchInputId"
+            v-model="searchKeyword"
+            placeholder="搜索地点、POI"
+            clearable
+            @keyup.enter="handleSearch"
+          >
+            <template #append>
+              <el-button :loading="searchLoading" @click="handleSearch">
+                <el-icon><Search /></el-icon>搜索
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+
+        <div ref="mapContainerRef" class="amap-address-picker-map" />
+
+        <p v-if="pickedAddress" class="amap-address-picker-result">
+          已选:<span>{{ pickedAddress }}</span>
+        </p>
+        <p v-else class="amap-address-picker-hint">点击地图选点,或搜索后选择结果;可拖拽标记微调位置</p>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="mapVisible = false">
+            <el-icon><Close /></el-icon>取消
+          </el-button>
+          <el-button type="primary" :disabled="!pickedAddress" @click="confirmPick">
+            <el-icon><Check /></el-icon>确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Check, Close, Location, Search } from '@element-plus/icons-vue'
+import {
+  formatPoiAddress,
+  isAmapConfigured,
+  loadAmap,
+  loadAmapPlugins,
+  resolveLngLat,
+  type AMapAutoComplete,
+  type AMapGeocoder,
+  type AMapGlobal,
+  type AMapLngLat,
+  type AMapMap,
+  type AMapMarker,
+  type AMapPlaceSearch,
+  type AMapPoi
+} from '@/utils/amap/loader'
+
+export interface AmapAddressPickResult {
+  address: string
+  lng?: number
+  lat?: number
+}
+
+const props = withDefaults(
+  defineProps<{
+    modelValue?: string
+    lng?: number | null
+    lat?: number | null
+    placeholder?: string
+    disabled?: boolean
+    city?: string
+  }>(),
+  {
+    modelValue: '',
+    lng: null,
+    lat: null,
+    placeholder: '请输入或地图选点',
+    disabled: false,
+    city: ''
+  }
+)
+
+const emit = defineEmits<{
+  'update:modelValue': [value: string]
+  'update:lng': [value: number | null]
+  'update:lat': [value: number | null]
+  change: [payload: AmapAddressPickResult]
+}>()
+
+const inputId = `amap-address-input-${useId()}`
+const searchInputId = `amap-address-search-${useId()}`
+
+const innerAddress = ref(props.modelValue || '')
+const mapVisible = ref(false)
+const mapOpening = ref(false)
+const searchKeyword = ref('')
+const searchLoading = ref(false)
+const pickedAddress = ref('')
+const pickedLngLat = ref<[number, number] | null>(null)
+
+const mapContainerRef = ref<HTMLDivElement>()
+let amapRef: AMapGlobal | null = null
+let mapInstance: AMapMap | null = null
+let markerInstance: AMapMarker | null = null
+let geocoder: AMapGeocoder | null = null
+let placeSearch: AMapPlaceSearch | null = null
+let autoComplete: AMapAutoComplete | null = null
+let mainAutoComplete: AMapAutoComplete | null = null
+
+watch(
+  () => props.modelValue,
+  value => {
+    if (value !== innerAddress.value) {
+      innerAddress.value = value || ''
+    }
+  }
+)
+
+onMounted(() => {
+  initMainAutoComplete()
+})
+
+onBeforeUnmount(() => {
+  destroyMainAutoComplete()
+  destroyMapResources()
+})
+
+function setCoordinates(lng?: number | null, lat?: number | null) {
+  const nextLng = lng ?? null
+  const nextLat = lat ?? null
+  emit('update:lng', nextLng)
+  emit('update:lat', nextLat)
+}
+
+function handleAddressInput(value: string) {
+  emit('update:modelValue', value)
+  setCoordinates(null, null)
+  emit('change', { address: value })
+}
+
+async function ensureAmapReady() {
+  if (!isAmapConfigured()) {
+    ElMessage.warning('未配置高德地图 Key,请在 .env.development.local 中设置 VITE_AMAP_KEY')
+    return false
+  }
+  try {
+    amapRef = await loadAmap()
+    await loadAmapPlugins()
+    return true
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '高德地图加载失败'
+    ElMessage.error(message)
+    return false
+  }
+}
+
+function bindAutoComplete(
+  target: AMapAutoComplete | null,
+  onSelect: (poi?: AMapPoi) => void
+) {
+  if (!target) return
+  const handler = (event: { poi?: AMapPoi }) => onSelect(event.poi)
+  target.on('select', handler)
+}
+
+async function initMainAutoComplete() {
+  const ready = await ensureAmapReady()
+  if (!ready || !amapRef) return
+
+  amapRef.plugin(['AMap.AutoComplete'], () => {
+    if (mainAutoComplete) return
+    mainAutoComplete = new amapRef!.AutoComplete({
+      input: inputId,
+      city: props.city || '全国',
+      citylimit: !!props.city
+    })
+    bindAutoComplete(mainAutoComplete, poi => {
+      const address = formatPoiAddress(poi)
+      if (!address) return
+      innerAddress.value = address
+      emit('update:modelValue', address)
+      if (poi?.location) {
+        const [lng, lat] = resolveLngLat(poi.location)
+        setCoordinates(lng, lat)
+        emit('change', { address, lng, lat })
+      } else {
+        setCoordinates(null, null)
+        emit('change', { address })
+      }
+    })
+  })
+}
+
+function destroyMainAutoComplete() {
+  mainAutoComplete = null
+}
+
+async function openMapDialog() {
+  if (props.disabled) return
+  mapOpening.value = true
+  try {
+    const ready = await ensureAmapReady()
+    if (!ready) return
+    pickedAddress.value = innerAddress.value || ''
+    pickedLngLat.value =
+      props.lng != null && props.lat != null ? [Number(props.lng), Number(props.lat)] : null
+    searchKeyword.value = innerAddress.value || ''
+    mapVisible.value = true
+  } finally {
+    mapOpening.value = false
+  }
+}
+
+async function handleMapDialogOpened() {
+  await nextTick()
+  await initMapPicker()
+  if (pickedLngLat.value) {
+    applyLngLat({ lng: pickedLngLat.value[0], lat: pickedLngLat.value[1] }, pickedAddress.value)
+  } else if (searchKeyword.value.trim()) {
+    handleSearch()
+  }
+}
+
+function handleMapDialogClosed() {
+  destroyMapResources()
+}
+
+async function initMapPicker() {
+  if (!amapRef || !mapContainerRef.value) return
+
+  destroyMapResources()
+
+  mapInstance = new amapRef.Map(mapContainerRef.value, {
+    zoom: 12,
+    viewMode: '2D'
+  })
+
+  geocoder = new amapRef.Geocoder()
+  placeSearch = new amapRef.PlaceSearch({
+    map: mapInstance,
+    pageSize: 8,
+    city: props.city || '全国',
+    citylimit: !!props.city
+  })
+
+  autoComplete = new amapRef.AutoComplete({
+    input: searchInputId,
+    city: props.city || '全国',
+    citylimit: !!props.city
+  })
+  bindAutoComplete(autoComplete, poi => {
+    if (!poi?.location) return
+    applyPoiSelection(poi)
+  })
+
+  mapInstance.on('click', event => {
+    applyLngLat(event.lnglat)
+  })
+
+  window.setTimeout(() => {
+    mapInstance?.resize()
+  }, 120)
+}
+
+function applyPoiSelection(poi: AMapPoi) {
+  if (!poi.location) return
+  applyLngLat(poi.location, formatPoiAddress(poi))
+}
+
+function applyLngLat(lnglat: AMapLngLat, presetAddress?: string) {
+  if (!amapRef || !mapInstance) return
+  const position = resolveLngLat(lnglat)
+  pickedLngLat.value = position
+
+  if (!markerInstance) {
+    markerInstance = new amapRef.Marker({
+      position,
+      draggable: true
+    })
+    markerInstance.on('dragend', () => {
+      const pos = markerInstance?.getPosition()
+      if (pos) reverseGeocode(pos)
+    })
+    mapInstance.add(markerInstance)
+  } else {
+    markerInstance.setPosition(position)
+  }
+
+  mapInstance.setCenter(position)
+  mapInstance.setZoom(16)
+
+  if (presetAddress) {
+    pickedAddress.value = presetAddress
+    return
+  }
+  reverseGeocode(lnglat)
+}
+
+function reverseGeocode(lnglat: AMapLngLat) {
+  if (!geocoder) return
+  geocoder.getAddress(lnglat, (status, result) => {
+    if (status !== 'complete') return
+    const address = result.regeocode?.formattedAddress?.trim()
+    if (address) pickedAddress.value = address
+  })
+}
+
+function handleSearch() {
+  const keyword = searchKeyword.value.trim()
+  if (!keyword) {
+    ElMessage.warning('请输入搜索关键词')
+    return
+  }
+  if (!placeSearch) return
+
+  searchLoading.value = true
+  placeSearch.search(keyword, (status, result) => {
+    searchLoading.value = false
+    if (status !== 'complete') {
+      ElMessage.warning('未找到相关地点')
+      return
+    }
+    const poi = result.poiList?.pois?.[0]
+    if (!poi) {
+      ElMessage.warning('未找到相关地点')
+      return
+    }
+    applyPoiSelection(poi)
+  })
+}
+
+function confirmPick() {
+  if (!pickedAddress.value) return
+  innerAddress.value = pickedAddress.value
+  emit('update:modelValue', pickedAddress.value)
+  const payload: AmapAddressPickResult = { address: pickedAddress.value }
+  if (pickedLngLat.value) {
+    payload.lng = pickedLngLat.value[0]
+    payload.lat = pickedLngLat.value[1]
+    setCoordinates(payload.lng, payload.lat)
+  } else {
+    setCoordinates(null, null)
+  }
+  emit('change', payload)
+  mapVisible.value = false
+}
+
+function destroyMapResources() {
+  autoComplete = null
+  placeSearch = null
+  geocoder = null
+  markerInstance = null
+  if (mapInstance) {
+    mapInstance.destroy()
+    mapInstance = null
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.amap-address-picker {
+  width: 100%;
+}
+
+.amap-address-picker-body {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.amap-address-picker-map {
+  width: 100%;
+  height: 420px;
+  border: 1px solid var(--border, #d9e8e2);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.amap-address-picker-result,
+.amap-address-picker-hint {
+  margin: 0;
+  font-size: 13px;
+  color: var(--text-muted, #64748b);
+  line-height: 1.5;
+
+  span {
+    color: var(--text, #1e293b);
+  }
+}
+</style>
+
+<style lang="scss">
+.el-dialog.amap-address-picker-dialog.dialog-form-layout {
+  width: min(760px, calc(100vw - 80px)) !important;
+}
+</style>

+ 89 - 0
web-admin/src/components/business/pl-address-with-library/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="pl-address-with-library">
+    <div class="pl-address-with-library__row">
+      <AmapAddressPicker
+        v-model="innerAddress"
+        v-model:lng="innerLng"
+        v-model:lat="innerLat"
+        class="pl-address-with-library__picker"
+        :placeholder="placeholder"
+        :disabled="disabled"
+      />
+      <el-button
+        class="pl-address-with-library__library-btn btn-ghost"
+        :disabled="disabled"
+        @click="libraryVisible = true"
+      >
+        <el-icon><FolderOpened /></el-icon>从地址库选择
+      </el-button>
+    </div>
+    <PlTransportAddressSelectDialog v-model="libraryVisible" @select="handleLibrarySelect" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { FolderOpened } from '@element-plus/icons-vue'
+import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
+import PlTransportAddressSelectDialog from '@/components/business/pl-transport-address-select-dialog/index.vue'
+import type { TransportAddress } from '@/api/pl/index'
+
+const props = defineProps<{
+  modelValue: string
+  lng?: number | null
+  lat?: number | null
+  placeholder?: string
+  disabled?: boolean
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: string]
+  'update:lng': [value: number | null]
+  'update:lat': [value: number | null]
+}>()
+
+const libraryVisible = ref(false)
+
+const innerAddress = computed({
+  get: () => props.modelValue,
+  set: (value: string) => emit('update:modelValue', value)
+})
+
+const innerLng = computed({
+  get: () => props.lng ?? null,
+  set: (value: number | null) => emit('update:lng', value)
+})
+
+const innerLat = computed({
+  get: () => props.lat ?? null,
+  set: (value: number | null) => emit('update:lat', value)
+})
+
+function handleLibrarySelect(row: TransportAddress) {
+  innerAddress.value = row.fullAddress
+  innerLng.value = row.lng != null ? Number(row.lng) : null
+  innerLat.value = row.lat != null ? Number(row.lat) : null
+}
+</script>
+
+<style scoped lang="scss">
+.pl-address-with-library {
+  width: 100%;
+}
+
+.pl-address-with-library__row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+.pl-address-with-library__picker {
+  flex: 1 1 auto;
+  min-width: 0;
+}
+
+.pl-address-with-library__library-btn {
+  flex: 0 0 auto;
+  white-space: nowrap;
+}
+</style>

+ 83 - 57
web-admin/src/components/business/pl-plan-archive-dialog/index.vue

@@ -25,61 +25,64 @@
             />
           </section>
 
-          <section class="pl-plan-form-section">
+          <section class="pl-plan-form-section pl-plan-freight-section">
             <h4 class="pl-plan-subtitle">运费信息</h4>
-            <div class="form-grid-4">
-              <el-form-item label="是否有运输费用">
-                <el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
-              </el-form-item>
-              <template v-if="formData.hasFreight === 1">
-                <el-form-item label="运输公里数">
-                  <el-input-number
-                    v-model="formData.distanceKm"
-                    :min="0"
-                    :precision="2"
-                    style="width: 100%"
-                  />
-                </el-form-item>
-                <el-form-item label="运费计算方式">
-                  <el-select v-model="formData.freightType">
-                    <el-option label="按重量" value="by_weight" />
-                    <el-option label="按趟" value="by_trip" />
-                  </el-select>
-                </el-form-item>
-                <el-form-item
-                  v-if="formData.freightType === 'by_weight'"
-                  :label="`运输量(${freightUnitLabel})`"
-                >
-                  <el-input-number
-                    v-model="freightCalc.transportQty.value"
-                    :min="0"
-                    :precision="3"
-                    style="width: 100%"
-                    @change="freightCalc.markTransportQtyTouched()"
-                  />
-                </el-form-item>
-                <el-form-item
-                  v-if="formData.freightType === 'by_weight'"
-                  :label="`运输单价(元/${freightUnitLabel})`"
-                >
-                  <el-input-number
-                    v-model="freightCalc.freightPrice.value"
-                    :min="0"
-                    :precision="2"
-                    style="width: 100%"
-                  />
+            <el-form
+              :model="formData"
+              label-width="96px"
+              label-position="right"
+              class="pl-plan-freight-form"
+            >
+              <div class="form-grid-4">
+                <el-form-item label="是否有运输费用">
+                  <el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
                 </el-form-item>
-                <el-form-item label="运费金额(元)">
-                  <el-input-number
-                    v-model="freightCalc.freightAmount.value"
-                    :min="0"
-                    :precision="2"
-                    style="width: 100%"
-                    @change="freightCalc.markFreightAmountTouched()"
-                  />
-                </el-form-item>
-              </template>
-            </div>
+                <template v-if="formData.hasFreight === 1">
+                  <el-form-item label="运输公里数">
+                    <el-input-number
+                      v-model="formData.distanceKm"
+                      :min="0"
+                      :precision="2"
+                    />
+                  </el-form-item>
+                  <el-form-item label="运费计算方式">
+                    <el-select v-model="formData.freightType">
+                      <el-option label="按重量" value="by_weight" />
+                      <el-option label="按趟" value="by_trip" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item
+                    v-if="formData.freightType === 'by_weight'"
+                    :label="`运输量(${freightUnitLabel})`"
+                  >
+                    <el-input-number
+                      v-model="freightCalc.transportQty.value"
+                      :min="0"
+                      :precision="3"
+                      @change="freightCalc.markTransportQtyTouched()"
+                    />
+                  </el-form-item>
+                  <el-form-item
+                    v-if="formData.freightType === 'by_weight'"
+                    :label="`运输单价(元/${freightUnitLabel})`"
+                  >
+                    <el-input-number
+                      v-model="freightCalc.freightPrice.value"
+                      :min="0"
+                      :precision="2"
+                    />
+                  </el-form-item>
+                  <el-form-item label="运费金额(元)">
+                    <el-input-number
+                      v-model="freightCalc.freightAmount.value"
+                      :min="0"
+                      :precision="2"
+                      @change="freightCalc.markFreightAmountTouched()"
+                    />
+                  </el-form-item>
+                </template>
+              </div>
+            </el-form>
           </section>
         </div>
       </section>
@@ -384,10 +387,32 @@ async function handleSubmit() {
   margin-top: 0;
 }
 
-.pl-arrange-freight-muted {
-  margin-bottom: 12px;
-  font-size: 13px;
-  color: #64748b;
+.pl-plan-freight-form {
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+    align-items: center;
+  }
+
+  :deep(.el-form-item__label) {
+    height: auto !important;
+    line-height: 1.4 !important;
+    padding: 0 8px 0 0 !important;
+    justify-content: flex-end;
+  }
+
+  :deep(.el-form-item__content) {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .form-grid-4 {
+    align-items: center;
+  }
+
+  :deep(.el-select),
+  :deep(.el-input-number) {
+    width: 100%;
+  }
 }
 
 .pl-arrange-footer {
@@ -396,6 +421,7 @@ async function handleSubmit() {
   gap: 8px;
   justify-content: flex-end;
 }
+
 </style>
 
 <style lang="scss">

+ 33 - 2
web-admin/src/components/business/pl-plan-arrange-dialog/index.vue

@@ -132,7 +132,12 @@
                   <h4 class="pl-plan-subtitle">取货信息</h4>
                   <div class="form-grid-3">
                     <el-form-item label="取货地址">
-                      <el-input v-model="formData.pickupAddress" placeholder="取货地址" />
+                      <PlAddressWithLibrary
+                        v-model="formData.pickupAddress"
+                        v-model:lng="formData.pickupLng"
+                        v-model:lat="formData.pickupLat"
+                        placeholder="取货地址"
+                      />
                     </el-form-item>
                     <el-form-item label="取货时间">
                       <el-date-picker
@@ -174,7 +179,12 @@
                   <h4 class="pl-plan-subtitle">送货信息</h4>
                   <div class="form-grid-3">
                     <el-form-item label="送货地址">
-                      <el-input v-model="formData.destinationAddress" placeholder="送货地址" />
+                      <PlAddressWithLibrary
+                        v-model="formData.destinationAddress"
+                        v-model:lng="formData.destinationLng"
+                        v-model:lat="formData.destinationLat"
+                        placeholder="送货地址"
+                      />
                     </el-form-item>
                     <el-form-item label="送达时间">
                       <el-date-picker
@@ -315,7 +325,11 @@
             @click="
               copyPlanSummary({
                 pickupAddress: formData.pickupAddress,
+                pickupLng: formData.pickupLng,
+                pickupLat: formData.pickupLat,
                 destinationAddress: formData.destinationAddress,
+                destinationLng: formData.destinationLng,
+                destinationLat: formData.destinationLat,
                 pickupNotes: formData.pickupNotes,
                 destinationNotes: formData.destinationNotes,
                 licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate,
@@ -374,6 +388,7 @@ import {
   type VehiclePersonnelRow
 } from '@/api/pl'
 import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
+import PlAddressWithLibrary from '@/components/business/pl-address-with-library/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 PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
@@ -449,9 +464,13 @@ const formData = reactive({
   driverId: '',
   carrierId: '',
   pickupAddress: '',
+  pickupLng: null as number | null,
+  pickupLat: null as number | null,
   pickupTime: '',
   pickupNotes: '',
   destinationAddress: '',
+  destinationLng: null as number | null,
+  destinationLat: null as number | null,
   deliveryTime: '',
   destinationNotes: '',
   hasFreight: 0,
@@ -581,9 +600,13 @@ function applyVehiclePersonnel(personnelList: VehiclePersonnelRow[], preferredDr
 
 async function applyPrefillFromPlan(data: Plan) {
   formData.pickupAddress = data.pickupAddress || ''
+  formData.pickupLng = data.pickupLng != null ? Number(data.pickupLng) : null
+  formData.pickupLat = data.pickupLat != null ? Number(data.pickupLat) : null
   formData.pickupTime = formatDate(data.pickupTime) || ''
   formData.pickupNotes = data.pickupNotes || ''
   formData.destinationAddress = data.destinationAddress || ''
+  formData.destinationLng = data.destinationLng != null ? Number(data.destinationLng) : null
+  formData.destinationLat = data.destinationLat != null ? Number(data.destinationLat) : null
   formData.deliveryTime = formatDate(data.deliveryTime) || ''
   formData.destinationNotes = data.destinationNotes || ''
   formData.hasFreight = data.hasFreight ?? 0
@@ -657,9 +680,13 @@ function resetState() {
     driverId: '',
     carrierId: '',
     pickupAddress: '',
+    pickupLng: null,
+    pickupLat: null,
     pickupTime: '',
     pickupNotes: '',
     destinationAddress: '',
+    destinationLng: null,
+    destinationLat: null,
     deliveryTime: '',
     destinationNotes: '',
     hasFreight: 0,
@@ -829,9 +856,13 @@ async function handleSubmit() {
       driverId: formData.driverId,
       carrierId: formData.carrierId || undefined,
       pickupAddress: formData.pickupAddress || undefined,
+      pickupLng: formData.pickupLng,
+      pickupLat: formData.pickupLat,
       pickupTime: formData.pickupTime || undefined,
       pickupNotes: formData.pickupNotes || undefined,
       destinationAddress: formData.destinationAddress || undefined,
+      destinationLng: formData.destinationLng,
+      destinationLat: formData.destinationLat,
       deliveryTime: formData.deliveryTime || undefined,
       destinationNotes: formData.destinationNotes || undefined,
       transportMedium: transportMediumText.value || undefined,

+ 34 - 7
web-admin/src/components/business/pl-plan-form-dialog/index.vue

@@ -122,9 +122,14 @@
           <div class="pl-plan-logistics-row">
             <div class="pl-plan-logistics-block">
               <h4 class="pl-plan-subtitle">取货信息</h4>
-              <div class="form-grid-3">
-                <el-form-item label="取货地址">
-                  <el-input v-model="formData.pickupAddress" placeholder="取货地址" />
+              <div class="form-grid-2">
+                <el-form-item label="取货地址" class="form-col-span-2">
+                  <PlAddressWithLibrary
+                    v-model="formData.pickupAddress"
+                    v-model:lng="formData.pickupLng"
+                    v-model:lat="formData.pickupLat"
+                    placeholder="取货地址"
+                  />
                 </el-form-item>
                 <el-form-item label="取货时间">
                   <el-date-picker
@@ -164,9 +169,14 @@
 
             <div class="pl-plan-logistics-block">
               <h4 class="pl-plan-subtitle">送货信息</h4>
-              <div class="form-grid-3">
-                <el-form-item label="送货地址">
-                  <el-input v-model="formData.destinationAddress" placeholder="送货地址" />
+              <div class="form-grid-2">
+                <el-form-item label="送货地址" class="form-col-span-2">
+                  <PlAddressWithLibrary
+                    v-model="formData.destinationAddress"
+                    v-model:lng="formData.destinationLng"
+                    v-model:lat="formData.destinationLat"
+                    placeholder="送货地址"
+                  />
                 </el-form-item>
                 <el-form-item label="送达时间">
                   <el-date-picker
@@ -273,6 +283,7 @@ import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vu
 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 PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
 import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
 import {
   applyContractToPlanForm,
@@ -373,9 +384,13 @@ const formData = reactive({
   settlementMethod: '',
   hasFreight: 0,
   pickupAddress: '',
+  pickupLng: null as number | null,
+  pickupLat: null as number | null,
   pickupTime: '',
   pickupNotes: '',
   destinationAddress: '',
+  destinationLng: null as number | null,
+  destinationLat: null as number | null,
   deliveryTime: '',
   destinationNotes: '',
   vehicleId: '',
@@ -571,9 +586,13 @@ function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): Pl
     settlementMethod: formData.settlementMethod || undefined,
     hasFreight: formData.hasFreight,
     pickupAddress: formData.pickupAddress || undefined,
+    pickupLng: formData.pickupLng,
+    pickupLat: formData.pickupLat,
     pickupTime: formData.pickupTime || undefined,
     pickupNotes: formData.pickupNotes || undefined,
     destinationAddress: formData.destinationAddress || undefined,
+    destinationLng: formData.destinationLng,
+    destinationLat: formData.destinationLat,
     deliveryTime: formData.deliveryTime || undefined,
     destinationNotes: formData.destinationNotes || undefined,
     followUserId: formData.followUserId || undefined,
@@ -650,9 +669,13 @@ function resetForm() {
     settlementMethod: '',
     hasFreight: 0,
     pickupAddress: '',
+    pickupLng: null as number | null,
+    pickupLat: null as number | null,
     pickupTime: '',
     pickupNotes: '',
     destinationAddress: '',
+    destinationLng: null as number | null,
+    destinationLat: null as number | null,
     deliveryTime: '',
     destinationNotes: '',
     vehicleId: '',
@@ -705,9 +728,13 @@ async function loadPlanForEdit(planId: string) {
       settlementMethod: data.settlementMethod || '',
       hasFreight: data.hasFreight ?? 0,
       pickupAddress: data.pickupAddress || '',
+      pickupLng: data.pickupLng != null ? Number(data.pickupLng) : null,
+      pickupLat: data.pickupLat != null ? Number(data.pickupLat) : null,
       pickupTime: formatDate(data.pickupTime) || '',
       pickupNotes: data.pickupNotes || '',
       destinationAddress: data.destinationAddress || '',
+      destinationLng: data.destinationLng != null ? Number(data.destinationLng) : null,
+      destinationLat: data.destinationLat != null ? Number(data.destinationLat) : null,
       deliveryTime: formatDate(data.deliveryTime) || '',
       destinationNotes: data.destinationNotes || '',
       vehicleId: data.vehicleId || '',
@@ -1084,7 +1111,7 @@ async function handleSubmit() {
     margin-bottom: 0;
   }
 
-  .form-grid-3 {
+  .form-grid-2 {
     gap: 8px 12px;
   }
 }

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

@@ -77,7 +77,7 @@
               <ViewKvTable :fields="pickupViewFields" mode="quad" />
             </div>
 
-            <div class="party-flow-bridge" aria-hidden="true">
+            <div class="party-flow-bridge">
               <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">

+ 238 - 0
web-admin/src/components/business/pl-transport-address-select-dialog/index.vue

@@ -0,0 +1,238 @@
+<template>
+  <el-dialog
+    v-model="innerVisible"
+    title="从地址库选择"
+    width="1000px"
+    align-center
+    destroy-on-close
+    append-to-body
+    class="dialog-form-layout pl-transport-address-select-dialog"
+    modal-class="order-module-dialog-mask"
+    :close-on-click-modal="false"
+    style="backdrop-filter: none"
+    @open="handleOpen"
+  >
+    <div class="filter-block pl-transport-address-select-filter">
+      <el-form class="filter-form" @submit.prevent>
+        <div class="filter-grid">
+          <el-form-item label="名称/地址">
+            <el-input
+              v-model="keyword"
+              placeholder="搜索地址名称、详细地址、联系人或电话"
+              clearable
+              @keyup.enter="handleSearch"
+            />
+          </el-form-item>
+        </div>
+        <div class="filter-actions">
+          <div class="filter-actions-start" />
+          <div class="filter-actions-end">
+            <el-button class="btn-ghost" :loading="loading" @click="handleReset">
+              <el-icon><RefreshLeft /></el-icon>重置
+            </el-button>
+            <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
+              <el-icon><Search /></el-icon>搜索
+            </el-button>
+          </div>
+        </div>
+      </el-form>
+    </div>
+
+    <div class="panel dialog-list-panel">
+      <div class="list-body list-wrap dialog-list-body-only">
+        <el-table
+          class="list-table pl-transport-address-select-table"
+          :data="rows"
+          v-loading="loading"
+          stripe
+          row-key="id"
+          highlight-current-row
+          :max-height="480"
+          empty-text="暂无可用地址"
+          @row-dblclick="handleConfirm"
+        >
+          <el-table-column
+            type="index"
+            label="序号"
+            min-width="68"
+            align="center"
+            header-align="center"
+            :index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
+          />
+          <el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
+            <template #default="{ row }">
+              <el-link type="primary" @click="handleConfirm(row)">{{ row.addressName }}</el-link>
+            </template>
+          </el-table-column>
+          <el-table-column prop="fullAddress" label="详细地址" min-width="260" show-overflow-tooltip />
+          <el-table-column label="坐标" min-width="150" show-overflow-tooltip>
+            <template #default="{ row }">
+              <span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
+              <span v-else>—</span>
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="contactName"
+            label="联系人"
+            min-width="100"
+            align="center"
+            header-align="center"
+          />
+          <el-table-column
+            prop="contactPhone"
+            label="联系电话"
+            min-width="120"
+            align="center"
+            header-align="center"
+          />
+          <el-table-column
+            label="操作"
+            min-width="100"
+            align="center"
+            header-align="center"
+            fixed="right"
+            class-name="col-actions"
+          >
+            <template #default="{ row }">
+              <el-button link type="primary" @click="handleConfirm(row)">
+                <el-icon><Select /></el-icon>选择
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <ListPagination
+          v-show="pagination.total > 0"
+          :total="pagination.total"
+          v-model:page="pagination.page"
+          v-model:limit="pagination.pageSize"
+          @pagination="fetchList"
+        />
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="innerVisible = false">
+          <el-icon><Close /></el-icon>关闭
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { Close, RefreshLeft, Search, Select } from '@element-plus/icons-vue'
+import { transportAddressApi, type TransportAddress } from '@/api/pl/index'
+import ListPagination from '@/components/ListPagination/index.vue'
+import { pickBxPageResult } from '@/utils/http/bx'
+import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
+
+const props = defineProps<{
+  modelValue: boolean
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  select: [row: TransportAddress]
+}>()
+
+const innerVisible = computed({
+  get: () => props.modelValue,
+  set: (value: boolean) => emit('update:modelValue', value)
+})
+
+const loading = ref(false)
+const keyword = ref('')
+const rows = ref<TransportAddress[]>([])
+
+const pagination = reactive({
+  page: 1,
+  pageSize: DEFAULT_LIST_PAGE_SIZE,
+  total: 0
+})
+
+async function fetchList() {
+  loading.value = true
+  try {
+    const res = await transportAddressApi.list({
+      q: keyword.value.trim() || undefined,
+      status: '1',
+      page: pagination.page,
+      pageSize: pagination.pageSize
+    })
+    const { list, total } = pickBxPageResult<TransportAddress>(res)
+    rows.value = list
+    pagination.total = total
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  pagination.page = 1
+  fetchList()
+}
+
+function handleReset() {
+  keyword.value = ''
+  pagination.page = 1
+  fetchList()
+}
+
+function handleOpen() {
+  keyword.value = ''
+  pagination.page = 1
+  pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
+  pagination.total = 0
+  rows.value = []
+  fetchList()
+}
+
+function handleConfirm(row: TransportAddress) {
+  emit('select', row)
+  innerVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.pl-transport-address-select-filter {
+  padding: 12px 16px 10px;
+  background: var(--mint-surface, #fff);
+  border: 1px solid var(--mint-border, #d9e8e2);
+  border-radius: var(--mint-radius, 10px);
+  margin-bottom: 12px;
+
+  .filter-grid {
+    display: grid;
+    grid-template-columns: minmax(0, 1fr);
+    gap: 10px 14px;
+    max-width: 480px;
+  }
+
+  .filter-actions {
+    margin-top: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 10px;
+  }
+
+  .filter-actions-end {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-left: auto;
+  }
+
+  .filter-actions-end .el-button + .el-button {
+    margin-left: 0 !important;
+  }
+}
+
+.pl-transport-address-select-dialog {
+  :deep(.el-dialog__body) {
+    min-height: 620px;
+  }
+}
+</style>

+ 39 - 8
web-admin/src/composables/usePlPlanArrangeActions.ts

@@ -9,6 +9,7 @@ import { planApi } from '@/api/pl'
 import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
 import { useDictStore } from '@/store/modules/dict'
 import { formatDate } from '@/utils/format'
+import { appendAmapNavLine, buildAmapNavigationUrl } from '@/utils/amap/navigation'
 import { pickBxData } from '@/utils/http/bx'
 import { formatPlanProductListText } from '@/utils/pl-display'
 import { buildSiteSummaryFromInventoryLinks } from '@/utils/db-site-summary'
@@ -109,7 +110,11 @@ export function usePlPlanArrangeActions() {
     startPoint?: string
     endPoint?: string
     pickupAddress?: string
+    pickupLng?: number | null
+    pickupLat?: number | null
     destinationAddress?: string
+    destinationLng?: number | null
+    destinationLat?: number | null
     pickupNotes?: string
     destinationNotes?: string
     licensePlate?: string
@@ -124,15 +129,37 @@ export function usePlPlanArrangeActions() {
       .map(p => formatPlanProductListText(p, getUnitLabel, { usePlannedQuantity: true }))
       .join(';')
     lines.push(`产品:${productText || '—'}`)
-    lines.push(
-      `路线:${options?.pickupAddress || plan.value?.pickupAddress || options?.startPoint || '—'} → ${
-        options?.destinationAddress ||
-        plan.value?.destinationAddress ||
-        plan.value?.destinationName ||
-        options?.endPoint ||
-        '—'
-      }`
+
+    const pickupAddress =
+      options?.pickupAddress || plan.value?.pickupAddress || options?.startPoint || '—'
+    const destinationAddress =
+      options?.destinationAddress ||
+      plan.value?.destinationAddress ||
+      plan.value?.destinationName ||
+      options?.endPoint ||
+      '—'
+    lines.push(`取货地址:${pickupAddress}`)
+    lines.push(`送货地址:${destinationAddress}`)
+
+    const pickupLng = options?.pickupLng ?? plan.value?.pickupLng
+    const pickupLat = options?.pickupLat ?? plan.value?.pickupLat
+    const destinationLng = options?.destinationLng ?? plan.value?.destinationLng
+    const destinationLat = options?.destinationLat ?? plan.value?.destinationLat
+    appendAmapNavLine(
+      lines,
+      '取货导航',
+      buildAmapNavigationUrl(pickupLng, pickupLat, pickupAddress !== '—' ? pickupAddress : '取货点')
     )
+    appendAmapNavLine(
+      lines,
+      '送货导航',
+      buildAmapNavigationUrl(
+        destinationLng,
+        destinationLat,
+        destinationAddress !== '—' ? destinationAddress : '送货点'
+      )
+    )
+
     lines.push(`取货注意事项:${options?.pickupNotes || plan.value?.pickupNotes || '—'}`)
     lines.push(`送货注意事项:${options?.destinationNotes || plan.value?.destinationNotes || '—'}`)
     lines.push(`司机链接:${options?.driverUrl || driverUrl.value || '—'}`)
@@ -208,7 +235,11 @@ export function usePlPlanArrangeActions() {
     startPoint?: string
     endPoint?: string
     pickupAddress?: string
+    pickupLng?: number | null
+    pickupLat?: number | null
     destinationAddress?: string
+    destinationLng?: number | null
+    destinationLat?: number | null
     pickupNotes?: string
     destinationNotes?: string
     licensePlate?: string

+ 9 - 0
web-admin/src/env.d.ts

@@ -1,5 +1,14 @@
 /// <reference types="vite/client" />
 
+interface ImportMetaEnv {
+  readonly VITE_AMAP_KEY?: string
+  readonly VITE_AMAP_SECURITY_CODE?: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}
+
 declare module 'nprogress'
 
 declare module 'crypto-js'

+ 7 - 0
web-admin/src/router/modules/pl.ts

@@ -32,6 +32,13 @@ export const plRoutes: AppRouteRecord[] = [
         component: '/pl/vehicle/index',
         meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
       },
+      {
+        id: 'PL00000000000000000000000000004A',
+        name: 'PlTransportAddress',
+        path: 'transport-address',
+        component: '/pl/transport-address/index',
+        meta: { title: '地址库', keepAlive: true, menuCode: 'pl:transport-address:list' }
+      },
       {
         id: 'PL00000000000000000000000000004',
         name: 'PlPersonnel',

+ 168 - 0
web-admin/src/utils/amap/loader.ts

@@ -0,0 +1,168 @@
+declare global {
+  interface Window {
+    AMap?: AMapGlobal
+    _AMapSecurityConfig?: { securityJsCode: string }
+  }
+}
+
+export interface AMapLngLat {
+  lng: number
+  lat: number
+  getLng?: () => number
+  getLat?: () => number
+}
+
+export interface AMapPoi {
+  name?: string
+  address?: string
+  location?: AMapLngLat
+  district?: string
+}
+
+export interface AMapGlobal {
+  plugin: (names: string | string[], callback: () => void) => void
+  Map: new (container: HTMLElement | string, opts: Record<string, unknown>) => AMapMap
+  Marker: new (opts: Record<string, unknown>) => AMapMarker
+  AutoComplete: new (opts: Record<string, unknown>) => AMapAutoComplete
+  PlaceSearch: new (opts: Record<string, unknown>) => AMapPlaceSearch
+  Geocoder: new (opts?: Record<string, unknown>) => AMapGeocoder
+}
+
+export interface AMapMap {
+  on: (event: string, handler: (event: { lnglat: AMapLngLat }) => void) => void
+  destroy: () => void
+  resize: () => void
+  setCenter: (center: [number, number]) => void
+  setZoom: (zoom: number) => void
+  add: (overlay: unknown) => void
+  remove: (overlay: unknown) => void
+}
+
+export interface AMapMarker {
+  setPosition: (lnglat: [number, number] | AMapLngLat) => void
+  getPosition: () => AMapLngLat
+  on: (event: string, handler: () => void) => void
+}
+
+export interface AMapAutoComplete {
+  on: (event: string, handler: (event: { poi?: AMapPoi }) => void) => void
+  off: (event: string, handler: (event: { poi?: AMapPoi }) => void) => void
+}
+
+export interface AMapPlaceSearch {
+  search: (
+    keyword: string,
+    callback: (status: string, result: { poiList?: { pois?: AMapPoi[] } }) => void
+  ) => void
+}
+
+export interface AMapGeocoder {
+  getAddress: (
+    lnglat: [number, number] | AMapLngLat,
+    callback: (status: string, result: { regeocode?: { formattedAddress?: string } }) => void
+  ) => void
+  getLocation: (
+    address: string,
+    callback: (
+      status: string,
+      result: { geocodes?: Array<{ location?: { lng: number; lat: number } }> }
+    ) => void
+  ) => void
+}
+
+const AMAP_SCRIPT_ID = 'amap-js-sdk-v2'
+const DEFAULT_PLUGINS = ['AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geocoder']
+
+let loadPromise: Promise<AMapGlobal> | null = null
+let pluginsReadyPromise: Promise<void> | null = null
+
+export function getAmapKey() {
+  return String(import.meta.env.VITE_AMAP_KEY || '').trim()
+}
+
+export function getAmapSecurityCode() {
+  return String(import.meta.env.VITE_AMAP_SECURITY_CODE || '').trim()
+}
+
+export function isAmapConfigured() {
+  return !!getAmapKey()
+}
+
+function injectSecurityConfig() {
+  const securityJsCode = getAmapSecurityCode()
+  if (!securityJsCode) return
+  window._AMapSecurityConfig = { securityJsCode }
+}
+
+export function loadAmap(): Promise<AMapGlobal> {
+  const key = getAmapKey()
+  if (!key) {
+    return Promise.reject(new Error('未配置 VITE_AMAP_KEY'))
+  }
+
+  if (window.AMap) {
+    return Promise.resolve(window.AMap)
+  }
+
+  if (loadPromise) return loadPromise
+
+  injectSecurityConfig()
+
+  loadPromise = new Promise((resolve, reject) => {
+    const existing = document.getElementById(AMAP_SCRIPT_ID) as HTMLScriptElement | null
+    if (existing) {
+      existing.addEventListener('load', () => {
+        if (window.AMap) resolve(window.AMap)
+        else reject(new Error('高德地图 SDK 加载失败'))
+      })
+      existing.addEventListener('error', () => reject(new Error('高德地图 SDK 加载失败')))
+      return
+    }
+
+    const script = document.createElement('script')
+    script.id = AMAP_SCRIPT_ID
+    script.type = 'text/javascript'
+    script.async = true
+    script.src = `https://webapi.amap.com/maps?v=2.0&key=${encodeURIComponent(key)}`
+    script.onload = () => {
+      if (window.AMap) resolve(window.AMap)
+      else reject(new Error('高德地图 SDK 加载失败'))
+    }
+    script.onerror = () => reject(new Error('高德地图 SDK 加载失败'))
+    document.head.appendChild(script)
+  })
+
+  return loadPromise
+}
+
+export function loadAmapPlugins(plugins: string[] = DEFAULT_PLUGINS): Promise<void> {
+  if (pluginsReadyPromise) return pluginsReadyPromise
+
+  pluginsReadyPromise = loadAmap().then(
+    AMap =>
+      new Promise((resolve, reject) => {
+        const timer = window.setTimeout(() => {
+          pluginsReadyPromise = null
+          reject(new Error('高德地图插件加载超时'))
+        }, 15000)
+        AMap.plugin(plugins, () => {
+          window.clearTimeout(timer)
+          resolve()
+        })
+      })
+  )
+
+  return pluginsReadyPromise
+}
+
+export function resolveLngLat(lnglat: AMapLngLat): [number, number] {
+  const lng = typeof lnglat.getLng === 'function' ? lnglat.getLng() : lnglat.lng
+  const lat = typeof lnglat.getLat === 'function' ? lnglat.getLat() : lnglat.lat
+  return [lng, lat]
+}
+
+export function formatPoiAddress(poi?: AMapPoi) {
+  if (!poi) return ''
+  const parts = [poi.district, poi.address, poi.name].filter(Boolean)
+  return parts.join('').trim() || poi.name || poi.address || ''
+}

+ 20 - 0
web-admin/src/utils/amap/navigation.ts

@@ -0,0 +1,20 @@
+/** 构建高德 URI 导航链接(手机点击可唤起高德 App) */
+export function buildAmapNavigationUrl(
+  lng?: number | null,
+  lat?: number | null,
+  name?: string
+): string | null {
+  if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) {
+    return null
+  }
+  const lngText = Number(lng).toFixed(6)
+  const latText = Number(lat).toFixed(6)
+  const toName = encodeURIComponent((name || '目的地').trim() || '目的地')
+  return `https://uri.amap.com/navigation?to=${lngText},${latText},${toName}&mode=car&coordinate=gaode&callnative=1`
+}
+
+export function appendAmapNavLine(lines: string[], label: string, url: string | null) {
+  if (url) {
+    lines.push(`${label}:${url}`)
+  }
+}

+ 497 - 0
web-admin/src/views/pl/transport-address/index.vue

@@ -0,0 +1,497 @@
+<!-- 运输地址库 -->
+<template>
+  <div class="page-container">
+    <section class="panel">
+      <div class="panel-top">
+        <h2 class="title-bar-heading">地址库</h2>
+      </div>
+
+      <div class="filter-block">
+        <el-form :model="searchForm" class="filter-form" @submit.prevent>
+          <div class="filter-grid">
+            <el-form-item label="名称/地址">
+              <el-input
+                v-model="searchForm.q"
+                placeholder="搜索名称或详细地址"
+                clearable
+                @keyup.enter="handleSearch"
+              />
+            </el-form-item>
+            <el-form-item label="状态">
+              <el-select v-model="searchForm.status" placeholder="全部" clearable>
+                <el-option label="启用" value="1" />
+                <el-option label="停用" value="0" />
+              </el-select>
+            </el-form-item>
+          </div>
+          <div class="filter-actions">
+            <div class="filter-actions-end">
+              <el-button class="btn-ghost" :loading="loading" @click="handleReset">
+                <el-icon><RefreshLeft /></el-icon>重置
+              </el-button>
+              <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
+                <el-icon><Search /></el-icon>搜索
+              </el-button>
+            </div>
+          </div>
+        </el-form>
+      </div>
+
+      <div class="list-toolbar">
+        <div class="list-toolbar-row">
+          <div class="list-toolbar-selected">
+            已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
+          </div>
+          <el-button class="btn-main" type="primary" @click="handleAdd">
+            <el-icon><Plus /></el-icon>新增地址
+          </el-button>
+          <el-button
+            class="btn-ghost"
+            :disabled="selectedRows.length !== 1"
+            @click="handleToolbarEdit"
+          >
+            <el-icon><Edit /></el-icon>编辑
+          </el-button>
+          <el-button
+            class="btn-ghost"
+            type="danger"
+            :disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
+            :title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
+            @click="handleToolbarDelete"
+          >
+            <el-icon><Delete /></el-icon>删除
+          </el-button>
+        </div>
+      </div>
+
+      <div class="list-body list-wrap">
+        <el-table
+          class="list-table"
+          :data="addressRows"
+          v-loading="loading"
+          stripe
+          row-key="id"
+          @selection-change="handleSelectionChange"
+          @row-dblclick="handleView"
+        >
+          <el-table-column type="selection" min-width="55" align="center" header-align="center" />
+          <el-table-column
+            type="index"
+            label="序号"
+            min-width="68"
+            align="center"
+            header-align="center"
+            :index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
+          />
+          <el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
+            <template #default="{ row }">
+              <el-link type="primary" @click="handleView(row)">{{ row.addressName }}</el-link>
+            </template>
+          </el-table-column>
+          <el-table-column prop="fullAddress" label="详细地址" min-width="240" show-overflow-tooltip />
+          <el-table-column label="坐标" min-width="160" show-overflow-tooltip>
+            <template #default="{ row }">
+              <span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
+              <span v-else>—</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="contactName" label="联系人" min-width="100" align="center" />
+          <el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
+          <el-table-column prop="status" label="状态" min-width="90" align="center">
+            <template #default="{ row }">
+              <el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
+                {{ row.status === '1' ? '启用' : '停用' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
+        </el-table>
+
+        <div class="pagination list-pager-bar">
+          <el-pagination
+            v-model:current-page="pagination.page"
+            v-model:page-size="pagination.pageSize"
+            :total="pagination.total"
+            :page-sizes="[10, 20, 50, 100]"
+            layout="total, sizes, prev, pager, next, jumper"
+            @current-change="handlePageChange"
+            @size-change="handleSizeChange"
+          />
+        </div>
+      </div>
+    </section>
+
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="640px"
+      align-center
+      destroy-on-close
+      class="dialog-form-layout pl-transport-address-form-dialog"
+      modal-class="order-module-dialog-mask"
+      :close-on-click-modal="false"
+      style="backdrop-filter: none"
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-position="top"
+        scroll-to-error
+        :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
+      >
+        <div class="form-grid-2">
+          <el-form-item label="地址名称" prop="addressName">
+            <el-input v-model="formData.addressName" placeholder="如:安庆仓库、某某化工厂" />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select v-model="formData.status" placeholder="请选择状态">
+              <el-option label="启用" value="1" />
+              <el-option label="停用" value="0" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="详细地址" prop="fullAddress" class="form-col-span-2">
+            <AmapAddressPicker
+              v-model="formData.fullAddress"
+              v-model:lng="formData.lng"
+              v-model:lat="formData.lat"
+              placeholder="请地图选点或搜索 POI"
+            />
+          </el-form-item>
+          <el-form-item label="联系人">
+            <el-input v-model="formData.contactName" placeholder="联系人(可选)" />
+          </el-form-item>
+          <el-form-item label="联系电话">
+            <el-input v-model="formData.contactPhone" placeholder="联系电话(可选)" />
+          </el-form-item>
+          <el-form-item label="备注" class="form-col-span-2">
+            <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注(可选)" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false">
+            <el-icon><Close /></el-icon>取消
+          </el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
+            <el-icon><Check /></el-icon>确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <SystemViewDialog
+      v-model="viewVisible"
+      title="地址详情"
+      :loading="viewLoading"
+      :hero-title="viewDetail?.addressName || '—'"
+      :hero-meta="viewHeroMeta"
+      :sections="viewSections"
+    >
+      <template #hero-actions>
+        <el-button @click="handleEditFromView">
+          <el-icon><Edit /></el-icon>编辑
+        </el-button>
+        <el-button
+          type="danger"
+          :disabled="!viewDetail || !canDeleteByCreator(viewDetail)"
+          @click="handleDeleteFromView"
+        >
+          <el-icon><Delete /></el-icon>删除
+        </el-button>
+      </template>
+    </SystemViewDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
+import {
+  transportAddressApi,
+  type TransportAddress,
+  type TransportAddressPayload
+} from '@/api/pl/index'
+import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
+import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
+import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
+import { pickBxPageResult } from '@/utils/http/bx'
+import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
+import { formatDateTime } from '@/utils/format'
+import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
+
+const loading = ref(false)
+const submitLoading = ref(false)
+const addressRows = ref<TransportAddress[]>([])
+const selectedRows = ref<TransportAddress[]>([])
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formRef = ref()
+const isEdit = ref(false)
+const viewVisible = ref(false)
+const viewLoading = ref(false)
+const viewDetail = ref<TransportAddress | null>(null)
+
+const searchForm = reactive({
+  q: '',
+  status: ''
+})
+
+const pagination = reactive({
+  page: 1,
+  pageSize: DEFAULT_LIST_PAGE_SIZE,
+  total: 0
+})
+
+const formData = reactive({
+  id: '',
+  addressName: '',
+  fullAddress: '',
+  lng: null as number | null,
+  lat: null as number | null,
+  contactName: '',
+  contactPhone: '',
+  status: '1',
+  remark: ''
+})
+
+const formRules = {
+  addressName: [{ required: true, message: '请输入地址名称', trigger: 'blur' }],
+  fullAddress: [{ required: true, message: '请填写详细地址', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+  lng: [{ required: true, message: '请通过地图选点获取经度', trigger: 'change' }],
+  lat: [{ required: true, message: '请通过地图选点获取纬度', trigger: 'change' }]
+}
+
+const viewHeroMeta = computed(() => {
+  if (!viewDetail.value) return ''
+  return viewDetail.value.fullAddress || ''
+})
+
+const viewSections = computed<ViewSection[]>(() => {
+  const d = viewDetail.value
+  if (!d) return []
+  return [
+    {
+      title: '基本信息',
+      fields: [
+        { label: '地址名称', value: d.addressName },
+        { label: '详细地址', value: d.fullAddress, span: 'full' },
+        {
+          label: '坐标',
+          value: d.lng != null && d.lat != null ? `${d.lng}, ${d.lat}` : '—'
+        },
+        { label: '联系人', value: d.contactName || '—' },
+        { label: '联系电话', value: d.contactPhone || '—' },
+        {
+          label: '状态',
+          value: d.status === '1' ? '启用' : '停用',
+          tagType: d.status === '1' ? 'success' : 'info'
+        },
+        { label: '创建时间', value: formatDateTime(d.createTime) },
+        { label: '更新时间', value: formatDateTime(d.updateTime) },
+        { label: '备注', value: d.remark || '—', span: 'full' }
+      ]
+    }
+  ]
+})
+
+async function fetchData() {
+  loading.value = true
+  try {
+    const res = await transportAddressApi.list({
+      q: searchForm.q || undefined,
+      status: searchForm.status || undefined,
+      page: pagination.page,
+      pageSize: pagination.pageSize
+    })
+    const { list, total } = pickBxPageResult<TransportAddress>(res)
+    addressRows.value = list
+    pagination.total = total
+    const idSet = new Set(addressRows.value.map(item => item.id))
+    selectedRows.value = selectedRows.value.filter(item => idSet.has(item.id))
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleReset() {
+  searchForm.q = ''
+  searchForm.status = ''
+  selectedRows.value = []
+  pagination.page = 1
+  fetchData()
+}
+
+function handlePageChange() {
+  fetchData()
+}
+
+function handleSizeChange() {
+  pagination.page = 1
+  fetchData()
+}
+
+function handleSelectionChange(rows: TransportAddress[]) {
+  selectedRows.value = rows
+}
+
+function handleToolbarEdit() {
+  if (selectedRows.value.length !== 1) {
+    ElMessage.warning('请选择一条地址记录')
+    return
+  }
+  handleEdit(selectedRows.value[0])
+}
+
+function handleToolbarDelete() {
+  if (selectedRows.value.length !== 1) {
+    ElMessage.warning('请选择一条地址记录')
+    return
+  }
+  handleDelete(selectedRows.value[0])
+}
+
+function handleAdd() {
+  dialogTitle.value = '新增地址'
+  isEdit.value = false
+  Object.assign(formData, {
+    id: '',
+    addressName: '',
+    fullAddress: '',
+    lng: null,
+    lat: null,
+    contactName: '',
+    contactPhone: '',
+    status: '1',
+    remark: ''
+  })
+  dialogVisible.value = true
+}
+
+async function handleView(row: TransportAddress) {
+  viewVisible.value = true
+  viewLoading.value = true
+  viewDetail.value = null
+  try {
+    const res = await transportAddressApi.get(row.id)
+    viewDetail.value = (res.data ?? res) as TransportAddress
+  } catch {
+    viewVisible.value = false
+    ElMessage.error('加载地址详情失败')
+  } finally {
+    viewLoading.value = false
+  }
+}
+
+async function handleEdit(row: TransportAddress) {
+  dialogTitle.value = '编辑地址'
+  isEdit.value = true
+  dialogVisible.value = true
+  try {
+    const res = await transportAddressApi.get(row.id)
+    const detail = (res.data ?? res) as TransportAddress
+    Object.assign(formData, {
+      id: detail.id,
+      addressName: detail.addressName,
+      fullAddress: detail.fullAddress,
+      lng: detail.lng != null ? Number(detail.lng) : null,
+      lat: detail.lat != null ? Number(detail.lat) : null,
+      contactName: detail.contactName || '',
+      contactPhone: detail.contactPhone || '',
+      status: detail.status,
+      remark: detail.remark || ''
+    })
+  } catch {
+    ElMessage.error('加载地址详情失败')
+  }
+}
+
+function handleEditFromView() {
+  if (!viewDetail.value) return
+  viewVisible.value = false
+  handleEdit(viewDetail.value)
+}
+
+async function handleDeleteFromView() {
+  if (!viewDetail.value) return
+  await handleDelete(viewDetail.value)
+  viewVisible.value = false
+  viewDetail.value = null
+}
+
+function validateCoordinates(): boolean {
+  if (formData.lng == null || formData.lat == null) {
+    ElMessage.warning('请通过地图选点填写经纬度')
+    return false
+  }
+  return true
+}
+
+async function handleSubmit() {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid || !validateCoordinates()) return
+
+  submitLoading.value = true
+  try {
+    const payload: TransportAddressPayload = {
+      addressName: formData.addressName.trim(),
+      fullAddress: formData.fullAddress.trim(),
+      lng: formData.lng as number,
+      lat: formData.lat as number,
+      contactName: formData.contactName.trim() || undefined,
+      contactPhone: formData.contactPhone.trim() || undefined,
+      status: formData.status,
+      remark: formData.remark.trim() || undefined
+    }
+
+    if (isEdit.value) {
+      await transportAddressApi.update(formData.id, payload)
+    } else {
+      await transportAddressApi.create(payload)
+    }
+    ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
+    dialogVisible.value = false
+    fetchData()
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+function handleDialogClose() {
+  formRef.value?.resetFields()
+}
+
+async function handleDelete(row: TransportAddress) {
+  if (!canDeleteByCreator(row)) {
+    ElMessage.warning(CREATOR_DELETE_TIP)
+    return
+  }
+  await ElMessageBox.confirm('确定删除该地址吗?', '提示', { type: 'warning' })
+  await transportAddressApi.delete(row.id)
+  ElMessage.success('删除成功')
+  if (viewDetail.value?.id === row.id) {
+    viewVisible.value = false
+    viewDetail.value = null
+  }
+  fetchData()
+}
+
+useDbInstantSearch(
+  () => ({
+    q: searchForm.q,
+    status: searchForm.status
+  }),
+  handleSearch
+)
+
+onMounted(() => {
+  fetchData()
+})
+</script>

+ 59 - 0
web-admin/src/views/public/pl/transport/index.vue

@@ -15,6 +15,35 @@
           <van-cell title="路线" :value="routeDisplay" class="route-cell" />
         </van-cell-group>
 
+        <van-cell-group
+          v-if="pickupNavUrl || destinationNavUrl"
+          inset
+          class="section-card nav-card"
+          title="导航"
+        >
+          <div class="nav-actions">
+            <van-button
+              v-if="pickupNavUrl"
+              type="primary"
+              block
+              round
+              @click="openNavigation(pickupNavUrl)"
+            >
+              导航到取货点
+            </van-button>
+            <van-button
+              v-if="destinationNavUrl"
+              type="primary"
+              plain
+              block
+              round
+              @click="openNavigation(destinationNavUrl)"
+            >
+              导航到送货点
+            </van-button>
+          </div>
+        </van-cell-group>
+
         <van-cell-group v-if="page.products.length" inset class="section-card" title="产品明细">
           <van-cell
             v-for="(item, idx) in page.products"
@@ -72,6 +101,7 @@
     type PublicTransportProduct
   } from '@/api/pl/public-transport'
   import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
+  import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
 
   import 'vant/es/toast/style'
   import '@/styles/public-mobile-vant.scss'
@@ -99,6 +129,28 @@
     return `${page.value.pickupAddress || '—'} → ${page.value.destinationAddress || '—'}`
   })
 
+  const pickupNavUrl = computed(() => {
+    if (!page.value) return null
+    return buildAmapNavigationUrl(
+      page.value.pickupLng,
+      page.value.pickupLat,
+      page.value.pickupAddress || '取货点'
+    )
+  })
+
+  const destinationNavUrl = computed(() => {
+    if (!page.value) return null
+    return buildAmapNavigationUrl(
+      page.value.destinationLng,
+      page.value.destinationLat,
+      page.value.destinationAddress || '送货点'
+    )
+  })
+
+  function openNavigation(url: string) {
+    window.location.href = url
+  }
+
   function formatProductQuantity(value?: number) {
     if (value == null || Number.isNaN(Number(value))) return '—'
     const num = Number(value)
@@ -207,4 +259,11 @@
   .uploader-wrap {
     padding: 12px 16px 16px;
   }
+
+  .nav-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    padding: 12px 16px 16px;
+  }
 </style>

Неке датотеке нису приказане због велике количине промена