This commit is contained in:
zlwu committed 2026-07-05 10:07:24 +08:00
1 parent 33c038b8d9
commit 54b27a3f0e
253 files changed
+30060 -14298

No files matched your search

+1 -1
View File
@@ -1 +1 @@
8001
25852
+7
View File
@@ -427,6 +427,13 @@ def delete_inventory(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
row = _get_inventory(db, tenant_no, id, user)
if row.status == "已完成":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="已完成的记录不能删除",
)
assert_row_deletable_by_creator(row, user_info_from_context(user))
row.del_flag = 1
row.update_by = user.id
+7
View File
@@ -296,6 +296,13 @@ def delete_plan(
) -> None:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
if plan.execution_status in ("已入库", "已出库"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="已完成的计划不能删除",
)
assert_row_deletable_by_creator(plan, user_info_from_context(user))
plan.del_flag = 1
plan.update_by = user.id
+3
View File
@@ -296,6 +296,9 @@ def list_document_reminders(
user: DbUserContext = Depends(get_db_current_user),
) -> list[DocumentReminderRead]:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
scan_cert_expiry(db, tenant_no)
db.commit()
query = db.query(PlDocumentReminder).filter(
PlDocumentReminder.tenant_no == tenant_no,
PlDocumentReminder.del_flag == 0,
+68 -14
View File
@@ -32,6 +32,7 @@ from app.schemas.pl.plan import (
PlanArchiveBody,
PlanCreate,
PlanCreateFull,
HazardCertReadyBody,
PlanListQuery,
PlanListResponse,
PlanProductCertsUpdate,
@@ -52,9 +53,13 @@ from app.services.pl_plan_service import (
build_plan_product,
calc_plan_total_amount,
clear_draft_transport_for_plan,
clear_hazard_cert_ready,
collect_hazard_product_ids,
determine_plan_type,
fetch_hazard_flags_map,
record_plan_change,
resolve_hazard_flags_from_crm,
set_hazard_cert_ready,
sync_transport_prefill_for_plan,
update_plan_product_certs,
validate_create_full,
@@ -201,6 +206,12 @@ def list_plans(
for product in all_products:
products_by_plan.setdefault(product.plan_id, []).append(product)
hazard_map = fetch_hazard_flags_map(
db,
tenant_no,
[product.product_id for product in all_products if product.product_id],
)
result_list: list[PlanWithProducts] = []
for plan in plans:
enriched_products: list[PlanProductRead] = []
@@ -209,12 +220,10 @@ def list_plans(
db,
tenant_no,
product.product_id,
product.is_toxic,
product.is_explosive,
hazard_map=hazard_map,
)
item = PlanProductRead.model_validate(product)
if is_toxic != int(product.is_toxic or 0) or is_explosive != int(product.is_explosive or 0):
item = item.model_copy(update={"is_toxic": is_toxic, "is_explosive": is_explosive})
item = item.model_copy(update={"is_toxic": is_toxic, "is_explosive": is_explosive})
enriched_products.append(item)
plan_item = PlanWithProducts.model_validate(plan)
@@ -409,8 +418,6 @@ def create_plan_full(
db,
tenant_no,
prod.product_id,
prod.is_toxic,
prod.is_explosive,
)
prod_payload = prod.model_copy(update={"is_toxic": is_toxic, "is_explosive": is_explosive})
pp = build_plan_product(
@@ -526,8 +533,6 @@ def create_from_contract(
db,
tenant_no,
cp.product_id,
0,
0,
)
pp = PlPlanProduct(
id=generate_uuid(),
@@ -577,18 +582,22 @@ def get_plan(
.all()
)
hazard_map = fetch_hazard_flags_map(
db,
tenant_no,
[product.product_id for product in products if product.product_id],
)
enriched_products = []
for product in products:
is_toxic, is_explosive = resolve_hazard_flags_from_crm(
db,
tenant_no,
product.product_id,
product.is_toxic,
product.is_explosive,
hazard_map=hazard_map,
)
item = PlanProductRead.model_validate(product)
if is_toxic != int(product.is_toxic or 0) or is_explosive != int(product.is_explosive or 0):
item = item.model_copy(update={"is_toxic": is_toxic, "is_explosive": is_explosive})
item = item.model_copy(update={"is_toxic": is_toxic, "is_explosive": is_explosive})
enriched_products.append(item)
result = PlanWithProducts.model_validate(plan)
@@ -611,6 +620,9 @@ def update_plan(
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
if plan.follow_status == "cancelled":
raise HTTPException(status_code=400, detail="已作废的计划不能修改")
# 更新主表字段
update_data = payload.model_dump(exclude_unset=True, exclude={"products", "transport"})
for key, value in update_data.items():
@@ -628,6 +640,7 @@ def update_plan(
)
.all()
)
old_hazard_product_ids = collect_hazard_product_ids(db, tenant_no, existing_products)
cert_by_product_id = {
row.product_id: (row.purchase_cert_url, row.transport_cert_url)
for row in existing_products
@@ -644,8 +657,6 @@ def update_plan(
db,
tenant_no,
prod.product_id,
prod.is_toxic,
prod.is_explosive,
)
purchase_cert = prod.purchase_cert_url
transport_cert = prod.transport_cert_url
@@ -673,6 +684,10 @@ def update_plan(
db.add(pp)
plan_products.append(pp)
new_hazard_product_ids = collect_hazard_product_ids(db, tenant_no, payload.products)
if old_hazard_product_ids != new_hazard_product_ids:
clear_hazard_cert_ready(plan)
plan.total_amount = calc_plan_total_amount(payload.products)
else:
plan_products = (
@@ -742,6 +757,38 @@ def patch_plan_product_certs(
return result
@router.patch("/{id}/hazard-cert-ready", response_model=PlanWithProducts)
def patch_plan_hazard_cert_ready(
id: str,
payload: HazardCertReadyBody,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> PlanWithProducts:
"""标记或取消危化品证书齐备(人工确认)"""
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
_get_plan(db, tenant_no, id, user)
set_hazard_cert_ready(
db,
tenant_no=tenant_no,
plan_id=id,
ready=payload.ready,
user_id=user.id,
)
plan = _get_plan(db, tenant_no, id, user)
products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == id,
PlPlanProduct.del_flag == 0,
)
.order_by(PlPlanProduct.order_num)
.all()
)
result = PlanWithProducts.model_validate(plan)
result.products = products
return result
def _build_driver_url(token: str) -> str:
origin = (settings.PUBLIC_WEB_ORIGIN or "").rstrip("/")
return f"{origin}/#/public/pl/transport/{token}"
@@ -989,6 +1036,13 @@ def delete_plan(
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
if plan.follow_status != "draft":
raise HTTPException(status_code=400, detail="只能删除草稿状态的运输计划")
if plan.transport_status != "pending":
raise HTTPException(status_code=400, detail="已安排运输的计划不能删除")
if plan.archive_status == "archived":
raise HTTPException(status_code=400, detail="已归档的计划不能删除")
assert_row_deletable_by_creator(plan, user_info_from_context(user))
plan.del_flag = 1
plan.update_by = user.id
+80 -10
View File
@@ -15,12 +15,16 @@ from app.core.database import get_db
from app.core.response import error_response, success_response
from app.core.storage import storage
from app.crud import oss as oss_svc
from app.models.pl_models import PlPlan, PlTransportSiteLink
from app.schemas.db.public_inventory import PublicInventoryFeedback, PublicInventoryPageRead
from app.models.pl_models import PlPlan, PlPlanProduct, PlTransportSiteLink
from app.schemas.db.public_inventory import PublicInventoryFeedback
from app.schemas.pl.transport import PublicTransportPlanProductRead, PublicTransportSitePageRead
from app.services.db.mappers import inventory_date_to_api
from app.utils.excel_export import load_dict_label_maps
router = APIRouter(prefix="/public/pl/site", tags=["运输计划-现场公共"])
CRM_PRODUCT_UNIT = "crm_product_unit"
def _parse_feedback_images(raw: Optional[str]) -> list[str]:
if not raw:
@@ -53,24 +57,90 @@ def _resolve_plan_voided(db: Session, link: PlTransportSiteLink) -> bool:
return bool(plan and plan.follow_status == "cancelled")
def _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicInventoryPageRead:
def _resolve_unit_label(unit_map: dict[str, str], unit_code: Optional[str]) -> str:
code = (unit_code or "").strip()
if not code:
return "吨"
return unit_map.get(code, code)
def _resolve_plan_product(db: Session, link: PlTransportSiteLink) -> Optional[PlPlanProduct]:
if not link.plan_product_id:
return None
return (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.id == link.plan_product_id,
PlPlanProduct.del_flag == 0,
)
.first()
)
def _build_site_product_read(
db: Session,
link: PlTransportSiteLink,
plan_product: Optional[PlPlanProduct],
) -> PublicTransportPlanProductRead:
unit_map = load_dict_label_maps(
db, [CRM_PRODUCT_UNIT], tenant_no=link.tenant_no
).get(CRM_PRODUCT_UNIT, {})
quantity = link.quantity
unit = "吨"
remark = None
if plan_product:
quantity = (
plan_product.quantity
or plan_product.inbound_estimate_qty
or plan_product.outbound_estimate_qty
or link.quantity
)
unit = _resolve_unit_label(unit_map, plan_product.unit)
remark = (plan_product.remark or "").strip() or None
return PublicTransportPlanProductRead(
product_name=link.product or (plan_product.product_name if plan_product else "") or "",
model=link.specification or (plan_product.model if plan_product else None),
package_spec=plan_product.package_spec if plan_product else None,
unit=unit,
quantity=quantity,
remark=remark,
)
def _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicTransportSitePageRead:
voided = _resolve_plan_voided(db, link)
return PublicInventoryPageRead(
batch_no=None,
plan = (
db.query(PlPlan)
.filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
.first()
)
plan_product = _resolve_plan_product(db, link)
product_read = _build_site_product_read(db, link, plan_product)
return PublicTransportSitePageRead(
business_type=link.business_type,
date=inventory_date_to_api(link.plan_date) or None,
partner_company=link.partner_company or "",
product=link.product or "",
specification=link.specification,
quantity=float(link.quantity or 0),
vehicle_no=link.vehicle_no or "",
plan_no=link.plan_no,
status=None,
remark=link.remark,
pickup_address=plan.pickup_address if plan else None,
pickup_lng=plan.pickup_lng if plan else None,
pickup_lat=plan.pickup_lat if plan else None,
pickup_time=plan.pickup_time if plan else None,
pickup_notes=plan.pickup_notes if plan else None,
destination_name=plan.destination_name if plan else None,
destination_address=(
(plan.destination_address or plan.destination_name) if plan else None
),
destination_lng=plan.destination_lng if plan else None,
destination_lat=plan.destination_lat if plan else None,
delivery_time=plan.delivery_time if plan else None,
destination_notes=plan.destination_notes if plan else None,
products=[product_read],
site_feedback_remark=link.site_feedback_remark,
site_feedback_images=_parse_feedback_images(link.site_feedback_images),
site_feedback_time=link.site_feedback_time,
read_only=False,
voided=voided,
)
+45 -1
View File
@@ -16,7 +16,13 @@ from app.core.database import get_db
from app.core.response import error_response, success_response
from app.crud import oss as oss_svc
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanTransport, PlTransport
from app.schemas.pl.transport import PublicTransportFeedback, PublicTransportPageRead, PublicTransportPlanProductRead
from app.schemas.pl.resource_snapshot import parse_resource_snapshot
from app.schemas.pl.transport import (
PublicTransportFeedback,
PublicTransportPageRead,
PublicTransportPersonnelRead,
PublicTransportPlanProductRead,
)
from app.utils.excel_export import load_dict_label_maps
router = APIRouter(prefix="/public/pl/transport", tags=["运输计划-司机公共"])
@@ -67,6 +73,35 @@ def _format_plan_date(value) -> Optional[str]:
return str(value)
def _resolve_other_personnel(transport: PlTransport, plan: Optional[PlPlan]) -> list[PublicTransportPersonnelRead]:
raw = transport.resource_snapshot or (plan.resource_snapshot if plan else None)
snapshot = parse_resource_snapshot(raw)
if not snapshot or not snapshot.personnel_list:
return []
selected_driver_id = (snapshot.selected_driver_id or transport.driver_id or "").strip()
driver_name = (transport.driver_name or "").strip()
rows: list[PublicTransportPersonnelRead] = []
for person in snapshot.personnel_list:
personnel_id = (person.personnel_id or "").strip()
personnel_name = (person.personnel_name or "").strip()
is_primary_driver = person.role == "driver" and (
person.is_default == 1
or (selected_driver_id and personnel_id == selected_driver_id)
or (driver_name and personnel_name == driver_name)
)
if is_primary_driver:
continue
rows.append(
PublicTransportPersonnelRead(
personnel_name=personnel_name or "—",
phone_no=person.phone_no,
role=person.role,
)
)
return rows
def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPageRead:
link = (
db.query(PlPlanTransport)
@@ -105,9 +140,12 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
package_spec=prod.package_spec,
unit=_resolve_unit_label(unit_map, prod.unit),
quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
remark=(prod.remark or "").strip() or None,
)
)
other_personnel = _resolve_other_personnel(transport, plan)
return PublicTransportPageRead(
plan_no=plan.plan_no if plan else transport.transport_no,
plan_date=_format_plan_date(plan.plan_date if plan else None),
@@ -115,14 +153,20 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
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,
pickup_time=plan.pickup_time if plan else None,
pickup_notes=plan.pickup_notes if plan else None,
destination_name=plan.destination_name 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,
delivery_time=plan.delivery_time if plan else None,
destination_notes=plan.destination_notes if plan else None,
license_plate=transport.license_plate,
driver_name=transport.driver_name,
driver_phone=transport.driver_phone,
other_personnel=other_personnel,
products=product_reads,
feedback_remark=transport.feedback_remark,
feedback_images=_parse_feedback_images(transport.feedback_images),
+6 -9
View File
@@ -51,7 +51,10 @@ from app.services.pl_transport_site_service import (
revoke_site_links_for_transport,
sync_site_links_for_transport,
)
from app.services.pl_plan_service import is_ton_product_unit
from app.services.pl_plan_service import (
is_ton_product_unit,
validate_hazard_cert_ready_for_transport,
)
from app.services.pl_transport_resource_snapshot_service import (
apply_resource_snapshot_json,
build_transport_resource_snapshot,
@@ -526,19 +529,13 @@ def arrange_transport(
if not driver_info:
raise HTTPException(status_code=400, detail="司机不存在")
# 检查产品属性(易制毒/易制爆需要证件)
# 检查产品属性(易制毒/易制爆需要证件,以 CRM 产品主数据为准)
products = db.query(PlPlanProduct).filter(
PlPlanProduct.plan_id == plan_id,
PlPlanProduct.del_flag == 0,
).all()
for prod in products:
if prod.is_explosive or prod.is_toxic:
if not prod.purchase_cert_url or not prod.transport_cert_url:
raise HTTPException(
status_code=400,
detail=f"产品【{prod.product_name}】为{'易制爆' if prod.is_explosive else '易制毒'},必须上传购买证和合法使用证明"
)
validate_hazard_cert_ready_for_transport(db, tenant_no, plan, products)
# 计算运费
freight_amount = payload.freight_amount
+2
View File
@@ -337,6 +337,8 @@ def update_clue(
_ensure_clue_in_scope(db, obj, user_info, obj.tenant_no)
if obj.is_converted == 1:
raise ValueError("已转化的线索不可修改")
if obj.status == "invalid":
raise ValueError("无效线索不可修改")
new_mobile = data.get("mobile")
if new_mobile and new_mobile != obj.mobile:
+5
View File
@@ -302,6 +302,11 @@ class PlPlan(Base):
follow_user_id = Column(String(64), nullable=True, comment="跟进人ID")
follow_user_name = Column(String(100), nullable=True, comment="跟进人姓名")
# 危化品证书齐备(人工确认)
hazard_cert_ready = Column(SmallInteger, nullable=False, default=0, comment="危化品证书是否已人工确认齐备 0-否 1-是")
hazard_cert_ready_by = Column(String(64), nullable=True, comment="危化品证书确认人")
hazard_cert_ready_time = Column(DateTime, nullable=True, comment="危化品证书确认时间")
# 公共字段
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
@@ -17,6 +17,7 @@ class PublicInventoryPageRead(BaseModel):
partner_company: str = Field(alias="partnerCompany")
product: str
specification: Optional[str] = None
package_spec: Optional[str] = Field(default=None, alias="packageSpec")
quantity: float
vehicle_no: str = Field(alias="vehicleNo")
plan_no: Optional[str] = Field(default=None, alias="planNo")
+3 -5
View File
@@ -46,7 +46,9 @@ class CarrierCreate(CarrierBase, CarrierQualificationBase):
business_license_urls: str = Field(..., max_length=2000, alias="businessLicenseUrls")
road_transport_license_urls: str = Field(..., max_length=2000, alias="roadTransportLicenseUrls")
transport_agreement_urls: str = Field(..., max_length=2000, alias="transportAgreementUrls")
transport_agreement_expire_date: date = Field(..., alias="transportAgreementExpireDate")
transport_agreement_expire_date: Optional[date] = Field(
default=None, alias="transportAgreementExpireDate"
)
model_config = {"populate_by_name": True}
@@ -101,10 +103,6 @@ class CarrierUpdate(BaseModel):
raise ValueError("请上传道路经营许可证")
if self.transport_agreement_urls is not None and not str(self.transport_agreement_urls).strip():
raise ValueError("请上传运输协议")
if self.transport_agreement_expire_date is None and any(
item is not None for item in fields[:3]
):
raise ValueError("请填写运输协议有效期")
return self
+1
View File
@@ -137,6 +137,7 @@ class FreightSettlementWithTransports(FreightSettlementRead):
transports: list = []
plans: list[SettlementPlanSummary] = []
approval_flow: list[SettlementApprovalFlowItem] = Field(default_factory=list, alias="approvalFlow")
approval_history: list[SettlementApprovalFlowItem] = Field(default_factory=list, alias="approvalHistory")
vouchers: list = []
plan_count: int = Field(default=0, alias="planCount")
license_plate_summary: Optional[str] = Field(default=None, alias="licensePlateSummary")
+8
View File
@@ -181,10 +181,18 @@ class PlanUpdate(BaseModel):
model_config = {"populate_by_name": True}
class HazardCertReadyBody(BaseModel):
ready: bool = Field(..., description="true=确认证书齐备 false=取消齐备")
model_config = {"populate_by_name": True}
class PlanRead(PlanBase, PlanResourceSnapshotMixin):
id: str
plan_no: str = Field(alias="planNo")
plan_date: date = Field(alias="planDate")
hazard_cert_ready: int = Field(default=0, alias="hazardCertReady", description="危化品证书是否已人工确认齐备")
hazard_cert_ready_by: Optional[str] = Field(default=None, alias="hazardCertReadyBy", description="危化品证书确认人")
hazard_cert_ready_time: Optional[datetime] = Field(default=None, alias="hazardCertReadyTime", description="危化品证书确认时间")
vehicle_id: Optional[str] = Field(default=None, alias="vehicleId")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
driver_id: Optional[str] = Field(default=None, alias="driverId")
+42
View File
@@ -188,6 +188,14 @@ class PublicTransportPlanProductRead(BaseModel):
package_spec: Optional[str] = Field(default=None, alias="packageSpec")
unit: Optional[str] = None
quantity: Optional[Decimal] = None
remark: Optional[str] = Field(default=None, alias="remark", description="运输计划产品备注")
model_config = {"populate_by_name": True}
class PublicTransportPersonnelRead(BaseModel):
personnel_name: str = Field(alias="personnelName")
phone_no: Optional[str] = Field(default=None, alias="phoneNo")
role: Optional[str] = None
model_config = {"populate_by_name": True}
@@ -198,12 +206,20 @@ class PublicTransportPageRead(BaseModel):
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")
pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
pickup_notes: Optional[str] = Field(default=None, alias="pickupNotes")
destination_name: Optional[str] = Field(default=None, alias="destinationName")
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")
delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
destination_notes: Optional[str] = Field(default=None, alias="destinationNotes")
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")
other_personnel: list[PublicTransportPersonnelRead] = Field(
default_factory=list, alias="otherPersonnel"
)
products: list[PublicTransportPlanProductRead] = Field(default_factory=list)
feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark")
feedback_images: list[str] = Field(default_factory=list, alias="feedbackImages")
@@ -212,6 +228,32 @@ class PublicTransportPageRead(BaseModel):
model_config = {"populate_by_name": True}
class PublicTransportSitePageRead(BaseModel):
business_type: str = Field(alias="businessType")
date: Optional[str] = None
partner_company: str = Field(alias="partnerCompany")
vehicle_no: str = Field(alias="vehicleNo")
plan_no: Optional[str] = Field(default=None, alias="planNo")
remark: Optional[str] = None
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")
pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
pickup_notes: Optional[str] = Field(default=None, alias="pickupNotes")
destination_name: Optional[str] = Field(default=None, alias="destinationName")
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")
delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
destination_notes: Optional[str] = Field(default=None, alias="destinationNotes")
products: list[PublicTransportPlanProductRead] = Field(default_factory=list)
site_feedback_remark: Optional[str] = Field(default=None, alias="siteFeedbackRemark")
site_feedback_images: list[str] = Field(default_factory=list, alias="siteFeedbackImages")
site_feedback_time: Optional[datetime] = Field(default=None, alias="siteFeedbackTime")
voided: bool = False
model_config = {"populate_by_name": True}
# =============================================================================
# 审批
# =============================================================================
@@ -100,5 +100,3 @@ def validate_carrier_qualification_fields(
raise HTTPException(status_code=400, detail="请上传道路经营许可证")
if not _urls_nonempty(transport_agreement_urls):
raise HTTPException(status_code=400, detail="请上传运输协议")
if transport_agreement_expire_date is None:
raise HTTPException(status_code=400, detail="请填写运输协议有效期")
+21 -9
View File
@@ -110,6 +110,8 @@ def save_owner_certs(
cert.del_flag = 1
cert.update_by = user_id
scan_cert_expiry(db, tenant_no)
def nearest_expire_date(
db: Session, tenant_no: str, owner_type: str, owner_id: str
@@ -170,9 +172,16 @@ def scan_cert_expiry(db: Session, tenant_no: Optional[str] = None) -> int:
created_count = 0
for cert in query.all():
days_remaining = (cert.expire_date - today).days
if days_remaining < 0 or days_remaining > cert.remind_days:
# 仅跳过尚未进入提醒窗口的证书;已过期(days_remaining < 0)仍需提醒
if days_remaining > cert.remind_days:
continue
owner_label = _resolve_owner_label(
db, cert.tenant_no, cert.owner_type, cert.owner_id
)
driver_id = cert.owner_id if cert.owner_type == "personnel" else None
driver_name = owner_label if cert.owner_type == "personnel" else None
existing = (
db.query(PlDocumentReminder)
.filter(
@@ -183,14 +192,17 @@ def scan_cert_expiry(db: Session, tenant_no: Optional[str] = None) -> int:
.first()
)
if existing:
existing.owner_type = cert.owner_type
existing.owner_id = cert.owner_id
existing.owner_label = owner_label
existing.driver_id = driver_id
existing.driver_name = driver_name
existing.document_type = cert.cert_name
existing.document_no = cert.cert_no
existing.expire_date = cert.expire_date
existing.days_remaining = days_remaining
continue
owner_label = _resolve_owner_label(
db, cert.tenant_no, cert.owner_type, cert.owner_id
)
driver_id = cert.owner_id if cert.owner_type == "personnel" else None
driver_name = owner_label if cert.owner_type == "personnel" else None
reminder = PlDocumentReminder(
id=generate_uuid(),
tenant_no=cert.tenant_no,
@@ -205,8 +217,8 @@ def scan_cert_expiry(db: Session, tenant_no: Optional[str] = None) -> int:
expire_date=cert.expire_date,
days_remaining=days_remaining,
status="pending",
create_by="system",
create_dept_id="",
create_by=cert.create_by or "system",
create_dept_id=cert.create_dept_id or "",
)
db.add(reminder)
created_count += 1
@@ -968,6 +968,23 @@ def get_approval_flow(db: Session, settlement_id: str) -> List[Dict[str, Any]]:
return [_approval_to_dict(r) for r in rows]
def get_approval_history(db: Session, settlement_id: str) -> List[Dict[str, Any]]:
"""返回全部已完成的审批动作(含撤回/重提前的历史轮次),按时间正序。"""
rows = (
db.query(PlFreightSettlementApproval)
.filter(
PlFreightSettlementApproval.settlement_id == settlement_id,
PlFreightSettlementApproval.approval_result.in_(["approved", "rejected"]),
)
.order_by(
PlFreightSettlementApproval.approval_time.asc(),
PlFreightSettlementApproval.create_time.asc(),
)
.all()
)
return [_approval_to_dict(r) for r in rows]
def _current_approver_name(db: Session, settlement_id: str) -> Optional[str]:
row = (
db.query(PlFreightSettlementApproval)
@@ -1230,6 +1247,7 @@ def get_settlement_detail(
base["transports"] = transport_payload
base["plans"] = plan_summaries
base["approvalFlow"] = get_approval_flow(db, settlement_id)
base["approvalHistory"] = get_approval_history(db, settlement_id)
base["vouchers"] = voucher_payload
return base
+155 -17
View File
@@ -1,7 +1,7 @@
"""运输计划业务逻辑"""
from datetime import date
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
from typing import Optional, Union
from fastapi import HTTPException
from sqlalchemy.orm import Session
@@ -221,28 +221,44 @@ def _calc_freight_amount(
return Decimal(str(freight_amount or 0))
def _validate_toxic_products(products: list[PlPlanProduct]) -> None:
for prod in products:
if prod.is_explosive or prod.is_toxic:
if not prod.purchase_cert_url or not prod.transport_cert_url:
label = "易制爆" if prod.is_explosive else "易制毒"
raise HTTPException(
status_code=400,
detail=f"产品【{prod.product_name}】为{label},必须上传购买证和合法使用证明",
)
def fetch_hazard_flags_map(
db: Session,
tenant_no: str,
product_ids: Optional[list[str]] = None,
) -> dict[str, tuple[int, int]]:
"""批量从 CRM 产品主数据读取易制毒/易制爆标识。"""
from app.models.crm_models import CrmProduct
ids = [str(pid) for pid in (product_ids or []) if pid]
if not ids:
return {}
rows = (
db.query(CrmProduct.id, CrmProduct.is_toxic, CrmProduct.is_explosive)
.filter(
CrmProduct.id.in_(ids),
CrmProduct.tenant_no == tenant_no,
CrmProduct.del_flag == 0,
)
.all()
)
return {
str(row.id): (int(row.is_toxic or 0), int(row.is_explosive or 0))
for row in rows
}
def resolve_hazard_flags_from_crm(
db: Session,
tenant_no: str,
product_id: Optional[str],
is_toxic: int = 0,
is_explosive: int = 0,
*,
hazard_map: Optional[dict[str, tuple[int, int]]] = None,
) -> tuple[int, int]:
if is_toxic or is_explosive:
return int(is_toxic or 0), int(is_explosive or 0)
"""从 CRM 产品主数据读取易制毒/易制爆标识(不读计划产品快照)。"""
if not product_id:
return 0, 0
if hazard_map is not None:
return hazard_map.get(str(product_id), (0, 0))
from app.models.crm_models import CrmProduct
row = (
@@ -259,6 +275,110 @@ def resolve_hazard_flags_from_crm(
return int(row.is_toxic or 0), int(row.is_explosive or 0)
def plan_has_hazard_products(
db: Session,
tenant_no: str,
products: list[PlPlanProduct],
) -> bool:
return bool(collect_hazard_product_ids(db, tenant_no, products))
def collect_hazard_product_ids(
db: Session,
tenant_no: str,
products: list[Union[PlPlanProduct, PlanProductCreate]],
) -> frozenset[str]:
product_ids = [str(p.product_id) for p in products if getattr(p, "product_id", None)]
if not product_ids:
return frozenset()
hazard_map = fetch_hazard_flags_map(db, tenant_no, product_ids)
hazard_ids: set[str] = set()
for prod in products:
product_id = getattr(prod, "product_id", None)
if not product_id:
continue
is_toxic, is_explosive = resolve_hazard_flags_from_crm(
db,
tenant_no,
product_id,
hazard_map=hazard_map,
)
if is_toxic or is_explosive:
hazard_ids.add(str(product_id))
return frozenset(hazard_ids)
def clear_hazard_cert_ready(plan: PlPlan) -> None:
plan.hazard_cert_ready = 0
plan.hazard_cert_ready_by = None
plan.hazard_cert_ready_time = None
def validate_hazard_cert_ready_for_transport(
db: Session,
tenant_no: str,
plan: PlPlan,
products: list[PlPlanProduct],
) -> None:
if not plan_has_hazard_products(db, tenant_no, products):
return
if not int(plan.hazard_cert_ready or 0):
raise HTTPException(
status_code=400,
detail="该计划危化品证书尚未确认齐备,请至危化品证上传页操作",
)
def set_hazard_cert_ready(
db: Session,
*,
tenant_no: str,
plan_id: str,
ready: bool,
user_id: str,
) -> PlPlan:
plan = (
db.query(PlPlan)
.filter(
PlPlan.id == plan_id,
PlPlan.tenant_no == tenant_no,
PlPlan.del_flag == 0,
)
.first()
)
if not plan:
raise HTTPException(status_code=404, detail="计划不存在")
if plan.archive_status == "archived":
raise HTTPException(status_code=400, detail="已归档的计划不能修改证书齐备状态")
if plan.follow_status == "cancelled":
raise HTTPException(status_code=400, detail="已作废的计划不能修改证书齐备状态")
products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == plan_id,
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
)
.all()
)
has_hazard = plan_has_hazard_products(db, tenant_no, products)
if ready:
if not has_hazard:
raise HTTPException(status_code=400, detail="该计划不含易制毒/易制爆产品,无需确认证书齐备")
plan.hazard_cert_ready = 1
plan.hazard_cert_ready_by = user_id
plan.hazard_cert_ready_time = datetime.now()
else:
clear_hazard_cert_ready(plan)
plan.update_by = user_id
db.commit()
db.refresh(plan)
return plan
def update_plan_product_certs(
db: Session,
*,
@@ -281,6 +401,21 @@ def update_plan_product_certs(
if plan.archive_status == "archived":
raise HTTPException(status_code=400, detail="已归档的计划不能修改")
existing_rows = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == plan_id,
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
)
.all()
)
hazard_map = fetch_hazard_flags_map(
db,
tenant_no,
[row.product_id for row in existing_rows if row.product_id],
)
updated: list[PlPlanProduct] = []
for item in items:
row = (
@@ -295,7 +430,10 @@ def update_plan_product_certs(
)
if not row:
raise HTTPException(status_code=404, detail=f"计划产品不存在: {item.id}")
if not (row.is_toxic or row.is_explosive):
is_toxic, is_explosive = resolve_hazard_flags_from_crm(
db, tenant_no, row.product_id, hazard_map=hazard_map
)
if not (is_toxic or is_explosive):
raise HTTPException(status_code=400, detail=f"产品【{row.product_name}】无需上传危化品证书")
row.purchase_cert_url = (item.purchase_cert_url or "").strip() or None
row.transport_cert_url = (item.transport_cert_url or "").strip() or None
@@ -652,7 +790,7 @@ def create_transport_for_plan(
if not vehicle:
raise HTTPException(status_code=400, detail="车辆不存在")
_validate_toxic_products(products)
validate_hazard_cert_ready_for_transport(db, tenant_no, plan, products)
driver_id, driver_name, driver_phone = _resolve_driver(
db, tenant_no, transport_payload.vehicle_id, transport_payload.driver_id
+8 -1
View File
@@ -21,6 +21,7 @@ def init_scheduled_tasks(get_db_session):
warn_customer_recycle,
)
from app.tasks.db_auto_plan import auto_generate_monthly_plans_job
from app.tasks.pl_cert_expiry import scan_pl_cert_expiry_job
scheduler.add_job(
check_customer_protect_time,
@@ -52,11 +53,17 @@ def init_scheduled_tasks(get_db_session):
id="warn_customer_recycle",
replace_existing=True,
)
scheduler.add_job(
scan_pl_cert_expiry_job,
"cron", hour=8, minute=0,
id="scan_pl_cert_expiry",
replace_existing=True,
)
logger.info(
"已注册定时任务: check_customer_protect_time(01:00), "
"db_auto_generate_monthly_plans(每月1日01:30), "
"pool_recovery_job(02:00), remind_customer_recycle(09:00), "
"warn_customer_recycle(10:00)"
"warn_customer_recycle(10:00), scan_pl_cert_expiry(08:00)"
)
+23
View File
@@ -0,0 +1,23 @@
"""运输计划 - 证件到期提醒定时扫描"""
import logging
from app.core.database import SessionLocal
from app.services.pl_cert_service import scan_cert_expiry
logger = logging.getLogger(__name__)
def scan_pl_cert_expiry_job():
"""每日扫描 pl_cert,生成证件到期提醒。"""
db = SessionLocal()
try:
logger.info("======== 开始执行证件到期扫描任务 ========")
created_count = scan_cert_expiry(db)
db.commit()
logger.info("证件到期扫描完成,新建提醒 %d 条", created_count)
except Exception:
db.rollback()
logger.exception("证件到期扫描任务失败")
raise
finally:
db.close()
+25 -1
View File
@@ -108,8 +108,10 @@
"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",
@@ -119,24 +121,46 @@
"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",
"149": "Community 149",
"150": "Community 150",
"151": "Community 151"
"151": "Community 151",
"152": "Community 152",
"153": "Community 153",
"154": "Community 154",
"155": "Community 155",
"156": "Community 156",
"157": "Community 157",
"158": "Community 158",
"159": "Community 159",
"160": "Community 160",
"161": "Community 161",
"162": "Community 162",
"163": "Community 163"
}
+318 -198
View File
@@ -1,16 +1,16 @@
# Graph Report - backend (2026-07-01)
# Graph Report - backend (2026-07-05)
## Corpus Check
- 229 files · ~159,883 words
- 230 files · ~160,983 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 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)
- 5150 nodes · 35327 edges · 164 communities (162 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 17344 edges (avg confidence: 0.5)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `8e70047f`
- Built from commit: `33c038b8`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -124,35 +124,59 @@
- [[_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]]
- [[_COMMUNITY_Community 152|Community 152]]
- [[_COMMUNITY_Community 153|Community 153]]
- [[_COMMUNITY_Community 154|Community 154]]
- [[_COMMUNITY_Community 155|Community 155]]
- [[_COMMUNITY_Community 156|Community 156]]
- [[_COMMUNITY_Community 157|Community 157]]
- [[_COMMUNITY_Community 158|Community 158]]
- [[_COMMUNITY_Community 159|Community 159]]
- [[_COMMUNITY_Community 160|Community 160]]
- [[_COMMUNITY_Community 161|Community 161]]
- [[_COMMUNITY_Community 162|Community 162]]
- [[_COMMUNITY_Community 163|Community 163]]
## God Nodes (most connected - your core abstractions)
1. `Session` - 1123 edges
2. `BaseModel` - 423 edges
2. `BaseModel` - 426 edges
3. `int` - 336 edges
4. `get_tenant_no()` - 334 edges
4. `get_tenant_no()` - 335 edges
5. `Any` - 331 edges
6. `DbUserContext` - 282 edges
6. `DbUserContext` - 283 edges
7. `SysUser` - 282 edges
8. `success_response()` - 259 edges
9. `Session` - 257 edges
@@ -170,31 +194,31 @@
- `int` --uses--> `LocalStorage` [INFERRED]
scripts/run_db_auto_generate_plans.py → app/core/storage.py
## Communities (140 total, 2 thin omitted)
## Communities (164 total, 2 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.14
Nodes (105): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementCreate (+97 more)
Nodes (102): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementUpdate (+94 more)
### Community 1 - "Community 1"
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.33
Nodes (38): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+30 more)
### Community 2 - "Community 2"
Cohesion: 0.35
Nodes (220): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+212 more)
Cohesion: 0.36
Nodes (219): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas, CRM 产品模块 Pydantic Schemas (+211 more)
### Community 3 - "Community 3"
Cohesion: 0.07
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)
Cohesion: 0.05
Nodes (225): attachment_preview_url(), create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post(), create_role_management(), create_site() (+217 more)
### Community 4 - "Community 4"
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)
Cohesion: 0.04
Nodes (70): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+62 more)
### Community 5 - "Community 5"
Cohesion: 0.07
Nodes (41): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+33 more)
Cohesion: 0.08
Nodes (40): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+32 more)
### Community 6 - "Community 6"
Cohesion: 0.14
@@ -205,76 +229,76 @@ 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
Cohesion: 0.12
Nodes (45): Any, CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, Decimal, _build_customer_address(), _calc_amount_with_tax(), _change_row_to_dict() (+37 more)
### Community 9 - "Community 9"
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)
Cohesion: 0.28
Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
### Community 10 - "Community 10"
Cohesion: 0.03
Nodes (185): register_quote_routes(), CrmContractProductPriceTier, Query, Any, bool, int, Session, str (+177 more)
Cohesion: 0.09
Nodes (39): CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), date, Decimal, Query, _build_quote_filters(), calculate_quote_amount(), create_quote(), _decimal_value() (+31 more)
### Community 11 - "Community 11"
Cohesion: 0.16
Nodes (26): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+18 more)
Nodes (36): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+28 more)
### Community 12 - "Community 12"
Cohesion: 0.10
Nodes (92): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+84 more)
Cohesion: 0.11
Nodes (90): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+82 more)
### Community 13 - "Community 13"
Cohesion: 0.13
Nodes (8): bool, Path, str, BaseStorage, LocalStorage, OSSStorage, QiniuStorage, Path
Cohesion: 0.06
Nodes (61): Any, bool, bytes, int, str, Exception, bool, bytes (+53 more)
### Community 14 - "Community 14"
Cohesion: 0.09
Nodes (58): Any, bool, CrmContract, datetime, int, Query, Session, str (+50 more)
Nodes (55): Any, bool, datetime, int, Session, str, _advance_contract_after_approval(), approve_contract() (+47 more)
### Community 15 - "Community 15"
Cohesion: 0.14
Nodes (29): str, T, bool, date, datetime, int, Session, str (+21 more)
Cohesion: 0.19
Nodes (23): bool, date, datetime, int, Session, str, allocate_reimburse_title_seq(), format_reimburse_title() (+15 more)
### Community 16 - "Community 16"
Cohesion: 0.03
Nodes (201): register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, date (+193 more)
Cohesion: 0.09
Nodes (75): register_contract_routes(), bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, Session, str, _apply_contract_entity_fields(), _apply_contract_scalar_fields() (+67 more)
### Community 17 - "Community 17"
Cohesion: 0.18
Nodes (55): Any, date, Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal (+47 more)
Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
### Community 18 - "Community 18"
Cohesion: 0.14
Nodes (31): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+23 more)
Cohesion: 0.13
Nodes (28): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+20 more)
### Community 19 - "Community 19"
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)
Cohesion: 0.27
Nodes (22): 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() (+14 more)
### Community 20 - "Community 20"
Cohesion: 0.28
Nodes (34): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+26 more)
Cohesion: 0.21
Nodes (47): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+39 more)
### Community 21 - "Community 21"
Cohesion: 0.05
Nodes (114): DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), Session, str, date, 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), bool (+106 more)
Cohesion: 0.07
Nodes (61): CertRead, date, int, Session, str, CertRead, Session, str (+53 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)
Nodes (36): Any, Session, str, PlFreightSettlement, PlFreightSettlementApproval, _advance_after_approval(), _approval_to_dict(), can_user_approve_settlement() (+28 more)
### Community 23 - "Community 23"
Cohesion: 0.24
Nodes (44): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, int, str (+36 more)
Cohesion: 0.23
Nodes (38): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, str, BatchOffsetRequest (+30 more)
### Community 24 - "Community 24"
Cohesion: 0.12
Nodes (92): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+84 more)
Cohesion: 0.11
Nodes (95): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+87 more)
### Community 25 - "Community 25"
Cohesion: 0.05
Nodes (126): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, PlTransportSiteLink (+118 more)
Cohesion: 0.34
Nodes (35): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, FreightSettlementApprovalActionBody (+27 more)
### Community 26 - "Community 26"
Cohesion: 0.20
@@ -282,107 +306,107 @@ Nodes (18): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_me
### Community 27 - "Community 27"
Cohesion: 0.11
Nodes (58): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+50 more)
Nodes (57): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+49 more)
### Community 28 - "Community 28"
Cohesion: 0.14
Nodes (25): bool, BxUserContext, int, Session, str, Any, bool, int (+17 more)
Cohesion: 0.11
Nodes (33): Any, str, bool, BxUserContext, int, Session, str, Any (+25 more)
### Community 29 - "Community 29"
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 (41): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+33 more)
Cohesion: 0.06
Nodes (56): BaseModel, AdvanceApplyOut, AttachmentOut, ContactByCustomerBody, ContactDetailBody, ContactSetPrimaryBody, CRM 联系人 Pydantic Schemas, ContractApprovalPassBody (+48 more)
### Community 31 - "Community 31"
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.15
Nodes (30): Any, bool, int, Session, str, approve_quote(), can_user_approve(), create_approval_records() (+22 more)
### Community 32 - "Community 32"
Cohesion: 0.10
Nodes (99): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+91 more)
Cohesion: 0.09
Nodes (110): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+102 more)
### Community 33 - "Community 33"
Cohesion: 0.13
Nodes (54): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+46 more)
Cohesion: 0.12
Nodes (58): customer_create(), Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作) (+50 more)
### Community 34 - "Community 34"
Cohesion: 0.23
Nodes (28): Engine, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main() (+20 more)
Cohesion: 0.22
Nodes (29): Engine, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str() (+21 more)
### Community 35 - "Community 35"
Cohesion: 0.03
Nodes (196): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+188 more)
Cohesion: 0.05
Nodes (47): FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, PlPlan, PlFreightSettlementApproval, PlFreightSettlementTransport, PlRemittanceVoucher (+39 more)
### Community 36 - "Community 36"
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
### Community 37 - "Community 37"
Cohesion: 0.33
Nodes (14): Any, int, Session, str, create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options() (+6 more)
Cohesion: 0.19
Nodes (27): CrmContract, Query, CrmContractProductPriceTier, date, datetime, int, Query, _parse_letter_oss_ids_from_tier() (+19 more)
### Community 38 - "Community 38"
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.16
Nodes (23): datetime, int, str, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), generate_random_password() (+15 more)
Cohesion: 0.23
Nodes (27): register_quote_routes(), bool, int, Session, str, accept_quote(), _ensure_quote_in_scope(), generate_pdf() (+19 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.13
Nodes (40): Any, bool, date, int, Session, str, Session, str (+32 more)
Cohesion: 0.12
Nodes (41): Any, bool, date, int, Session, str, Session, str (+33 more)
### Community 42 - "Community 42"
Cohesion: 0.08
Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
### Community 43 - "Community 43"
Cohesion: 0.08
Nodes (48): Any, int, str, float, int, str, Any, int (+40 more)
Cohesion: 0.21
Nodes (16): Any, int, str, BusinessException, InvalidStatusTransitionException, log_api_request(), log_delivery_operation(), log_operation() (+8 more)
### Community 44 - "Community 44"
Cohesion: 0.21
Nodes (37): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+29 more)
Cohesion: 0.19
Nodes (41): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+33 more)
### Community 45 - "Community 45"
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)
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)
### 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)
Cohesion: 0.19
Nodes (18): str, Any, bool, Session, str, CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _yes_no(), _build_workbook() (+10 more)
### Community 47 - "Community 47"
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)
Cohesion: 0.07
Nodes (27): _build_contract_filters(), export_contracts(), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), Java: queryList for export (+19 more)
### Community 48 - "Community 48"
Cohesion: 0.18
Nodes (23): BxUserContext, int, Session, str, Any, str, create_workflow(), delete_workflow() (+15 more)
Cohesion: 0.10
Nodes (34): Session, str, TransportInventoryLinkRead, PlTransportSiteLink, Session, str, TransportSiteLinkRead, CreateFromContractRequest (+26 more)
### Community 49 - "Community 49"
Cohesion: 0.31
Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
### Community 50 - "Community 50"
Cohesion: 0.11
Nodes (39): CRM 报价单 API 路由 — 挂接到 crm.router, str, CRM 报价单 Pydantic Schemas, AdvanceApprovalCreate, ContractApprovalPassBody, CustomerCheckDuplicateBody, CustomerPoolDetailBody, CustomerPoolListBody (+31 more)
Cohesion: 0.08
Nodes (25): bool, _clear_settlement_approval_records(), 软删除结算单历史审批记录(重新提交审批前清理), 软删除结算单历史审批记录(重新提交审批前清理), 软删除结算单历史审批记录(重新提交审批前清理), 草稿结算单按当前运输单运费刷新明细快照与合计。, 草稿结算单按当前运输单运费刷新明细快照与合计。, 草稿/已驳回结算单按当前运输单运费刷新明细快照与合计。 (+17 more)
### Community 51 - "Community 51"
Cohesion: 0.22
Nodes (18): bytes, int, str, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url() (+10 more)
Cohesion: 0.07
Nodes (102): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, CreateFromContractRequest (+94 more)
### Community 52 - "Community 52"
Cohesion: 0.16
Nodes (33): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+25 more)
Cohesion: 0.12
Nodes (41): Decimal, str, Any, bool, CrmContractProductPriceTier, date, Decimal, int (+33 more)
### Community 53 - "Community 53"
Cohesion: 0.24
@@ -413,44 +437,44 @@ 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.35
Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
Cohesion: 0.07
Nodes (45): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+37 more)
### Community 61 - "Community 61"
Cohesion: 0.08
Nodes (85): int, PlTransport, Session, str, UploadFile, bool, date, DbInboundOutbound (+77 more)
Cohesion: 0.16
Nodes (38): bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan, PlPlanProduct, PlTransport (+30 more)
### Community 62 - "Community 62"
Cohesion: 0.10
Nodes (56): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+48 more)
Cohesion: 0.08
Nodes (76): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+68 more)
### Community 63 - "Community 63"
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)
Cohesion: 0.24
Nodes (12): Session, str, iter_user_menu_perm_codes(), _map_role_name_to_web(), Web 管理端登录:基于 V0.3 sys_user / sys_user_role / sys_role 的角色与菜单权限解析, 将 sys_role.role_name 映射为 web-admin 使用的 role 字符串。, 根据用户关联角色解析 Web 端 role(admin / super_admin)。, 根据用户关联角色解析 Web 端 role(admin / super_admin)。 (+4 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)
Cohesion: 0.11
Nodes (22): Any, _fill_contract_linked_count(), list_quotes(), quote_row_to_dict(), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相) (+14 more)
### Community 65 - "Community 65"
Cohesion: 0.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
### Community 66 - "Community 66"
Cohesion: 0.14
Nodes (14): 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove, 删除供方 — Java: CrmSupplierInfoController.remove (+6 more)
Cohesion: 0.05
Nodes (116): DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str, VehicleRead (+108 more)
### Community 67 - "Community 67"
Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
### Community 68 - "Community 68"
Cohesion: 0.07
Nodes (61): Any, CertRead, date, int, Session, str, CertRead, Session (+53 more)
Cohesion: 0.09
Nodes (22): batch_assign_owner(), download_contract_pdf(), Java: batchAssignOwner, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner (+14 more)
### Community 69 - "Community 69"
Cohesion: 0.33
Nodes (22): DbUserContext, float, Session, str, get_db_current_user(), DashboardCargoStat, DashboardPartnerCargoStat, DashboardStats (+14 more)
Cohesion: 0.28
Nodes (25): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+17 more)
### Community 70 - "Community 70"
Cohesion: 0.29
@@ -465,40 +489,40 @@ Cohesion: 0.34
Nodes (19): check_python(), create_venv(), ensure_dirs(), ensure_valid_venv(), ensure_venv_and_runtime(), install_dependencies(), is_running(), log_error() (+11 more)
### Community 73 - "Community 73"
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)
Cohesion: 0.25
Nodes (28): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, MenuCreateBody (+20 more)
### Community 74 - "Community 74"
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.03
Nodes (72): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+64 more)
### Community 75 - "Community 75"
Cohesion: 0.07
Nodes (95): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+87 more)
Cohesion: 0.05
Nodes (129): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+121 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.23
Nodes (29): DbUserContext, DbWeighingSlip, Session, str, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing() (+21 more)
Cohesion: 0.20
Nodes (30): DbUserContext, DbWeighingSlip, Session, str, Session, str, weighing_to_read(), apply_product_snapshot_to_inventory() (+22 more)
### Community 78 - "Community 78"
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)
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)
### 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.07
Nodes (46): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+38 more)
Cohesion: 0.10
Nodes (17): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), Session, str, _auto_fill_create_dept_id(), get_db(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID (+9 more)
### Community 81 - "Community 81"
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
Cohesion: 0.40
Nodes (4): CreateLinkForPlanPayload, InboundOutboundBase, InboundOutboundCreate, InboundOutboundUpdate
### Community 82 - "Community 82"
Cohesion: 0.29
@@ -509,40 +533,36 @@ Cohesion: 0.18
Nodes (9): 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, 报销(bx)Pydantic 模型 — 响应协议与 bx 原系统保持一致, bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。 (+1 more)
### Community 84 - "Community 84"
Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
Cohesion: 0.28
Nodes (11): operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), Session, OperLogDetailBody, OperLogListBody (+3 more)
### Community 85 - "Community 85"
Cohesion: 0.23
Nodes (47): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+39 more)
### Community 86 - "Community 86"
Cohesion: 0.30
Nodes (13): Any, bool, bytes, int, str, ApiCryptoError, _b64url_decode(), _b64url_encode() (+5 more)
Cohesion: 0.11
Nodes (48): Session, str, datetime, int, str, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime() (+40 more)
### Community 87 - "Community 87"
Cohesion: 0.19
Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
Cohesion: 0.08
Nodes (85): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Any (+77 more)
### Community 88 - "Community 88"
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.16
Nodes (12): float, int, str, 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMes, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n (+4 more)
### Community 89 - "Community 89"
Cohesion: 0.20
Nodes (6): int, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(PDF 等 iframe 场景)。, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
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 90 - "Community 90"
Cohesion: 0.16
Nodes (26): Any, bool, bytes, int, Request, str, bool, int (+18 more)
Cohesion: 0.22
Nodes (18): Any, bool, bytes, int, Request, str, _claims_from_auth(), _format_body_bytes() (+10 more)
### Community 91 - "Community 91"
Cohesion: 0.32
Nodes (7): str, eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci()
### Community 92 - "Community 92"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
Cohesion: 0.10
Nodes (21): Java: createNewVersion — 返回新报价单 id, Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: statisticsOverview (+13 more)
### Community 93 - "Community 93"
Cohesion: 0.23
@@ -553,116 +573,156 @@ Cohesion: 0.18
Nodes (11): generate_contract_name(), Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName (+3 more)
### Community 95 - "Community 95"
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
Cohesion: 0.18
Nodes (11): 解析司机信息:优先 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。 (+3 more)
### Community 96 - "Community 96"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### Community 97 - "Community 97"
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.07
Nodes (29): BaseModel, AdvanceApplyOut, AttachmentOut, ReimburseApplyCreate, Config, PageResult, ResponseModel, ContactByCustomerBody (+21 more)
### Community 100 - "Community 100"
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 97 - "Community 97"
Cohesion: 0.20
Nodes (32): pool_rule_delete(), pool_rule_toggle(), pool_rule_update(), Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑, int, Session (+24 more)
### Community 98 - "Community 98"
Cohesion: 0.35
Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
### Community 99 - "Community 99"
Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
### Community 100 - "Community 100"
Cohesion: 0.07
Nodes (56): CRM 报价单 API 路由 — 挂接到 crm.router, str, CRM 报价单 Pydantic Schemas, ReimburseApplyCreate, CustomerPoolDetailBody, CustomerPoolListBody, QuoteAcceptBody, QuoteApprovalApproveBody (+48 more)
### Community 101 - "Community 101"
Cohesion: 0.40
Nodes (3): BaseSettings, Config, Settings
### Community 102 - "Community 102"
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)
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)
### Community 103 - "Community 103"
Cohesion: 0.07
Nodes (141): bool, PlCarrier, PlVehicle, VehicleCreate, VehicleRead, VehicleUpdate, bool, DbUserContext (+133 more)
Nodes (142): bool, VehicleCreate, VehicleUpdate, bool, DbUserContext, float, int, PlPlan (+134 more)
### Community 104 - "Community 104"
Cohesion: 0.11
Nodes (49): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, Session, str, _T (+41 more)
Cohesion: 0.13
Nodes (42): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, Session, str, _T (+34 more)
### Community 105 - "Community 105"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
Cohesion: 0.50
Nodes (3): Config, PageResult, ResponseModel
### Community 106 - "Community 106"
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()
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 107 - "Community 107"
Cohesion: 0.34
Nodes (25): BxUserContext, Session, str, ApiResponse, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+17 more)
Cohesion: 0.40
Nodes (3): AdvanceApplyCreate, AdvanceApplyUpdate, _coerce_optional_id()
### Community 108 - "Community 108"
Cohesion: 0.40
Nodes (3): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单)
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
### Community 109 - "Community 109"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### 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 无登录调用)。
Cohesion: 0.20
Nodes (13): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度) 遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+5 more)
### Community 112 - "Community 112"
Cohesion: 0.35
Nodes (30): BxUserContext, int, Session, str, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+22 more)
### Community 122 - "Community 122"
Cohesion: 0.12
Nodes (17): create_new_version(), Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id (+9 more)
### Community 123 - "Community 123"
Cohesion: 0.12
Nodes (17): PlFreightSettlement, 运费结算表 pl_freight_settlement, 计划-运输关联表 pl_plan_transport, 运输计划现场链接 pl_transport_site_link(与地磅出入库单解耦), 运输计划现场链接 pl_transport_site_link(与地磅出入库单解耦), 运费结算表 pl_freight_settlement, 计划-运输关联表 pl_plan_transport, 运费结算表 pl_freight_settlement (+9 more)
### Community 124 - "Community 124"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
### Community 125 - "Community 125"
Cohesion: 0.22
Nodes (11): Any, bool, int, Session, str, generate_id(), camel_to_snake(), CRUDBase (+3 more)
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.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 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.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
Cohesion: 0.33
Nodes (14): Any, int, Session, str, create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options() (+6 more)
### Community 129 - "Community 129"
Cohesion: 0.15
Nodes (14): get_current_user_profile(), get_web_auth_menu_codes(), logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户信息(与 sys_user 字段对应)。 (+6 more)
### Community 130 - "Community 130"
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)
Cohesion: 0.15
Nodes (14): datetime, _parse_optional_datetime(), _period_key(), Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend, Java: statisticsTrend (+6 more)
### Community 131 - "Community 131"
Cohesion: 0.08
Nodes (43): bool, Session, str, Any, int, Session, str, _append_user_scope_conditions() (+35 more)
Nodes (43): Session, str, Any, int, Session, str, _append_user_scope_conditions(), apply_data_scope() (+35 more)
### Community 132 - "Community 132"
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 133 - "Community 133"
Cohesion: 0.44
Nodes (8): bool, int, Session, str, ApiRequestLogCRUD, SysApiRequestLog, SysApiRequestLogHis, SysApiRequestLog
### Community 134 - "Community 134"
Cohesion: 0.38
Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
Cohesion: 0.17
Nodes (12): 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据), 审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据) (+4 more)
### Community 135 - "Community 135"
Cohesion: 0.25
Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4. 配置文件, 5. 与 Nginx 配合, 6. 常见问题, shanxia2.0 Backend 生产部署说明
### Community 136 - "Community 136"
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()
Cohesion: 0.18
Nodes (11): Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus (+3 more)
### Community 137 - "Community 137"
Cohesion: 0.24
Nodes (10): datetime, Decimal, PlTransport, 运输单运费变更后,同步关联草稿结算单的明细快照与合计。, 运输单运费变更后,同步关联草稿结算单的明细快照与合计。, 运输单运费变更后,同步关联草稿结算单的明细快照与合计。, 运输单运费变更后,同步关联草稿结算单的明细快照与合计。, _recalc_settlement_total_freight_from_links() (+2 more)
### 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): 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单, 批量纠正已有汇款凭证但仍为 approved 的结算单 (+2 more)
### Community 140 - "Community 140"
Cohesion: 0.83
Nodes (3): _add_user_info_kwarg(), patch_file(), str
### Community 141 - "Community 141"
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位序号}
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 143 - "Community 143"
Cohesion: 0.18
Nodes (31): Any, CrmOpportunity, Decimal, int, Session, str, create_from_opportunity(), _opportunity_products_to_contract_products() (+23 more)
Cohesion: 0.32
Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 more)
### Community 144 - "Community 144"
Cohesion: 0.17
@@ -672,18 +732,78 @@ 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.22
Nodes (9): batch_submit_contract_approval(), Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval, Java: batchSubmitApproval (+1 more)
### Community 147 - "Community 147"
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)
### Community 148 - "Community 148"
Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 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)
Cohesion: 0.22
Nodes (9): Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签 (+1 more)
### Community 150 - "Community 150"
Cohesion: 0.33
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
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)
### Community 151 - "Community 151"
Cohesion: 0.50
Nodes (4): datetime, object, parse_optional_plan_datetime(), 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
### Community 152 - "Community 152"
Cohesion: 0.36
Nodes (7): bool, Redis, get_sync_redis_client(), 共享 Redis 客户端(同步:会话鉴权;异步:API 加密 nonce)。, 同步 Redis(auth 会话等,在线程池中调用)。, _redis_common_kwargs(), redis_enabled()
### Community 153 - "Community 153"
Cohesion: 0.15
Nodes (16): _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), 与 web-admin `userInfo` 字段约定一致。, 与 web-admin `userInfo` 字段约定一致。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。, Web 端登录:账号为 sys_user.user_no,密码为 sys_user.password(bcrypt)。 (+8 more)
### Community 154 - "Community 154"
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 155 - "Community 155"
Cohesion: 0.33
Nodes (6): attachment_download_url(), 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。
### Community 156 - "Community 156"
Cohesion: 0.33
Nodes (5): CRM 客户管理 Pydantic Schemas, CustomerAssignPoolBody, CustomerCheckDuplicateBody, CustomerDetailBody, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作)
### Community 157 - "Community 157"
Cohesion: 0.40
Nodes (5): list_roles_for_designer(), 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID
### Community 158 - "Community 158"
Cohesion: 0.38
Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
### Community 159 - "Community 159"
Cohesion: 0.50
Nodes (4): list_roles_for_select(), 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。
### Community 160 - "Community 160"
Cohesion: 0.33
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
### Community 161 - "Community 161"
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 162 - "Community 162"
Cohesion: 0.50
Nodes (3): PurchasePlanBase, PurchasePlanCreate, PurchasePlanUpdate
### Community 163 - "Community 163"
Cohesion: 0.67
Nodes (3): public_quote_share(), 公开分享查看报价单 — Java: GET /share/{shareCode}(无 JWT), 公开分享查看报价单 — Java: GET /share/{shareCode}(无 JWT)
## Knowledge Gaps
- **70 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+65 more)
These have ≤1 connection - possible missing edges or undocumented components.
@@ -692,12 +812,12 @@ Nodes (4): datetime, object, parse_optional_plan_datetime(), 运输计划相关
## 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 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._
- **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 23`, `Community 25`, `Community 27`, `Community 28`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 39`, `Community 40`, `Community 41`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 49`, `Community 51`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 61`, `Community 63`, `Community 64`, `Community 65`, `Community 67`, `Community 68`, `Community 69`, `Community 71`, `Community 73`, `Community 74`, `Community 75`, `Community 77`, `Community 78`, `Community 80`, `Community 84`, `Community 85`, `Community 87`, `Community 92`, `Community 93`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 102`, `Community 103`, `Community 104`, `Community 106`, `Community 108`, `Community 109`, `Community 112`, `Community 122`, `Community 123`, `Community 124`, `Community 125`, `Community 126`, `Community 127`, `Community 128`, `Community 129`, `Community 130`, `Community 131`, `Community 133`, `Community 136`, `Community 141`, `Community 143`, `Community 146`, `Community 147`, `Community 149`, `Community 150`, `Community 153`, `Community 154`, `Community 155`, `Community 157`, `Community 159`, `Community 163`?**
_High betweenness centrality (0.233) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 100` to `Community 0`, `Community 1`, `Community 2`, `Community 132`, `Community 7`, `Community 8`, `Community 11`, `Community 12`, `Community 20`, `Community 21`, `Community 23`, `Community 24`, `Community 156`, `Community 29`, `Community 158`, `Community 30`, `Community 160`, `Community 32`, `Community 162`, `Community 44`, `Community 45`, `Community 48`, `Community 51`, `Community 60`, `Community 62`, `Community 66`, `Community 69`, `Community 71`, `Community 73`, `Community 75`, `Community 77`, `Community 81`, `Community 84`, `Community 86`, `Community 87`, `Community 103`, `Community 105`, `Community 107`, `Community 112`?**
_High betweenness centrality (0.073) - this node is a cross-community bridge._
- **Why does `SysUser` connect `Community 87` to `Community 129`, `Community 130`, `Community 3`, `Community 131`, `Community 2`, `Community 6`, `Community 5`, `Community 8`, `Community 137`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 22`, `Community 24`, `Community 153`, `Community 27`, `Community 31`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 39`, `Community 50`, `Community 52`, `Community 56`, `Community 64`, `Community 69`, `Community 73`, `Community 85`, `Community 103`, `Community 104`?**
_High betweenness centrality (0.047) - 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?**
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_site_py", "label": "public_site.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L1"}, {"id": "pl_public_site_parse_feedback_images", "label": "_parse_feedback_images()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L29"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L29"}, {"id": "pl_public_site_get_site_link_by_token", "label": "_get_site_link_by_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L41"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L41"}, {"id": "pltransportsitelink", "label": "PlTransportSiteLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L41"}, {"id": "pl_public_site_resolve_plan_voided", "label": "_resolve_plan_voided()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L55"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L55"}, {"id": "pl_public_site_resolve_unit_label", "label": "_resolve_unit_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L60"}, {"id": "pl_public_site_resolve_plan_product", "label": "_resolve_plan_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L67"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L67"}, {"id": "pl_public_site_build_site_product_read", "label": "_build_site_product_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L80"}, {"id": "publictransportplanproductread", "label": "PublicTransportPlanProductRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L80"}, {"id": "pl_public_site_build_public_page", "label": "_build_public_page()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L110"}, {"id": "publictransportsitepageread", "label": "PublicTransportSitePageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L110"}, {"id": "pl_public_site_get_public_site", "label": "get_public_site()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L149"}, {"id": "pl_public_site_save_public_site_feedback", "label": "save_public_site_feedback()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L156"}, {"id": "publicinventoryfeedback", "label": "PublicInventoryFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L156"}, {"id": "pl_public_site_format_file_size", "label": "_format_file_size()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L177"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L177"}, {"id": "pl_public_site_upload_public_site_attachment", "label": "upload_public_site_attachment()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L186"}, {"id": "uploadfile", "label": "UploadFile", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L186"}, {"id": "pl_public_site_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212 \u00b7 \u5de5\u5382\u73b0\u573a\u4eba\u5458\u514d\u767b\u5f55\u516c\u5171 API\uff08pl_transport_site_link\uff09", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_site_py", "target": "json", "relation": "imports", "context": "import"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "label": "public_transport.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L1"}, {"id": "pl_public_transport_resolve_unit_label", "label": "_resolve_unit_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L33"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L33"}, {"id": "pl_public_transport_get_transport_by_token", "label": "_get_transport_by_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "pl_public_transport_parse_feedback_images", "label": "_parse_feedback_images()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L54"}, {"id": "pl_public_transport_format_plan_date", "label": "_format_plan_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L66"}, {"id": "pl_public_transport_resolve_other_personnel", "label": "_resolve_other_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "publictransportpersonnelread", "label": "PublicTransportPersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "pl_public_transport_build_public_page", "label": "_build_public_page()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L105"}, {"id": "publictransportpageread", "label": "PublicTransportPageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L105"}, {"id": "pl_public_transport_get_public_transport", "label": "get_public_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L182"}, {"id": "pl_public_transport_save_public_feedback", "label": "save_public_feedback()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L189"}, {"id": "publictransportfeedback", "label": "PublicTransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L189"}, {"id": "pl_public_transport_format_file_size", "label": "_format_file_size()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L210"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L210"}, {"id": "pl_public_transport_upload_public_attachment", "label": "upload_public_attachment()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L219"}, {"id": "uploadfile", "label": "UploadFile", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L219"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "json", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "os", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "datetime", "relation": "imports_froLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L1"}, {"id": "tasks_init_init_scheduled_tasks", "label": "init_scheduled_tasks()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L10"}, {"id": "tasks_init_shutdown_scheduler", "label": "shutdown_scheduler()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L70"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_init_py", "target": "logging", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_init_py", "target": "apscheduler_schedulers_background", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_init_py", "target": "tasks_init_init_scheduled_tasks", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_init_py", "target": "tasks_init_shutdown_scheduler", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L70", "weight": 1.0}], "raw_calls": [{"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "start", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L14"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "info", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L15"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L26"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L32"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L38"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L44"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L50"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "add_job", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L56"}, {"caller_nid": "tasks_init_init_scheduled_tasks", "callee": "info", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L62"}, {"caller_nid": "tasks_init_shutdown_scheduler", "callee": "shutdown", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L72"}, {"caller_nid": "tasks_init_shutdown_scheduler", "callee": "info", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py", "source_location": "L73"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_crud_crm_clue_py", "label": "clue.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L1"}, {"id": "crm_clue_ensure_clue_in_scope", "label": "_ensure_clue_in_scope()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L18"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L18"}, {"id": "crmclue", "label": "CrmClue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L18"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L18"}, {"id": "crm_clue_total_pages", "label": "total_pages()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L22"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L22"}, {"id": "crm_clue_parse_optional_date", "label": "_parse_optional_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L32"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L32"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L32"}, {"id": "crm_clue_parse_optional_datetime", "label": "_parse_optional_datetime()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L50"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L50"}, {"id": "crm_clue_resolve_user_display_name", "label": "_resolve_user_display_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L66"}, {"id": "crm_clue_enrich_user_names", "label": "_enrich_user_names()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L76"}, {"id": "crm_clue_row_to_dict", "label": "_row_to_dict()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L103"}, {"id": "crm_clue_list_clues", "label": "list_clues()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L148"}, {"id": "crm_clue_get_clue", "label": "get_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L248"}, {"id": "crm_clue_create_clue", "label": "create_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L259"}, {"id": "crm_clue_update_clue", "label": "update_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L324"}, {"id": "crm_clue_soft_delete_clue", "label": "soft_delete_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L390"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L390"}, {"id": "crm_clue_assign_clues", "label": "assign_clues()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L413"}, {"id": "crm_clue_convert_clue", "label": "convert_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L457"}, {"id": "crm_clue_validate_convertible", "label": "_validate_convertible()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L557"}, {"id": "crm_clue_check_convert", "label": "check_convert()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L570"}, {"id": "crm_clue_invalid_clue", "label": "invalid_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py", "source_location": "L580"}, {"id": "crm_clue_check_duplicate_clue", "label": "check_duplicate_clue()", "file_type": "code", "source_file": "/Users/zlwu/work/0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L97"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L243"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L567"}, {"id": "pl_plan_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "pl_plan_patch_plan_product_certs", "label": "patch_plan_product_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L726"}, {"id": "planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/apiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_service_py", "label": "pl_cert_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L1"}, {"id": "services_pl_cert_service_list_owner_certs", "label": "list_owner_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L14"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L14"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L14"}, {"id": "plcert", "label": "PlCert", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L14"}, {"id": "services_pl_cert_service_certs_to_read", "label": "certs_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L30"}, {"id": "certread", "label": "CertRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L30"}, {"id": "services_pl_cert_service_normalize_cert_row", "label": "_normalize_cert_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L52"}, {"id": "certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L52"}, {"id": "services_pl_cert_service_save_owner_certs", "label": "save_owner_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L58"}, {"id": "services_pl_cert_service_nearest_expire_date", "label": "nearest_expire_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L116"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L116"}, {"id": "services_pl_cert_service_resolve_owner_label", "label": "_resolve_owner_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L134"}, {"id": "services_pl_cert_service_scan_cert_expiry", "label": "scan_cert_expiry()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L161"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L161"}, {"id": "services_pl_cert_service_rationale_68", "label": "\u6309 cert_id upsert\uff1b\u672a\u51fa\u73b0\u5728\u5217\u8868\u4e2d\u7684\u65e7\u8bc1\u8f6f\u5220\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L68"}, {"id": "services_pl_cert_service_rationale_162", "label": "\u626b\u63cf pl_cert \u751f\u6210\u5230\u671f\u63d0\u9192\uff0c\u8fd4\u56de\u65b0\u5efa\u6761\u6570\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L162"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_service_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_service_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_service_py", "target": "app_core_utils", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py", "source_location": "L9", "weighLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_transport_py", "label": "transport.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L1"}, {"id": "pl_transport_transportbase", "label": "TransportBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L17"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_transport_transportcreate", "label": "TransportCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L41"}, {"id": "pl_transport_transportarrange", "label": "TransportArrange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L46"}, {"id": "pl_transport_transportinventorylinkread", "label": "TransportInventoryLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L74"}, {"id": "pl_transport_transportsitelinkread", "label": "TransportSiteLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L84"}, {"id": "pl_transport_transportupdate", "label": "TransportUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L98"}, {"id": "pl_transport_transportread", "label": "TransportRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L122"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_transport_transportarrangeresult", "label": "TransportArrangeResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L141"}, {"id": "pl_transport_planarrangeinforead", "label": "PlanArrangeInfoRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L151"}, {"id": "pl_transport_transportwithplans", "label": "TransportWithPlans", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L161"}, {"id": "pl_transport_transportfeedback", "label": "TransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L170"}, {"id": "pl_transport_publictransportfeedback", "label": "PublicTransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L179"}, {"id": "pl_transport_publictransportplanproductread", "label": "PublicTransportPlanProductRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L185"}, {"id": "pl_transport_publictransportpersonnelread", "label": "PublicTransportPersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L195"}, {"id": "pl_transport_publictransportpageread", "label": "PublicTransportPageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L202"}, {"id": "pl_transport_publictransportsitepageread", "label": "PublicTransportSitePageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L231"}, {"id": "pl_transport_approvalsubmit", "label": "ApprovalSubmit", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L262"}, {"id": "pl_transport_approvalrequest", "label": "ApprovalRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L267"}, {"id": "pl_transport_approvalrecordread", "label": "ApprovalRecordRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L273"}, {"id": "pl_transport_transportlistquery", "label": "TransportListQuery", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L292"}, {"id": "pl_transport_transportlistresponse", "label": "TransportListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L304"}], "edges": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_plan_service_py", "label": "pl_plan_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L1"}, {"id": "services_pl_plan_service_determine_plan_type", "label": "determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L38"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L38"}, {"id": "services_pl_plan_service_is_ton_product_unit", "label": "is_ton_product_unit()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L46"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L46"}, {"id": "services_pl_plan_service_product_quantity", "label": "_product_quantity()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L53"}, {"id": "planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L53"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L53"}, {"id": "services_pl_plan_service_product_create_actual_qty", "label": "_product_create_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L63"}, {"id": "services_pl_plan_service_plan_product_actual_qty", "label": "_plan_product_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L69"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L69"}, {"id": "services_pl_plan_service_sum_product_actual_quantities", "label": "sum_product_actual_quantities()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L75"}, {"id": "services_pl_plan_service_sum_plan_product_actual_quantities", "label": "sum_plan_product_actual_quantities()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L79"}, {"id": "services_pl_plan_service_estimate_qty_fields", "label": "_estimate_qty_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L83"}, {"id": "services_pl_plan_service_build_plan_product", "label": "build_plan_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L94"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L94"}, {"id": "services_pl_plan_service_apply_vehicle_snapshot_to_plan_transport", "label": "_apply_vehicle_snapshot_to_plan_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L140"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L140"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L140"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L140"}, {"id": "plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L140"}, {"id": "services_pl_plan_service_resolve_driver", "label": "_resolve_driver()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L158"}, {"id": "services_pl_plan_service_calc_freight_amount", "label": "_calc_freight_amount()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py", "source_location": "L203"}, {"id": "services_pl_plan_service_fetch_hazard_flags_mapLine truncated
@@ -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": "L25"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py", "source_location": "L25"}, {"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": "L33"}, {"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": "L37"}, {"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": "L49"}, {"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": "L69"}, {"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": "L76"}], "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
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L97"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L243"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L567"}, {"id": "pl_plan_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "pl_plan_patch_plan_product_certs", "label": "patch_plan_product_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L729"}, {"id": "planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/apiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_freight_settlement_service_py", "label": "pl_freight_settlement_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L1"}, {"id": "services_pl_freight_settlement_service_resolve_user_display_name", "label": "_resolve_user_display_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L44"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L44"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L44"}, {"id": "services_pl_freight_settlement_service_occupied_transport_ids_subquery", "label": "_occupied_transport_ids_subquery()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L54"}, {"id": "services_pl_freight_settlement_service_get_settlement_row", "label": "get_settlement_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L71"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L71"}, {"id": "plfreightsettlement", "label": "PlFreightSettlement", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L71"}, {"id": "services_pl_freight_settlement_service_get_settlement_by_transport_id", "label": "get_settlement_by_transport_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L95"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L95"}, {"id": "services_pl_freight_settlement_service_list_unsettled_plans", "label": "list_unsettled_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L135"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L135"}, {"id": "services_pl_freight_settlement_service_resolve_transports_for_create", "label": "_resolve_transports_for_create()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L216"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L216"}, {"id": "services_pl_freight_settlement_service_create_settlement", "label": "create_settlement()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L266"}, {"id": "services_pl_freight_settlement_service_recalc_settlement_total_freight_from_links", "label": "_recalc_settlement_total_freight_from_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L310"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L310"}, {"id": "services_pl_freight_settlement_service_sync_draft_settlement_freight_for_transport", "label": "sync_draft_settlement_freight_for_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L343"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L343"}, {"id": "services_pl_freight_settlement_service_refresh_draft_settlement_freight", "label": "refresh_draft_settlement_freight()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py", "source_location": "L390"}, {"id": "services_pl_freight_settlement_service_delete_draft_settlement", "label": "delete_draft_settlement()", "file_type": "code", "source_file": "/Users/zlwu/woLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_freight_py", "label": "freight.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L1"}, {"id": "pl_freight_freightsettlementbase", "label": "FreightSettlementBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L14"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_freight_freightsettlementcreate", "label": "FreightSettlementCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L26"}, {"id": "pl_freight_freightsettlementcreate_check_ids", "label": ".check_ids()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L32"}, {"id": "pl_freight_freightsettlementupdate", "label": "FreightSettlementUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L38"}, {"id": "pl_freight_freightsettlementread", "label": "FreightSettlementRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L50"}, {"id": "pl_freight_freightsettlementlistitem", "label": "FreightSettlementListItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L64"}, {"id": "pl_freight_freightsettlementtransportread", "label": "FreightSettlementTransportRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L78"}, {"id": "pl_freight_unsettledplanitem", "label": "UnsettledPlanItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L88"}, {"id": "pl_freight_unsettledplanlistresponse", "label": "UnsettledPlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L108"}, {"id": "pl_freight_settlementplansummary", "label": "SettlementPlanSummary", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L113"}, {"id": "pl_freight_settlementapprovalflowitem", "label": "SettlementApprovalFlowItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L122"}, {"id": "pl_freight_freightsettlementwithtransports", "label": "FreightSettlementWithTransports", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L136"}, {"id": "pl_freight_freightsettlementsubmitapprovalbody", "label": "FreightSettlementSubmitApprovalBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L154"}, {"id": "pl_freight_freightsettlementapprovalactionbody", "label": "FreightSettlementApprovalActionBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L161"}, {"id": "pl_freight_freightsettlementdisbursebody", "label": "FreightSettlementDisburseBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L166"}, {"id": "pl_freight_remittancevouchercreate", "label": "RemittanceVoucherCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L179"}, {"id": "pl_freight_remittancevoucherread", "label": "RemittanceVoucherRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L188"}, {"id": "pl_freight_freightsettlementlistquery", "label": "FreightSettlementListQuery", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L207"}, {"id": "pl_freight_freightsettlementlistresponse", "label": "FreightSettlementListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L216"}, {"id": "pl_freight_freightsettlementbytransportread", "label": "FreightSettlementByTransportRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py", "source_location": "L221"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_freight_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_inventory_py", "label": "inventory.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L1"}, {"id": "pl_inventory_get_inventory_ledger", "label": "get_inventory_ledger()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "inventoryledgerresponse", "label": "InventoryLedgerResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L49"}, {"id": "pl_inventory_set_warning_threshold", "label": "set_warning_threshold()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L80"}, {"id": "inventorywarningthreshold", "label": "InventoryWarningThreshold", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L80"}, {"id": "pl_inventory_list_warnings", "label": "list_warnings()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L129"}, {"id": "inventorywarningread", "label": "InventoryWarningRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L129"}, {"id": "pl_inventory_update_warning", "label": "update_warning()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L148"}, {"id": "inventorywarningupdate", "label": "InventoryWarningUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L148"}, {"id": "pl_inventory_list_transactions", "label": "list_transactions()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L188"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L188"}, {"id": "inventorytransactionresponse", "label": "InventoryTransactionResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L188"}, {"id": "pl_inventory_get_summary", "label": "get_summary()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L231"}, {"id": "inventorysummaryresponse", "label": "InventorySummaryResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L231"}, {"id": "pl_inventory_list_document_reminders", "label": "list_document_reminders()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L293"}, {"id": "documentreminderread", "label": "DocumentReminderRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L293"}, {"id": "pl_inventory_update_document_reminder", "label": "update_document_reminder()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L315"}, {"id": "documentreminderupdate", "label": "DocumentReminderUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L315"}, {"id": "pl_inventory_check_document_expiry", "label": "check_document_expiry()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L352"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_inventory_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_inventory_py", "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_transport_py", "label": "transport.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L1"}, {"id": "pl_transport_generate_transport_no", "label": "_generate_transport_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L67"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L67"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L67"}, {"id": "pl_transport_resolve_driver_info", "label": "_resolve_driver_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L91"}, {"id": "pl_transport_get_vehicle_info", "label": "_get_vehicle_info()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L127"}, {"id": "pl_transport_assert_vehicle_carrier_eligible", "label": "_assert_vehicle_carrier_eligible()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L177"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L177"}, {"id": "pl_transport_ensure_driver_access_token", "label": "_ensure_driver_access_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L212"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L212"}, {"id": "pl_transport_rotate_driver_access_token", "label": "_rotate_driver_access_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L220"}, {"id": "pl_transport_is_rearrange", "label": "_is_rearrange()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L226"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L226"}, {"id": "pl_transport_transport_has_db_links", "label": "_transport_has_db_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L234"}, {"id": "pl_transport_apply_plan_logistics_from_payload", "label": "_apply_plan_logistics_from_payload()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L247"}, {"id": "transportarrange", "label": "TransportArrange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L247"}, {"id": "pl_transport_resolve_transport_endpoints", "label": "_resolve_transport_endpoints()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L272"}, {"id": "pl_transport_apply_transport_arrange_fields", "label": "_apply_transport_arrange_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L287"}, {"id": "float", "label": "float", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L287"}, {"id": "pl_transport_build_driver_url", "label": "_build_driver_url()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L321"}, {"id": "pl_transport_build_arrange_result", "label": "_build_arrange_result()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L326"}, {"id": "transportsitelinkread", "label": "TransportSiteLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L326"}, {"id": "transportinventorylinkread", "label": "TransportInventoryLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L326"}, {"id": "transportarrangeresult", "label": "TransportArrangeResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py", "source_location": "L326"}, {"id": "pl_transport_list_transports", "label": "list_transports()", "file_tyLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_transport_address_py", "label": "transport_address.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L1"}, {"id": "pl_transport_address_address_to_read", "label": "_address_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L25"}, {"id": "pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L25"}, {"id": "transportaddressread", "label": "TransportAddressRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L25"}, {"id": "pl_transport_address_get_address", "label": "_get_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L29"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L29"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L29"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L29"}, {"id": "pl_transport_address_build_address_query", "label": "_build_address_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L47"}, {"id": "pl_transport_address_list_transport_addresses", "label": "list_transport_addresses()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L57"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L57"}, {"id": "transportaddresslistresponse", "label": "TransportAddressListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L57"}, {"id": "pl_transport_address_list_all_transport_addresses", "label": "list_all_transport_addresses()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L97"}, {"id": "pl_transport_address_create_transport_address", "label": "create_transport_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L124"}, {"id": "transportaddresscreate", "label": "TransportAddressCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L124"}, {"id": "pl_transport_address_get_transport_address", "label": "get_transport_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L151"}, {"id": "pl_transport_address_update_transport_address", "label": "update_transport_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L162"}, {"id": "transportaddressupdate", "label": "TransportAddressUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L162"}, {"id": "pl_transport_address_delete_transport_address", "label": "delete_transport_address()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L183"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_transport_address_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_transport_address_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_transport_address_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backenLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_pl_cert_expiry_py", "label": "pl_cert_expiry.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L1"}, {"id": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "label": "scan_pl_cert_expiry_job()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L10"}, {"id": "tasks_pl_cert_expiry_rationale_11", "label": "\u6bcf\u65e5\u626b\u63cf pl_cert\uff0c\u751f\u6210\u8bc1\u4ef6\u5230\u671f\u63d0\u9192\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L11"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_pl_cert_expiry_py", "target": "logging", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_pl_cert_expiry_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_pl_cert_expiry_py", "target": "app_services_pl_cert_service", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_tasks_pl_cert_expiry_py", "target": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L10", "weight": 1.0}, {"source": "tasks_pl_cert_expiry_rationale_11", "target": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L11", "weight": 1.0}], "raw_calls": [{"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "SessionLocal", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L12"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "info", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L14"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "scan_cert_expiry", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L15"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "commit", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L16"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "info", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L17"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "rollback", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L19"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "exception", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L20"}, {"caller_nid": "tasks_pl_cert_expiry_scan_pl_cert_expiry_job", "callee": "close", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py", "source_location": "L23"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_db_inventory_py", "label": "inventory.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L1"}, {"id": "db_inventory_base_query", "label": "_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L47"}, {"id": "db_inventory_generate_batch_no", "label": "_generate_batch_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L54"}, {"id": "db_inventory_resolve_tenant_no", "label": "_resolve_tenant_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L74"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L74"}, {"id": "db_inventory_get_inventory_by_id", "label": "_get_inventory_by_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L92"}, {"id": "dbinboundoutbound", "label": "DbInboundOutbound", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L92"}, {"id": "db_inventory_get_inventory", "label": "_get_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L106"}, {"id": "db_inventory_apply_inventory_product_update", "label": "_apply_inventory_product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L123"}, {"id": "db_inventory_apply_inventory_update", "label": "_apply_inventory_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L139"}, {"id": "db_inventory_maybe_promote_third_party_status", "label": "_maybe_promote_third_party_status()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L157"}, {"id": "db_inventory_list_inventory", "label": "list_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L168"}, {"id": "inboundoutboundread", "label": "InboundOutboundRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L168"}, {"id": "db_inventory_create_inventory", "label": "create_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L273"}, {"id": "inboundoutboundcreate", "label": "InboundOutboundCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L273"}, {"id": "db_inventory_create_link_for_plan", "label": "create_link_for_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L321"}, {"id": "createlinkforplanpayload", "label": "CreateLinkForPlanPayload", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L321"}, {"id": "db_inventory_get_inventory_site_url", "label": "get_inventory_site_url()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L372"}, {"id": "inventorysiteurlread", "label": "InventorySiteUrlRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L372"}, {"id": "db_inventory_update_inventory", "label": "update_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L405"}, {"id": "inboundoutboundupdate", "label": "InboundOutboundUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L405"}, {"id": "db_inventory_delete_inventory", "label": "delete_inventory()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py", "source_location": "L423"}, {"id": "db_inventory_inventoryrelateddata", "label": "InventoryRelatedData", "file_type": "coLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L97"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L243"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L567"}, {"id": "pl_plan_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "pl_plan_patch_plan_product_certs", "label": "patch_plan_product_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L729"}, {"id": "planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/apiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "label": "public_transport.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L1"}, {"id": "pl_public_transport_resolve_unit_label", "label": "_resolve_unit_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L33"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L33"}, {"id": "pl_public_transport_get_transport_by_token", "label": "_get_transport_by_token()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L40"}, {"id": "pl_public_transport_parse_feedback_images", "label": "_parse_feedback_images()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L54"}, {"id": "pl_public_transport_format_plan_date", "label": "_format_plan_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L66"}, {"id": "pl_public_transport_resolve_other_personnel", "label": "_resolve_other_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "publictransportpersonnelread", "label": "PublicTransportPersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L76"}, {"id": "pl_public_transport_build_public_page", "label": "_build_public_page()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L105"}, {"id": "publictransportpageread", "label": "PublicTransportPageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L105"}, {"id": "pl_public_transport_get_public_transport", "label": "get_public_transport()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L182"}, {"id": "pl_public_transport_save_public_feedback", "label": "save_public_feedback()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L189"}, {"id": "publictransportfeedback", "label": "PublicTransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L189"}, {"id": "pl_public_transport_format_file_size", "label": "_format_file_size()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L210"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L210"}, {"id": "pl_public_transport_upload_public_attachment", "label": "upload_public_attachment()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L219"}, {"id": "uploadfile", "label": "UploadFile", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L219"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "json", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "os", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_public_transport_py", "target": "datetime", "relation": "imports_froLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_planproductbase", "label": "PlanProductBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L16"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_plan_planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L45"}, {"id": "pl_plan_planproductupdate", "label": "PlanProductUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L49"}, {"id": "pl_plan_planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L70"}, {"id": "pl_plan_planbase", "label": "PlanBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L84"}, {"id": "pl_plan_plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L124"}, {"id": "pl_plan_plantransportcreateblock", "label": "PlanTransportCreateBlock", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L130"}, {"id": "pl_plan_planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L144"}, {"id": "pl_plan_hazardcertreadybody", "label": "HazardCertReadyBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L184"}, {"id": "pl_plan_planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L189"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_plan_planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L216"}, {"id": "pl_plan_createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L225"}, {"id": "pl_plan_plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L233"}, {"id": "pl_plan_planlistquery", "label": "PlanListQuery", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L244"}, {"id": "pl_plan_planproductcertitem", "label": "PlanProductCertItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L259"}, {"id": "pl_plan_planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L266"}, {"id": "pl_plan_planarchiveproductitem", "label": "PlanArchiveProductItem", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L271"}, {"id": "pl_plan_planarchivefreightblock", "label": "PlanArchiveFreightBlock", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L277"}, {"id": "pl_plan_planarchivebody", "label": "PlanArchiveBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L288"}, {"id": "pl_plan_planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L294"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/planLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_transport_py", "label": "transport.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L1"}, {"id": "pl_transport_transportbase", "label": "TransportBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L17"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_transport_transportcreate", "label": "TransportCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L41"}, {"id": "pl_transport_transportarrange", "label": "TransportArrange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L46"}, {"id": "pl_transport_transportinventorylinkread", "label": "TransportInventoryLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L74"}, {"id": "pl_transport_transportsitelinkread", "label": "TransportSiteLinkRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L84"}, {"id": "pl_transport_transportupdate", "label": "TransportUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L98"}, {"id": "pl_transport_transportread", "label": "TransportRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L122"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_transport_transportarrangeresult", "label": "TransportArrangeResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L141"}, {"id": "pl_transport_planarrangeinforead", "label": "PlanArrangeInfoRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L151"}, {"id": "pl_transport_transportwithplans", "label": "TransportWithPlans", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L161"}, {"id": "pl_transport_transportfeedback", "label": "TransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L170"}, {"id": "pl_transport_publictransportfeedback", "label": "PublicTransportFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L179"}, {"id": "pl_transport_publictransportplanproductread", "label": "PublicTransportPlanProductRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L185"}, {"id": "pl_transport_publictransportpersonnelread", "label": "PublicTransportPersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L195"}, {"id": "pl_transport_publictransportpageread", "label": "PublicTransportPageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L202"}, {"id": "pl_transport_approvalsubmit", "label": "ApprovalSubmit", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L236"}, {"id": "pl_transport_approvalrequest", "label": "ApprovalRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L241"}, {"id": "pl_transport_approvalrecordread", "label": "ApprovalRecordRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L247"}, {"id": "pl_transport_transportlistquery", "label": "TransportListQuery", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L266"}, {"id": "pl_transport_transportlistresponse", "label": "TransportListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py", "source_location": "L278"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_transport_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_db_plans_py", "label": "plans.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L1"}, {"id": "db_plans_base_query", "label": "_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L46"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L46"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L46"}, {"id": "db_plans_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L54"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L54"}, {"id": "purchaseplanwithfilllink", "label": "PurchasePlanWithFillLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L54"}, {"id": "db_plans_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L192"}, {"id": "db_plans_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L199"}, {"id": "purchaseplancreate", "label": "PurchasePlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L199"}, {"id": "purchaseplanread", "label": "PurchasePlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L199"}, {"id": "db_plans_autogeneratemonthlyresult", "label": "AutoGenerateMonthlyResult", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L242"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "db_plans_run_auto_generate_monthly", "label": "run_auto_generate_monthly()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L249"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L249"}, {"id": "db_plans_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L263"}, {"id": "db_plans_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L274"}, {"id": "purchaseplanupdate", "label": "PurchasePlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L274"}, {"id": "db_plans_delete_plan", "label": "delete_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L292"}, {"id": "dbpurchaseplan", "label": "DbPurchasePlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L312"}, {"id": "db_plans_get_plan_by_id_only", "label": "_get_plan_by_id_only()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L329"}, {"id": "db_plans_apply_plan_product_update", "label": "_apply_plan_product_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L343"}, {"id": "db_plans_apply_plan_update", "label": "_apply_plan_update()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L359"}, {"id": "db_plans_planrelateddata", "label": "PlanRelatedData", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L380"}, {"id": "db_plans_get_plan_related", "label": "get_plan_related()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L386"}, {"id": "db_plans_thirdpartyplanpagedata", "label": "ThirdPartyPlanPageData", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py", "source_location": "L427"}, {"id": "db_plans_get_third_party_plan_page", "label": "get_third_party_plan_page()", "file_Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "label": "public_inventory.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L1"}, {"id": "db_public_inventory_publicinventoryfeedback", "label": "PublicInventoryFeedback", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L7"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "db_public_inventory_publicinventorypageread", "label": "PublicInventoryPageRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L13"}, {"id": "db_public_inventory_inventorysiteurlread", "label": "InventorySiteUrlRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L34"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "pydantic", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "db_public_inventory_publicinventoryfeedback", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L7", "weight": 1.0}, {"source": "db_public_inventory_publicinventoryfeedback", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "db_public_inventory_publicinventorypageread", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L13", "weight": 1.0}, {"source": "db_public_inventory_publicinventorypageread", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L13", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_db_public_inventory_py", "target": "db_public_inventory_inventorysiteurlread", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L34", "weight": 1.0}, {"source": "db_public_inventory_inventorysiteurlread", "target": "basemodel", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py", "source_location": "L34", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L74"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L97"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L102"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L111"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L132"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L243"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L271"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L354"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L460"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L567"}, {"id": "pl_plan_update_plan", "label": "update_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L614"}, {"id": "pl_plan_patch_plan_product_certs", "label": "patch_plan_product_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L729"}, {"id": "planproductcertsupdate", "label": "PlanProductCertsUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/apiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_base_data_py", "label": "base_data.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L1"}, {"id": "pl_base_data_carrierbase", "label": "CarrierBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L19"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_base_data_carrierqualificationbase", "label": "CarrierQualificationBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L27"}, {"id": "pl_base_data_carriercreate", "label": "CarrierCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L45"}, {"id": "pl_base_data_carriercreate_validate_qualification", "label": ".validate_qualification()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L56"}, {"id": "pl_base_data_carrierupdate", "label": "CarrierUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L66"}, {"id": "pl_base_data_carrierupdate_validate_qualification_patch", "label": ".validate_qualification_patch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L88"}, {"id": "pl_base_data_carrierauditreject", "label": "CarrierAuditReject", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L109"}, {"id": "pl_base_data_carrierread", "label": "CarrierRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L115"}, {"id": "pl_base_data_carrier_to_read", "label": "carrier_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "pl_base_data_vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L170"}, {"id": "pl_base_data_certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L181"}, {"id": "pl_base_data_certread", "label": "CertRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L196"}, {"id": "pl_base_data_vehiclebase", "label": "VehicleBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L212"}, {"id": "pl_base_data_vehiclecreate", "label": "VehicleCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L225"}, {"id": "pl_base_data_vehicleupdate", "label": "VehicleUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L231"}, {"id": "pl_base_data_vehicleread", "label": "VehicleRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L245"}, {"id": "pl_base_data_vehicledetailread", "label": "VehicleDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L259"}, {"id": "pl_base_data_personnelbase", "label": "PersonnelBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L270"}, {"id": "pl_base_data_personnelcreate", "label": "PersonnelCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L279"}, {"id": "pl_base_data_personnelupdate", "label": "PersonnelUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L284"}, {"id": "pl_base_data_personnelread", "label": "PersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L295"}, {"id": "pl_base_data_personneldetailread", "label": "PersonnelDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L1"}, {"id": "models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L57"}, {"id": "models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L83"}, {"id": "models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L105"}, {"id": "models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L129"}, {"id": "models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L155"}, {"id": "models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L181"}, {"id": "models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L202"}, {"id": "models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L227"}, {"id": "models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L322"}, {"id": "models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L377"}, {"id": "models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L404"}, {"id": "models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L466"}, {"id": "models_pl_models_plplantransport", "label": "PlPlanTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L488"}, {"id": "models_pl_models_pltransportsitelink", "label": "PlTransportSiteLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L507"}, {"id": "models_pl_models_pltransportapproval", "label": "PlTransportApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L540"}, {"id": "models_pl_models_plfreightsettlement", "label": "PlFreightSettlement", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L569"}, {"id": "models_pl_models_plfreightsettlementapproval", "label": "PlFreightSettlementApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L601"}, {"id": "models_pl_models_plfreightsettlementtransport", "label": "PlFreightSettlementTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L628"}, {"id": "models_pl_models_plremittancevoucher", "label": "PlRemittanceVoucher", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L648"}, {"id": "models_pl_models_plinventory", "label": "PlInventory", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L675"}, {"id": "models_pl_models_plinventorytransaction", "label": "PlInventoryTransaction", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L707"}, {"id": "models_pl_models_plinventorywarning", "label": "PlInventoryWarning", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line 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":1953,"mtime_ns":1780805101147405418,"hash":"d2a416ff9daedad88780cbe3f14ea75e8299044f8787afb60e731f43963dc405"},"/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":6698,"mtime_ns":1782790876685112837,"hash":"9d955a6baa70f78ddfcf29407d9711915a04e59a7f616254d06d25d25e8d0327"},"/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":6698,"mtime_ns":1782790876685112837,"hash":"9d955a6baa70f78ddfcf29407d9711915a04e59a7f616254d06d25d25e8d0327"},"/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
File diff suppressed because it is too large. Load diff
+503
View File
@@ -0,0 +1,503 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>产品查看弹窗 · 信息展示样式 Demo(非表格)</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--surface: #fff;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.45;
color: var(--text);
background: #f4f7f6;
padding: 24px 20px 48px;
}
.page { max-width: 760px; margin: 0 auto; }
.page h1 { margin: 0 0 6px; font-size: 20px; font-weight: 700; }
.page > .sub { margin: 0 0 14px; color: var(--text-muted); font-size: 13px; line-height: 1.5; }
.tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.tab {
border: 1px solid var(--border);
background: var(--surface);
border-radius: 8px;
padding: 7px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
font-family: inherit;
color: var(--text);
}
.tab.is-active {
background: var(--primary-bg);
border-color: #a7f3d0;
color: var(--primary-dark);
}
.panel { display: none; }
.panel.is-active { display: block; }
.note {
margin: 0 0 12px;
padding: 8px 12px;
border-radius: 8px;
background: var(--primary-bg);
border: 1px solid #a7f3d0;
color: var(--primary-dark);
font-size: 12px;
line-height: 1.55;
}
.note strong { font-weight: 600; }
/* ── 弹窗壳(统一) ── */
.dialog {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.1);
overflow: hidden;
}
.hero {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px 8px;
border-bottom: 1px solid #f1f5f9;
}
.hero-main { flex: 1; min-width: 0; }
.hero h2 { margin: 0 0 2px; font-size: 16px; font-weight: 700; }
.hero .meta { margin: 0; font-size: 12px; color: var(--text-muted); }
.hero-actions { display: flex; gap: 6px; flex-shrink: 0; }
.body { padding: 10px 16px 12px; }
.footer {
padding: 10px 16px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
background: #f8fafc;
}
.btn {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: default;
}
.btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
.btn-danger { color: #dc2626; border-color: #fecaca; }
.btn-ghost { color: var(--text); }
.tag {
display: inline-flex;
height: 20px;
padding: 0 7px;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
align-items: center;
background: #ecfdf5;
color: #059669;
}
.tag.danger { background: #fef2f2; color: #dc2626; }
.money { color: var(--primary-dark); font-weight: 700; }
.field-label {
margin: 10px 0 6px;
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.imgs { display: flex; gap: 6px; flex-wrap: wrap; }
.imgs img {
width: 48px;
height: 48px;
border-radius: 6px;
object-fit: cover;
border: 1px solid var(--border);
}
/* ══ 方案 A:四列栅格(ViewKvGrid,label 上 value 下)══ */
.kv-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px 14px;
}
.kv-grid .kv dt {
margin: 0 0 3px;
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.kv-grid .kv dd {
margin: 0;
font-size: 13px;
font-weight: 600;
word-break: break-word;
line-height: 1.4;
}
.kv-grid .kv.span-2 { grid-column: span 2; }
.kv-grid .kv.span-full { grid-column: 1 / -1; }
.kv-grid .kv dd.muted { font-weight: 400; line-height: 1.5; }
/* ══ 方案 B:左右行列表(label 左 value 右,无表格线)══ */
.kv-rows { display: grid; gap: 0; }
.kv-row {
display: grid;
grid-template-columns: 108px 1fr;
gap: 10px;
padding: 7px 0;
border-bottom: 1px solid #f1f5f9;
align-items: start;
}
.kv-row:last-child { border-bottom: none; }
.kv-row .k {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
line-height: 1.45;
}
.kv-row .v {
font-size: 13px;
font-weight: 600;
word-break: break-word;
line-height: 1.45;
}
.kv-row .v.muted { font-weight: 400; }
.kv-row.is-core .v { color: var(--primary-dark); font-weight: 700; }
/* ══ 方案 C:核心指标条 + 双列紧凑栅格 ══ */
.core-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f1f5f9;
}
.core-chip {
flex: 1 1 140px;
min-width: 0;
padding: 8px 10px;
border-radius: 8px;
background: var(--primary-bg);
border: 1px solid #a7f3d0;
}
.core-chip .k { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
.core-chip .v { font-size: 15px; font-weight: 700; color: var(--primary-dark); }
.kv-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
}
.kv-grid-2 .kv dt { margin: 0 0 2px; font-size: 12px; color: var(--text-muted); }
.kv-grid-2 .kv dd { margin: 0; font-size: 13px; font-weight: 600; }
.kv-grid-2 .kv.span-full { grid-column: 1 / -1; }
.kv-grid-2 .kv dd.muted { font-weight: 400; line-height: 1.5; }
/* ══ 方案 D:分组标签 + 内联文本(最简)══ */
.group { margin-bottom: 10px; }
.group:last-child { margin-bottom: 0; }
.group-title {
margin: 0 0 6px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.03em;
text-transform: uppercase;
}
.inline-kv {
margin: 0;
font-size: 13px;
line-height: 1.65;
color: var(--text);
}
.inline-kv .pair { white-space: nowrap; }
.inline-kv .pair .k { color: var(--text-muted); font-weight: 500; }
.inline-kv .pair .k::after { content: ' '; }
.inline-kv .pair .v { font-weight: 600; }
.inline-kv .sep { color: #cbd5e1; margin: 0 8px; user-select: none; }
.inline-kv-block {
margin: 0;
font-size: 13px;
line-height: 1.55;
}
.inline-kv-block .k {
color: var(--text-muted);
font-weight: 500;
margin-right: 6px;
}
@media (max-width: 640px) {
.kv-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kv-row { grid-template-columns: 88px 1fr; }
}
</style>
</head>
<body>
<div class="page">
<h1>产品查看弹窗 · 信息展示样式(非表格)</h1>
<p class="sub">先在 Demo 中对比选型,确认后再改 <code>SystemViewDialog</code> / 产品管理页代码。以下均<strong>不含 table 表格模式</strong>。</p>
<div class="tabs">
<button class="tab is-active" data-target="demo-a">A · 四列栅格</button>
<button class="tab" data-target="demo-b">B · 左右行列表</button>
<button class="tab" data-target="demo-c">C · 核心条 + 双列</button>
<button class="tab" data-target="demo-d">D · 分组内联</button>
</div>
<!-- 方案 A -->
<section id="demo-a" class="panel is-active">
<p class="note"><strong>对应组件:</strong><code>ViewKvGrid</code>(4 列,label 在上 value 在下)。规范默认方案,与 <code>SystemViewDialog</code> 原生一致,改造成本最低。</p>
<div class="dialog">
<div class="hero">
<div class="hero-main">
<h2>工业级无水乙醇</h2>
<p class="meta">产品编号:P20260328001</p>
</div>
<div class="hero-actions">
<button class="btn btn-primary" type="button">编辑</button>
<button class="btn btn-danger" type="button">删除</button>
</div>
</div>
<div class="body">
<dl class="kv-grid">
<div class="kv"><dt>标准价(元)</dt><dd class="money">¥86.00</dd></div>
<div class="kv"><dt>成本价(元)</dt><dd class="money">¥52.50</dd></div>
<div class="kv"><dt>库存数量</dt><dd>128 瓶</dd></div>
<div class="kv"><dt>状态</dt><dd><span class="tag">在售</span></dd></div>
<div class="kv"><dt>产品分类</dt><dd>化工原料 / 有机溶剂</dd></div>
<div class="kv"><dt>规格型号</dt><dd>AR 500ml</dd></div>
<div class="kv"><dt>条码</dt><dd>6901234567890</dd></div>
<div class="kv"><dt>库存预警值</dt><dd>20</dd></div>
<div class="kv"><dt>销售次数</dt><dd>36</dd></div>
<div class="kv"><dt>税率</dt><dd>13%</dd></div>
<div class="kv"><dt>是否易制毒</dt><dd><span class="tag danger">是</span></dd></div>
<div class="kv"><dt>是否易制爆</dt><dd>否</dd></div>
<div class="kv span-2"><dt>创建时间</dt><dd>2026-03-28 14:32:10</dd></div>
<div class="kv span-full"><dt>产品描述</dt><dd class="muted">适用于实验室分析、工业清洗;阴凉干燥储存,远离火源。</dd></div>
<div class="kv span-full"><dt>备注</dt><dd class="muted">2026 Q1 采购批次,供应商资质已核验。</dd></div>
</dl>
<div class="field-label">产品图片</div>
<div class="imgs">
<img src="https://picsum.photos/seed/p1/96/96" alt="" />
<img src="https://picsum.photos/seed/p2/96/96" alt="" />
</div>
</div>
<div class="footer"><button class="btn btn-ghost" type="button">关闭</button></div>
</div>
</section>
<!-- 方案 B -->
<section id="demo-b" class="panel">
<p class="note"><strong>特点:</strong>每行「标签左 · 值右」,无竖线表格感,纵向阅读清晰;长文本自然换行。需新增 <code>ViewKvRows</code> 或在 dense 模式加 <code>layout="rows"</code>。</p>
<div class="dialog">
<div class="hero">
<div class="hero-main">
<h2>工业级无水乙醇</h2>
<p class="meta">产品编号:P20260328001</p>
</div>
<div class="hero-actions">
<button class="btn btn-primary" type="button">编辑</button>
<button class="btn btn-danger" type="button">删除</button>
</div>
</div>
<div class="body">
<div class="kv-rows">
<div class="kv-row is-core"><span class="k">标准价(元)</span><span class="v money">¥86.00</span></div>
<div class="kv-row is-core"><span class="k">成本价(元)</span><span class="v money">¥52.50</span></div>
<div class="kv-row is-core"><span class="k">库存数量</span><span class="v">128 瓶</span></div>
<div class="kv-row"><span class="k">状态</span><span class="v"><span class="tag">在售</span></span></div>
<div class="kv-row"><span class="k">产品分类</span><span class="v">化工原料 / 有机溶剂</span></div>
<div class="kv-row"><span class="k">规格型号</span><span class="v">AR 500ml</span></div>
<div class="kv-row"><span class="k">条码</span><span class="v">6901234567890</span></div>
<div class="kv-row"><span class="k">库存预警值</span><span class="v">20</span></div>
<div class="kv-row"><span class="k">销售次数</span><span class="v">36</span></div>
<div class="kv-row"><span class="k">税率</span><span class="v">13%</span></div>
<div class="kv-row"><span class="k">是否易制毒</span><span class="v"><span class="tag danger">是</span></span></div>
<div class="kv-row"><span class="k">是否易制爆</span><span class="v">否</span></div>
<div class="kv-row"><span class="k">创建时间</span><span class="v">2026-03-28 14:32:10</span></div>
<div class="kv-row"><span class="k">产品描述</span><span class="v muted">适用于实验室分析、工业清洗;阴凉干燥储存,远离火源。</span></div>
<div class="kv-row"><span class="k">备注</span><span class="v muted">2026 Q1 采购批次,供应商资质已核验。</span></div>
</div>
<div class="field-label">产品图片</div>
<div class="imgs">
<img src="https://picsum.photos/seed/p1/96/96" alt="" />
<img src="https://picsum.photos/seed/p2/96/96" alt="" />
</div>
</div>
<div class="footer"><button class="btn btn-ghost" type="button">关闭</button></div>
</div>
</section>
<!-- 方案 C -->
<section id="demo-c" class="panel">
<p class="note"><strong>特点:</strong>价/库存/状态先「核心条」高亮,其余字段双列栅格;信息聚拢、层次清楚。可复用现有 <code>highlights</code> + <code>ViewKvGrid</code> 两列。</p>
<div class="dialog">
<div class="hero">
<div class="hero-main">
<h2>工业级无水乙醇</h2>
<p class="meta">产品编号:P20260328001</p>
</div>
<div class="hero-actions">
<button class="btn btn-primary" type="button">编辑</button>
<button class="btn btn-danger" type="button">删除</button>
</div>
</div>
<div class="body">
<div class="core-strip">
<div class="core-chip"><div class="k">标准价</div><div class="v">¥86.00</div></div>
<div class="core-chip"><div class="k">成本价</div><div class="v">¥52.50</div></div>
<div class="core-chip"><div class="k">库存</div><div class="v">128 瓶</div></div>
<div class="core-chip"><div class="k">状态</div><div class="v"><span class="tag">在售</span></div></div>
</div>
<dl class="kv-grid-2">
<div class="kv"><dt>产品分类</dt><dd>化工原料 / 有机溶剂</dd></div>
<div class="kv"><dt>规格型号</dt><dd>AR 500ml</dd></div>
<div class="kv"><dt>条码</dt><dd>6901234567890</dd></div>
<div class="kv"><dt>库存预警值</dt><dd>20</dd></div>
<div class="kv"><dt>销售次数</dt><dd>36</dd></div>
<div class="kv"><dt>税率</dt><dd>13%</dd></div>
<div class="kv"><dt>是否易制毒</dt><dd><span class="tag danger">是</span></dd></div>
<div class="kv"><dt>是否易制爆</dt><dd>否</dd></div>
<div class="kv span-full"><dt>创建时间</dt><dd>2026-03-28 14:32:10</dd></div>
<div class="kv span-full"><dt>产品描述</dt><dd class="muted">适用于实验室分析、工业清洗;阴凉干燥储存。</dd></div>
<div class="kv span-full"><dt>备注</dt><dd class="muted">2026 Q1 采购批次。</dd></div>
</dl>
<div class="field-label">产品图片</div>
<div class="imgs">
<img src="https://picsum.photos/seed/p1/96/96" alt="" />
<img src="https://picsum.photos/seed/p2/96/96" alt="" />
</div>
</div>
<div class="footer"><button class="btn btn-ghost" type="button">关闭</button></div>
</div>
</section>
<!-- 方案 D -->
<section id="demo-d" class="panel">
<p class="note"><strong>特点:</strong>按「价格 / 库存 / 其他 / 说明」分组,组内用「标签 值 · 标签 值」内联,最省纵向空间。适合字段多但希望一屏看完。</p>
<div class="dialog">
<div class="hero">
<div class="hero-main">
<h2>工业级无水乙醇</h2>
<p class="meta">产品编号:P20260328001</p>
</div>
<div class="hero-actions">
<button class="btn btn-primary" type="button">编辑</button>
<button class="btn btn-danger" type="button">删除</button>
</div>
</div>
<div class="body">
<div class="group">
<div class="group-title">价格与状态</div>
<p class="inline-kv">
<span class="pair"><span class="k">标准价</span><span class="v money">¥86.00</span></span><span class="sep">·</span>
<span class="pair"><span class="k">成本价</span><span class="v money">¥52.50</span></span><span class="sep">·</span>
<span class="pair"><span class="k">状态</span><span class="v"><span class="tag">在售</span></span></span>
</p>
</div>
<div class="group">
<div class="group-title">库存</div>
<p class="inline-kv">
<span class="pair"><span class="k">库存数量</span><span class="v">128 瓶</span></span><span class="sep">·</span>
<span class="pair"><span class="k">预警值</span><span class="v">20</span></span><span class="sep">·</span>
<span class="pair"><span class="k">销售次数</span><span class="v">36</span></span>
</p>
</div>
<div class="group">
<div class="group-title">基本信息</div>
<p class="inline-kv">
<span class="pair"><span class="k">分类</span><span class="v">化工原料 / 有机溶剂</span></span><span class="sep">·</span>
<span class="pair"><span class="k">规格</span><span class="v">AR 500ml</span></span><span class="sep">·</span>
<span class="pair"><span class="k">条码</span><span class="v">6901234567890</span></span><span class="sep">·</span>
<span class="pair"><span class="k">税率</span><span class="v">13%</span></span><span class="sep">·</span>
<span class="pair"><span class="k">易制毒</span><span class="v"><span class="tag danger">是</span></span></span><span class="sep">·</span>
<span class="pair"><span class="k">易制爆</span><span class="v">否</span></span><span class="sep">·</span>
<span class="pair"><span class="k">创建</span><span class="v">2026-03-28 14:32</span></span>
</p>
</div>
<div class="group">
<div class="group-title">说明</div>
<p class="inline-kv-block"><span class="k">产品描述</span>适用于实验室分析、工业清洗;阴凉干燥储存。</p>
<p class="inline-kv-block" style="margin-top:6px"><span class="k">备注</span>2026 Q1 采购批次。</p>
</div>
<div class="field-label">产品图片</div>
<div class="imgs">
<img src="https://picsum.photos/seed/p1/96/96" alt="" />
<img src="https://picsum.photos/seed/p2/96/96" alt="" />
</div>
</div>
<div class="footer"><button class="btn btn-ghost" type="button">关闭</button></div>
</div>
</section>
</div>
<script>
document.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('is-active'))
document.querySelectorAll('.panel').forEach((p) => p.classList.remove('is-active'))
tab.classList.add('is-active')
document.getElementById(tab.dataset.target).classList.add('is-active')
})
})
</script>
</body>
</html>
@@ -1,25 +0,0 @@
-- =============================================================================
-- 运输计划:车辆/人员/证件整包快照字段
-- 用途:安排运输时将挂车号、人员摘要及 resource_snapshot JSON 写入计划与运输单
-- 适用:MySQL 5.7+,库名以实际环境为准(如 shanxia2.0 门户库)
-- 说明:若某列已存在,对应 ALTER 会报 Duplicate column,跳过该条即可
-- =============================================================================
USE `shanxia2.0`;
-- pl_plan:挂车号、人员摘要、整包快照 JSON
ALTER TABLE `pl_plan`
ADD COLUMN `trailer_no` varchar(20) DEFAULT NULL COMMENT '挂车号(安排运输时快照)' AFTER `license_plate`;
ALTER TABLE `pl_plan`
ADD COLUMN `personnel_summary` varchar(500) DEFAULT NULL COMMENT '关联人员摘要(安排运输时快照)' AFTER `driver_name`;
ALTER TABLE `pl_plan`
ADD COLUMN `resource_snapshot` text COMMENT '车辆/人员/证件整包快照 JSON' AFTER `personnel_summary`;
-- pl_transport:挂车号、整包快照 JSON
ALTER TABLE `pl_transport`
ADD COLUMN `trailer_no` varchar(20) DEFAULT NULL COMMENT '挂车号(安排运输时快照)' AFTER `license_plate`;
ALTER TABLE `pl_transport`
ADD COLUMN `resource_snapshot` text COMMENT '车辆/人员/证件整包快照 JSON' AFTER `driver_phone`;
@@ -1,5 +0,0 @@
-- CRM 产品:标准价、成本价、计量单位改为可空(产品模块不再维护,由报价/合同行维护)
ALTER TABLE `crm_product`
MODIFY COLUMN `unit` varchar(20) DEFAULT NULL COMMENT '单位',
MODIFY COLUMN `price` decimal(15,2) DEFAULT NULL COMMENT '标准单价',
MODIFY COLUMN `cost_price` decimal(15,2) DEFAULT NULL COMMENT '成本价';
@@ -1,6 +0,0 @@
-- 运输计划取货/送货地址坐标(高德导航)
ALTER TABLE `pl_plan`
ADD COLUMN `pickup_lng` decimal(10,6) DEFAULT NULL COMMENT '取货经度' AFTER `pickup_address`,
ADD COLUMN `pickup_lat` decimal(10,6) DEFAULT NULL COMMENT '取货纬度' AFTER `pickup_lng`,
ADD COLUMN `destination_lng` decimal(10,6) DEFAULT NULL COMMENT '送货经度' AFTER `destination_address`,
ADD COLUMN `destination_lat` decimal(10,6) DEFAULT NULL COMMENT '送货纬度' AFTER `destination_lng`;
@@ -1,15 +0,0 @@
-- 运费结算:新增「已驳回」状态字典(审批驳回后 settlement_status = rejected)
INSERT INTO `sys_dict_data` (
`id`, `dict_label`, `dict_value`, `tag_type`, `dict_type`, `tenant_no`, `order_num`,
`remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'PL_SETTLE_REJECTED', '已驳回', 'rejected', 'danger', 'pl_settlement_status', t.tenant_no, 2,
NULL, 'system', '', NOW(), NULL, NOW(), 0
FROM (
SELECT DISTINCT `tenant_no` FROM `sys_dict_type` WHERE `dict_type` = 'pl_settlement_status' AND `del_flag` = 0
) t
WHERE NOT EXISTS (
SELECT 1 FROM `sys_dict_data` d
WHERE d.id = 'PL_SETTLE_REJECTED' AND d.tenant_no = t.tenant_no AND d.del_flag = 0
);
@@ -1,22 +0,0 @@
-- 运输通用地址库
CREATE TABLE IF NOT EXISTS `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='运输通用地址库';
@@ -0,0 +1,5 @@
-- 运输计划:危化品证书人工确认齐备
ALTER TABLE `pl_plan`
ADD COLUMN `hazard_cert_ready` tinyint NOT NULL DEFAULT 0 COMMENT '危化品证书是否已人工确认齐备 0-否 1-是' AFTER `follow_user_name`,
ADD COLUMN `hazard_cert_ready_by` varchar(64) DEFAULT NULL COMMENT '危化品证书确认人' AFTER `hazard_cert_ready`,
ADD COLUMN `hazard_cert_ready_time` datetime DEFAULT NULL COMMENT '危化品证书确认时间' AFTER `hazard_cert_ready_by`;
+9 -5
View File
@@ -2786,6 +2786,9 @@ CREATE TABLE `pl_plan` (
`notify_message` text COMMENT '通知信息(标准模板)',
`follow_user_id` varchar(64) DEFAULT NULL COMMENT '跟进人ID',
`follow_user_name` varchar(100) DEFAULT NULL COMMENT '跟进人姓名',
`hazard_cert_ready` tinyint NOT NULL DEFAULT '0' COMMENT '危化品证书是否已人工确认齐备 0-否 1-是',
`hazard_cert_ready_by` varchar(64) DEFAULT NULL COMMENT '危化品证书确认人',
`hazard_cert_ready_time` datetime DEFAULT NULL COMMENT '危化品证书确认时间',
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
@@ -9059,8 +9062,8 @@ INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `co
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('CRM2000000000000000000000003007', '商机跟进', 'CRM2000000000000000000000000010', 'F', '', NULL, 'crm:opportunity:follow', 1, '1', '1', 'shanxia2.0', 7, NULL, NULL, '', '2026-05-31 16:17:46', NULL, '2026-05-31 16:17:46', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000001', '地磅管理', '2', 'M', '/db', NULL, NULL, 1, '1', '1', 'shanxia2.0', 9, '地磅业务模块(计划/出入库/地磅单/车牌)', NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000002', '仪表盘', 'DB00000000000000000000000000001', 'C', 'dashboard', '/db/dashboard/index', 'db:dashboard:list', 1, '1', '1', 'shanxia2.0', 1, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000003', '计划管理', 'DB00000000000000000000000000001', 'C', 'plans', '/db/plans/index', 'db:plan:list', 1, '1', '1', 'shanxia2.0', 2, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000004', '出入库管理', 'DB00000000000000000000000000001', 'C', 'inventory', '/db/inventory/index', 'db:inventory:list', 1, '1', '1', 'shanxia2.0', 3, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000003', '地磅计划', 'DB00000000000000000000000000001', 'C', 'plans', '/db/plans/index', 'db:plan:list', 1, '1', '1', 'shanxia2.0', 2, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000004', '地磅出入库', 'DB00000000000000000000000000001', 'C', 'inventory', '/db/inventory/index', 'db:inventory:list', 1, '1', '1', 'shanxia2.0', 3, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB00000000000000000000000000005', '地磅单管理', 'DB00000000000000000000000000001', 'C', 'weighing', '/db/weighing/index', 'db:weighing:list', 1, '1', '1', 'shanxia2.0', 4, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB10000000000000000000000000001', '计划新增', 'DB00000000000000000000000000003', 'F', '', NULL, 'db:plan:add', 1, '1', '1', 'shanxia2.0', 1, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('DB10000000000000000000000000002', '计划编辑', 'DB00000000000000000000000000003', 'F', '', NULL, 'db:plan:edit', 1, '1', '1', 'shanxia2.0', 2, NULL, NULL, '', '2026-06-07 10:00:00', NULL, '2026-06-07 10:00:00', 0);
@@ -9078,7 +9081,7 @@ INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `co
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PIL00000000000000000000000000001', '中试服务', '2', 'M', '/pilot-service', '/index/index', NULL, 1, '1', '1', 'shanxia2.0', 3, NULL, NULL, '', '2026-05-27 16:26:30', NULL, '2026-05-30 18:15:56', 1);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_CARRIER', '承运商管理', 'PL_MENU_ROOT', 'C', 'carrier', 'pl/carrier/index', 'pl:carrier:list', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-11 05:17:12', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_DOC_REMINDER', '证件提醒', 'PL_MENU_ROOT', 'C', 'document-reminder', 'pl/inventory/document-reminder', 'pl:document:list', 1, '1', '1', 'shanxia2.0', 50, NULL, 'system', '', '2026-06-11 05:17:13', NULL, '2026-06-11 05:17:13', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_DRIVER', '人员管理', 'PL_MENU_ROOT', 'C', 'personnel', 'pl/personnel/index', 'pl:driver:list', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-19 22:55:10', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_DRIVER', '驾押人员管理', 'PL_MENU_ROOT', 'C', 'personnel', 'pl/personnel/index', 'pl:driver:list', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-19 22:55:10', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_FREIGHT', '运费结算', 'PL_MENU_ROOT', 'C', 'freight', 'pl/freight/index', 'pl:freight:list', 1, '1', '1', 'shanxia2.0', 30, NULL, 'system', '', '2026-06-11 05:17:13', NULL, '2026-06-28 16:41:25', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_FREIGHT_A', '审批', 'PL_FREIGHT', 'F', '', '', 'pl:freight:approve', 1, '1', '1', 'shanxia2.0', 5, NULL, 'system', '', '2026-06-25 06:29:42', NULL, '2026-06-25 06:29:42', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_FREIGHT_APPROVAL', '运费结算审批', 'PL_MENU_ROOT', 'C', 'freight-approval', 'pl/freight-approval/index', 'pl:freight-approval:list', 1, '1', '1', 'shanxia2.0', 31, '待审批与已审批结算单', 'system', '', '2026-06-28 16:41:25', NULL, '2026-06-28 16:41:25', 0);
@@ -9099,7 +9102,7 @@ INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `co
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_ARCHIVE_E', '编辑', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:edit', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', '2026-06-25 06:14:17', NULL, '2026-06-25 06:14:17', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_ARCHIVE_Q', '查询', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:query', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', '2026-06-25 06:14:17', NULL, '2026-06-25 06:14:17', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_PLAN_CERT', '危化品证上传', 'PL_MENU_ROOT', 'C', 'plan-cert', 'pl/plan-cert/index', 'pl:plan-cert:list', 1, '1', '1', 'shanxia2.0', 12, '危化品购买证/合法使用证明上传', 'system', '', '2026-06-20 18:18:10', NULL, '2026-06-25 06:14:17', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_SELF_VEHICLE', '山峡自有车辆', 'PL_MENU_ROOT', 'C', 'self-vehicle', 'pl/self-vehicle/index', 'pl:self-vehicle:list', 1, '1', '1', 'shanxia2.0', 2, '展示全部自有类型车辆', 'system', '', '2026-06-20 08:07:00', NULL, '2026-06-28 16:01:18', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_SELF_VEHICLE', '山峡自有车辆', 'PL_MENU_ROOT', 'C', 'self-vehicle', 'pl/self-vehicle/index', 'pl:self-vehicle:list', 1, '1', '1', 'shanxia2.0', 3, '展示全部自有类型车辆', 'system', '', '2026-06-20 08:07:00', NULL, '2026-06-28 16:01:18', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_TRANSPORT', '运输管理', 'PL_MENU_ROOT', 'C', 'transport', 'pl/transport/index', 'pl:transport:list', 1, '0', '1', 'shanxia2.0', 20, NULL, 'system', '', '2026-06-11 05:17:13', NULL, '2026-06-20 21:42:04', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEH_MAINT_APPLY', '车辆维保申请', 'PL_MENU_ROOT', 'C', 'vehicle-maintenance-apply', 'pl/vehicle-maintenance-apply/index', 'pl:vehicle-maintenance-apply:list', 1, '1', '1', 'shanxia2.0', 33, NULL, 'system', '', '2026-06-29 09:02:16', NULL, '2026-06-29 09:13:01', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEH_MAINT_PROCESS', '车辆维保处理登记', 'PL_MENU_ROOT', 'C', 'vehicle-maintenance-process', 'pl/vehicle-maintenance-process/index', 'pl:vehicle-maintenance-process:list', 1, '1', '1', 'shanxia2.0', 34, NULL, 'system', '', '2026-06-29 09:02:16', NULL, '2026-06-29 09:13:01', 0);
@@ -9107,7 +9110,8 @@ INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `co
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEH_RECHARGE_RECORD', 'ETC加油发放登记', 'PL_MENU_ROOT', 'C', 'vehicle-recharge-record', 'pl/vehicle-recharge-record/index', 'pl:vehicle-recharge-record:list', 1, '1', '1', 'shanxia2.0', 32, NULL, 'system', '', '2026-06-29 08:32:30', NULL, '2026-06-29 08:37:03', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEH_UREA_APPLY', '车辆尿素申请', 'PL_MENU_ROOT', 'C', 'vehicle-urea-apply', 'pl/vehicle-urea-apply/index', 'pl:vehicle-urea-apply:list', 1, '1', '1', 'shanxia2.0', 35, NULL, 'system', '', '2026-06-29 09:26:30', NULL, '2026-06-29 09:26:30', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEH_UREA_DISBURSE', '车辆尿素发放登记', 'PL_MENU_ROOT', 'C', 'vehicle-urea-disburse', 'pl/vehicle-urea-disburse/index', 'pl:vehicle-urea-disburse:list', 1, '1', '1', 'shanxia2.0', 36, NULL, 'system', '', '2026-06-29 09:26:30', NULL, '2026-06-29 09:26:30', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEHICLE', '车辆管理', 'PL_MENU_ROOT', 'C', 'vehicle', 'pl/vehicle/index', 'pl:vehicle:list', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-28 16:01:18', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_TRANSPORT_ADDRESS', '地址库', 'PL_MENU_ROOT', 'C', 'transport-address', 'pl/transport-address/index', 'pl:transport-address:list', 1, '1', '1', 'shanxia2.0', 5, '运输通用地址库', 'system', '', '2026-07-01 19:54:09', NULL, '2026-07-01 19:54:09', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VEHICLE', '全部车辆管理', 'PL_MENU_ROOT', 'C', 'vehicle', 'pl/vehicle/index', 'pl:vehicle:list', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', '2026-06-11 05:17:12', NULL, '2026-06-28 16:01:18', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VMA_A', '新增', 'PL_VEH_MAINT_APPLY', 'F', '', '', 'pl:vehicle-maintenance-apply:add', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', '2026-06-29 09:02:16', NULL, '2026-06-29 09:13:01', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VMA_C', '作废', 'PL_VEH_MAINT_APPLY', 'F', '', '', 'pl:vehicle-maintenance-apply:cancel', 1, '1', '1', 'shanxia2.0', 5, NULL, 'system', '', '2026-06-29 09:02:16', NULL, '2026-06-29 09:13:01', 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('PL_VMA_E', '编辑', 'PL_VEH_MAINT_APPLY', 'F', '', '', 'pl:vehicle-maintenance-apply:edit', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', '2026-06-29 09:02:16', NULL, '2026-06-29 09:13:01', 0);
@@ -0,0 +1,63 @@
# 危化品证「证书齐备」人工确认 — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在危化品证上传页增加计划级「证书齐备」人工确认;安排运输只校验该标志,不再强制双证 URL;零附件可确认,确认后可继续补传附件。
**Architecture:** `pl_plan` 新增 `hazard_cert_ready` + 审计字段;PATCH API 标记/取消;`validate_hazard_cert_ready_for_transport` 校验 plan 标志;计划危化品产品集合变更时自动撤销;前端 plan-cert 列表列 + 工具栏按钮,安排运输弹窗改提示。
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus
**Spec:** `docs/superpowers/specs/2026-07-04-pl-hazard-cert-ready-design.md`
**Status:** ✅ 已实现(2026-07-04)
---
## 文件结构
| 文件 | 职责 |
|------|------|
| `docs/sql/migrations/2026-07-04_pl_plan_hazard_cert_ready.sql` | 增量 DDL |
| `docs/sql/shanxia2.0.sql` | CREATE TABLE 同步 |
| `backend/app/models/pl_models.py` | PlPlan 三字段 |
| `backend/app/schemas/pl/plan.py` | PlanRead 字段 + HazardCertReadyBody |
| `backend/app/services/pl_plan_service.py` | 校验/设置/撤销齐备 |
| `backend/app/api/pl/plan.py` | PATCH endpoint + update_plan 撤销 |
| `backend/app/api/pl/transport.py` | 双 URL 校验改为齐备标志 |
| `web-admin/src/api/pl/index.ts` | Plan 类型 + setHazardCertReady |
| `web-admin/src/composables/usePlToxicProductCertCheck.ts` | planHasHazardProducts + 齐备校验 helper |
| `web-admin/src/views/pl/plan-cert/index.vue` | 列 + 按钮 |
| `web-admin/src/components/business/pl-plan-cert-upload-dialog/index.vue` | 放宽保存校验 |
| `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue` | 改齐备校验 |
---
### Task 1: 数据库与 Model ✅
- [x] 迁移 SQL + shanxia2.0.sql + PlPlan model
### Task 2: Schema + Service ✅
- [x] PlanRead / HazardCertReadyBody
- [x] validate_hazard_cert_ready_for_transport / set_hazard_cert_ready / collect_hazard_product_ids
### Task 3: API ✅
- [x] PATCH `/{id}/hazard-cert-ready`
- [x] update_plan 危化品产品集合变更撤销齐备
- [x] transport.py 校验替换
### Task 4–6: 前端 ✅
- [x] API + composable + plan-cert 页 + 上传弹窗 + 安排运输弹窗
---
## 部署注意
上线前在目标库执行:
```bash
mysql ... < docs/sql/migrations/2026-07-04_pl_plan_hazard_cert_ready.sql
```
@@ -0,0 +1,124 @@
# Portal 恢复左侧子菜单 + 可折叠 — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Portal 布局恢复左侧二/三级菜单并支持折叠,顶栏保留一级模块下拉,移除水平子导航。
**Architecture:** 在 `views/index/index.vue` Portal 分支恢复 `#app-sidebar` + `ArtSidebarMenu`;`ArtPortalHeader` 增加折叠按钮并卸载 `ArtPortalSubNav`/`ArtPortalMobileMenu`;删除 `art-sidebar-menu` 中 Portal 强制展开逻辑;`style.portal.scss` 改为 flex row 并补充窄屏 overlay。
**Tech Stack:** Vue 3 · Element Plus · Pinia · vue-router · SCSS
**Spec:** `docs/superpowers/specs/2026-07-04-portal-sidebar-restore-design.md`
---
### Task 1: 恢复 Portal 布局 DOM 结构
**Files:**
- Modify: `web-admin/src/views/index/index.vue`
- Modify: `web-admin/src/views/index/style.portal.scss`
- [ ] **Step 1:** 在 Portal 分支 `#app-portal-body` 内恢复侧栏结构
```vue
<div id="app-portal-body">
<aside id="app-sidebar">
<ArtSidebarMenu />
</aside>
<main id="app-workspace">
<div id="app-content">
<ArtPageContent />
</div>
</main>
</div>
```
- [ ] **Step 2:** `onMounted` 中删除无条件 `setMenuOpen(true)`,保留 PORTAL 强制与 `enforceSinglePageTabs`
- [ ] **Step 3:** `style.portal.scss` 将 `#app-portal-body` 改为 `display: flex; flex-direction: row; min-height: 0; flex: 1`
- [ ] **Step 4:** 新增 `#app-sidebar { flex-shrink: 0; height: 100% }` 与 `#app-workspace { flex: 1; min-width: 0 }`
- [ ] **Step 5:** 复制 `style.scss` 中 ≤800px `#app-sidebar` fixed overlay 规则到 `.app-layout-portal` 作用域
### Task 2: 顶栏增加折叠按钮并精简
**Files:**
- Modify: `web-admin/src/components/core/layouts/art-portal-header/index.vue`
- [ ] **Step 1:** 移除 `<ArtPortalSubNav />` 与 `<ArtPortalMobileMenu />` 模板引用
- [ ] **Step 2:** 在 `portal-header-actions` 最前增加折叠按钮
```vue
<ArtIconButton
icon="fold"
class="portal-header-icon-btn portal-header-fold-btn"
@click="toggleSidebar"
/>
```
- [ ] **Step 3:** script 中注入 `useSettingStore`、`menuOpen`,实现 `toggleSidebar`
```typescript
const settingStore = useSettingStore()
const { menuOpen } = storeToRefs(settingStore)
const toggleSidebar = () => settingStore.setMenuOpen(!menuOpen.value)
```
- [ ] **Step 4:** 删除 template 中 `portal-sub-nav` 相关 `:deep` 样式块;保留 module-select 与 actions 样式
- [ ] **Step 5:** 调整 `portal-header-nav`:仅保留 `ArtPortalModuleSelect`(去掉 sub-nav flex:1 占位)
### Task 3: 放开 Portal 侧栏折叠
**Files:**
- Modify: `web-admin/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue`
- Modify: `web-admin/src/config/setting.ts`
- [ ] **Step 1:** 删除 `watch(width)` 中 `isPortalMenu` 强制 `setMenuOpen(true)` 分支,改为与 LEFT 布局相同的移动端处理
```typescript
watch(width, (newWidth) => {
if (newWidth < MOBILE_BREAKPOINT) {
settingStore.setMenuOpen(false)
if (!menuOpen.value) showMobileModal.value = false
} else {
showMobileModal.value = false
}
})
```
- [ ] **Step 2:** `config/setting.ts` 将 `menuOpenWidth` 从 `230` 改为 `220`
- [ ] **Step 3:** 确认 Portal 侧栏 `menu-model` 遮罩在移动端仍可点击关闭(已有逻辑,手测)
### Task 4: 全局样式对齐
**Files:**
- Modify: `web-admin/src/assets/styles/core/app.scss`
- [ ] **Step 1:** 在 `#app-sidebar` 规则后补充 Portal 作用域(若 scoped 未覆盖)
```scss
.app-layout-portal #app-sidebar .layout-sidebar {
height: 100%;
border-right: 1px solid var(--mint-border) !important;
background: var(--mint-surface);
}
```
- [ ] **Step 2:** 确认 `.layout-sidebar-portal` 高度为 `100%`(`art-sidebar-menu/style.scss` 已有)
### Task 5: 验证
- [ ] **Step 1:** `cd web-admin && pnpm lint`
- [ ] **Step 2:** `./scripts/update-graph.sh frontend`(shanxia2.0 根目录)
- [ ] **Step 3:** 浏览器手测
- 登录后侧栏展示当前模块子菜单
- 顶栏折叠按钮切换 220px / 64px
- 一级下拉切换模块后侧栏更新
- 视口 ≤800px 侧栏抽屉 + 遮罩
- 刷新后 `menuOpen` 状态保持
@@ -0,0 +1,253 @@
# 危化品证「证书齐备」人工确认 — 设计文档
> **日期**: 2026-07-04
> **状态**: 已实现(2026-07-04)
> **前置**: 现有 `pl_plan_product.purchase_cert_url` / `transport_cert_url` 双证校验逻辑;`/#/pl/plan-cert` 危化品证上传页
> **界面模式**: P06 复杂业务列表 + 弹窗表单(延续危化品证上传页)
## 背景与问题
### 现状
| 环节 | 行为 |
|------|------|
| 危化品识别 | 以 CRM 产品主数据 `isToxic` / `isExplosive` 为准 |
| 证书存储 | 计划产品行 `pl_plan_product.purchase_cert_url`(购买证)、`transport_cert_url`(合法使用证明) |
| 上传页校验 | 上传弹窗保存时 **强制** 每个危化品产品两个 URL 均非空 |
| 安排运输校验 | 前端 `buildMissingHazardCertMessage` + 后端 `transport.py` / `_validate_toxic_products` 均要求 **双证齐全** |
### 客户反馈
实际业务中,是否需要购买证、合法使用证明,以及是否两个都要,**取决于多种现场条件**(产品类型、单据类型、客户资质、运输方式等),规则复杂且频繁变化,**难以在系统中穷举**。
当前「双证强制」导致:
- 只需上传一种证的业务场景无法保存、无法标记完成;
- 操作员被迫上传占位文件或反复与开发沟通改规则;
- 系统规则与客户实际合规判断脱节。
### 目标
1. **危化品证上传页**增加「证书齐备」人工确认能力;
2. **运输计划安排运输**改为校验「证书齐备状态」,**不再**校验两个 URL 是否同时存在;
3. 证书文件仍可在上传页按需上传(0~2 个均可),供存档与查看。
---
## 方案对比
### 方案 A:计划级「证书齐备」标志(推荐)
在 `pl_plan` 增加 `hazard_cert_ready`(0/1)及可选审计字段(`hazard_cert_ready_by`、`hazard_cert_ready_time`)。
| 优点 | 缺点 |
|------|------|
| 与「安排运输按整单校验」一致,改动面小 | 一单多危化品时无法逐行独立确认 |
| 列表可加一列 Tag,一眼可见 | 计划产品变更后需定义是否自动撤销齐备 |
| 后端一处字段即可 gate 安排运输 | 依赖人工责任心,需审计字段 |
### 方案 B:产品级「证书齐备」标志
在 `pl_plan_product` 每行增加 `cert_ready`。
| 优点 | 缺点 |
|------|------|
| 多产品粒度更细 | 安排运输需「所有危化品行齐备」聚合逻辑 |
| 与现有证书 URL 同表,语义近 | UI 更复杂(每行按钮或弹窗内逐行确认) |
### 方案 C:配置化规则引擎(按产品/单据类型判定要几个证)
| 优点 | 缺点 |
|------|------|
| 理论上可自动化 | 客户已明确规则复杂、难维护,与诉求相反 |
| | 开发成本高、持续运维 |
**推荐:方案 A(计划级标志)**,配合审计字段与产品变更自动撤销。若后续确需逐产品确认,可演进为方案 B,API 可预留。
---
## 已确认设计(其余待拍板项见 §七)
| # | 决策 | 说明 |
|---|------|------|
| D1 | **齐备粒度:计划级** | `pl_plan.hazard_cert_ready`,仅对「含至少一个危化品产品」的计划有意义 |
| D2 | **无危化品计划免校验** | 计划不含易制毒/易制爆产品时,安排运输不检查齐备标志 |
| D3 | **上传不再强制双证** | 上传弹窗保存时允许购买证/合法使用证明 **0~2 个任意组合**;至少保留「有危化品产品行」才允许保存附件 |
| D4 | **安排运输只查齐备标志** | 替换现有 `purchase_cert_url && transport_cert_url` 校验(前后端一致) |
| D5 | **齐备为人工显式操作** | 【危化品证上传】列表工具栏 + 查看弹窗 hero 提供「证书齐备」;需二次确认 |
| D6 | **可撤销齐备** | 提供「取消齐备」;已归档/已作废计划不可操作 |
| D7 | **产品变更自动撤销** | 计划编辑导致危化品产品集合变化(增删改 productId 或危化品行)时,自动 `hazard_cert_ready=0` |
| D8 | **上传证书不自动齐备** | 保存附件 ≠ 证书齐备;须单独点击「证书齐备」 |
| D9 | **运输计划页无上传入口** | 维持已移除「上传证书」;仅提示「请至危化品证上传页确认证书齐备」 |
| D10 | **零附件可确认齐备** ✅ 2026-07-04 | 不要求任何附件即可点击「证书齐备」;人工确认即视为通过(证在客户处、纯备案等场景) |
| D11 | **齐备后可继续补传附件** ✅ 2026-07-04 | `hazard_cert_ready=1` **不阻断** 后续在危化品证上传页打开上传弹窗、补充/更新购买证或合法使用证明;齐备状态保持,除非 D7 触发撤销 |
---
## 一、数据模型
### 1.1 `pl_plan` 新增字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `hazard_cert_ready` | tinyint, default 0 | 危化品证书是否已人工确认齐备:0-否 1-是 |
| `hazard_cert_ready_by` | varchar(64), nullable | 确认人 user id |
| `hazard_cert_ready_time` | datetime, nullable | 确认时间 |
DDL 写入 `docs/sql/shanxia2.0.sql`(及迁移片段如项目惯例)。
### 1.2 既有字段保留
- `pl_plan_product.purchase_cert_url` / `transport_cert_url` **保留**,继续用于存档与列表「有/无」展示;
- **不再**作为安排运输的硬性条件。
---
## 二、后端 API
### 2.1 标记 / 取消齐备
```
PATCH /api/transport/plan/{id}/hazard-cert-ready
Body: { "ready": true | false }
```
规则:
- 计划存在且未归档、未作废;
- `ready=true`:计划须含至少一个危化品产品(CRM 标识),否则 400;**不校验** 是否已上传任何附件(D10);
- `ready=false`:清空 `hazard_cert_ready_by/time`;
- 写 `update_by`;可选写操作日志。
权限:`pl:plan-cert:edit` 或与上传证书同权限 `pl:plan-cert:list` + 编辑(与菜单权限对齐,实现时查现有 plan-cert 权限码)。
### 2.2 安排运输校验(替换)
`_validate_toxic_products` 与 `transport.py` 内联校验改为:
```python
# 伪代码
if plan_has_hazard_products(plan):
if not plan.hazard_cert_ready:
raise HTTPException(400, "该计划危化品证书尚未确认齐备,请至危化品证上传页操作")
```
删除对 `purchase_cert_url` / `transport_cert_url` 的非空检查。
### 2.3 计划更新时撤销齐备
在 `update_plan`(或产品明细 diff 逻辑)中:若危化品产品集合或危化品行 id 集合变化 → `hazard_cert_ready=0`,清空 by/time。
### 2.4 列表 / 详情返回
`_row_to_dict` / Plan schema 增加:
- `hazardCertReady` (bool/int)
- `hazardCertReadyBy` / `hazardCertReadyTime`(详情与危化品证上传列表展示)
---
## 三、前端
### 3.1 危化品证上传页 `plan-cert/index.vue`
**列表**
- 新增列「证书齐备」:`el-tag`,success=已齐备 / warning=待确认 / info=无需(无危化品);
- 工具栏(选中单行且含危化品、未归档、未作废):
- 未齐备 → 主按钮「证书齐备」(`CircleCheck` 图标),确认框后调 API;
- 已齐备 → 描边「取消齐备」(`CircleClose`),确认框后调 API。
**上传弹窗** `PlPlanCertUploadDialog`
- 去掉保存时对双证非空的 `ElMessage.warning` 拦截;
- **已齐备计划仍可打开上传弹窗**并保存附件(D11);不因 `hazardCertReady=1` 禁用「上传证书」;
- 提示文案改为:「请按实际业务上传所需证书;可先点击『证书齐备』放行运输,之后再补充附件。」
**查看弹窗 hero-actions**
- 与工具栏同样的齐备 / 取消齐备按钮。
### 3.2 运输计划 `plan/index.vue` + `PlPlanArrangeDialog`
- 提交前:若计划含危化品且 `!hazardCertReady`,`ElMessageBox.alert`:
「该计划危化品证书尚未确认齐备,请至【危化品证上传】页面操作。」
- 移除 `buildMissingHazardCertMessage` 对 URL 的检查(可保留 composable 供列表展示「有/无」,或重命名用途)。
### 3.3 列表证书展示
- `PlPlanListCertCell` / `PlPlanListProductCell` 继续展示各证「有/无」,**不作为**运输门槛;
- 可选:计划级齐备 Tag 与产品级有/无并存,便于操作员自查。
---
## 四、状态流转
```
[计划含危化品]
│
├──(可选) 零附件──► 点击「证书齐备」+ 确认 ──► hazard_cert_ready=1 ──► 可安排运输
│
└──(可选) 先传部分附件 ──► 同上
│
└── 已齐备后仍可「上传证书」补传/更新附件(齐备状态不变,D11)
│
编辑计划(危化品行变化) ──► 自动撤销齐备 (hazard_cert_ready=0)
点击「取消齐备」 ──► hazard_cert_ready=0
```
无危化品计划:`hazard_cert_ready` 保持 0,安排运输跳过校验。
---
## 五、影响范围(实现清单)
| 层 | 文件/模块 |
|----|-----------|
| SQL | `docs/sql/shanxia2.0.sql` |
| Model | `backend/app/models/pl_models.py` |
| Schema | `backend/app/schemas/pl/plan.py` |
| Service | `pl_plan_service.py`:`_validate_toxic_products`、`update_plan` 撤销逻辑、新增 `set_hazard_cert_ready` |
| API | `plan.py`:PATCH hazard-cert-ready;`transport.py`:校验替换 |
| FE API | `web-admin/src/api/pl/index.ts` |
| FE 页 | `plan-cert/index.vue` |
| FE 组件 | `pl-plan-cert-upload-dialog`、`pl-plan-arrange-dialog` |
| FE 类型 | `Plan` interface、`usePlToxicProductCertCheck.ts`(调整或拆分) |
**非目标**:不改 CRM 产品危化品标识;不改司机/车辆危化品证(`hazmat_cert_no`)逻辑。
---
## 六、风险与缓解
| 风险 | 缓解 |
|------|------|
| 人工误点齐备导致无证运输 | 二次确认文案强调合规责任;记录确认人/时间;列表保留附件有/无 |
| 计划变更后仍显示已齐备 | D7 自动撤销 |
| 历史已双证齐全的计划 | 迁移:`含危化品且双证 historically 齐全` 可批量 `hazard_cert_ready=1`(可选脚本,非必须) |
| 后端被绕过 | 安排运输 API 服务端强制校验齐备标志 |
---
## 七、待产品确认(实现前需答复)
| # | 问题 | 状态 |
|---|------|------|
| 1 | **齐备粒度**:计划级还是产品级? | 默认 **计划级**(D1),若无异议按此实现 |
| 2 | **零附件是否允许齐备** | ✅ **已确认:允许**(D10);人工点确认即通过 |
| 3 | **齐备后是否可补传附件** | ✅ **已确认:可以**(D11);齐备不锁死上传入口 |
| 4 | **权限**:「证书齐备」是否与「上传证书」同一角色即可? | 待确认(默认:是,沿用 `pl:plan-cert:list` 菜单权限) |
| 5 | **历史数据**:是否对已有双证齐全的计划批量标记已齐备? | 待确认(默认:否,由操作员按需点确认) |
---
## 八、验收标准
- [ ] 危化品证上传页可对含危化品的计划执行「证书齐备 / 取消齐备」,列表 Tag 正确
- [ ] 上传弹窗可只传 0/1/2 个证并保存成功
- [ ] **零附件**也可点击「证书齐备」并成功;安排运输可通过
- [ ] **已齐备后**仍可打开上传弹窗补充/更新附件,齐备状态不被上传动作清除
- [ ] 未齐备时,运输计划「安排运输」前后端均拦截,提示去危化品证上传页
- [ ] 已齐备且无其他阻塞时可正常安排运输
- [ ] 编辑计划变更危化品产品后,齐备状态自动清除
- [ ] 无危化品计划安排运输不受本功能影响
@@ -0,0 +1,233 @@
# web-admin 门户布局 — 恢复左侧子菜单 + 可折叠
> **日期**: 2026-07-04
> **状态**: 已确认(方案 A)
> **范围**: `web-admin` 管理端框架(Portal 壳层布局 + 菜单展示)
> **前置文档**: `2026-06-30-portal-top-nav-only-design.md`(当前实现:顶栏全量导航)
> **更早参考**: `2026-06-20-web-admin-portal-layout-design.md`(顶栏一级 + 左侧二三级,但无折叠)
> **UI 规范**: `docs/web-admin-ui-spec-fresh-mint.md`(侧栏 220px、顶栏 56px、主色 `#10b981`)
## 背景与目标
客户反馈顶栏水平菜单在 CRM 等模块项数较多时可发现性差(ellipsis「更多」隐藏深),希望 **将二/三级菜单移回左侧**,并 **支持侧栏收起/展开**。
**已选方案**:**方案 A — Portal 顶栏一级下拉 + 左侧可折叠子菜单**(2026-07-04 确认)。
---
## 已确认决策
| # | 决策 | 说明 |
|---|------|------|
| D1 | **布局形态** | 恢复 `#app-sidebar`;`#app-portal-body` 为 `flex row`(侧栏 + workspace) |
| D2 | **一级模块** | 保留顶栏 `ArtPortalModuleSelect` 下拉(CRM / 报销 / 地磅 / 运输 / 工作流 / 系统) |
| D3 | **二/三级** | 左侧 `ArtSidebarMenu`(`MenuTypeEnum.PORTAL` 分支),仅渲染当前模块 `children` |
| D4 | **顶栏二三级** | 移除 `ArtPortalSubNav` 水平菜单 |
| D5 | **折叠** | 顶栏增加折叠按钮;`menuOpen` 可切换;收起宽 64px,展开宽 220px |
| D6 | **侧栏品牌区** | Portal 侧栏不展示 Logo / 系统名(顶栏已有) |
| D7 | **权限与路由** | 不改 `MenuProcessor`、路由注册、`menuCode` 过滤链路 |
| D8 | **菜单图标** | 侧栏二/三级遵守 OpenSpec:更深层仅文字(`SidebarSubmenu` `level >= 1` 无 icon) |
| D9 | **WorkTab** | 维持 `showWorkTab: false` 严格单页策略,不恢复标签栏 |
| D10 | **移动端** | ≤800px 侧栏为 fixed 抽屉 overlay;顶栏保留一级下拉 + 折叠按钮 |
---
## 非目标(本期不做)
- 业务页面、API、权限后端改造
- 切换为纯 LEFT / DUAL_MENU 布局
- 恢复顶栏横向一级卡片(`ArtPortalTopNav`)
- 设置面板开放 Left / Top / Mixed 布局切换
- 删除 `ArtPortalSubNav` 源文件(保留 dead code 供回滚,仅从 Portal Header 卸载引用)
---
## 一、目标布局
```
┌────────────────────────────────────────────────────────────────────────┐
│ [Logo 系统名] │ [CRM ▼] [折叠] [全屏] [用户] │ 56px
├──────────────┬─────────────────────────────────────────────────────────┤
│ 二/三级菜单 │ ArtPageContent(业务页面) │
│ 可折叠 │ │
│ 220px→64px │ │
└──────────────┴─────────────────────────────────────────────────────────┘
```
**与 2026-06-30 对比**
| 项目 | 2026-06-30(现) | 本改造 |
|------|------------------|--------|
| 一级 | 顶栏下拉 | 顶栏下拉(不变) |
| 二三级 | 顶栏水平菜单 | 左侧树形菜单 |
| 侧栏 | 无 | 220px / 收起 64px |
| 折叠 | 无 | 顶栏折叠按钮 |
| 内容区 | 全宽 | 侧栏右侧剩余宽度 |
**DOM 结构(`views/index/index.vue`)**
```
.app-layout-portal
├── #app-portal-header → ArtPortalHeader(去掉 SubNav,加折叠按钮)
└── #app-portal-body → flex row
├── #app-sidebar → ArtSidebarMenu(PORTAL 模式)
└── #app-workspace → flex column,flex: 1
└── #app-content → ArtPageContent
```
---
## 二、顶栏(ArtPortalHeader)
### 2.1 保留
- 品牌区:`ArtLogo` + `AppConfig.systemInfo.name`
- 一级模块下拉:`ArtPortalModuleSelect`
- 全屏按钮、`ArtUserMenu`
### 2.2 新增
- **折叠按钮**(`ArtIconButton`,icon `fold` / `expand` 或沿用 `menuOpen` 态切换)
- 点击:`settingStore.setMenuOpen(!menuOpen)`
- 位置:操作区最左侧(全屏按钮之前)
### 2.3 移除(Portal 引用)
- `ArtPortalSubNav`
- `ArtPortalMobileMenu`(侧栏抽屉接管窄屏二三级导航;一级仍用下拉)
### 2.4 清理
- 删除 `portal-sub-nav` 相关 `:deep` 样式块(header 内)
---
## 三、左侧菜单(ArtSidebarMenu · PORTAL)
### 3.1 渲染范围(沿用现有逻辑)
- `isPortalMenu === true` 时,`menuList` = 当前一级模块的 `children`
- `filterSidebarRoutes` + `SidebarSubmenu`,`level` 从 1 起(二级无 icon)
- iframe 路由:`findIframeMenuList` 逻辑不变
### 3.2 折叠行为
| 状态 | 宽度 | UI |
|------|------|-----|
| 展开 `menuOpen=true` | 220px(`menuOpenWidth` 从 230 调整为 220) | 完整树形菜单 |
| 收起 `menuOpen=false` | 64px | `ElMenu :collapse="true"`,仅图标(二三级无 icon 时显示首字或占位) |
### 3.3 需修改的 Portal 限制(删除)
1. `art-sidebar-menu/index.vue` 中 `watch(width)` 对 `isPortalMenu` 强制 `setMenuOpen(true)` — **删除该分支**
2. `views/index/index.vue` `onMounted` 中 `setMenuOpen(true)` — **改为尊重 localStorage 持久化状态**(或仅首次默认 true)
### 3.4 侧栏不展示
- Logo / 系统名 header(`v-if="!isPortalMenu"` 已存在)
- 双列菜单折叠条(仅 `isDualMenu`)
---
## 四、样式
| 项 | 值 |
|----|-----|
| 顶栏高度 | 56px |
| 侧栏展开宽 | **220px**(对齐 `web-admin-ui-spec-fresh-mint.md` §3.2) |
| 侧栏收起宽 | 64px |
| 侧栏高度 | `calc(100vh - 56px)`(portal body 内,非全屏高) |
| 顶栏背景 | `var(--mint-primary)` |
| 侧栏背景 | `var(--mint-surface)` + 右边框 `var(--mint-border)` |
| 内容区 padding | `--mint-content-padding`(24px) |
**`style.portal.scss` 变更**
- `#app-portal-body`:`display: flex; flex-direction: row`
- 新增 `#app-sidebar { flex-shrink: 0 }`
- `#app-workspace { flex: 1; min-width: 0 }`
- ≤800px:`#app-sidebar` fixed overlay + `menu-model` 遮罩(复用 `style.scss` 中 LEFT 布局规则,复制到 portal 作用域)
**`app.scss`**
- 补充 `.app-layout-portal #app-sidebar` 与现有 `#app-sidebar` 规则对齐
---
## 五、导航联动(不变或微调)
| 场景 | 行为 |
|------|------|
| 切换一级模块 | 下拉选择 → `getFirstAccessiblePath`;侧栏子树切换 |
| 直接 deep link `/pl/plan` | 下拉高亮运输;侧栏高亮对应项 |
| WorkTab 关闭全部 | 维持现有 `getPortalDefaultPathForRoute`(无 WorkTab 时影响小) |
| 折叠态点击菜单项 | 路由跳转;收起态 popper 子菜单正常弹出 |
---
## 六、响应式
| 断点 | 策略 |
|------|------|
| ≥1180px | 侧栏 inline;顶栏单行 |
| 800–1180px | 侧栏 inline;系统名可缩短 |
| ≤800px | 侧栏默认收起;点击折叠按钮打开 fixed 抽屉 + 遮罩;顶栏一级下拉仍可用 |
---
## 七、影响文件清单
**修改**
| 文件 | 变更 |
|------|------|
| `views/index/index.vue` | Portal 分支恢复 `#app-sidebar` |
| `views/index/style.portal.scss` | flex row + 侧栏 + 窄屏 overlay |
| `components/core/layouts/art-portal-header/index.vue` | 加折叠按钮;移除 SubNav / MobileMenu |
| `components/core/layouts/art-menus/art-sidebar-menu/index.vue` | 删除 Portal 强制展开 |
| `config/setting.ts` | `menuOpenWidth: 220` |
| `assets/styles/core/app.scss` | Portal 侧栏样式补充 |
**不修改(本期)**
- `utils/router/portal-menu.ts`
- `ArtPortalModuleSelect`
- 业务 `views/**`
- `ArtPortalSubNav` 文件(保留,仅从 header 卸载)
---
## 八、测试计划
| 场景 | 预期 |
|------|------|
| super_admin 登录 | 顶栏下拉 6 模块;侧栏显示当前模块子树 |
| 切换 CRM → 报销 | 侧栏子树切换;路由进入报销首个可访问页 |
| 点击折叠按钮 | 侧栏 220px ↔ 64px 动画;内容区宽度自适应 |
| 刷新页面 | `menuOpen` 状态从 localStorage 恢复 |
| CRM 10+ 二级项 | 侧栏纵向滚动;无顶栏 ellipsis 问题 |
| 含三级目录 | SubMenu 展开;三级可点击 |
| 直接 URL `/pl/plan` | 下拉选中运输;侧栏高亮 |
| ≤800px | 侧栏抽屉 + 遮罩可开关 |
| 控制台 `/dashboard` | 不出现在下拉与侧栏 |
---
## 九、风险
1. **收起态无 icon 的二级项**:`SidebarSubmenu` 在 collapse 模式下可能仅显示文字缩写 — 需验收 Element Plus collapse 表现。
2. **窄屏双入口**:若误留 MobileMenu 与侧栏抽屉并存会混淆 — 本期明确移除 MobileMenu 引用。
3. **与 2026-06-30 spec 冲突**:属新产品决策,以本文档为准。
4. **menuOpenWidth 230→220**:可能影响用户 localStorage 中已存配置 — 影响极小。
---
## 十、验收标准
- [ ] Portal 布局下有左侧栏,展示当前模块二/三级菜单
- [ ] 顶栏无水平二三级菜单(`ArtPortalSubNav` 未挂载)
- [ ] 顶栏折叠按钮可收起/展开侧栏(220px / 64px)
- [ ] 一级模块仍通过顶栏下拉切换
- [ ] 权限、路由跳转与改造前一致
- [ ] Fresh Mint 色值与无 blur 遮罩符合 UI 规范
- [ ] ≤800px 侧栏抽屉可用
+421 -371
View File
File diff suppressed because it is too large. Load diff
+10 -17
View File
@@ -245,7 +245,6 @@
"243": "Community 243",
"244": "Community 244",
"245": "Community 245",
"246": "Community 246",
"247": "Community 247",
"248": "Community 248",
"249": "Community 249",
@@ -285,8 +284,6 @@
"283": "Community 283",
"284": "Community 284",
"285": "Community 285",
"286": "Community 286",
"287": "Community 287",
"288": "Community 288",
"289": "Community 289",
"290": "Community 290",
@@ -315,7 +312,6 @@
"313": "Community 313",
"314": "Community 314",
"315": "Community 315",
"316": "Community 316",
"317": "Community 317",
"318": "Community 318",
"319": "Community 319",
@@ -335,7 +331,6 @@
"333": "Community 333",
"334": "Community 334",
"335": "Community 335",
"336": "Community 336",
"337": "Community 337",
"338": "Community 338",
"339": "Community 339",
@@ -343,7 +338,6 @@
"341": "Community 341",
"342": "Community 342",
"343": "Community 343",
"344": "Community 344",
"345": "Community 345",
"346": "Community 346",
"347": "Community 347",
@@ -367,7 +361,6 @@
"365": "Community 365",
"366": "Community 366",
"367": "Community 367",
"368": "Community 368",
"369": "Community 369",
"370": "Community 370",
"371": "Community 371",
@@ -554,7 +547,6 @@
"552": "Community 552",
"553": "Community 553",
"554": "Community 554",
"555": "Community 555",
"556": "Community 556",
"557": "Community 557",
"558": "Community 558",
@@ -628,7 +620,6 @@
"626": "Community 626",
"627": "Community 627",
"628": "Community 628",
"629": "Community 629",
"630": "Community 630",
"631": "Community 631",
"632": "Community 632",
@@ -661,7 +652,6 @@
"659": "Community 659",
"660": "Community 660",
"661": "Community 661",
"662": "Community 662",
"663": "Community 663",
"664": "Community 664",
"665": "Community 665",
@@ -672,11 +662,9 @@
"670": "Community 670",
"671": "Community 671",
"672": "Community 672",
"673": "Community 673",
"674": "Community 674",
"675": "Community 675",
"676": "Community 676",
"677": "Community 677",
"678": "Community 678",
"679": "Community 679",
"680": "Community 680",
@@ -707,24 +695,29 @@
"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",
"716": "Community 716",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"721": "Community 721",
"722": "Community 722",
"723": "Community 723",
"724": "Community 724",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"735": "Community 735",
"732": "Community 732",
"733": "Community 733",
"734": "Community 734",
"736": "Community 736",
"739": "Community 739",
"741": "Community 741"
"737": "Community 737",
"740": "Community 740"
}
+1
View File
@@ -0,0 +1 @@
25877
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+12
View File
@@ -22,6 +22,8 @@
import { toggleTransition } from './utils/ui/animation'
import { checkStorageCompatibility } from './utils/storage'
import { initializeTheme } from './hooks/core/useTheme'
import { refreshFilterCollapse, setupFilterCollapseObserver } from './utils/ui/filter-collapse'
import { setupListTableRowSelect } from './utils/ui/list-table-row-select'
const userStore = useUserStore()
const { language } = storeToRefs(userStore)
@@ -33,6 +35,8 @@
let buttonIconObserver: MutationObserver | null = null
let panelTitleObserver: MutationObserver | null = null
let filterCollapseObserver: MutationObserver | null = null
let teardownListTableRowSelect: (() => void) | null = null
const iconClassByText: Array<[RegExp, string]> = [
[/(新增|新建|创建|添加)/, 'auto-icon-plus'],
@@ -147,6 +151,7 @@
systemUpgrade()
refreshButtonAutoIcons()
refreshPanelTitleIcons()
refreshFilterCollapse()
buttonIconObserver = new MutationObserver(() => {
refreshButtonAutoIcons()
@@ -163,6 +168,9 @@
childList: true,
subtree: true
})
filterCollapseObserver = setupFilterCollapseObserver()
teardownListTableRowSelect = setupListTableRowSelect()
})
onBeforeUnmount(() => {
@@ -170,5 +178,9 @@
buttonIconObserver = null
panelTitleObserver?.disconnect()
panelTitleObserver = null
filterCollapseObserver?.disconnect()
filterCollapseObserver = null
teardownListTableRowSelect?.()
teardownListTableRowSelect = null
})
</script>
+7
View File
@@ -394,7 +394,11 @@ export interface Plan {
notifyMessage?: string
followUserId?: string
followUserName?: string
hazardCertReady?: number
hazardCertReadyBy?: string
hazardCertReadyTime?: string
remark?: string
createBy?: string
createTime: string
products?: PlanProduct[]
}
@@ -522,6 +526,8 @@ export const planApi = {
id: string,
products: Array<{ id: string; purchaseCertUrl?: string; transportCertUrl?: string }>
) => request.patch<Plan>(`/transport/plan/${id}/product-certs`, { products }),
setHazardCertReady: (id: string, ready: boolean) =>
request.patch<Plan>(`/transport/plan/${id}/hazard-cert-ready`, { ready }),
cancel: (id: string) => request.post<Plan>(`/transport/plan/${id}/cancel`),
getArrangeInfo: (id: string) => request.get<PlanArrangeInfo>(`/transport/plan/${id}/arrange-info`),
archive: (id: string, data?: PlanArchiveBody) =>
@@ -769,6 +775,7 @@ export interface FreightSettlement {
transports?: Transport[]
plans?: SettlementPlanSummary[]
approvalFlow?: SettlementApprovalFlowItem[]
approvalHistory?: SettlementApprovalFlowItem[]
vouchers?: RemittanceVoucher[]
}
+13 -6
View File
@@ -1,21 +1,28 @@
import { publicPlGet, publicPlPost, publicPlUpload } from '@/utils/http/public-pl'
import type { PublicTransportProduct } from '@/api/pl/public-transport'
export interface PublicTransportSitePage {
batchNo?: string
businessType: string
date?: string
partnerCompany: string
product: string
specification?: string
quantity: number
vehicleNo: string
planNo?: string
status?: string
remark?: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupTime?: string
pickupNotes?: string
destinationName?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryTime?: string
destinationNotes?: string
products: PublicTransportProduct[]
siteFeedbackRemark?: string
siteFeedbackImages: string[]
siteFeedbackTime?: string
readOnly: boolean
voided: boolean
}
+13
View File
@@ -6,6 +6,13 @@ export interface PublicTransportProduct {
packageSpec?: string
unit?: string
quantity?: number
remark?: string
}
export interface PublicTransportPersonnel {
personnelName: string
phoneNo?: string
role?: string
}
export interface PublicTransportPage {
@@ -15,12 +22,18 @@ export interface PublicTransportPage {
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupTime?: string
pickupNotes?: string
destinationName?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryTime?: string
destinationNotes?: string
licensePlate?: string
driverName?: string
driverPhone?: string
otherPersonnel?: PublicTransportPersonnel[]
products: PublicTransportProduct[]
feedbackRemark?: string
feedbackImages: string[]
+180 -26
View File
@@ -125,7 +125,7 @@ body {
// 盒子默认边框
.page-content {
border: 1px solid var(--mint-border) !important;
border-radius: var(--mint-radius) !important;
border-radius: 0 !important;
box-shadow: var(--mint-shadow) !important;
}
@@ -359,8 +359,8 @@ body {
.panel {
background: var(--mint-surface);
border: 1px solid var(--mint-border);
border-radius: var(--mint-radius);
border: none;
border-radius: 0;
box-shadow: var(--mint-shadow);
/* 不要用 overflow:hidden:会导致 el-table 右侧固定列(fixed="right")无法正确粘贴/层级错乱 */
overflow: visible;
@@ -941,7 +941,11 @@ body {
.filter-block {
padding: 12px 16px 10px;
background: var(--mint-surface);
border-bottom: 1px solid var(--mint-border);
border-bottom: none;
&.is-filter-collapsed {
display: none !important;
}
}
.filter-hint {
@@ -1066,7 +1070,7 @@ body {
.toolbar {
padding: 0;
background: var(--mint-table-head);
border-bottom: 1px solid var(--mint-border);
border-bottom: none;
}
.list-toolbar-row,
@@ -1077,6 +1081,7 @@ body {
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
background: var(--mint-table-head);
}
/* 列表工具栏按钮间距统一由 gap 控制,清除 Element 默认相邻按钮 margin */
@@ -1093,15 +1098,93 @@ body {
.toolbar-left {
display: inline-flex;
align-items: center;
gap: 0;
min-height: 36px;
padding: 0 10px 0 0;
margin-right: 4px;
border-right: 1px solid var(--mint-border);
border-right: none;
font-size: 12px;
color: var(--mint-text-muted);
font-weight: 600;
}
.btn-list-filter-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin-right: 8px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--mint-text-muted);
cursor: pointer;
flex-shrink: 0;
transition:
background-color 0.2s ease,
color 0.2s ease;
&::before {
content: '';
width: 16px;
height: 16px;
background-color: currentColor;
opacity: 0.95;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/%3E%3C/svg%3E")
center / contain no-repeat;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/%3E%3C/svg%3E")
center / contain no-repeat;
}
&:hover {
color: var(--mint-primary-dark);
background: var(--mint-primary-bg);
}
&[aria-expanded='true'] {
color: var(--mint-primary-dark);
background: var(--mint-primary-bg);
}
&:focus-visible {
outline: 2px solid var(--mint-primary);
outline-offset: 1px;
}
}
.quick-search-group {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-right: 8px;
gap: 6px;
.quick-search-input {
width: 280px;
margin-right: 0;
}
.btn-list-filter-toggle {
margin-right: 0;
}
}
.quick-search-input:not(.quick-search-group .quick-search-input) {
width: 280px;
}
.quick-search-input .quick-search-icon {
cursor: pointer;
color: var(--mint-text-muted);
transition: color 0.2s ease;
&:hover {
color: var(--mint-primary-dark);
}
}
.sel-num {
color: var(--mint-primary-dark);
font-weight: 700;
@@ -1113,7 +1196,7 @@ body {
}
.list-wrap {
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
border-radius: 0;
overflow: visible;
}
@@ -1131,9 +1214,9 @@ body {
gap: 8px 12px;
margin-top: 0;
padding: 10px 16px 12px;
border-top: 1px solid var(--mint-border);
border-top: none;
background: var(--mint-surface);
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
border-radius: 0;
}
/* 分页条左侧说明文案(BX range / 分类 total-count 等)与右侧控件区分 */
@@ -1147,14 +1230,14 @@ body {
justify-content: flex-end;
margin-top: 0;
padding: 10px 16px 12px;
border-top: 1px solid var(--mint-border);
border-top: none;
background: var(--mint-surface);
}
.search-bar {
background: var(--mint-surface);
border: 1px solid var(--mint-border);
border-radius: var(--mint-radius);
border-radius: 0;
box-shadow: var(--mint-shadow);
}
@@ -1162,7 +1245,7 @@ body {
margin-bottom: 14px;
padding: 14px 16px;
border: 1px solid var(--mint-border);
border-radius: var(--mint-radius);
border-radius: 0;
background: var(--mint-primary-bg);
}
@@ -1179,7 +1262,7 @@ body {
}
.detail-section {
border-radius: var(--mint-radius);
border-radius: 0;
overflow: hidden;
margin-bottom: 14px;
}
@@ -1243,6 +1326,14 @@ body {
grid-column: 1 / -1;
}
/* 弹窗表单:栅格块与后续表单项/栅格块之间保留行间距(与模块弹窗 form-grid row-gap 一致) */
.el-dialog.dialog-form-layout .el-form {
[class*='form-grid-'] + .el-form-item,
[class*='form-grid-'] + [class*='form-grid-'] {
margin-top: 16px;
}
}
/* 表单栅格内 el-input-number 默认约 150px,须拉满与 el-input / el-select 同宽 */
.el-dialog.dialog-form-layout .el-form-item .el-input-number,
.page-container .el-form-item .el-input-number,
@@ -1288,11 +1379,11 @@ body {
}
}
/* 标准 CRUD 弹窗:相对视口上下各保留至少 50px 可视留白,正文区纵向滚动(不占满视口高度) */
/* 标准 CRUD 弹窗:相对视口上下各 20px 留白,正文区纵向滚动 */
.el-dialog.dialog-form-layout {
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px);
max-height: var(--dialog-max-height, calc(100vh - 40px));
overflow: hidden;
}
@@ -1300,6 +1391,14 @@ body {
flex-shrink: 0;
}
.el-dialog.dialog-form-layout .el-dialog__headerbtn {
display: flex;
align-items: center;
justify-content: center;
top: 4px;
right: 4px;
}
.el-dialog.dialog-form-layout .el-dialog__body {
flex: 1;
min-height: 0;
@@ -1321,13 +1420,18 @@ body {
backdrop-filter: none !important;
.el-overlay-dialog {
margin-top: 16px;
margin-top: 0;
margin-bottom: auto;
padding-top: var(--dialog-viewport-inset, 20px);
box-sizing: border-box;
}
.el-dialog.dialog-form-layout,
.el-dialog.order-module-dialog {
max-width: calc(100vw - 48px);
max-height: calc(
100vh - var(--dialog-viewport-inset, 20px) * 2
);
}
}
@@ -1365,6 +1469,12 @@ body {
background: var(--mint-surface);
}
.app-layout-portal #app-sidebar .layout-sidebar {
height: 100%;
border-right: 1px solid var(--mint-border) !important;
background: var(--mint-surface);
}
#app-sidebar .menu-left .header {
height: 56px;
line-height: 56px;
@@ -1383,6 +1493,14 @@ body {
/* 弹窗 / 抽屉内键值展示:上下布局、深色正文(对齐 zkxw Token 链) */
:is(.el-dialog, .system-view-dialog, .el-drawer) {
.el-form-item__label {
font-size: 13px !important;
}
.view-kv-table th {
font-size: 13px !important;
}
:is(.view-kv, [class*='-view-kv']:not([class*='-grid']), .pl-plan-kv) {
display: flex !important;
flex-direction: column !important;
@@ -1393,7 +1511,7 @@ body {
dt {
margin: 0;
font-size: 13px;
font-size: 13px !important;
font-weight: 400 !important;
color: var(--mint-text-muted) !important;
line-height: 1.4;
@@ -1409,7 +1527,7 @@ body {
}
.view-field-label {
font-size: 13px;
font-size: 13px !important;
font-weight: 400 !important;
color: var(--mint-text-muted) !important;
line-height: 1.4;
@@ -1427,8 +1545,39 @@ body {
}
}
/* CRM 新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行 */
.crm-dialog-form {
.el-form-item {
align-items: center;
}
/* 栅格内纵向间距由 gap 承担,避免与 margin 叠加 */
[class*='form-grid-'] > .el-form-item {
margin-bottom: 0;
}
/* 左右布局单行更矮,适当加大栅格行间距 */
[class*='form-grid-'] {
row-gap: 16px;
}
.el-form-item__label {
white-space: normal;
word-break: break-word;
line-height: 1.45;
height: auto !important;
padding-right: 12px;
align-self: center;
}
.el-form-item__content {
min-width: 0;
}
}
.el-dialog.dialog-form-layout {
.el-form-item__label {
font-size: 13px !important;
color: var(--mint-text) !important;
font-weight: 400 !important;
}
@@ -1451,10 +1600,14 @@ body {
/* 弹窗 / 查看态内嵌列表面板(CRM 详情 Tab、表单弹窗、选择器等) */
:is(.el-dialog, .system-view-dialog) {
.list-body.list-wrap {
border-top: 1px solid var(--mint-border);
}
.dialog-list-panel,
.customer-tab-list-panel {
> .list-toolbar:first-child {
border-radius: var(--mint-radius) var(--mint-radius) 0 0;
border-radius: 0;
overflow: hidden;
}
@@ -1488,19 +1641,14 @@ body {
flex-wrap: wrap;
}
.list-body {
border-top: 1px solid var(--mint-border);
}
> .list-body.list-wrap {
overflow: hidden;
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
border-radius: 0;
}
> .list-body.list-wrap.dialog-list-body-only,
> .list-body.list-wrap.customer-tab-list-body-only {
border-top: none;
border-radius: var(--mint-radius);
border-radius: 0;
}
.pagination.list-pager-bar {
@@ -1544,6 +1692,12 @@ body {
white-space: nowrap;
}
}
/* 区块标题 / 筛选行后紧跟、未包 list-body 的裸 list-table:表头顶部分隔线 */
:is(.dialog-section-title, .dlg-view-section-title) + .list-table.el-table thead th.el-table__cell,
:is(.dialog-section-title, .dlg-view-section-title) + .inline-row + .list-table.el-table thead th.el-table__cell {
border-top: 1px solid var(--mint-border) !important;
}
}
/* 筛选区 label:13px 次要色(对齐 zkxw filter-form 实践;filter-hint 仍为 11px) */
+85 -4
View File
@@ -58,6 +58,11 @@
padding: 0 0 4px !important;
}
// 弹窗/抽屉内表单 label(含 label-right):统一 13px,避免 EP 默认 14px
:is(.el-dialog, .el-drawer) .el-form-item__label {
font-size: 13px !important;
}
// 日期选择器
.el-date-range-picker {
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
@@ -149,12 +154,17 @@ html.dark .el-card {
border-radius: var(--mint-radius) !important;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12) !important;
display: flex;
flex-direction: column;
max-height: var(--dialog-max-height, calc(100vh - 40px));
margin: 0;
}
.el-dialog__header {
padding: 16px 22px 14px !important;
border-bottom: 1px solid var(--mint-border);
background: linear-gradient(135deg, var(--mint-primary-bg) 0%, var(--mint-surface) 60%);
flex-shrink: 0;
.el-dialog__title {
font-size: 16px;
@@ -163,15 +173,24 @@ html.dark .el-card {
}
}
.el-dialog__headerbtn {
top: 4px;
right: 4px;
}
.el-dialog__body {
padding: 18px 22px !important;
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.el-dialog__footer {
padding: 12px 22px !important;
border-top: 1px solid var(--mint-border);
background: var(--mint-table-head);
flex-shrink: 0;
}
.el-dialog.el-dialog-border {
@@ -372,8 +391,8 @@ html.dark .el-card {
// 修改 el-message-box 样式
.el-message-box__headerbtn .el-message-box__close,
.el-dialog__headerbtn .el-dialog__close {
top: 7px;
right: 7px;
top: 0;
right: 0;
width: 30px;
height: 30px;
border-radius: 5px;
@@ -747,6 +766,56 @@ body .el-table thead {
font-size: 14px;
}
// 主列表(含多选列):行可单击切换选中
.page-container .list-table.el-table:has(.el-table-column--selection) tbody tr.el-table__row,
.console-container .list-table.el-table:has(.el-table-column--selection) tbody tr.el-table__row {
cursor: pointer;
}
// 主列表:无顶/底外框;左右外缘 + 列间竖线 + 行间横线
.page-container .list-table.el-table,
.console-container .list-table.el-table,
.el-dialog .list-table.el-table,
.system-view-dialog .list-table.el-table {
border: none !important;
&::before,
&::after {
display: none !important;
}
.el-table__inner-wrapper::before,
.el-table__inner-wrapper::after {
display: none !important;
}
.el-table__border-left-patch,
.el-table__border-right-patch,
.el-table__border-bottom-patch {
display: none !important;
}
th.el-table__cell,
td.el-table__cell {
border-right: 1px solid var(--mint-border) !important;
border-bottom: 1px solid var(--mint-border) !important;
border-left: none !important;
border-top: none !important;
}
th.el-table__cell:first-child,
td.el-table__cell:first-child {
border-left: 1px solid var(--mint-border) !important;
}
.el-table__fixed-right {
th.el-table__cell:last-child,
td.el-table__cell:last-child {
border-right: 1px solid var(--mint-border) !important;
}
}
}
.page-container .el-table th.el-table__cell,
.console-container .el-table th.el-table__cell {
color: var(--mint-text-muted);
@@ -1196,12 +1265,24 @@ textarea::placeholder {
color: #147a3c !important;
}
// 列表分割线统一:每个分区只保留一条线
// 工具栏与列表之间:保留 .list-body 顶部线,去掉 .list-toolbar 底部线
// 列表分割线统一:工具栏与列表之间保留 list-body 顶部分隔线
.page-container .list-toolbar,
.console-container .list-toolbar,
.el-dialog .list-toolbar {
border-bottom: none !important;
background: var(--mint-table-head) !important;
}
.page-container .list-toolbar-row,
.console-container .list-toolbar-row,
.el-dialog .list-toolbar-row {
background: var(--mint-table-head);
}
.page-container .list-body,
.console-container .list-body,
.el-dialog .list-body {
border-top: 1px solid var(--mint-border);
}
// 列表与分页之间:保留列表底部线,去掉分页顶部线;分页控件右对齐(§7.9)
@@ -48,4 +48,8 @@
--danger-bg: #fef2f2;
--info: #64748b;
--info-bg: #f8fafc;
/* 弹窗相对视口上下留白(各 20px,总 40px) */
--dialog-viewport-inset: 20px;
--dialog-max-height: calc(100vh - var(--dialog-viewport-inset) * 2);
}
+9
View File
@@ -33,3 +33,12 @@
// 地磅(db)模块 Fresh Mint 公共样式
@use '../../styles/db-module.scss';
// 运输(pl)模块样式
@use '../../styles/pl-module.scss';
// 工作流(wf)模块样式
@use '../../styles/wf-module.scss';
// 系统管理(sys)模块样式
@use '../../styles/sys-module.scss';
@@ -1,5 +1,11 @@
<template>
<div class="attachment-upload">
<div
class="attachment-upload"
:class="{
'attachment-upload--compact': compact,
'attachment-upload--readonly': compact && disabled
}"
>
<div class="attachment-list">
<el-upload
v-if="!disabled"
@@ -11,7 +17,7 @@
@change="handleAttachmentChange"
>
<div class="upload-card">
<el-icon :size="28" color="#1ca851"><Plus /></el-icon>
<el-icon :size="previewIconSize" color="#1ca851"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
@@ -23,18 +29,18 @@
>
<div
class="attachment-preview"
:class="{ 'is-clickable': canPreview(file) }"
@click="canPreview(file) ? handlePreviewAttachment(file) : undefined"
:class="{ 'is-clickable': Boolean(file.fileUrl) }"
@click="file.fileUrl ? handleAttachmentClick(file) : undefined"
>
<template v-if="isImageFile(file) && file.fileUrl">
<img :src="resolveFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else-if="isImageFile(file) && !file.fileUrl">
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
<el-icon :size="previewIconSize" color="#94a3b8"><Loading /></el-icon>
</template>
<template v-else-if="isVideoFile(file)">
<div class="attachment-preview__icon attachment-preview__icon--video">
<el-icon :size="28"><VideoPlay /></el-icon>
<el-icon :size="previewIconSize"><VideoPlay /></el-icon>
</div>
</template>
<template v-else>
@@ -42,13 +48,22 @@
class="attachment-preview__icon"
:class="isPdfFile(file) ? 'attachment-preview__icon--pdf' : 'attachment-preview__icon--doc'"
>
<el-icon :size="28"><Document /></el-icon>
<el-icon :size="previewIconSize"><Document /></el-icon>
<span v-if="getFileExtension(file.fileName)" class="attachment-preview__ext">
{{ getFileExtension(file.fileName) }}
</span>
</div>
</template>
</div>
<button
v-if="compact && !disabled"
type="button"
class="attachment-compact-delete"
title="删除"
@click.stop="removeAttachment(index)"
>
<el-icon :size="12"><Delete /></el-icon>
</button>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div v-if="!disabled" class="attachment-actions">
@@ -71,7 +86,7 @@
</div>
</div>
</div>
<span v-if="!disabled" class="upload-hint">{{ hint }}</span>
<span v-if="!disabled && !compact" class="upload-hint">{{ hint }}</span>
<el-dialog
v-model="previewVisible"
@@ -148,6 +163,8 @@ const props = withDefaults(
accept?: string
hint?: string
disabled?: boolean
/** 紧凑模式:小缩略图,只读时隐藏文件名与操作栏 */
compact?: boolean
}>(),
{
modelValue: '',
@@ -155,7 +172,8 @@ const props = withDefaults(
limit: 15,
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
hint: '支持图片、视频、PDF、Word、Excel等文件',
disabled: false
disabled: false,
compact: false
}
)
@@ -170,6 +188,11 @@ const previewFile = ref<AttachmentItem | null>(null)
const previewPdfSrc = ref('')
const previewLoading = ref(false)
const previewIconSize = computed(() => {
if (!props.compact) return 28
return props.disabled ? 14 : 20
})
function resolveFileUrl(url: string) {
const trimmed = String(url || '').trim()
if (!trimmed) return ''
@@ -325,6 +348,18 @@ function handlePreviewAttachment(file: AttachmentItem) {
})
}
function handleAttachmentClick(file: AttachmentItem) {
if (!file.fileUrl) return
if (canPreview(file)) {
handlePreviewAttachment(file)
return
}
const url = resolveFileUrl(file.fileUrl)
if (url) {
window.open(url, '_blank', 'noopener,noreferrer')
}
}
function handlePreviewClosed() {
previewFile.value = null
previewPdfSrc.value = ''
@@ -532,4 +567,108 @@ defineExpose({ hasUploading })
:deep(.el-upload) {
display: block;
}
.attachment-upload--compact {
.attachment-list {
gap: 6px;
}
.upload-card-text {
display: none;
}
.attachment-preview__ext {
display: none;
}
.attachment-footer {
display: none;
}
}
/* 表格编辑:中等缩略图,支持预览 + 角标删除 */
.attachment-upload--compact:not(.attachment-upload--readonly) {
.upload-card {
width: 88px;
height: 66px;
gap: 0;
border-radius: 8px;
}
.attachment-item {
position: relative;
width: 88px;
border-radius: 8px;
&:hover {
box-shadow: 0 2px 6px rgba(28, 168, 81, 0.12);
}
}
.attachment-preview {
height: 66px;
}
.attachment-preview__icon {
width: 36px;
min-height: 36px;
border-radius: 8px;
}
.attachment-compact-delete {
position: absolute;
top: 4px;
right: 4px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 999px;
background: rgba(15, 23, 42, 0.72);
color: #fff;
cursor: pointer;
transition: background 0.15s ease;
&:hover {
background: #ef4444;
}
}
}
/* 只读紧凑:更小缩略图(查看弹窗等) */
.attachment-upload--compact.attachment-upload--readonly {
.attachment-list {
gap: 4px;
}
.upload-card {
width: 40px;
height: 40px;
gap: 0;
border-radius: 6px;
}
.attachment-item {
width: 40px;
border-radius: 6px;
&:hover {
box-shadow: 0 1px 4px rgba(28, 168, 81, 0.12);
}
}
.attachment-preview {
height: 40px;
}
.attachment-preview__icon {
width: 24px;
min-height: 24px;
border-radius: 6px;
}
}
</style>
@@ -0,0 +1,33 @@
<!-- 列表工具栏多功能检索框(漏斗切换由 filter-collapse 注入到 .quick-search-group) -->
<template>
<el-input
:model-value="modelValue"
class="quick-search-input"
:placeholder="placeholder"
clearable
@update:model-value="emit('update:modelValue', $event ?? '')"
@keyup.enter="emit('search')"
@clear="emit('clear')"
>
<template #suffix>
<el-icon class="quick-search-icon" @click="emit('search')"><Search /></el-icon>
</template>
</el-input>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
defineOptions({ name: 'ListQuickSearchInput' })
defineProps<{
modelValue: string
placeholder: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
search: []
clear: []
}>()
</script>
@@ -1,90 +1,84 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
v-model="innerVisible"
<SystemViewDialog
v-model:visible="innerVisible"
:title="carrier?.carrierName || '承运商详情'"
width="1080px"
align-center
destroy-on-close
class="dialog-form-layout carrier-view-dialog"
:icon="OfficeBuilding"
:meta="heroMeta"
:sections="viewSections"
:loading="loading"
width="1400px"
dialog-class="pl-carrier-view-dialog"
modal-class="order-module-dialog-mask"
append-to-body
style="backdrop-filter: none"
@closed="handleClosed"
@update:visible="handleVisibleChange"
>
<div class="carrier-view-body" v-loading="loading">
<div v-if="$slots['hero-actions']" class="carrier-view-hero-actions">
<slot name="hero-actions" />
</div>
<section class="carrier-view-top">
<h3 class="carrier-view-block-title">承运商信息</h3>
<ViewKvTable v-if="carrier" :fields="carrierViewFields" mode="triple" />
<template #hero-actions>
<slot name="hero-actions" />
</template>
<h3 class="carrier-view-block-title carrier-view-attachment-title">资质附件</h3>
<div v-if="carrier" class="carrier-view-attachments form-grid-2">
<div class="carrier-view-attachment-item">
<div class="carrier-view-attachment-label">营业执照</div>
<AttachmentUploadField
:model-value="carrier.businessLicenseUrls || ''"
disabled
folder="pl/carrier/license"
/>
</div>
<div class="carrier-view-attachment-item">
<div class="carrier-view-attachment-label">道路经营许可证</div>
<AttachmentUploadField
:model-value="carrier.roadTransportLicenseUrls || ''"
disabled
folder="pl/carrier/permit"
/>
</div>
<div class="carrier-view-attachment-item form-col-span-2">
<div class="carrier-view-attachment-label">运输协议</div>
<AttachmentUploadField
:model-value="carrier.transportAgreementUrls || ''"
disabled
folder="pl/carrier/agreement"
/>
</div>
<template #carrierAttachments>
<div v-if="carrier" class="carrier-view-attachments form-grid-3">
<div class="carrier-view-attachment-item">
<div class="carrier-view-attachment-label">营业执照</div>
<AttachmentUploadField
:model-value="carrier.businessLicenseUrls || ''"
disabled
folder="pl/carrier/license"
/>
</div>
<div class="carrier-view-attachment-item">
<div class="carrier-view-attachment-label">运输协议</div>
<AttachmentUploadField
:model-value="carrier.transportAgreementUrls || ''"
disabled
folder="pl/carrier/agreement"
/>
</div>
<div class="carrier-view-attachment-item">
<div class="carrier-view-attachment-label">道路经营许可证</div>
<AttachmentUploadField
:model-value="carrier.roadTransportLicenseUrls || ''"
disabled
folder="pl/carrier/permit"
/>
</div>
</section>
<section class="carrier-view-bottom">
<h3 class="carrier-view-block-title">关联车辆</h3>
<p v-if="!vehicleBundles.length && !loading" class="carrier-view-empty">暂无关联车辆</p>
<el-tabs v-else v-model="activeTab" class="carrier-vehicle-tabs">
<el-tab-pane
v-for="bundle in vehicleBundles"
:key="bundle.detail.id"
:label="bundle.detail.licensePlate"
:name="bundle.detail.id"
>
<PlVehicleViewPanel
:detail="bundle.detail"
:personnel-list="bundle.personnelList"
/>
</el-tab-pane>
</el-tabs>
</section>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</div>
</template>
</el-dialog>
<template #carrierVehicles>
<p v-if="!vehicleBundles.length && !loading" class="carrier-view-empty">暂无关联车辆</p>
<el-tabs v-else v-model="activeTab" class="carrier-vehicle-tabs">
<el-tab-pane
v-for="bundle in vehicleBundles"
:key="bundle.detail.id"
:label="bundle.detail.licensePlate"
:name="bundle.detail.id"
lazy
>
<PlVehicleViewPanel
v-if="activeTab === bundle.detail.id"
:key="`${viewSessionKey}-${bundle.detail.id}`"
:detail="bundle.detail"
:personnel-list="bundle.personnelList"
table-mode="quint"
cert-layout="table"
/>
</el-tab-pane>
</el-tabs>
</template>
</SystemViewDialog>
</template>
<script setup lang="ts">
import { Close } from '@element-plus/icons-vue'
import { OfficeBuilding } from '@element-plus/icons-vue'
import { carrierApi, vehicleApi, type Carrier } from '@/api/pl'
import { pickBxList } from '@/utils/http/bx'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import {
loadVehicleViewBundles,
@@ -116,44 +110,130 @@ const loading = ref(false)
const carrier = ref<Carrier | null>(null)
const vehicleBundles = ref<VehicleViewBundle[]>([])
const activeTab = ref('')
const viewSessionKey = ref(0)
const carrierViewFields = computed<ViewField[]>(() => {
const heroMeta = computed(() => {
const c = carrier.value
if (!c) return ''
const parts = [
c.contactName || undefined,
c.contactPhone || undefined,
getPlCarrierStatusLabel(c.status),
getPlCarrierAuditStatusLabel(c.auditStatus)
].filter(Boolean)
return parts.join(' · ')
})
const carrierInfoRow1Fields = computed<ViewField[]>(() => {
const c = carrier.value
if (!c) return []
return [
{ label: '承运商名称', value: c.carrierName },
{ label: '联系人', value: c.contactName || '—' },
{ label: '联系电话', value: c.contactPhone || '—' },
{ label: '承运商名称', value: c.carrierName, long: false },
{ label: '联系人', value: c.contactName || '—', long: false },
{ label: '联系电话', value: c.contactPhone || '—', long: false },
{
label: '合作状态',
value: getPlCarrierStatusLabel(c.status),
tagType: getPlCarrierStatusTagType(c.status)
},
tagType: getPlCarrierStatusTagType(c.status),
long: false
}
]
})
const carrierInfoRow2Fields = computed<ViewField[]>(() => {
const c = carrier.value
if (!c) return []
return [
{
label: '审核状态',
value: getPlCarrierAuditStatusLabel(c.auditStatus),
tagType: getPlCarrierAuditStatusTagType(c.auditStatus)
tagType: getPlCarrierAuditStatusTagType(c.auditStatus),
long: false
},
{
label: '审核时间',
value: c.auditTime ? formatDateTime(c.auditTime) : '—'
value: c.auditTime ? formatDateTime(c.auditTime) : '—',
long: false
},
{ label: '审核备注', value: c.auditRemark || '—', long: false, fullSpan: false }
]
})
const carrierInfoRow3Fields = computed<ViewField[]>(() => {
const c = carrier.value
if (!c) return []
return [
{ label: '创建时间', value: formatDateTime(c.createTime), long: false },
{
label: '协议有效期',
value: c.transportAgreementExpired
? '协议已过期'
: c.transportAgreementExpireDate || '—',
tagType: c.transportAgreementExpired ? 'danger' : undefined
tagType: c.transportAgreementExpired ? 'danger' : undefined,
long: false
},
{
label: '资质状态',
value: c.qualificationComplete ? '已齐全' : '资质不全',
tagType: c.qualificationComplete ? 'success' : 'warning'
tagType: c.qualificationComplete ? 'success' : 'warning',
long: false
},
{ label: '创建时间', value: formatDateTime(c.createTime) },
{ label: '审核备注', value: c.auditRemark || '—', span: 'full' },
{ label: '备注', value: c.remark || '—', span: 'full' }
{ label: '备注', value: c.remark || '—', long: false, fullSpan: false }
]
})
const viewSections = computed<ViewSection[]>(() => {
if (!carrier.value) {
return [
{
title: '承运商信息',
fieldGroups: [
{ fieldLayout: 'table', tableMode: 'quad', fields: [] },
{ fieldLayout: 'table', tableMode: 'quad', fields: [] },
{ fieldLayout: 'table', tableMode: 'triple', fields: [] }
],
fields: []
},
{ title: '资质附件', fields: [], slot: 'carrierAttachments' },
{ title: '关联车辆', fields: [], slot: 'carrierVehicles' }
]
}
return [
{
title: '承运商信息',
fieldGroups: [
{
fieldLayout: 'table',
tableMode: 'quad',
fields: carrierInfoRow1Fields.value
},
{
fieldLayout: 'table',
tableMode: 'quad',
fields: carrierInfoRow3Fields.value
},
{
fieldLayout: 'table',
tableMode: 'triple',
fields: carrierInfoRow2Fields.value
}
],
fields: []
},
{
title: '资质附件',
fields: [],
slot: 'carrierAttachments'
},
{
title: '关联车辆',
fields: [],
slot: 'carrierVehicles'
}
]
})
@@ -171,6 +251,7 @@ async function loadDetail(carrierId: string) {
carrier.value = null
vehicleBundles.value = []
activeTab.value = ''
viewSessionKey.value += 1
try {
const [carrierRes, vehicleListRes] = await Promise.all([
carrierApi.get(carrierId),
@@ -188,47 +269,24 @@ async function loadDetail(carrierId: string) {
}
}
function handleClosed() {
carrier.value = null
vehicleBundles.value = []
activeTab.value = ''
function handleVisibleChange(value: boolean) {
if (!value) {
carrier.value = null
vehicleBundles.value = []
activeTab.value = ''
viewSessionKey.value += 1
}
}
async function reload() {
if (!props.visible || !props.carrierId) return
await loadDetail(props.carrierId)
}
defineExpose({ reload })
</script>
<style scoped lang="scss">
.carrier-view-body {
display: flex;
flex-direction: column;
gap: 20px;
min-height: 120px;
}
.carrier-view-hero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.carrier-view-block-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
.carrier-view-top {
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.carrier-view-attachment-title {
margin-top: 20px;
}
.carrier-view-attachments {
display: grid;
gap: 16px;
@@ -238,13 +296,13 @@ function handleClosed() {
margin-bottom: 8px;
font-size: 11px;
font-weight: 500;
color: #1e293b;
color: #64748b;
}
.carrier-view-empty {
margin: 0;
font-size: 13px;
color: #1e293b;
color: var(--mint-text-muted, #64748b);
}
.carrier-vehicle-tabs {
@@ -257,3 +315,27 @@ function handleClosed() {
}
}
</style>
<style lang="scss">
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group--spaced {
margin-top: 0;
}
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group .view-kv-table-wrap {
border-radius: 0;
}
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group:first-child .view-kv-table-wrap {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group--spaced .view-kv-table-wrap {
border-top: none;
}
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group:last-child .view-kv-table-wrap {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
</style>
@@ -9,6 +9,92 @@
<div v-if="!certList.length" class="cert-empty">暂无证书,点击「添加证书」</div>
<el-table v-else-if="layout === 'table'" :data="certList" stripe class="list-table cert-form-table">
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="showVehiclePartColumn"
label="车牌号"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-if="hasTrailerPlateOption"
:model-value="normalizeVehicleCertPart(row.vehiclePart)"
placeholder="请选择车牌号"
style="width: 100%"
@update:model-value="handleVehiclePartChange(row, $event)"
>
<el-option
v-for="option in vehiclePlateOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<span v-else>{{ headPlateLabel }}</span>
</template>
</el-table-column>
<el-table-column label="证书名称" min-width="160">
<template #default="{ row }">
<el-select
v-model="row.certName"
placeholder="请选择或输入"
filterable
allow-create
default-first-option
>
<el-option v-for="name in certNameOptions" :key="name" :label="name" :value="name" />
</el-select>
</template>
</el-table-column>
<el-table-column label="证号" min-width="140">
<template #default="{ row }">
<el-input v-model="row.certNo" placeholder="请输入证号" />
</template>
</el-table-column>
<el-table-column label="有效期" min-width="150">
<template #default="{ row }">
<el-date-picker
v-model="row.expireDate"
type="date"
placeholder="选择有效期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="提醒天数" min-width="110" align="center" header-align="center">
<template #default="{ row }">
<el-input-number
v-model="row.remindDays"
:min="1"
:max="365"
:controls="false"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="附件" min-width="220">
<template #default="{ row, $index }">
<AttachmentUploadField
:ref="(el) => setCertUploadRef($index, el)"
v-model="row.attachmentUrls"
:folder="uploadFolder"
compact
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="60" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="handleRemoveCert($index)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="cert-card-list">
<div v-for="(cert, index) in certList" :key="cert._key" class="cert-card">
<div class="cert-card-head">
@@ -43,9 +129,6 @@
/>
</el-form-item>
<el-form-item label="提醒天数">
<template #label>
提醒天数<span class="form-label-tip">默认 30 天</span>
</template>
<el-input-number
v-model="cert.remindDays"
:min="1"
@@ -71,6 +154,11 @@
import { Delete, Plus } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import type { CertRow } from '@/api/pl'
import {
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
normalizeVehicleCertPart
} from '@/utils/pl-vehicle-cert'
type CertRowWithKey = CertRow & { _key: string }
type CertUploadRef = InstanceType<typeof AttachmentUploadField>
@@ -80,13 +168,45 @@ const props = withDefaults(
title?: string
uploadFolder: string
certNameOptions?: string[]
layout?: 'card' | 'table'
/** 列表模式:展示车牌号列,可从主车/挂车车牌中选择 */
showVehiclePartColumn?: boolean
licensePlate?: string
trailerNo?: string
}>(),
{
title: '证书',
certNameOptions: () => []
certNameOptions: () => [],
layout: 'card',
showVehiclePartColumn: false,
licensePlate: '',
trailerNo: ''
}
)
const headPlateLabel = computed(() => props.licensePlate?.trim() || '—')
const hasTrailerPlateOption = computed(() => Boolean(props.trailerNo?.trim()))
const vehiclePlateOptions = computed(() => {
const options = [
{
label: headPlateLabel.value,
value: PL_VEHICLE_CERT_PART_HEAD
}
]
if (hasTrailerPlateOption.value) {
options.push({
label: props.trailerNo?.trim() || '挂车',
value: PL_VEHICLE_CERT_PART_TRAILER
})
}
return options
})
function handleVehiclePartChange(row: CertRowWithKey, value: string) {
row.vehiclePart = normalizeVehicleCertPart(value)
}
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
@@ -94,6 +214,7 @@ const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
function createEmptyCert(): CertRowWithKey {
return {
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
vehiclePart: PL_VEHICLE_CERT_PART_HEAD,
certName: '',
certNo: '',
expireDate: '',
@@ -161,6 +282,29 @@ defineExpose({ hasUploading, clearUploadRefs })
background: #f8fafc;
}
.cert-form-table {
border-top: 1px solid var(--mint-border, #d9e8e2);
:deep(.el-select),
:deep(.el-input),
:deep(.el-date-editor) {
width: 100%;
}
:deep(.attachment-upload--compact:not(.attachment-upload--readonly)) {
min-width: 0;
.attachment-upload {
width: auto;
}
.attachment-list {
flex-wrap: wrap;
align-items: flex-start;
}
}
}
.cert-card-list {
display: flex;
flex-direction: column;
@@ -207,7 +351,7 @@ defineExpose({ hasUploading, clearUploadRefs })
}
:deep(.el-form-item__label) {
font-size: 11px;
font-size: 13px;
color: var(--mint-text);
padding-bottom: 4px;
}
@@ -1,6 +1,85 @@
<template>
<div class="cert-view-section">
<p v-if="!certList.length" class="view-empty-hint">暂无证书</p>
<p v-if="!certList.length && layout === 'card'" class="view-empty-hint">暂无证书</p>
<div v-else-if="layout === 'table'" class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
:data="certList"
class="list-table"
stripe
empty-text="暂无证书"
>
<el-table-column
type="index"
label="序号"
min-width="60"
align="center"
header-align="center"
:index="index => index + 1"
/>
<el-table-column
v-if="showVehiclePartColumn"
label="车牌号"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ resolveVehiclePartLabel(row) }}
</template>
</el-table-column>
<el-table-column
prop="certName"
label="证书名称"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="certNo"
label="证号"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.certNo || '—' }}
</template>
</el-table-column>
<el-table-column label="有效期" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag
v-if="row.expireDate"
:type="getExpireTagType(row.expireDate)"
size="small"
>
{{ row.expireDate }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column label="提醒天数" min-width="96" align="center" header-align="center">
<template #default="{ row }">{{ row.remindDays ?? 30 }} 天</template>
</el-table-column>
<el-table-column label="附件" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<div v-if="row.attachmentUrls" class="cert-view-attachment-cell">
<AttachmentUploadField
:model-value="row.attachmentUrls"
disabled
compact
/>
</div>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-else class="cert-view-list">
<div
@@ -11,15 +90,16 @@
<h4 class="cert-view-card-title">证书 {{ index + 1 }}</h4>
<div class="cert-view-kv-table-group">
<ViewKvTable :fields="buildCertViewFields(cert)" mode="triple" />
<ViewKvTable :fields="buildCertViewFields(cert)" :mode="tableMode" />
<table
v-if="cert.attachmentUrls"
class="cert-view-attachment-table view-kv-table view-kv-table--triple"
class="cert-view-attachment-table view-kv-table"
:class="`view-kv-table--${tableMode}`"
>
<tbody>
<tr class="row-long">
<th>附件</th>
<td colspan="5">
<td :colspan="attachmentColspan">
<AttachmentUploadField
:model-value="cert.attachmentUrls"
disabled
@@ -37,18 +117,51 @@
<script setup lang="ts">
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import type { ViewField, ViewTableMode } from '@/components/business/system-view-dialog/types'
import type { CertRead } from '@/api/pl'
import {
PL_VEHICLE_CERT_PART_TRAILER,
normalizeVehicleCertPart
} from '@/utils/pl-vehicle-cert'
withDefaults(
const props = withDefaults(
defineProps<{
certList?: CertRead[]
tableMode?: ViewTableMode
/** card:卡片 + KV 表格;table:列表表格 + 小缩略图 */
layout?: 'card' | 'table'
/** 列表模式:有挂车时展示主车/挂车部位列 */
showVehiclePartColumn?: boolean
headLabel?: string
trailerLabel?: string
}>(),
{
certList: () => []
certList: () => [],
tableMode: 'triple',
layout: 'card',
showVehiclePartColumn: false,
headLabel: '主车',
trailerLabel: '挂车'
}
)
const attachmentColspanMap: Record<ViewTableMode, number> = {
pair: 1,
dual: 3,
triple: 5,
quad: 7,
quint: 9
}
const attachmentColspan = computed(() => attachmentColspanMap[props.tableMode])
function resolveVehiclePartLabel(cert: CertRead) {
if (normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER) {
return props.trailerLabel?.trim() || '挂车'
}
return props.headLabel?.trim() || '主车'
}
function getExpireTagType(dateStr: string | undefined) {
if (!dateStr) return 'info'
const expDate = new Date(dateStr)
@@ -85,6 +198,20 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
line-height: 1.5;
}
.cert-view-attachment-cell {
display: inline-flex;
justify-content: center;
width: 100%;
:deep(.attachment-upload) {
width: auto;
}
:deep(.attachment-list) {
justify-content: center;
}
}
.cert-view-list {
display: flex;
flex-direction: column;
@@ -129,7 +256,6 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
border-top: 1px solid #eef2f6;
th {
width: 10%;
padding: 10px 12px;
background: var(--table-head, #f8fafc);
color: var(--text-muted, #64748b);
@@ -152,4 +278,12 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
line-height: 1.45;
}
}
.cert-view-attachment-table.view-kv-table--triple th {
width: 10%;
}
.cert-view-attachment-table.view-kv-table--quint th {
width: 6.5%;
}
</style>
@@ -2,7 +2,7 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="640px"
width="740px"
align-center
append-to-body
destroy-on-close
@@ -14,7 +14,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -31,9 +33,7 @@
</el-form-item>
<el-form-item v-if="actionType === 'submit'" prop="approverIds" class="form-col-span-2">
<template #label>
选择审批人<span class="form-label-tip">支持搜索用户姓名</span>
</template>
<template #label>选择审批人</template>
<el-select
v-model="form.approverIds"
multiple
@@ -71,9 +71,7 @@
</el-form-item>
<el-form-item v-if="actionType === 'submit'" prop="remark" class="form-col-span-2">
<template #label>
提交说明<span class="form-label-tip">可选</span>
</template>
<template #label>提交说明</template>
<el-input
v-model="form.remark"
type="textarea"
@@ -60,7 +60,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -15,7 +15,9 @@
v-loading="loading"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -30,30 +30,39 @@
<section v-if="settlement" class="view-section">
<h4 class="view-section-title">发票信息</h4>
<div class="view-kv-grid view-kv-grid--cols-3">
<div class="view-kv-item">
<span class="view-kv-label">发票号</span>
<span class="view-kv-value">{{ settlement.invoiceNo || '—' }}</span>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
:data="invoiceRows"
class="list-table"
stripe
empty-text="暂无数据"
>
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="invoiceNo" label="发票号" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.invoiceNo || '—' }}</template>
</el-table-column>
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" align="center">
<template #default="{ row }">{{ row.invoiceDate || '—' }}</template>
</el-table-column>
<el-table-column prop="invoiceAmount" label="发票金额(元)" min-width="120" align="right">
<template #default="{ row }">
{{ row.invoiceAmount != null ? formatAmount(row.invoiceAmount) : '—' }}
</template>
</el-table-column>
<el-table-column label="发票附件" min-width="160">
<template #default="{ row }">
<div v-if="row.invoiceAttachmentUrls" class="invoice-attachment-half">
<AttachmentUploadField
:model-value="row.invoiceAttachmentUrls"
disabled
/>
</div>
<span v-else class="view-kv-value">—</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="view-kv-item">
<span class="view-kv-label">开票日期</span>
<span class="view-kv-value">{{ settlement.invoiceDate || '—' }}</span>
</div>
<div class="view-kv-item">
<span class="view-kv-label">发票金额</span>
<span class="view-kv-value">{{
settlement.invoiceAmount ? formatAmount(settlement.invoiceAmount) : '—'
}}</span>
</div>
</div>
<div class="view-kv-item view-kv-item--full">
<span class="view-kv-label">发票附件</span>
<AttachmentUploadField
v-if="settlement.invoiceAttachmentUrls"
:model-value="settlement.invoiceAttachmentUrls"
disabled
/>
<span v-else class="view-kv-value">—</span>
</div>
<div class="view-kv-grid view-kv-grid--cols-2 view-kv-grid--mt">
<div class="view-kv-item">
@@ -120,9 +129,16 @@
</div>
</section>
<section v-if="settlement?.approvalFlow?.length" class="view-section">
<section
v-if="settlement && (settlement.approvalFlow?.length || settlement.approvalHistory?.length)"
class="view-section"
>
<h4 class="view-section-title">审批流程</h4>
<ApprovalFlowView :nodes="approvalNodes" :loading="false" />
<BxApprovalFlowTabs
:nodes="approvalNodes"
:history="approvalHistoryItems"
:loading="false"
/>
</section>
<section
@@ -170,18 +186,12 @@
</section>
</div>
<template #footer>
<el-button @click="visible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { Close } from '@element-plus/icons-vue'
import { freightApi, type FreightSettlement } from '@/api/pl'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { normalizeAttachmentUrls } from '@/utils/download-attachment'
import {
@@ -201,6 +211,29 @@ const approvalNodes = computed(() =>
mapApprovalFlowToNodes(settlement.value?.approvalFlow || [])
)
const approvalHistoryItems = computed(() =>
mapApprovalFlowToNodes(settlement.value?.approvalHistory || [])
)
const invoiceRows = computed(() => {
const current = settlement.value
if (!current) return []
const hasInvoice =
Boolean(current.invoiceNo?.trim()) ||
Boolean(current.invoiceDate) ||
(current.invoiceAmount != null && Number(current.invoiceAmount) > 0) ||
Boolean(current.invoiceAttachmentUrls?.trim())
if (!hasInvoice) return []
return [
{
invoiceNo: current.invoiceNo,
invoiceDate: current.invoiceDate,
invoiceAmount: current.invoiceAmount,
invoiceAttachmentUrls: current.invoiceAttachmentUrls
}
]
})
function formatAmount(amount?: number) {
if (!amount) return '¥0.00'
return formatMoneyDisplay(amount)
@@ -357,6 +390,53 @@ defineExpose({ open, reload })
}
}
.invoice-attachment-half {
:deep(.attachment-list) {
gap: 6px;
}
:deep(.attachment-item) {
width: 70px;
}
:deep(.attachment-preview) {
height: 48px;
}
:deep(.attachment-preview__icon) {
width: 24px;
min-height: 24px;
.el-icon {
font-size: 14px;
}
}
:deep(.attachment-preview__ext) {
margin-top: 2px;
font-size: 9px;
}
:deep(.attachment-footer) {
padding: 3px 4px;
}
:deep(.attachment-name) {
font-size: 9px;
-webkit-line-clamp: 1;
}
:deep(.attachment-actions) {
gap: 4px;
margin-top: 2px;
}
:deep(.action-btn) {
width: 16px;
height: 16px;
}
}
@media (max-width: 900px) {
.view-kv-grid--cols-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -17,7 +17,7 @@
<section class="pl-arrange-form-panel">
<div v-loading="loading" class="pl-arrange-form-scroll">
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">产品实际数量</h3>
<h4 class="pl-plan-subtitle">产品实际数量</h4>
<PlPlanFormProductTable
:products="productRows"
show-actual-qty
@@ -29,7 +29,7 @@
<h4 class="pl-plan-subtitle">运费信息</h4>
<el-form
:model="formData"
label-width="96px"
label-width="140px"
label-position="right"
class="pl-plan-freight-form"
>
@@ -119,6 +119,7 @@ import {
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { useDictStore } from '@/store/modules/dict'
import { pickBxData } from '@/utils/http/bx'
@@ -258,7 +259,9 @@ async function handleOpened() {
throw new Error('运输计划数据无效')
}
plan.value = data
productRows.value = mapPlanProducts(Array.isArray(data.products) ? data.products : [])
const rawProducts = Array.isArray(data.products) ? data.products : []
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
productRows.value = mapPlanProducts(enrichedProducts)
await applyFreightPrefill(data)
} catch (error) {
innerVisible.value = false
@@ -356,6 +359,9 @@ async function handleSubmit() {
}
.pl-arrange-form-scroll {
display: flex;
flex-direction: column;
gap: 20px;
max-height: 52vh;
overflow-y: auto;
padding-right: 4px;
@@ -377,14 +383,21 @@ async function handleSubmit() {
}
.pl-plan-subtitle {
margin: 16px 0 10px;
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.pl-plan-subtitle:first-of-type {
margin-top: 0;
.pl-plan-freight-section {
.pl-plan-subtitle {
margin-top: 4px;
}
:deep(.el-form-item__label) {
width: 140px !important;
max-width: 140px;
}
}
.pl-plan-freight-form {
@@ -25,42 +25,13 @@
:title="
isRearrange
? hasSiteInventoryRecords
? '运输已调整,司机与现场链接已更新,旧链接已失效'
: '运输已调整,司机链接已更新,旧链接已失效'
? '运输已调整,司机与现场链接已更新,旧链接已失效。请在下方「运输信息」中复制司机链接。'
: '运输已调整,司机链接已更新,旧链接已失效。请在下方「运输信息」中复制司机链接。'
: hasSiteInventoryRecords
? '运输已安排,可复制司机链接、现场链接或简述发给相关人员'
: '运输已安排,可复制司机链接或计划简述'
? '运输已安排,请在下方「运输信息」中复制链接。'
: '运输已安排,请在下方「运输信息」中复制司机链接。'
"
/>
<div class="pl-arrange-link-row">
<span class="pl-arrange-link-label">司机链接</span>
<el-input :model-value="driverUrl || arrangeResult?.driverUrl || ''" readonly />
<el-button @click="copyDriverUrl(arrangeResult?.driverUrl)">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
<template v-if="hasSiteInventoryRecords">
<div v-if="siteRecords.length === 1" class="pl-arrange-link-row">
<span class="pl-arrange-link-label">现场链接</span>
<el-input :model-value="siteRecords[0].siteUrl || ''" readonly />
<el-button @click="copySiteUrl(siteRecords[0].siteUrl)">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
<template v-else>
<div
v-for="record in siteRecords"
:key="record.siteLinkId"
class="pl-arrange-link-row"
>
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
<el-input :model-value="record.siteUrl || ''" readonly />
<el-button @click="copySiteUrl(record.siteUrl)">
<el-icon><CopyDocument /></el-icon>复制本条
</el-button>
</div>
</template>
</template>
</section>
<section v-else class="pl-plan-form-section">
@@ -73,7 +44,7 @@
:title="rearrangeBlockedMessage"
class="pl-arrange-block-alert"
/>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<h4 class="pl-plan-subtitle">车辆信息</h4>
<div class="form-grid-4">
<el-form-item label="选择车辆" prop="vehicleId">
@@ -130,8 +101,8 @@
<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="取货地址">
<div class="pl-plan-logistics-fields">
<el-form-item label="取货地址" class="pl-logistics-address-item">
<PlAddressWithLibrary
v-model="formData.pickupAddress"
v-model:lng="formData.pickupLng"
@@ -139,18 +110,20 @@
placeholder="取货地址"
/>
</el-form-item>
<el-form-item label="取货时间">
<el-date-picker
v-model="formData.pickupTime"
type="date"
placeholder="选择取货时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="注意事项">
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
</el-form-item>
<div class="form-grid-2 pl-logistics-meta-row">
<el-form-item label="取货时间">
<el-date-picker
v-model="formData.pickupTime"
type="date"
placeholder="选择取货时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="注意事项">
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
</el-form-item>
</div>
</div>
</div>
@@ -177,8 +150,8 @@
<div class="pl-plan-logistics-block">
<h4 class="pl-plan-subtitle">送货信息</h4>
<div class="form-grid-3">
<el-form-item label="送货地址">
<div class="pl-plan-logistics-fields">
<el-form-item label="送货地址" class="pl-logistics-address-item">
<PlAddressWithLibrary
v-model="formData.destinationAddress"
v-model:lng="formData.destinationLng"
@@ -186,71 +159,27 @@
placeholder="送货地址"
/>
</el-form-item>
<el-form-item label="送达时间">
<el-date-picker
v-model="formData.deliveryTime"
type="date"
placeholder="选择送达时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="注意事项">
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
</el-form-item>
<div class="form-grid-2 pl-logistics-meta-row">
<el-form-item label="送达时间">
<el-date-picker
v-model="formData.deliveryTime"
type="date"
placeholder="选择送达时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="注意事项">
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
</el-form-item>
</div>
</div>
</div>
</div>
<h4 class="pl-plan-subtitle">运费信息</h4>
<div v-if="formData.hasFreight !== 1" class="pl-arrange-freight-muted">本计划无运输费用</div>
<div v-else class="form-grid-4">
<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-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>
</div>
<h4 class="pl-plan-subtitle">安排项</h4>
<div class="form-grid-4">
<el-form-item label="运输介质">
<el-form-item label="运输介质" class="pl-arrange-medium-item">
<el-input :model-value="transportMediumText" readonly placeholder="—" />
</el-form-item>
<el-form-item label="备注" class="pl-arrange-remark-item">
@@ -287,9 +216,6 @@
勾选后将按产品进行地磅单生成(当月已有则复用),并在地磅出入库管理中产生对应记录。
</template>
</p>
<p v-if="planSupportsSiteInventory" class="pl-arrange-dibang-hint">
入库/出库计划安排运输时将自动生成工厂现场链接(与是否勾选地磅单无关),不会在地磅出入库管理中产生记录。
</p>
</div>
</el-form>
</section>
@@ -298,7 +224,15 @@
<section class="pl-arrange-view-panel">
<h3 class="pl-arrange-view-title">计划单查看</h3>
<PlPlanViewTabs v-if="planId" ref="tabsRef" :plan-id="planId" embedded />
<PlPlanViewTabs
v-if="planId"
ref="tabsRef"
:plan-id="planId"
embedded
:show-arrange-links="arrangeSuccess"
:arrange-driver-url="driverUrl || arrangeResult?.driverUrl || ''"
:arrange-site-records="siteRecords"
/>
</section>
</div>
@@ -345,12 +279,6 @@
>
<el-icon><CopyDocument /></el-icon>复制给现场
</el-button>
<el-button
v-if="hasSiteInventoryRecords && siteRecords.length > 1"
@click="copyAllSiteUrls()"
>
<el-icon><CopyDocument /></el-icon>复制全部现场链接
</el-button>
<el-button
v-if="hasDibangInventoryRecords"
type="primary"
@@ -393,9 +321,9 @@ import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-stri
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
import { buildMissingHazardCertMessage } from '@/composables/usePlToxicProductCertCheck'
import { buildMissingHazardCertReadyMessage } from '@/composables/usePlToxicProductCertCheck'
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
import { useDictStore } from '@/store/modules/dict'
import { isTonProductUnit } from '@/utils/crm-product-display'
@@ -438,8 +366,6 @@ const {
resetArrangeActions,
applyArrangeResult,
copyDriverUrl,
copySiteUrl,
copyAllSiteUrls,
copySiteSummary,
copyPlanSummary,
copyPoundSlipQrcode
@@ -492,8 +418,6 @@ const dialogTitle = computed(() => {
const isRearrange = computed(() => plan.value.transportStatus === 'arranged')
const freightUnitLabel = computed(() => resolveFreightUnitLabel(productRows.value))
function getUnitLabel(unit: string) {
return dictStore.getLabel('crm_product_unit', unit) || unit
}
@@ -811,9 +735,9 @@ async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
const certMessage = buildMissingHazardCertMessage(products.value)
const certMessage = buildMissingHazardCertReadyMessage(plan.value, products.value)
if (certMessage) {
await ElMessageBox.alert(`${certMessage}。请先上传购买证和合法使用证明。`, '无法安排运输', {
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法安排运输', {
type: 'warning'
})
return
@@ -884,7 +808,8 @@ async function handleSubmit() {
applyArrangeResult(result, plan.value, products.value)
arrangeSuccess.value = true
ElMessage.success(isRearrange.value ? '运输调整成功' : '运输安排成功')
tabsRef.value?.reload()
await tabsRef.value?.reload()
tabsRef.value?.setActiveTab('transport')
emit('success')
} catch (e: unknown) {
if (e === 'cancel') return
@@ -899,20 +824,18 @@ async function handleSubmit() {
<style scoped lang="scss">
.pl-arrange-layout {
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
min-height: 480px;
min-height: 0;
overflow: hidden;
}
.pl-arrange-form-panel {
flex: 0 0 auto;
max-height: 52vh;
overflow: hidden;
}
.pl-arrange-form-scroll {
max-height: 52vh;
overflow-y: auto;
padding-right: 4px;
}
@@ -931,10 +854,18 @@ async function handleSubmit() {
margin-top: 0;
}
.pl-arrange-freight-muted {
margin-bottom: 12px;
font-size: 13px;
color: #64748b;
.pl-plan-logistics-fields {
display: flex;
flex-direction: column;
gap: 8px;
}
.pl-logistics-address-item {
margin-bottom: 0;
}
.pl-logistics-meta-row {
gap: 8px 12px;
}
.pl-plan-logistics-row {
@@ -1004,10 +935,6 @@ async function handleSubmit() {
:deep(.el-form-item) {
margin-bottom: 0;
}
.form-grid-3 {
gap: 8px 12px;
}
}
@media (max-width: 1100px) {
@@ -1029,13 +956,40 @@ async function handleSubmit() {
.pl-arrange-view-panel {
flex: 1 1 auto;
min-height: 280px;
min-height: 0;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
padding: 12px 16px 16px;
overflow: hidden;
:deep(.pl-plan-view-tabs) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.pl-plan-view-tabs__nav) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.pl-plan-view-tabs__nav > .el-tabs__header) {
flex-shrink: 0;
}
:deep(.pl-plan-view-tabs__nav > .el-tabs__content) {
flex: 1;
min-height: 0;
max-height: none;
overflow-y: auto;
padding-right: 4px;
}
}
.pl-arrange-view-title {
@@ -1090,6 +1044,7 @@ async function handleSubmit() {
min-width: 0;
}
.pl-arrange-medium-item,
.pl-arrange-remark-item {
grid-column: span 2;
}
@@ -1100,18 +1055,6 @@ async function handleSubmit() {
gap: 12px;
}
.pl-arrange-link-row {
display: grid;
grid-template-columns: 72px 1fr auto;
gap: 8px;
align-items: center;
}
.pl-arrange-link-label {
font-size: 13px;
color: #64748b;
}
.pl-arrange-footer {
display: flex;
flex-wrap: wrap;
@@ -1125,4 +1068,11 @@ async function handleSubmit() {
width: min(1840px, calc(100vw - 48px)) !important;
max-height: calc(100vh - 48px);
}
.el-dialog.pl-plan-arrange-dialog.dialog-form-layout .el-dialog__body {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
</style>
@@ -19,7 +19,7 @@
type="info"
:closable="false"
show-icon
title="易制毒或易制爆产品须上传购买证、合法使用证明;安排运输时将校验是否齐全。"
title="易制毒或易制爆产品请按实际业务上传所需证书;可先点击「证书齐备」放行运输,之后再补充附件。"
class="cert-tip"
/>
@@ -32,7 +32,7 @@
<el-tag v-if="item.isToxic" type="warning" size="small">易制毒</el-tag>
</div>
<div class="form-grid-2 product-cert-grid">
<el-form-item label="购买证" required>
<el-form-item label="购买证">
<AttachmentUploadField
v-model="item.purchaseCertUrl"
folder="pl/plan-cert"
@@ -40,7 +40,7 @@
hint="支持图片、PDF 等,上传购买证"
/>
</el-form-item>
<el-form-item label="合法使用证明" required>
<el-form-item label="合法使用证明">
<AttachmentUploadField
v-model="item.transportCertUrl"
folder="pl/plan-cert"
@@ -67,7 +67,7 @@
import { Check, Close } from '@element-plus/icons-vue'
import { planApi, type Plan, type PlanProduct } from '@/api/pl'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
import { enrichPlanProductsHazardFromCrm, isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
type HazardProductRow = PlanProduct & {
purchaseCertUrl?: string
@@ -114,7 +114,8 @@ async function loadPlan(planId: string) {
const data = (res.data ?? res) as Plan
planBrief.planNo = data.planNo || ''
planBrief.customerName = data.customerName || ''
hazardProducts.value = (data.products || [])
const enrichedProducts = await enrichPlanProductsHazardFromCrm(data.products || [])
hazardProducts.value = enrichedProducts
.filter(p => isHazardProduct(p))
.map(p => ({
...p,
@@ -129,14 +130,6 @@ async function loadPlan(planId: string) {
async function handleSubmit() {
if (!props.planId || !hazardProducts.value.length) return
const incomplete = hazardProducts.value.filter(
p => !p.purchaseCertUrl?.trim() || !p.transportCertUrl?.trim()
)
if (incomplete.length) {
ElMessage.warning('请为每个危化品产品上传购买证和合法使用证明')
return
}
submitLoading.value = true
try {
await planApi.updateProductCerts(
@@ -62,6 +62,25 @@
</template>
</el-table-column>
<el-table-column v-else prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column
v-if="showSelect"
label="操作"
min-width="100"
align="center"
fixed="right"
class-name="col-actions"
>
<template #default="{ row, $index }">
<el-button
link
type="primary"
:disabled="isProductSelected(row)"
@click="emit('select', row, $index)"
>
<el-icon><Plus /></el-icon>选择
</el-button>
</template>
</el-table-column>
<el-table-column v-if="showDelete" label="操作" min-width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" @click="emit('remove', $index)">
@@ -75,15 +94,18 @@
</template>
<script setup lang="ts">
import { Delete } from '@element-plus/icons-vue'
import { Delete, Plus } from '@element-plus/icons-vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
import DictTag from '@/components/DictTag/index.vue'
defineProps<{
const props = defineProps<{
products: PlanFormProductRow[]
showDelete?: boolean
showSelect?: boolean
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
selectedProductIds?: string[]
editableRemark?: boolean
editableQty?: boolean
/** 新增/编辑弹窗默认不展示实际数量 */
@@ -94,15 +116,20 @@ defineProps<{
const emit = defineEmits<{
remove: [index: number]
select: [row: PlanFormProductRow, index: number]
}>()
const selectedProductIdSet = computed(
() => new Set((props.selectedProductIds || []).map(id => String(id)))
)
function isProductSelected(row: PlanFormProductRow) {
return selectedProductIdSet.value.has(String(row.productId))
}
</script>
<style scoped lang="scss">
.pl-plan-product-table-wrap {
margin-top: 8px;
}
.dialog-list-body-only {
margin-top: 0;
}
</style>
@@ -17,7 +17,7 @@
<div class="pl-plan-form-body">
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">基础信息</h3>
<el-form ref="basicFormRef" :model="formData" :rules="basicRules" label-position="top">
<el-form ref="basicFormRef" :model="formData" :rules="basicRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-4">
<el-form-item label="计划日期" prop="planDate">
<el-date-picker
@@ -74,13 +74,19 @@
</div>
<h4 class="pl-plan-subtitle">合同产品预览</h4>
<PlPlanFormProductTable :products="contractProductRows" />
<p class="pl-plan-form-empty-hint">点击「选择」将合同产品加入下方产品信息;已添加的产品不可重复选择</p>
<PlPlanFormProductTable
:products="contractProductRows"
show-select
:selected-product-ids="selectedPlanProductIds"
@select="handleSelectContractProduct"
/>
</template>
</section>
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">运输信息</h3>
<el-form :model="formData" label-position="top">
<el-form :model="formData" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-4">
<el-form-item label="车牌号">
<el-select
@@ -194,43 +200,45 @@
</div>
</div>
<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%" />
<div class="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>
<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-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>
<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-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>
</div>
</el-form>
</section>
@@ -467,6 +475,10 @@ const contractProductRows = computed<PlanFormProductRow[]>(() =>
mapContractProducts(contractSnapshot.value?.products)
)
const selectedPlanProductIds = computed(() =>
formData.products.map(item => String(item.productId))
)
function enrichProductsFromContract(products: PlanFormProductRow[], contract?: ContractVO | null) {
if (!contract?.products?.length) return products
const contractMap = new Map(
@@ -845,12 +857,24 @@ async function handleContractConfirm(contract: ContractVO) {
contractSnapshot.value = contract
Object.assign(formData, {
...mapped,
products: [],
followUserId: resolveCurrentUserId(),
followUserName: resolveCurrentUserName()
})
formData.products = await enrichPlanProductsHazardFromCrm(
enrichProductsFromContract(formData.products, contract)
freightCalc.resetFreightCalc()
}
async function handleSelectContractProduct(row: PlanFormProductRow) {
const productId = String(row.productId)
if (formData.products.some(item => String(item.productId) === productId)) {
ElMessage.warning('该产品已添加到运输计划中')
return
}
const [enriched] = await enrichPlanProductsHazardFromCrm(
enrichProductsFromContract([{ ...row }], contractSnapshot.value)
)
formData.products.push(enriched)
freightCalc.resetFreightCalc()
}
@@ -994,11 +1018,22 @@ async function handleSubmit() {
color: #1e293b;
}
.pl-plan-freight-section {
.pl-plan-subtitle {
margin-top: 16px;
}
:deep(.el-form-item__label) {
width: 140px !important;
max-width: 140px;
}
}
.pl-plan-form-empty-hint,
.text-muted {
margin: 0;
margin: 0 0 8px;
font-size: 13px;
color: #1e293b;
color: #64748b;
}
.pl-plan-contract-kv-rows {
@@ -1166,7 +1201,7 @@ async function handleSubmit() {
<style lang="scss">
.el-dialog.pl-plan-form-dialog.dialog-form-layout {
width: min(1680px, calc(100vw - 80px)) !important;
max-height: calc(100vh - 48px);
max-height: var(--dialog-max-height, calc(100vh - 40px));
}
.el-dialog.pl-plan-form-dialog.dialog-form-layout .el-dialog__body {
@@ -0,0 +1,88 @@
<template>
<div class="pl-plan-hazard-cert-view-table">
<p v-if="!products.length" class="pl-plan-hazard-cert-view-table__empty">暂无易制毒/易制爆产品</p>
<div v-else class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
:data="products"
class="list-table"
stripe
empty-text="暂无易制毒/易制爆产品"
>
<el-table-column
type="index"
label="序号"
min-width="60"
align="center"
header-align="center"
:index="index => index + 1"
/>
<el-table-column label="产品名称" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<PlProductNameHazardCell :product="row" />
</template>
</el-table-column>
<el-table-column label="购买证" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<div v-if="row.purchaseCertUrl" class="pl-plan-hazard-cert-view-table__attachment">
<AttachmentUploadField :model-value="row.purchaseCertUrl" disabled compact />
</div>
<span v-else class="pl-plan-hazard-cert-view-table__missing">未上传</span>
</template>
</el-table-column>
<el-table-column label="合法使用证明" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<div v-if="row.transportCertUrl" class="pl-plan-hazard-cert-view-table__attachment">
<AttachmentUploadField :model-value="row.transportCertUrl" disabled compact />
</div>
<span v-else class="pl-plan-hazard-cert-view-table__missing">未上传</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PlanProduct } from '@/api/pl'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
withDefaults(
defineProps<{
products?: PlanProduct[]
}>(),
{
products: () => []
}
)
</script>
<style scoped lang="scss">
.pl-plan-hazard-cert-view-table__empty {
margin: 0;
font-size: 13px;
color: var(--mint-text-muted);
}
.pl-plan-hazard-cert-view-table__attachment {
display: inline-flex;
justify-content: center;
width: 100%;
:deep(.attachment-upload) {
width: auto;
}
:deep(.attachment-list) {
justify-content: center;
}
}
.pl-plan-hazard-cert-view-table__missing {
color: var(--mint-text-muted);
font-size: 13px;
}
</style>
@@ -1,5 +1,9 @@
<template>
<div v-if="hazardProducts.length" class="pl-plan-list-cert-cell">
<div
v-if="hazardProducts.length"
class="pl-plan-list-cert-cell"
:class="{ 'pl-plan-list-cert-cell--compact': compact }"
>
<div
v-for="(product, index) in hazardProducts"
:key="product.id || `${product.productNo}-${index}`"
@@ -7,10 +11,22 @@
>
<div class="pl-plan-list-cert-cell__product">
<span class="pl-plan-list-cert-cell__name">{{ product.productName || '—' }}</span>
<el-tag v-if="product.isToxic" type="warning" size="small">易制毒</el-tag>
<el-tag v-if="product.isExplosive" type="danger" size="small">易制爆</el-tag>
<el-tag v-if="product.isToxic" type="warning" size="small">
{{ useShortHazardTag ? '毒' : '易制毒' }}
</el-tag>
<el-tag v-if="product.isExplosive" type="danger" size="small">
{{ useShortHazardTag ? '爆' : '易制爆' }}
</el-tag>
</div>
<div class="pl-plan-list-cert-cell__certs">
<div v-if="compact" class="pl-plan-list-cert-cell__certs pl-plan-list-cert-cell__certs--text">
<span class="pl-plan-list-cert-cell__cert-text">
购买证({{ product.purchaseCertUrl ? '有' : '无' }})
</span>
<span class="pl-plan-list-cert-cell__cert-text">
合法使用证明({{ product.transportCertUrl ? '有' : '无' }})
</span>
</div>
<div v-else class="pl-plan-list-cert-cell__certs">
<div class="pl-plan-list-cert-cell__cert-item">
<AttachmentUploadField
v-if="product.purchaseCertUrl"
@@ -44,12 +60,20 @@ import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
const props = withDefaults(
defineProps<{
products?: PlanProduct[]
/** 列表紧凑模式:文字展示证书有无,不显示缩略图 */
compact?: boolean
/** 危化标签短文案:毒 / 爆 */
shortHazardTag?: boolean
}>(),
{
products: () => []
products: () => [],
compact: false,
shortHazardTag: false
}
)
const useShortHazardTag = computed(() => props.compact || props.shortHazardTag)
const hazardProducts = computed(() => (props.products || []).filter(product => isHazardProduct(product)))
</script>
@@ -137,4 +161,29 @@ const hazardProducts = computed(() => (props.products || []).filter(product => i
:deep(.attachment-footer) {
display: none;
}
.pl-plan-list-cert-cell--compact {
gap: 6px;
.pl-plan-list-cert-cell__group {
gap: 4px;
}
.pl-plan-list-cert-cell__name {
font-size: 12px;
}
.pl-plan-list-cert-cell__certs--text {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.pl-plan-list-cert-cell__cert-text {
color: var(--mint-text);
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
}
}
</style>
@@ -5,20 +5,40 @@
:key="product.id || `${product.productNo}-${index}`"
class="pl-plan-list-product-cell__row"
>
<span class="pl-plan-list-product-cell__line">
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
<el-tag v-if="getHazardFlags(product).isToxic" type="warning" size="small" class="pl-plan-list-product-cell__mark">
毒
</el-tag>
<el-tag
v-if="getHazardFlags(product).isExplosive"
type="danger"
size="small"
class="pl-plan-list-product-cell__mark"
<span class="pl-plan-list-product-cell__index">{{ index + 1 }}.</span>
<div class="pl-plan-list-product-cell__content">
<span class="pl-plan-list-product-cell__line">
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
<el-tag v-if="getHazardFlags(product).isToxic" type="warning" size="small" class="pl-plan-list-product-cell__mark">
毒
</el-tag>
<el-tag
v-if="getHazardFlags(product).isExplosive"
type="danger"
size="small"
class="pl-plan-list-product-cell__mark"
>
爆
</el-tag>
</span>
<div
v-if="showCertStatus && isHazardProduct(product)"
class="pl-plan-list-product-cell__certs"
>
爆
</el-tag>
</span>
<span
class="pl-plan-list-product-cell__cert-text"
:class="{ 'pl-plan-list-product-cell__cert-text--missing': !product.purchaseCertUrl }"
>
购买证({{ product.purchaseCertUrl ? '有' : '无' }})
</span>
<span
class="pl-plan-list-product-cell__cert-text"
:class="{ 'pl-plan-list-product-cell__cert-text--missing': !product.transportCertUrl }"
>
合法使用证明({{ product.transportCertUrl ? '有' : '无' }})
</span>
</div>
</div>
</div>
</div>
<span v-else class="pl-plan-list-product-cell__empty">—</span>
@@ -26,16 +46,19 @@
<script setup lang="ts">
import type { PlanProduct } from '@/api/pl'
import { resolveProductHazardFlags } from '@/composables/usePlToxicProductCertCheck'
import { isHazardProduct, resolveProductHazardFlags } from '@/composables/usePlToxicProductCertCheck'
import { useDictStore } from '@/store/modules/dict'
import { formatPlanProductListText } from '@/utils/pl-display'
withDefaults(
defineProps<{
products?: PlanProduct[]
/** 危化品行下方展示购买证/合法使用证明有无 */
showCertStatus?: boolean
}>(),
{
products: () => []
products: () => [],
showCertStatus: false
}
)
@@ -58,22 +81,47 @@ function getHazardFlags(product: PlanProduct) {
.pl-plan-list-product-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
width: 100%;
text-align: left;
}
.pl-plan-list-product-cell__row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 4px;
width: 100%;
min-width: 0;
}
.pl-plan-list-product-cell__index {
flex-shrink: 0;
min-width: 1.25em;
font-size: 13px;
line-height: 1.5;
color: var(--mint-text);
text-align: right;
}
.pl-plan-list-product-cell__content {
flex: 1;
min-width: 0;
}
.pl-plan-list-product-cell__line {
display: inline-flex;
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 4px;
width: 100%;
max-width: 100%;
font-size: 13px;
line-height: 1.5;
color: var(--text, #1e293b);
text-align: left;
}
.pl-plan-list-product-cell__text {
@@ -85,6 +133,30 @@ function getHazardFlags(product: PlanProduct) {
font-weight: 600;
}
.pl-plan-list-product-cell__certs {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 0;
width: 100%;
margin-top: 2px;
text-align: left;
}
.pl-plan-list-product-cell__cert-text {
color: var(--mint-text);
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
&--missing {
color: var(--danger, #dc2626);
font-weight: 500;
}
}
.pl-plan-list-product-cell__empty {
color: var(--mint-text);
}
@@ -13,50 +13,32 @@
@closed="handleClosed"
@opened="handleOpened"
>
<section v-if="showArrangeActions" class="pl-plan-arrange-summary">
<h3 class="pl-plan-arrange-summary-title">运输安排结果</h3>
<div class="pl-arrange-link-row">
<span class="pl-arrange-link-label">司机链接</span>
<el-input :model-value="driverUrl" readonly />
<el-button @click="copyDriverUrl()">
<el-icon><CopyDocument /></el-icon>复制链接
<div
v-if="$slots['hero-actions'] || showArrangeActions"
class="pl-plan-view-hero-actions"
>
<template v-if="showArrangeActions">
<el-button @click="copyPlanSummary()">
<el-icon><CopyDocument /></el-icon>复制给司机
</el-button>
<el-button v-if="hasSiteInventoryRecords" @click="copySiteSummary()">
<el-icon><CopyDocument /></el-icon>复制给现场
</el-button>
<el-button v-if="hasDibangInventoryRecords" @click="copyPoundSlipQrcode()">
<el-icon><CopyDocument /></el-icon>复制磅单二维码
</el-button>
</div>
<p v-if="!hasSiteInventoryRecords && planSupportsInventory" class="pl-plan-arrange-summary-hint">
暂无现场链接
</p>
<template v-if="hasSiteInventoryRecords">
<h4 class="pl-plan-arrange-summary-subtitle">现场链接</h4>
<div v-if="siteRecords.length === 1" class="pl-arrange-link-row">
<span class="pl-arrange-link-label">现场链接</span>
<el-input :model-value="siteRecords[0].siteUrl || ''" readonly />
<el-button @click="copySiteUrl(siteRecords[0].siteUrl)">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
<template v-else>
<div
v-for="record in siteRecords"
:key="record.siteLinkId"
class="pl-arrange-link-row"
>
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
<el-input :model-value="record.siteUrl || ''" readonly />
<el-button @click="copySiteUrl(record.siteUrl)">
<el-icon><CopyDocument /></el-icon>复制本条
</el-button>
</div>
</template>
</template>
</section>
<div v-if="$slots['hero-actions']" class="pl-plan-view-hero-actions">
<slot name="hero-actions" />
</div>
<PlPlanViewTabs v-if="planId" ref="tabsRef" :plan-id="planId" />
<PlPlanViewTabs
v-if="planId"
ref="tabsRef"
:plan-id="planId"
:show-arrange-links="showArrangeActions"
:arrange-driver-url="driverUrl"
:arrange-site-records="siteRecords"
/>
<DbPoundSlipQrcodeDialog
v-model:visible="qrcodeVisible"
@@ -65,39 +47,11 @@
ref="qrcodeDialogRef"
/>
<template #footer>
<div class="dialog-footer pl-plan-view-footer">
<template v-if="showArrangeActions">
<el-button @click="copyPlanSummary()">
<el-icon><CopyDocument /></el-icon>复制给司机
</el-button>
<el-button v-if="hasSiteInventoryRecords" @click="copySiteSummary()">
<el-icon><CopyDocument /></el-icon>复制给现场
</el-button>
<el-button
v-if="hasSiteInventoryRecords && siteRecords.length > 1"
@click="copyAllSiteUrls()"
>
<el-icon><CopyDocument /></el-icon>复制全部现场链接
</el-button>
<el-button
v-if="hasDibangInventoryRecords"
type="primary"
@click="copyPoundSlipQrcode()"
>
<el-icon><CopyDocument /></el-icon>复制磅单二维码
</el-button>
</template>
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { Close, CopyDocument } from '@element-plus/icons-vue'
import { CopyDocument } from '@element-plus/icons-vue'
import { planApi } from '@/api/pl'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
@@ -126,27 +80,17 @@ const {
hasDibangInventoryRecords,
driverUrl,
siteRecords,
inventoryRecords,
plan,
qrcodeVisible,
selectedInventoryId,
selectedInventoryUnit,
qrcodeDialogRef,
resetArrangeActions,
loadArrangeActions,
copyDriverUrl,
copySiteUrl,
copyAllSiteUrls,
copySiteSummary,
copyPlanSummary,
copyPoundSlipQrcode
} = usePlPlanArrangeActions()
const planSupportsInventory = computed(() => {
const planType = plan.value?.planType
return planType === 'inbound' || planType === 'outbound'
})
const dialogTitle = computed(() =>
planNo.value ? `查看运输计划 · ${planNo.value}` : '查看运输计划'
)
@@ -168,6 +112,21 @@ function handleClosed() {
planNo.value = ''
resetArrangeActions()
}
async function reload() {
if (!props.visible || !props.planId) return
try {
const res = await planApi.get(props.planId)
const data = res.data ?? res
planNo.value = data?.planNo || ''
await loadArrangeActions(props.planId)
await tabsRef.value?.reload()
} catch {
/* 查看弹窗仍打开时静默失败,避免打断用户 */
}
}
defineExpose({ reload })
</script>
<style scoped lang="scss">
@@ -186,52 +145,42 @@ function handleClosed() {
gap: 8px;
margin-bottom: 12px;
}
</style>
.pl-plan-arrange-summary {
margin-bottom: 12px;
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #f8fafc;
<style lang="scss">
.el-dialog.pl-plan-view-dialog.dialog-form-layout {
width: min(1680px, calc(100vw - 80px)) !important;
height: var(--dialog-max-height, calc(100vh - 40px));
max-height: var(--dialog-max-height, calc(100vh - 40px));
}
.pl-plan-arrange-summary-title {
margin: 0 0 10px;
padding-left: 10px;
font-size: 14px;
font-weight: 600;
color: #1e293b;
border-left: 3px solid #1ca851;
line-height: 1;
.el-dialog.pl-plan-view-dialog.dialog-form-layout .el-dialog__body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.pl-plan-arrange-summary-subtitle {
margin: 12px 0 10px;
font-size: 13px;
font-weight: 600;
color: #475569;
.el-dialog.pl-plan-view-dialog.dialog-form-layout .pl-plan-view-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.pl-arrange-link-row {
display: grid;
grid-template-columns: 72px 1fr auto;
gap: 8px;
align-items: center;
& + & {
margin-top: 8px;
}
.el-dialog.pl-plan-view-dialog.dialog-form-layout .pl-plan-view-tabs__nav.el-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.pl-arrange-link-label {
font-size: 13px;
color: #64748b;
}
.pl-plan-arrange-summary-hint {
margin: 10px 0 0;
font-size: 12px;
color: #64748b;
line-height: 1.5;
.el-dialog.pl-plan-view-dialog.dialog-form-layout
.pl-plan-view-tabs__nav.el-tabs
> .el-tabs__content {
flex: 1;
min-height: 0;
max-height: none;
overflow-y: auto;
}
</style>
@@ -5,7 +5,7 @@
<el-tab-pane label="计划概况" name="overview">
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">基础信息</h3>
<ViewKvTable :fields="overviewBasicViewFields" mode="quad" />
<ViewKvTable :fields="overviewBasicViewFields" mode="quint" />
</section>
<section class="pl-plan-form-section">
@@ -16,40 +16,7 @@
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">危化品证书信息</h3>
<p v-if="!hazardProducts.length && !loading" class="pl-plan-form-empty-hint"
>暂无易制毒/易制爆产品</p
>
<div v-for="item in hazardProducts" :key="item.id" class="pl-plan-cert-block">
<div class="pl-plan-cert-head">
<span class="pl-plan-cert-name">{{ item.productName }}</span>
<el-tag v-if="item.isToxic" type="warning" size="small">易制毒</el-tag>
<el-tag v-if="item.isExplosive" type="danger" size="small">易制爆</el-tag>
</div>
<dl class="pl-plan-kv-grid pl-plan-kv-grid-2 pl-plan-cert-grid">
<div class="pl-plan-kv">
<dt>购买证</dt>
<dd>
<AttachmentUploadField
v-if="item.purchaseCertUrl"
:model-value="item.purchaseCertUrl"
disabled
/>
<span v-else>未上传</span>
</dd>
</div>
<div class="pl-plan-kv">
<dt>合法使用证明</dt>
<dd>
<AttachmentUploadField
v-if="item.transportCertUrl"
:model-value="item.transportCertUrl"
disabled
/>
<span v-else>未上传</span>
</dd>
</div>
</dl>
</div>
<PlPlanHazardCertViewTable :products="hazardProducts" />
</section>
<section class="pl-plan-form-section">
@@ -59,6 +26,45 @@
</el-tab-pane>
<el-tab-pane label="运输信息" name="transport">
<section v-if="showArrangeLinks" class="pl-plan-form-section pl-plan-arrange-links">
<h3 class="pl-plan-form-section-title">运输链接</h3>
<div class="pl-arrange-link-row">
<span class="pl-arrange-link-label">司机链接</span>
<el-input :model-value="arrangeDriverUrl" readonly />
<el-button @click="copyArrangeDriverUrl">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
<template v-if="hasArrangeSiteLinks">
<div v-if="arrangeSiteRecords.length === 1" class="pl-arrange-link-row">
<span class="pl-arrange-link-label">现场链接</span>
<el-input :model-value="arrangeSiteRecords[0].siteUrl || ''" readonly />
<el-button @click="copyArrangeSiteUrl(arrangeSiteRecords[0].siteUrl)">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
<template v-else>
<div
v-for="record in arrangeSiteRecords"
:key="record.siteLinkId"
class="pl-arrange-link-row"
>
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
<el-input :model-value="record.siteUrl || ''" readonly />
<el-button @click="copyArrangeSiteUrl(record.siteUrl)">
<el-icon><CopyDocument /></el-icon>复制链接
</el-button>
</div>
</template>
</template>
<p
v-else-if="showArrangeLinks && planSupportsInventory"
class="pl-plan-form-empty-hint pl-plan-arrange-links-hint"
>
暂无现场链接
</p>
</section>
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">车辆与承运</h3>
<PlVehicleSummaryStrip
@@ -110,13 +116,47 @@
<div class="pl-plan-feedback-region">
<h4 class="pl-plan-feedback-region__title">司机回传信息</h4>
<p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
<template v-else>
<ViewKvTable :fields="driverFeedbackViewFields" mode="quad" />
<div v-if="feedbackImageCsv" class="pl-plan-feedback-attachments">
<span class="pl-plan-feedback-attachments__label">回传附件</span>
<AttachmentUploadField :model-value="feedbackImageCsv" disabled />
</div>
</template>
<div v-else class="view-subtable-wrap">
<el-table :data="driverFeedbackTableRows" class="view-subtable list-table" stripe>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="feedbackTime"
label="回传时间"
min-width="160"
align="center"
/>
<el-table-column
prop="feedbackQty"
label="回传数量"
min-width="100"
align="center"
>
<template #default="{ row }">
{{ row.feedbackQty ?? '—' }}
</template>
</el-table-column>
<el-table-column
prop="feedbackRemark"
label="回传备注"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="回传附件" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<div v-if="row.feedbackImages" class="pl-plan-feedback-table__attachment">
<AttachmentUploadField :model-value="row.feedbackImages" disabled compact />
</div>
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="pl-plan-feedback-region">
@@ -124,38 +164,69 @@
<p v-if="!siteInventoryRows.length && !loading" class="pl-plan-form-empty-hint">
暂无现场链接
</p>
<template v-else>
<div
v-for="(row, idx) in siteInventoryRows"
:key="`site-feedback-${row.link.siteLinkId || idx}`"
class="pl-plan-site-feedback-block"
>
<div class="pl-plan-site-feedback-head">
<span class="pl-plan-dibang-product">{{ row.link.productName || '—' }}</span>
<div class="pl-plan-dibang-head-actions">
<el-button class="btn-op-item" @click="copySiteSummaryForRow(row)">
<div v-else class="view-subtable-wrap">
<el-table :data="siteFeedbackTableRows" class="view-subtable list-table" stripe>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="productName"
label="产品"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
prop="siteFeedbackTime"
label="保存时间"
min-width="160"
align="center"
/>
<el-table-column label="业务类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="row.businessType && row.businessType !== '—'"
:type="businessTypeTagType(row.businessType)"
size="small"
>
{{ row.businessType }}
</el-tag>
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
</template>
</el-table-column>
<el-table-column
prop="siteFeedbackRemark"
label="现场备注"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="现场附件" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<div v-if="row.siteFeedbackImages" class="pl-plan-feedback-table__attachment">
<AttachmentUploadField :model-value="row.siteFeedbackImages" disabled compact />
</div>
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="120"
align="center"
fixed="right"
class-name="col-actions"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="copySiteSummaryForRow(row.sourceRow)">
<el-icon><CopyDocument /></el-icon>
复制给现场
</el-button>
<el-button class="btn-op-item" @click="copySiteLinkForRow(row)">
<el-icon><CopyDocument /></el-icon>
复制现场链接
</el-button>
</div>
</div>
<template v-if="hasSiteFeedback(row)">
<ViewKvTable :fields="siteFeedbackViewFields(row)" mode="quad" />
<div
v-if="siteFeedbackImageCsv(row)"
class="pl-plan-feedback-attachments pl-plan-site-feedback-attachments"
>
<span class="pl-plan-feedback-attachments__label">现场附件</span>
<AttachmentUploadField :model-value="siteFeedbackImageCsv(row)" disabled />
</div>
</template>
<p v-else class="pl-plan-form-empty-hint pl-plan-site-feedback-empty">暂无现场回传</p>
</div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</div>
<h4 class="pl-plan-subtitle">地磅单信息</h4>
@@ -355,6 +426,7 @@
import { CopyDocument, View } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue'
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
@@ -365,8 +437,10 @@
} from '@/components/business/db/db-related-view'
import type { ContractVO } from '@/api/crm/contract/types'
import { inventoryApi } from '@/api/db/inventory'
import type { TransportSiteLink } from '@/api/pl'
import { mapContractProducts, type PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportDibangRow, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
import { useDictStore } from '@/store/modules/dict'
import { formatDate, formatDateTime } from '@/utils/format'
@@ -396,8 +470,16 @@
defineProps<{
planId: string
embedded?: boolean
showArrangeLinks?: boolean
arrangeDriverUrl?: string
arrangeSiteRecords?: TransportSiteLink[]
}>(),
{ embedded: false }
{
embedded: false,
showArrangeLinks: false,
arrangeDriverUrl: '',
arrangeSiteRecords: () => []
}
)
const activeTab = ref('overview')
@@ -436,22 +518,28 @@
return Array.isArray(imgs) ? imgs.filter(Boolean).join(',') : ''
}
function hasSiteFeedback(row: PlPlanTransportSiteRow) {
return Boolean(
row.link.siteFeedbackRemark ||
siteFeedbackImageCsv(row) ||
row.link.siteFeedbackTime
)
}
function siteFeedbackViewFields(row: PlPlanTransportSiteRow): ViewField[] {
const driverFeedbackTableRows = computed(() => {
if (!hasDriverFeedback.value) return []
return [
{ label: '保存时间', value: formatDateTime(row.link.siteFeedbackTime) || '—' },
{ label: '业务类型', value: row.link.businessType || '—' },
{ label: '现场备注', value: row.link.siteFeedbackRemark, span: 'full' }
{
feedbackTime: displayFeedbackTime.value,
feedbackQty: transport.value?.feedbackQty,
feedbackRemark: transport.value?.feedbackRemark || '—',
feedbackImages: feedbackImageCsv.value
}
]
}
})
const siteFeedbackTableRows = computed(() =>
siteInventoryRows.value.map((row) => ({
sourceRow: row,
productName: row.link.productName || '—',
siteFeedbackTime: formatDateTime(row.link.siteFeedbackTime) || '—',
businessType: row.link.businessType || '—',
siteFeedbackRemark: row.link.siteFeedbackRemark || '—',
siteFeedbackImages: siteFeedbackImageCsv(row)
}))
)
function dibangBasicViewFields(row: PlPlanTransportDibangRow): ViewField[] {
const businessType = row.inventory?.businessType
@@ -478,13 +566,32 @@
const siteInventoryRows = computed(() => siteRows.value)
async function copySiteLinkForRow(row: PlPlanTransportSiteRow) {
const url = row.link.siteUrl?.trim()
const hasArrangeSiteLinks = computed(
() => props.showArrangeLinks && (props.arrangeSiteRecords?.length ?? 0) > 0
)
const planSupportsInventory = computed(() => {
const planType = plan.value?.planType
return planType === 'inbound' || planType === 'outbound'
})
async function copyArrangeDriverUrl() {
const url = props.arrangeDriverUrl?.trim()
if (!url) {
ElMessage.warning('暂无现场链接')
ElMessage.warning('暂无司机链接')
return
}
await navigator.clipboard.writeText(url)
ElMessage.success('已复制司机链接')
}
async function copyArrangeSiteUrl(url?: string) {
const target = url?.trim()
if (!target) {
ElMessage.warning('暂无现场链接')
return
}
await navigator.clipboard.writeText(target)
ElMessage.success('已复制现场链接')
}
@@ -548,6 +655,18 @@
return amount ? formatMoneyDisplay(Number(amount)) : '—'
})
const freightUnitLabel = computed(() => {
const unitCode = resolveFreightUnitLabel(productRows.value)
return dictStore.getLabel('crm_product_unit', unitCode) || unitCode || '吨'
})
const displayHasInvoiceLabel = computed(() => {
if (transport.value?.hasInvoice != null) {
return getPlYesNoLabel(transport.value.hasInvoice)
}
return getPlHasInvoiceLabelFromSettlement(freightSettlement.value)
})
function mergeTaxRatesFromContract(
rows: PlanFormProductRow[],
contract: ContractVO | null
@@ -576,8 +695,8 @@
const pickupViewFields = computed<ViewField[]>(() => [
{ label: '取货地址', value: plan.value?.pickupAddress, span: 'full' },
{ label: '取货时间', value: formatDate(plan.value?.pickupTime) || '—' },
{ label: '注意事项', value: plan.value?.pickupNotes, span: 'full' }
{ label: '取货时间', value: formatDate(plan.value?.pickupTime) || '—', long: false },
{ label: '注意事项', value: plan.value?.pickupNotes, long: false, fullSpan: false }
])
const deliveryViewFields = computed<ViewField[]>(() => [
@@ -586,40 +705,38 @@
value: plan.value?.destinationAddress || plan.value?.destinationName,
span: 'full'
},
{ label: '送达时间', value: formatDate(plan.value?.deliveryTime) || '—' },
{ label: '注意事项', value: plan.value?.destinationNotes, span: 'full' }
{ label: '送达时间', value: formatDate(plan.value?.deliveryTime) || '—', long: false },
{ label: '注意事项', value: plan.value?.destinationNotes, long: false, fullSpan: false }
])
const freightViewFields = computed<ViewField[]>(() => {
const fields: ViewField[] = [
{ label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) }
const hasFreight = plan.value?.hasFreight === 1
const unit = freightUnitLabel.value
const isByWeight = hasFreight && displayFreightType.value === 'by_weight'
return [
{ label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) },
{ label: '运输公里数', value: hasFreight ? displayDistanceKm.value : '—' },
{
label: '运费计算方式',
value: hasFreight ? getPlFreightTypeLabel(displayFreightType.value) : '—'
},
{
label: `运输量(${unit})`,
value: isByWeight ? displayTransportQty.value : '—'
},
{
label: `运输单价(元/${unit})`,
value: isByWeight ? displayFreightPrice.value : '—',
money: isByWeight
},
{
label: '运费金额(元)',
value: hasFreight ? displayFreightAmount.value : '—',
money: hasFreight
},
{ label: '是否开票', value: hasFreight ? displayHasInvoiceLabel.value : '—' }
]
if (plan.value?.hasFreight !== 1) return fields
fields.push(
{ label: '运输公里数', value: displayDistanceKm.value },
{ label: '运费计算方式', value: getPlFreightTypeLabel(displayFreightType.value) }
)
if (displayFreightType.value === 'by_weight') {
fields.push(
{ label: '运输量(吨)', value: displayTransportQty.value },
{ label: '运输单价(元/吨)', value: displayFreightPrice.value, money: true }
)
}
fields.push(
{ label: '运费金额(元)', value: displayFreightAmount.value, money: true },
{ label: '是否开票', value: getPlHasInvoiceLabelFromSettlement(freightSettlement.value) }
)
return fields
})
const driverFeedbackViewFields = computed<ViewField[]>(() => {
const fields: ViewField[] = [{ label: '回传时间', value: displayFeedbackTime.value }]
if (transport.value?.feedbackQty != null) {
fields.push({ label: '回传数量', value: transport.value.feedbackQty })
}
fields.push({ label: '回传备注', value: transport.value?.feedbackRemark, span: 'full' })
return fields
})
const contractBasicViewFields = computed<ViewField[]>(() => {
@@ -712,7 +829,11 @@
if (props.planId) return loadDetail(props.planId)
}
defineExpose({ reload })
function setActiveTab(name: string) {
activeTab.value = name
}
defineExpose({ reload, setActiveTab })
</script>
<style scoped lang="scss">
@@ -785,6 +906,32 @@
color: var(--mint-text-muted);
}
.pl-plan-arrange-links {
display: flex;
flex-direction: column;
gap: 10px;
}
.pl-plan-arrange-links-hint {
margin-top: 2px;
}
.pl-arrange-link-row {
display: grid;
grid-template-columns: 72px 1fr auto;
gap: 8px;
align-items: center;
&--readonly {
grid-template-columns: 72px 1fr;
}
}
.pl-arrange-link-label {
font-size: 13px;
color: #64748b;
}
.pl-plan-view-remark {
margin: 0;
font-size: 13px;
@@ -931,43 +1078,52 @@
line-height: 1;
}
.pl-plan-feedback-attachments {
margin-top: 12px;
padding: 12px;
.pl-plan-feedback-region .view-subtable-wrap {
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
overflow: hidden;
background: #fff;
}
.pl-plan-feedback-attachments__label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: var(--mint-text-muted);
}
.pl-plan-feedback-region .view-subtable-wrap :deep(.el-table.view-subtable) {
--el-table-header-bg-color: #f8fafc;
--el-table-header-text-color: #1e293b;
--el-table-border-color: #e2e8f0;
width: 100%;
.pl-plan-cert-block {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
.el-table__header th.el-table__cell,
.el-table__body td.el-table__cell {
font-size: 14px;
padding: 8px 0;
}
& + & {
margin-top: 10px;
.el-table__header th.el-table__cell .cell {
font-weight: 500;
color: #1e293b;
}
.el-table__body td.el-table__cell .cell {
color: #1e293b;
}
}
.pl-plan-cert-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
.pl-plan-feedback-table__attachment {
display: inline-flex;
justify-content: center;
width: 100%;
:deep(.attachment-upload) {
width: auto;
}
:deep(.attachment-list) {
justify-content: center;
}
}
.pl-plan-cert-name {
.pl-plan-feedback-table__empty-cell {
color: var(--mint-text-muted);
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
.money-display {
@@ -986,38 +1142,6 @@
}
}
.pl-plan-site-feedback-block {
padding: 12px 14px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
& + & {
margin-top: 10px;
}
}
.pl-plan-site-feedback-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.pl-plan-site-feedback-empty {
margin: 0;
}
.pl-plan-site-feedback-batch {
font-size: 12px;
color: var(--mint-text-muted);
}
.pl-plan-site-feedback-attachments {
margin-top: 10px;
}
.pl-plan-dibang-head {
display: flex;
align-items: center;
@@ -111,18 +111,11 @@
</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 { 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'
@@ -1,6 +1,23 @@
<template>
<div class="vehicle-cert-part-tabs">
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-tabs__tabs">
<PlCertFormList
v-if="layout === 'table'"
ref="unifiedCertFormRef"
v-model="certList"
:title="title"
:upload-folder="uploadFolder"
:cert-name-options="certNameOptions"
layout="table"
show-vehicle-part-column
:license-plate="licensePlate"
:trailer-no="trailerNo"
/>
<el-tabs
v-else-if="hasTrailer"
v-model="activePart"
class="vehicle-cert-part-tabs__tabs"
>
<el-tab-pane :label="headTabLabel" name="head">
<PlCertFormList
ref="headCertFormRef"
@@ -8,6 +25,7 @@
:title="headTabLabel"
:upload-folder="uploadFolder"
:cert-name-options="certNameOptions"
:layout="layout"
/>
</el-tab-pane>
<el-tab-pane :label="trailerTabLabel" name="trailer">
@@ -17,6 +35,7 @@
:title="trailerTabLabel"
:upload-folder="uploadFolder"
:cert-name-options="certNameOptions"
:layout="layout"
/>
</el-tab-pane>
</el-tabs>
@@ -28,6 +47,7 @@
:title="title"
:upload-folder="uploadFolder"
:cert-name-options="certNameOptions"
layout="card"
/>
</div>
</template>
@@ -52,18 +72,21 @@ const props = withDefaults(
uploadFolder: string
certNameOptions?: string[]
title?: string
layout?: 'card' | 'table'
}>(),
{
licensePlate: '',
trailerNo: '',
certNameOptions: () => [],
title: '车辆证书'
title: '车辆证书',
layout: 'card'
}
)
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
const unifiedCertFormRef = ref<CertFormListRef>()
const headCertFormRef = ref<CertFormListRef>()
const trailerCertFormRef = ref<CertFormListRef>()
@@ -102,6 +125,7 @@ watch(hasTrailer, value => {
[],
false
)
unifiedCertFormRef.value?.clearUploadRefs()
trailerCertFormRef.value?.clearUploadRefs()
return
}
@@ -111,12 +135,16 @@ watch(hasTrailer, value => {
})
function hasUploading() {
if (props.layout === 'table') {
return Boolean(unifiedCertFormRef.value?.hasUploading?.())
}
return Boolean(
headCertFormRef.value?.hasUploading?.() || trailerCertFormRef.value?.hasUploading?.()
)
}
function clearUploadRefs() {
unifiedCertFormRef.value?.clearUploadRefs()
headCertFormRef.value?.clearUploadRefs()
trailerCertFormRef.value?.clearUploadRefs()
}
@@ -1,23 +1,52 @@
<template>
<div class="vehicle-cert-part-view">
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-view__tabs">
<el-tab-pane :label="headTabLabel" name="head">
<PlCertViewList :cert-list="headCertList" />
</el-tab-pane>
<el-tab-pane :label="trailerTabLabel" name="trailer">
<PlCertViewList :cert-list="trailerCertList" />
</el-tab-pane>
</el-tabs>
<PlCertViewList
v-if="certLayout === 'table'"
:cert-list="scopedCertList"
:table-mode="tableMode"
layout="table"
:show-vehicle-part-column="hasTrailer"
:head-label="headTabLabel"
:trailer-label="trailerTabLabel"
/>
<PlCertViewList v-else :cert-list="headCertList" />
<template v-else>
<el-tabs
v-if="hasTrailer"
:key="tabsInstanceKey"
v-model="activePart"
class="vehicle-cert-part-view__tabs"
>
<el-tab-pane :label="headTabLabelWithHint" :name="headPartName">
<PlCertViewList :cert-list="headCertList" :table-mode="tableMode" :layout="certLayout" />
</el-tab-pane>
<el-tab-pane :label="trailerTabLabelWithHint" :name="trailerPartName">
<PlCertViewList
:cert-list="trailerCertList"
:table-mode="tableMode"
:layout="certLayout"
/>
</el-tab-pane>
</el-tabs>
<PlCertViewList
v-else
:cert-list="headCertList"
:table-mode="tableMode"
:layout="certLayout"
/>
</template>
</div>
</template>
<script setup lang="ts">
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
import type { CertRead } from '@/api/pl'
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
import {
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
getSelfVehiclePartCertMissingSuffix,
splitVehicleCertsByPart
} from '@/utils/pl-vehicle-cert'
@@ -26,22 +55,69 @@ const props = withDefaults(
certList?: CertRead[]
licensePlate?: string
trailerNo?: string
vehicleId?: string
checkSelfVehicleCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
}>(),
{
certList: () => [],
licensePlate: '',
trailerNo: ''
trailerNo: '',
vehicleId: '',
checkSelfVehicleCerts: false,
tableMode: 'triple',
certLayout: 'card'
}
)
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
const activePart = ref('')
const tabsInstanceKey = computed(
() => `${props.vehicleId || props.licensePlate || 'vehicle'}-${props.trailerNo || 'head'}`
)
const headPartName = computed(
() => `${PL_VEHICLE_CERT_PART_HEAD}-${props.vehicleId || props.licensePlate || 'vehicle'}`
)
const trailerPartName = computed(
() =>
`${PL_VEHICLE_CERT_PART_TRAILER}-${props.vehicleId || props.trailerNo || props.licensePlate || 'vehicle'}`
)
const hasTrailer = computed(() => Boolean(props.trailerNo?.trim()))
const headTabLabel = computed(() => props.licensePlate?.trim() || '主车')
const trailerTabLabel = computed(() => props.trailerNo?.trim() || '挂车')
const headCertList = computed(() => splitVehicleCertsByPart(props.certList).headCerts)
const trailerCertList = computed(() => splitVehicleCertsByPart(props.certList).trailerCerts)
const scopedCertList = computed(() => {
const list = props.certList || []
if (!props.vehicleId) return list
return list.filter(cert => !cert.ownerId || cert.ownerId === props.vehicleId)
})
const headCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).headCerts)
const trailerCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).trailerCerts)
const headTabLabelWithHint = computed(() => {
if (!props.checkSelfVehicleCerts) return headTabLabel.value
return headTabLabel.value + getSelfVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD)
})
const trailerTabLabelWithHint = computed(() => {
if (!props.checkSelfVehicleCerts) return trailerTabLabel.value
return (
trailerTabLabel.value +
getSelfVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER)
)
})
watch(
tabsInstanceKey,
() => {
activePart.value = headPartName.value
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
@@ -1,10 +1,15 @@
<template>
<el-tabs v-model="activeTab" class="vehicle-detail-tabs">
<el-tab-pane label="车辆证书" name="vehicle-cert">
<el-tabs :key="tabsInstanceKey" v-model="activeTab" class="vehicle-detail-tabs">
<el-tab-pane :label="vehicleCertTabLabel" :name="vehicleCertTabName">
<PlVehicleCertPartView
:key="`${vehicleId || licensePlate}-cert`"
:cert-list="certList"
:license-plate="licensePlate"
:trailer-no="trailerNo"
:vehicle-id="vehicleId"
:check-self-vehicle-certs="checkSelfVehicleCerts"
:table-mode="tableMode"
:cert-layout="certLayout"
/>
</el-tab-pane>
<el-tab-pane
@@ -13,7 +18,12 @@
:label="getPersonTabLabel(person)"
:name="getPersonKey(person, index)"
>
<PersonnelViewPanel :person="person" />
<PersonnelViewPanel
:person="person"
:check-self-vehicle-certs="checkSelfVehicleCerts"
:table-mode="tableMode"
:cert-layout="certLayout"
/>
</el-tab-pane>
</el-tabs>
</template>
@@ -23,26 +33,57 @@ import PlVehicleCertPartView from '@/components/business/pl-vehicle-cert-part-vi
import PersonnelViewPanel from '@/components/business/pl-vehicle-personnel-view-list/PersonnelViewPanel.vue'
import type { CertRead } from '@/api/pl'
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
import { getSelfVehicleCertMissingSuffix } from '@/utils/pl-vehicle-cert'
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
const props = withDefaults(
defineProps<{
certList?: CertRead[]
licensePlate?: string
trailerNo?: string
vehicleId?: string
personnelList?: VehiclePersonnelViewRow[]
checkSelfVehicleCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
}>(),
{
certList: () => [],
licensePlate: '',
trailerNo: '',
personnelList: () => []
vehicleId: '',
personnelList: () => [],
checkSelfVehicleCerts: false,
tableMode: 'triple',
certLayout: 'card'
}
)
const activeTab = ref('vehicle-cert')
const vehicleCertTabName = computed(
() => `vehicle-cert-${props.vehicleId || props.licensePlate || 'default'}`
)
const tabsInstanceKey = computed(
() => `${props.vehicleId || props.licensePlate || 'vehicle'}-${props.trailerNo || 'none'}`
)
const vehicleCertTabLabel = computed(() => {
const label = '车辆证书'
if (!props.checkSelfVehicleCerts) return label
return (
label +
getSelfVehicleCertMissingSuffix({
certList: props.certList,
trailerNo: props.trailerNo
})
)
})
function getPersonKey(person: VehiclePersonnelViewRow, index: number) {
return `person-${person.personnelId || `${person.personnelName}-${index}`}`
return `person-${props.vehicleId || props.licensePlate || 'vehicle'}-${person.personnelId || `${person.personnelName}-${index}`}`
}
function getRoleLabel(role: string) {
@@ -56,23 +97,31 @@ function getRoleLabel(role: string) {
function getPersonTabLabel(person: VehiclePersonnelViewRow) {
const name = person.personnelName || '—'
const role = getRoleLabel(person.role)
return `${name}(${role})`
const suffix =
props.checkSelfVehicleCerts && person.role
? getPersonnelCertMissingSuffix(person.certList || [], person.role as 'driver' | 'escort')
: ''
return `${name}(${role})${suffix}`
}
watch(
tabsInstanceKey,
() => {
activeTab.value = vehicleCertTabName.value
},
{ immediate: true }
)
watch(
() => props.personnelList,
list => {
if (!list.length) {
activeTab.value = 'vehicle-cert'
return
}
if (activeTab.value === 'vehicle-cert') return
if (activeTab.value === vehicleCertTabName.value) return
const keys = list.map((person, index) => getPersonKey(person, index))
if (!keys.includes(activeTab.value)) {
activeTab.value = 'vehicle-cert'
activeTab.value = vehicleCertTabName.value
}
},
{ immediate: true, deep: true }
{ deep: true }
)
</script>
@@ -1,7 +1,7 @@
<template>
<div class="personnel-view-panel">
<h4 v-if="showTitle" class="personnel-view-card-title">
{{ person.personnelName || '—' }}
{{ person.personnelName || '—' }}{{ personnelCertMissingSuffix }}
<el-tag :type="getRoleTagType(person.role)" size="small">
{{ getRoleLabel(person.role) }}
</el-tag>
@@ -10,11 +10,15 @@
</el-tag>
</h4>
<ViewKvTable :fields="personnelViewFields" mode="triple" />
<ViewKvTable :fields="personnelViewFields" :mode="tableMode" />
<div class="personnel-certs-block">
<p class="personnel-certs-title">人员证书</p>
<PlCertViewList :cert-list="person.certList || []" />
<PlCertViewList
:cert-list="person.certList || []"
:table-mode="tableMode"
:layout="certLayout"
/>
</div>
</div>
</template>
@@ -22,8 +26,9 @@
<script setup lang="ts">
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import type { ViewField, ViewTableMode } from '@/components/business/system-view-dialog/types'
import type { CertRead, VehiclePersonnelRow } from '@/api/pl'
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
type VehiclePersonnelViewRow = VehiclePersonnelRow & { certList?: CertRead[] }
@@ -31,12 +36,26 @@ const props = withDefaults(
defineProps<{
person: VehiclePersonnelViewRow
showTitle?: boolean
checkSelfVehicleCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
}>(),
{
showTitle: false
showTitle: false,
checkSelfVehicleCerts: false,
tableMode: 'triple',
certLayout: 'card'
}
)
const personnelCertMissingSuffix = computed(() => {
if (!props.checkSelfVehicleCerts || !props.person.role) return ''
return getPersonnelCertMissingSuffix(
props.person.certList || [],
props.person.role as 'driver' | 'escort'
)
})
const personnelViewFields = computed<ViewField[]>(() => {
const person = props.person
const fields: ViewField[] = [
@@ -58,7 +77,7 @@ const personnelViewFields = computed<ViewField[]>(() => {
})
}
fields.push({ label: '备注', value: person.remark || '—', span: 'full' })
fields.push({ label: '备注', value: person.remark || '—', long: false })
return fields
})
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff. Show more