This commit is contained in:
zlwu committed 2026-07-12 17:55:11 +08:00
1 parent c2cba3e668
commit 2142b33cfa
24 files changed
+4834 -3684

No files matched your search

BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
6907
27076
+42 -6
View File
@@ -26,6 +26,7 @@ from app.schemas.pl import (
CarrierRead,
CarrierUpdate,
carrier_to_read,
CertRead,
DriverCreate,
DriverListResponse,
DriverRead,
@@ -45,6 +46,7 @@ from app.schemas.pl import (
from app.services.pl_cert_service import (
certs_to_read,
list_owner_certs,
list_owner_certs_batch,
nearest_expire_date,
save_owner_certs,
)
@@ -276,12 +278,16 @@ def _vehicle_to_read(
vehicle: PlVehicle,
summary: Optional[str] = None,
default_driver_name: Optional[str] = None,
*,
cert_list: Optional[list[CertRead]] = None,
) -> VehicleRead:
data = VehicleRead.model_validate(vehicle)
if summary is not None:
data.personnel_summary = summary
if default_driver_name is not None:
data.default_driver_name = default_driver_name
if cert_list is not None:
data.cert_list = cert_list
return data
@@ -296,7 +302,7 @@ def _vehicle_to_detail(
)
base = _vehicle_to_read(vehicle, summary, default_name)
return VehicleDetailRead(
**base.model_dump(by_alias=False),
**base.model_dump(by_alias=False, exclude={"cert_list"}),
personnel_list=personnel_list,
cert_list=cert_list,
)
@@ -373,8 +379,15 @@ def list_vehicles(
summaries = load_vehicle_personnel_summaries(
db, tenant_no, [v.id for v in vehicles]
)
certs_by_owner = list_owner_certs_batch(
db, tenant_no, "vehicle", [v.id for v in vehicles]
)
items = [
_vehicle_to_read(v, *summaries.get(v.id, ("", None)))
_vehicle_to_read(
v,
*summaries.get(v.id, ("", None)),
cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
)
for v in vehicles
]
return VehicleListResponse(total=len(items), list=items)
@@ -385,10 +398,17 @@ def list_vehicles(
summaries = load_vehicle_personnel_summaries(
db, tenant_no, [v.id for v in vehicles]
)
certs_by_owner = list_owner_certs_batch(
db, tenant_no, "vehicle", [v.id for v in vehicles]
)
return VehicleListResponse(
total=total,
list=[
_vehicle_to_read(v, *summaries.get(v.id, ("", None)))
_vehicle_to_read(
v,
*summaries.get(v.id, ("", None)),
cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
)
for v in vehicles
],
)
@@ -591,11 +611,17 @@ def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str, user: DbUserConte
def _personnel_to_read(
db: Session, tenant_no: str, personnel: PlPersonnel
db: Session,
tenant_no: str,
personnel: PlPersonnel,
*,
cert_list: Optional[list[CertRead]] = None,
) -> PersonnelRead:
nearest = nearest_expire_date(db, tenant_no, "personnel", personnel.id)
data = PersonnelRead.model_validate(personnel)
data.nearest_expire_date = nearest
if cert_list is not None:
data.cert_list = cert_list
return data
@@ -607,7 +633,7 @@ def _personnel_to_detail(
list_owner_certs(db, tenant_no, "personnel", personnel.id)
)
return PersonnelDetailRead(
**base.model_dump(by_alias=False),
**base.model_dump(by_alias=False, exclude={"cert_list"}),
cert_list=cert_list,
)
@@ -653,9 +679,19 @@ def list_personnel(
total = query.count()
offset = (page - 1) * page_size
rows = query.offset(offset).limit(page_size).all()
personnel_ids = [p.id for p in rows]
certs_by_owner = list_owner_certs_batch(db, tenant_no, "personnel", personnel_ids)
return PersonnelListResponse(
total=total,
list=[_personnel_to_read(db, tenant_no, p) for p in rows],
list=[
_personnel_to_read(
db,
tenant_no,
p,
cert_list=certs_to_read(certs_by_owner.get(p.id, [])),
)
for p in rows
],
)
+2
View File
@@ -247,6 +247,7 @@ class VehicleRead(VehicleBase):
tenant_no: str = Field(alias="tenantNo")
order_num: Optional[int] = Field(default=None, alias="orderNum")
personnel_summary: Optional[str] = Field(default=None, alias="personnelSummary")
cert_list: Optional[list[CertRead]] = Field(default=None, alias="certList")
create_by: Optional[str] = Field(default=None, alias="createBy")
create_dept_id: Optional[str] = Field(default=None, alias="createDeptId")
create_time: datetime = Field(alias="createTime")
@@ -313,6 +314,7 @@ class PersonnelRead(PersonnelBase):
tenant_no: str = Field(alias="tenantNo")
order_num: Optional[int] = Field(default=None, alias="orderNum")
nearest_expire_date: Optional[date] = Field(default=None, alias="nearestExpireDate")
cert_list: Optional[list[CertRead]] = Field(default=None, alias="certList")
create_by: Optional[str] = Field(default=None, alias="createBy")
create_dept_id: Optional[str] = Field(default=None, alias="createDeptId")
create_time: datetime = Field(alias="createTime")
@@ -18,7 +18,6 @@ def carrier_qualification_complete(carrier: PlCarrier) -> bool:
_urls_nonempty(carrier.business_license_urls)
and _urls_nonempty(carrier.road_transport_license_urls)
and _urls_nonempty(carrier.transport_agreement_urls)
and carrier.transport_agreement_expire_date is not None
)
+22
View File
@@ -27,6 +27,28 @@ def list_owner_certs(
)
def list_owner_certs_batch(
db: Session, tenant_no: str, owner_type: str, owner_ids: list[str]
) -> dict[str, list[PlCert]]:
if not owner_ids:
return {}
rows = (
db.query(PlCert)
.filter(
PlCert.tenant_no == tenant_no,
PlCert.owner_type == owner_type,
PlCert.owner_id.in_(owner_ids),
PlCert.del_flag == 0,
)
.order_by(PlCert.order_num.asc(), PlCert.create_time.asc())
.all()
)
grouped: dict[str, list[PlCert]] = {}
for cert in rows:
grouped.setdefault(cert.owner_id, []).append(cert)
return grouped
def certs_to_read(certs: list[PlCert]) -> list[CertRead]:
return [
CertRead(
+2 -2
View File
@@ -1,7 +1,7 @@
# Graph Report - backend (2026-07-12)
## Corpus Check
- 248 files · ~173,671 words
- 248 files · ~173,666 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
@@ -10,7 +10,7 @@
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ffc89d9b`
- Built from commit: `c2cba3e6`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "label": "pl_carrier_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L1"}, {"id": "services_pl_carrier_service_urls_nonempty", "label": "_urls_nonempty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L12"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L12"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L12"}, {"id": "services_pl_carrier_service_carrier_qualification_complete", "label": "carrier_qualification_complete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L16"}, {"id": "plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L16"}, {"id": "services_pl_carrier_service_carrier_agreement_expired", "label": "carrier_agreement_expired()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L24"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L24"}, {"id": "services_pl_carrier_service_carrier_audit_approved", "label": "carrier_audit_approved()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L32"}, {"id": "services_pl_carrier_service_carrier_eligible_for_transport", "label": "carrier_eligible_for_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L36"}, {"id": "services_pl_carrier_service_assert_carrier_eligible_for_transport", "label": "assert_carrier_eligible_for_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L48"}, {"id": "services_pl_carrier_service_normalize_carrier_urls", "label": "normalize_carrier_urls()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L68"}, {"id": "services_pl_carrier_service_validate_carrier_qualification_fields", "label": "validate_carrier_qualification_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L75"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "target": "app_models_pl_models", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_carrier_service_py", "target": "services_pl_carrier_service_urls_nonempty", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L12", "weight": 1.0}, {"source": "services_pl_carrier_service_urls_nonempty", "target": "str", "relation": "references", "context": "generic_arg", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L12", "weight": 1.0}, {"source": "services_pl_carrier_service_urls_nLine truncated
+1 -1
View File
@@ -1 +1 @@
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6716,"mtime_ns":1783761682571576393,"hash":"bbe64081c9790941eb2fa8f9c18ee39daf151068b3503f1fdb74c2cb9f7b5b45"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6716,"mtime_ns":1783761682571576393,"hash":"bbe64081c9790941eb2fa8f9c18ee39daf151068b3503f1fdb74c2cb9f7b5b45"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
+24 -24
View File
@@ -31011,7 +31011,7 @@
"label": "carrier_agreement_expired()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L25",
"source_location": "L24",
"id": "services_pl_carrier_service_carrier_agreement_expired",
"community": 4,
"norm_label": "carrier_agreement_expired()"
@@ -31020,7 +31020,7 @@
"label": "date",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L25",
"source_location": "L24",
"id": "app_services_pl_carrier_service_py_date",
"community": 4,
"norm_label": "date"
@@ -31029,7 +31029,7 @@
"label": "carrier_audit_approved()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L33",
"source_location": "L32",
"id": "services_pl_carrier_service_carrier_audit_approved",
"community": 4,
"norm_label": "carrier_audit_approved()"
@@ -31038,7 +31038,7 @@
"label": "carrier_eligible_for_transport()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L37",
"source_location": "L36",
"id": "services_pl_carrier_service_carrier_eligible_for_transport",
"community": 4,
"norm_label": "carrier_eligible_for_transport()"
@@ -31047,7 +31047,7 @@
"label": "assert_carrier_eligible_for_transport()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L49",
"source_location": "L48",
"id": "services_pl_carrier_service_assert_carrier_eligible_for_transport",
"community": 4,
"norm_label": "assert_carrier_eligible_for_transport()"
@@ -31056,7 +31056,7 @@
"label": "normalize_carrier_urls()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L69",
"source_location": "L68",
"id": "services_pl_carrier_service_normalize_carrier_urls",
"community": 4,
"norm_label": "normalize_carrier_urls()"
@@ -31065,7 +31065,7 @@
"label": "validate_carrier_qualification_fields()",
"file_type": "code",
"source_file": "app/services/pl_carrier_service.py",
"source_location": "L76",
"source_location": "L75",
"id": "services_pl_carrier_service_validate_carrier_qualification_fields",
"community": 4,
"norm_label": "validate_carrier_qualification_fields()"
@@ -355014,9 +355014,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L55",
"weight": 1.0,
"confidence_score": 1.0,
"source": "app_api_pl_plan_py",
"target": "services_pl_plan_service_validate_hazard_cert_ready_for_transport",
"confidence_score": 1.0
"target": "services_pl_plan_service_validate_hazard_cert_ready_for_transport"
},
{
"relation": "imports",
@@ -355302,9 +355302,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L85",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_85",
"target": "pl_plan_generate_plan_no",
"confidence_score": 1.0
"target": "pl_plan_generate_plan_no"
},
{
"relation": "uses",
@@ -356476,9 +356476,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L121",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_121",
"target": "pl_plan_normalize_subpack_slip_no",
"confidence_score": 1.0
"target": "pl_plan_normalize_subpack_slip_no"
},
{
"relation": "calls",
@@ -356614,9 +356614,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L154",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_154",
"target": "pl_plan_ensure_subpack_slip_no",
"confidence_score": 1.0
"target": "pl_plan_ensure_subpack_slip_no"
},
{
"relation": "uses",
@@ -359341,9 +359341,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L448",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_448",
"target": "pl_plan_create_plan_full",
"confidence_score": 1.0
"target": "pl_plan_create_plan_full"
},
{
"relation": "uses",
@@ -361376,9 +361376,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L834",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_834",
"target": "pl_plan_patch_plan_product_certs",
"confidence_score": 1.0
"target": "pl_plan_patch_plan_product_certs"
},
{
"relation": "uses",
@@ -362056,9 +362056,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L902",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_902",
"target": "pl_plan_get_plan_arrange_info",
"confidence_score": 1.0
"target": "pl_plan_get_plan_arrange_info"
},
{
"relation": "uses",
@@ -362250,9 +362250,9 @@
"source_file": "app/api/pl/plan.py",
"source_location": "L1016",
"weight": 1.0,
"confidence_score": 1.0,
"source": "pl_plan_rationale_1016",
"target": "pl_plan_cancel_plan",
"confidence_score": 1.0
"target": "pl_plan_cancel_plan"
},
{
"relation": "rationale_for",
@@ -452721,5 +452721,5 @@
}
],
"hyperedges": [],
"built_at_commit": "ffc89d9b57bbaab7a896d352ecd3490e29f8bab1"
"built_at_commit": "c2cba3e6686ec33b687bc004150df8717ee53a08"
}
+29 -19
View File
@@ -445,7 +445,7 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
"mtime": 1783845455.3648205,
"mtime": 1783848308.6951036,
"ast_hash": "6553f54ca1fd19086c170095f402a628",
"semantic_hash": ""
},
@@ -920,8 +920,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py": {
"mtime": 1783176693.3633482,
"ast_hash": "bded41d6221c370a4a575df2a2932842",
"mtime": 1783848338.6043966,
"ast_hash": "fed11a0ecf60f2e55671c4f97efc16e7",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py": {
@@ -2530,8 +2530,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-personnel-cert.ts": {
"mtime": 1783845064.4713657,
"ast_hash": "0e5d989ac54b364a5983a5f9af9fd6d6",
"mtime": 1783849542.7557807,
"ast_hash": "b063105c550403d69893f65d3da2f70c",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/patch-el-select-filterable.ts": {
@@ -2685,8 +2685,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-vehicle-cert.ts": {
"mtime": 1783844738.0471256,
"ast_hash": "873ca5c7a0f7afbfa44925292f3f68ff",
"mtime": 1783849840.3343875,
"ast_hash": "d7537c9326ca88ffb699852e5ce1832b",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/protocol-payment-installment.ts": {
@@ -3585,8 +3585,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-cert-form-list/index.vue": {
"mtime": 1783844774.5544193,
"ast_hash": "675188fc7671fde3487a942e3b9af0d3",
"mtime": 1783849061.800741,
"ast_hash": "952e011c4aa5d0432a808ad15fd11e88",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/product-view-dialog/index.vue": {
@@ -3980,8 +3980,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/index.ts": {
"mtime": 1783845066.0273972,
"ast_hash": "fa2ffcb5318c1ed1a6a06855422880d9",
"mtime": 1783849851.9140031,
"ast_hash": "b4a408e66eb8617b69385fcf85fb61d5",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/public-transport.ts": {
@@ -4165,8 +4165,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle/index.vue": {
"mtime": 1783845088.2792692,
"ast_hash": "23f9cedc7e3b87b4d670e429cf736a55",
"mtime": 1783849865.9450529,
"ast_hash": "f9e41919cf3d272be092e81cdfe5e099",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle-recharge-record/index.vue": {
@@ -4290,18 +4290,18 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/self-vehicle/index.vue": {
"mtime": 1783845096.14697,
"ast_hash": "25357bf2f887716989f8197285b5964e",
"mtime": 1783849866.0242345,
"ast_hash": "fa30de07b7abafca0ece57dd6b0fd8fb",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/carrier/index.vue": {
"mtime": 1783841744.362471,
"ast_hash": "1fcfd92cfd0d2b344d2110d7164b4b2a",
"mtime": 1783848560.1515188,
"ast_hash": "f1d3d5928211e9ad85159f91829d7cf5",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/personnel/index.vue": {
"mtime": 1783845137.1568706,
"ast_hash": "f12fbbc1e2a8e2e7cc1d3c6df3ceff6f",
"mtime": 1783849448.5590773,
"ast_hash": "d71558db33a5c5531066cf3983b02a7a",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle-maintenance-process/index.vue": {
@@ -7418,5 +7418,15 @@
"mtime": 1783845019.9129975,
"ast_hash": "f97c57b5fac34dc5d47491e9de4c1769",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-personnel-cert-badges/index.vue": {
"mtime": 1783849544.4685943,
"ast_hash": "65e1bb7e36f003b903fe47d3e6aaf85c",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-cert-badges/index.vue": {
"mtime": 1783849842.8025723,
"ast_hash": "d20bf8839872ffdd5c9939159ded0772",
"semantic_hash": ""
}
}
+7 -7
View File
@@ -267,12 +267,14 @@
"265": "Community 265",
"266": "Community 266",
"267": "Community 267",
"268": "Community 268",
"269": "Community 269",
"270": "Community 270",
"271": "Community 271",
"272": "Community 272",
"273": "Community 273",
"274": "Community 274",
"275": "Community 275",
"276": "Community 276",
"277": "Community 277",
"278": "Community 278",
@@ -616,6 +618,7 @@
"616": "Community 616",
"617": "Community 617",
"618": "Community 618",
"619": "Community 619",
"620": "Community 620",
"621": "Community 621",
"622": "Community 622",
@@ -679,6 +682,7 @@
"680": "Community 680",
"681": "Community 681",
"682": "Community 682",
"683": "Community 683",
"684": "Community 684",
"685": "Community 685",
"686": "Community 686",
@@ -707,6 +711,7 @@
"709": "Community 709",
"710": "Community 710",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
@@ -720,15 +725,10 @@
"723": "Community 723",
"724": "Community 724",
"725": "Community 725",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"732": "Community 732",
"733": "Community 733",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738",
"739": "Community 739",
"742": "Community 742"
"737": "Community 737"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2
View File
@@ -99,6 +99,7 @@ export interface Vehicle {
loadCapacity?: number
vehicleStatus: string
remark?: string
certList?: CertRead[]
createTime: string
}
@@ -152,6 +153,7 @@ export interface Personnel {
status: string
remark?: string
nearestExpireDate?: string
certList?: CertRead[]
createTime: string
}
@@ -9,7 +9,13 @@
<div v-if="!certList.length" class="cert-empty">{{ emptyHint }}</div>
<el-table v-else-if="layout === 'table'" :data="certList" stripe class="list-table cert-form-table">
<el-table
v-else-if="layout === 'table'"
:data="certList"
stripe
class="list-table cert-form-table"
:row-class-name="showVehiclePartColumn ? getCertRowClassName : undefined"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="showVehiclePartColumn"
@@ -298,6 +304,13 @@ function handleVehiclePartChange(row: CertRowWithKey, value: string) {
row.vehiclePart = normalizeVehicleCertPart(value)
}
function getCertRowClassName({ row }: { row: CertRowWithKey }) {
if (!props.showVehiclePartColumn) return ''
return normalizeVehicleCertPart(row.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER
? 'cert-row-trailer'
: 'cert-row-head'
}
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
watch(
@@ -407,6 +420,14 @@ defineExpose({ hasUploading, clearUploadRefs, refreshRequiredSlots })
.cert-form-table {
border-top: 1px solid var(--mint-border, #d9e8e2);
:deep(.cert-row-head td.el-table__cell) {
background-color: #ecfdf5 !important;
}
:deep(.cert-row-trailer td.el-table__cell) {
background-color: #fffbeb !important;
}
:deep(.el-select),
:deep(.el-input),
:deep(.el-date-editor) {
@@ -0,0 +1,80 @@
<template>
<div v-if="badges.length" class="pl-personnel-cert-badges">
<el-tooltip
v-for="badge in badges"
:key="badge.requiredName"
:content="badge.title"
placement="top"
>
<span
class="pl-personnel-cert-badges__item"
:class="`pl-personnel-cert-badges__item--${badge.status}`"
>
{{ badge.shortLabel }}
</span>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { CertRead } from '@/api/pl'
import { getPersonnelRequiredCertBadges } from '@/utils/pl-personnel-cert'
const props = withDefaults(
defineProps<{
personnelType?: string | null
certList?: CertRead[]
}>(),
{
personnelType: '',
certList: () => []
}
)
const badges = computed(() =>
getPersonnelRequiredCertBadges(props.personnelType, props.certList || [])
)
</script>
<style scoped lang="scss">
.pl-personnel-cert-badges {
display: inline-flex;
align-items: center;
gap: 4px;
}
.pl-personnel-cert-badges__item {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 4px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
line-height: 1;
cursor: default;
}
.pl-personnel-cert-badges__item--missing {
background: #f1f5f9;
color: #64748b;
}
.pl-personnel-cert-badges__item--valid {
background: #ecfdf5;
color: #059669;
}
.pl-personnel-cert-badges__item--expiring {
background: #fffbeb;
color: #d97706;
}
.pl-personnel-cert-badges__item--expired {
background: #fef2f2;
color: #dc2626;
}
</style>
@@ -0,0 +1,135 @@
<template>
<span v-if="badgeGroups.length" class="pl-vehicle-cert-badges">
<span class="pl-vehicle-cert-badges__paren">(</span>
<template v-for="(group, groupIndex) in badgeGroups" :key="group.key">
<span v-if="groupIndex > 0" class="pl-vehicle-cert-badges__divider">|</span>
<span class="pl-vehicle-cert-badges__group">
<el-tooltip
v-for="badge in group.badges"
:key="`${group.key}-${badge.requiredName}`"
:content="badge.title"
placement="top"
>
<span
class="pl-vehicle-cert-badges__item"
:class="`pl-vehicle-cert-badges__item--${badge.status}`"
>
{{ badge.shortLabel }}
</span>
</el-tooltip>
</span>
</template>
<span class="pl-vehicle-cert-badges__paren">)</span>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { CertRead } from '@/api/pl'
import {
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
getVehiclePartRequiredCertBadges,
type PlVehicleCertCheckType
} from '@/utils/pl-vehicle-cert'
const props = withDefaults(
defineProps<{
vehicleType?: PlVehicleCertCheckType
certList?: CertRead[]
trailerNo?: string | null
}>(),
{
vehicleType: 'self',
certList: () => [],
trailerNo: ''
}
)
const hasTrailer = computed(() => Boolean(props.trailerNo?.trim()))
const badgeGroups = computed(() => {
const groups = [
{
key: PL_VEHICLE_CERT_PART_HEAD,
badges: getVehiclePartRequiredCertBadges({
vehicleType: props.vehicleType,
certList: props.certList || [],
part: PL_VEHICLE_CERT_PART_HEAD,
partLabel: hasTrailer.value ? '主车' : ''
})
}
]
if (hasTrailer.value) {
groups.push({
key: PL_VEHICLE_CERT_PART_TRAILER,
badges: getVehiclePartRequiredCertBadges({
vehicleType: props.vehicleType,
certList: props.certList || [],
part: PL_VEHICLE_CERT_PART_TRAILER,
partLabel: '挂车'
})
})
}
return groups
})
</script>
<style scoped lang="scss">
.pl-vehicle-cert-badges {
display: inline-flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
white-space: nowrap;
}
.pl-vehicle-cert-badges__paren,
.pl-vehicle-cert-badges__divider {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
line-height: 1;
}
.pl-vehicle-cert-badges__group {
display: inline-flex;
align-items: center;
gap: 4px;
}
.pl-vehicle-cert-badges__item {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 4px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
line-height: 1;
cursor: default;
}
.pl-vehicle-cert-badges__item--missing {
background: #f1f5f9;
color: #64748b;
}
.pl-vehicle-cert-badges__item--valid {
background: #ecfdf5;
color: #059669;
}
.pl-vehicle-cert-badges__item--expiring {
background: #fffbeb;
color: #d97706;
}
.pl-vehicle-cert-badges__item--expired {
background: #fef2f2;
color: #dc2626;
}
</style>
+109
View File
@@ -72,6 +72,16 @@ export function getRequiredPersonnelCertNamesByType(
return PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
}
/** 车辆关联人员:从人员主数据类型映射为关联角色(司机/押运员) */
export function mapPersonnelTypeToVehicleRole(
personnelType?: string | null
): 'driver' | 'escort' | undefined {
if (personnelType === 'driver') return 'driver'
if (personnelType === 'escort') return 'escort'
if (personnelType === 'both') return 'driver'
return undefined
}
export function validatePersonnelTypeRoleCompat(
personnelType: string | undefined,
role: string | undefined,
@@ -176,3 +186,102 @@ export function getPersonnelCertMissingSuffix(
}
return hasPersonnelMasterCertMissing(certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
}
export const PL_PERSONNEL_CERT_SHORT_LABELS: Record<string, string> = {
身份证: '身',
驾驶证: '驾',
从业资格证: '业'
}
export type PersonnelCertBadgeStatus = 'missing' | 'valid' | 'expiring' | 'expired'
export interface PersonnelCertBadgeItem {
requiredName: string
shortLabel: string
status: PersonnelCertBadgeStatus
title: string
}
function getPersonnelCertMissingFieldLabels(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
requiredName: string
): string[] {
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
return ['证号', '有效期', '附件']
}
const missing: string[] = []
if (!cert.certNo?.trim()) missing.push('证号')
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
return missing
}
function formatPersonnelCertBadgeTooltip(
requiredName: string,
status: PersonnelCertBadgeStatus,
cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'>
): string {
if (status === 'missing') {
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
return `${requiredName}:缺失(未登记该证书)`
}
const missing = getPersonnelCertMissingFieldLabels(cert, requiredName)
return `${requiredName}:缺失(缺少${missing.join('、')})`
}
const expireDate = cert?.expireDate || '—'
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
const certNo = cert?.certNo?.trim()
const certNoText = certNo ? `,证号 ${certNo}` : ''
if (status === 'expired') {
return `${requiredName}:已过期,有效期 ${expireDate},已过期 ${Math.abs(diffDays)} 天${certNoText}`
}
if (status === 'expiring') {
return `${requiredName}:临期,有效期 ${expireDate},剩余 ${diffDays} 天${certNoText}`
}
return `${requiredName}:正常,有效期 ${expireDate}${certNoText}`
}
export function getPersonnelCertBadgeStatus(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'> | undefined,
requiredName: string
): PersonnelCertBadgeStatus {
if (!isPersonnelCertStrictComplete(cert, requiredName)) {
return 'missing'
}
const expireDate = cert?.expireDate
if (!expireDate) return 'missing'
const remindDays = cert?.remindDays ?? 30
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays <= 0) return 'expired'
if (diffDays <= remindDays) return 'expiring'
return 'valid'
}
export function getPersonnelRequiredCertBadges(
personnelType: string | null | undefined,
certList: CertRow[] = []
): PersonnelCertBadgeItem[] {
return getRequiredPersonnelCertNamesByType(personnelType).map(requiredName => {
const matched = certList.find(cert => matchRequiredCertName(cert.certName, requiredName))
const status = getPersonnelCertBadgeStatus(matched, requiredName)
const shortLabel = PL_PERSONNEL_CERT_SHORT_LABELS[requiredName] || requiredName.slice(0, 1)
return {
requiredName,
shortLabel,
status,
title: formatPersonnelCertBadgeTooltip(requiredName, status, matched)
}
})
}
+121
View File
@@ -235,3 +235,124 @@ export function validateVehicleCertNamesNotEmpty(
}
return true
}
export const PL_VEHICLE_CERT_SHORT_LABELS: Record<string, string> = {
行驶证: '行',
营运证: '营',
保险单: '保',
罐检报告: '罐'
}
export type VehicleCertBadgeStatus = 'missing' | 'valid' | 'expiring' | 'expired'
export interface VehicleCertBadgeItem {
requiredName: string
shortLabel: string
status: VehicleCertBadgeStatus
title: string
}
function matchVehicleRequiredCertName(
actualName: string | undefined,
requiredName: string
): boolean {
return (actualName?.trim() || '') === requiredName
}
function getVehicleCertMissingFieldLabels(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
requiredName: string
): string[] {
if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
return ['证号', '有效期', '附件']
}
const missing: string[] = []
if (!cert.certNo?.trim()) missing.push('证号')
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
return missing
}
function formatVehicleCertBadgeTooltip(
partLabel: string,
requiredName: string,
status: VehicleCertBadgeStatus,
cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'>
): string {
const prefix = partLabel ? `${partLabel} · ` : ''
if (status === 'missing') {
if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
return `${prefix}${requiredName}:缺失(未登记该证书)`
}
const missing = getVehicleCertMissingFieldLabels(cert, requiredName)
return `${prefix}${requiredName}:缺失(缺少${missing.join('、')})`
}
const expireDate = cert?.expireDate || '—'
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
const certNo = cert?.certNo?.trim()
const certNoText = certNo ? `,证号 ${certNo}` : ''
if (status === 'expired') {
return `${prefix}${requiredName}:已过期,有效期 ${expireDate},已过期 ${Math.abs(diffDays)} 天${certNoText}`
}
if (status === 'expiring') {
return `${prefix}${requiredName}:临期,有效期 ${expireDate},剩余 ${diffDays} 天${certNoText}`
}
return `${prefix}${requiredName}:正常,有效期 ${expireDate}${certNoText}`
}
export function getVehicleCertBadgeStatus(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls' | 'remindDays'> | undefined,
requiredName: string
): VehicleCertBadgeStatus {
if (!isVehicleCertUploaded(cert) || !matchVehicleRequiredCertName(cert?.certName, requiredName)) {
return 'missing'
}
const expireDate = cert?.expireDate
if (!expireDate) return 'missing'
const remindDays = cert?.remindDays ?? 30
const expDate = new Date(expireDate)
const today = new Date()
today.setHours(0, 0, 0, 0)
expDate.setHours(0, 0, 0, 0)
const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays <= 0) return 'expired'
if (diffDays <= remindDays) return 'expiring'
return 'valid'
}
export function getVehiclePartRequiredCertBadges(options: {
vehicleType: PlVehicleCertCheckType
certList?: CertRow[]
part: VehicleCertPart
partLabel?: string
}): VehicleCertBadgeItem[] {
const { vehicleType, certList = [], part, partLabel = '' } = options
const partCerts =
part === PL_VEHICLE_CERT_PART_TRAILER
? splitVehicleCertsByPart(certList).trailerCerts
: splitVehicleCertsByPart(certList).headCerts
const requiredNames =
part === PL_VEHICLE_CERT_PART_TRAILER
? getVehicleTrailerRequiredCertNames(vehicleType)
: getVehicleHeadRequiredCertNames(vehicleType)
return requiredNames.map(requiredName => {
const matched = partCerts.find(cert => matchVehicleRequiredCertName(cert.certName, requiredName))
const status = getVehicleCertBadgeStatus(matched, requiredName)
const shortLabel = PL_VEHICLE_CERT_SHORT_LABELS[requiredName] || requiredName.slice(0, 1)
return {
requiredName,
shortLabel,
status,
title: formatVehicleCertBadgeTooltip(partLabel, requiredName, status, matched)
}
})
}
+1 -1
View File
@@ -182,7 +182,7 @@
<el-tag v-else-if="row.qualificationComplete === false" type="warning" size="small">
资质不全
</el-tag>
<span v-else>—</span>
<el-tag v-else type="success" size="small">资质齐全</el-tag>
</template>
</el-table-column>
<el-table-column
+17 -2
View File
@@ -114,12 +114,18 @@
<el-table-column
prop="personnelName"
label="姓名"
min-width="100"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.personnelName }}</el-link>
<div class="personnel-name-cell">
<el-link type="primary" @click="handleView(row)">{{ row.personnelName }}</el-link>
<PlPersonnelCertBadges
:personnel-type="row.personnelType"
:cert-list="row.certList"
/>
</div>
</template>
</el-table-column>
<el-table-column
@@ -357,6 +363,7 @@ import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, User } from '@el
import { personnelApi, type Personnel, type PersonnelDetail, type CertRow } from '@/api/pl'
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
import PlPersonnelCertBadges from '@/components/business/pl-personnel-cert-badges/index.vue'
import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
@@ -824,4 +831,12 @@ onMounted(() => {
.sub-table {
margin-bottom: 8px;
}
.personnel-name-cell {
display: inline-flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
</style>
+35 -7
View File
@@ -113,14 +113,21 @@
<el-table-column
prop="licensePlate"
label="车牌号"
min-width="120"
min-width="340"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</el-link>
<div class="vehicle-plate-cell">
<el-link type="primary" @click="handleView(row)">
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</el-link>
<PlVehicleCertBadges
vehicle-type="self"
:cert-list="row.certList"
:trailer-no="row.trailerNo"
/>
</div>
</template>
</el-table-column>
<el-table-column label="车辆类型" min-width="90" align="center" header-align="center" v-if="isColumnVisible('vehicleType')">
@@ -216,7 +223,7 @@
/>
</el-form-item>
<el-form-item label="承运商" prop="carrierId">
<el-select v-model="formData.carrierId" placeholder="请选择承运商(选填)" clearable filterable>
<el-select v-model="formData.carrierId" placeholder="请选择承运商" filterable>
<el-option
v-for="c in carrierList"
:key="c.id"
@@ -390,6 +397,7 @@ import {
} from '@/api/pl'
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
import SystemViewDialog, {
type ViewSection
@@ -414,7 +422,11 @@ import {
PL_VEHICLE_CERT_NAME_OPTIONS,
getSelfVehicleCertMissingSuffix
} from '@/utils/pl-vehicle-cert'
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
import {
mapPersonnelTypeToVehicleRole,
validatePersonnelTypeRoleCompat,
validatePersonnelAffiliationVehicleCompat
} from '@/utils/pl-personnel-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
const VEHICLE_TYPE_SELF = 'self'
@@ -487,7 +499,8 @@ const formData = reactive({
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
vehicleStatus: [{ required: true, message: '请选择车辆状态', trigger: 'change' }]
vehicleStatus: [{ required: true, message: '请选择车辆状态', trigger: 'change' }],
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }]
}
function getStatusTagType(status: string) {
@@ -605,6 +618,7 @@ function handlePersonnelSelect(row: VehiclePersonnelRow, personnelId?: string) {
row.personnelName = ''
row.phoneNo = ''
row.age = null
row.role = ''
return
}
const person = personnelOptions.value.find(p => p.id === personnelId)
@@ -613,6 +627,11 @@ function handlePersonnelSelect(row: VehiclePersonnelRow, personnelId?: string) {
row.personnelName = person.personnelName
row.phoneNo = person.phoneNo || ''
row.age = person.age ?? null
const mappedRole = mapPersonnelTypeToVehicleRole(person.personnelType)
if (mappedRole) {
row.role = mappedRole
handleRoleChange(row, 0)
}
}
function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
@@ -1002,4 +1021,13 @@ onMounted(() => {
.sub-table {
margin-bottom: 8px;
}
.vehicle-plate-cell {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
gap: 6px;
}
</style>
+35 -7
View File
@@ -121,14 +121,21 @@
<el-table-column
prop="licensePlate"
label="车牌号"
min-width="120"
min-width="340"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</el-link>
<div class="vehicle-plate-cell">
<el-link type="primary" @click="handleView(row)">
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</el-link>
<PlVehicleCertBadges
:vehicle-type="row.vehicleType === 'carrier' ? 'carrier' : 'self'"
:cert-list="row.certList"
:trailer-no="row.trailerNo"
/>
</div>
</template>
</el-table-column>
<el-table-column label="车辆类型" min-width="100" align="center" header-align="center" v-if="isColumnVisible('vehicleType')">
@@ -233,7 +240,7 @@
/>
</el-form-item>
<el-form-item label="承运商" prop="carrierId">
<el-select v-model="formData.carrierId" placeholder="请选择承运商" clearable filterable>
<el-select v-model="formData.carrierId" placeholder="请选择承运商" filterable>
<el-option
v-for="c in carrierList"
:key="c.id"
@@ -412,6 +419,7 @@ import {
} from '@/api/pl'
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
import SystemViewDialog, {
type ViewSection
@@ -432,7 +440,11 @@ import {
getVehicleCertMissingSuffix,
type PlVehicleCertCheckType
} from '@/utils/pl-vehicle-cert'
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
import {
mapPersonnelTypeToVehicleRole,
validatePersonnelTypeRoleCompat,
validatePersonnelAffiliationVehicleCompat
} from '@/utils/pl-personnel-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
type CertListItem = CertRow & { _key?: string }
@@ -506,7 +518,8 @@ const formData = reactive({
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }]
vehicleType: [{ required: true, message: '请选择车辆类型', trigger: 'change' }],
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }]
}
function getStatusTagType(status: string) {
@@ -671,6 +684,7 @@ function handlePersonnelSelect(row: VehiclePersonnelRow, personnelId?: string) {
row.personnelName = ''
row.phoneNo = ''
row.age = null
row.role = ''
return
}
const person = personnelOptions.value.find(p => p.id === personnelId)
@@ -679,6 +693,11 @@ function handlePersonnelSelect(row: VehiclePersonnelRow, personnelId?: string) {
row.personnelName = person.personnelName
row.phoneNo = person.phoneNo || ''
row.age = person.age ?? null
const mappedRole = mapPersonnelTypeToVehicleRole(person.personnelType)
if (mappedRole) {
row.role = mappedRole
handleRoleChange(row, 0)
}
}
function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
@@ -1107,4 +1126,13 @@ onMounted(() => {
.sub-table {
margin-bottom: 8px;
}
.vehicle-plate-cell {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
gap: 6px;
}
</style>