diff --git a/backend/app/api/pl/base_data.py b/backend/app/api/pl/base_data.py index 74b9e6bc5..205f89c30 100644 --- a/backend/app/api/pl/base_data.py +++ b/backend/app/api/pl/base_data.py @@ -1,11 +1,20 @@ """运输计划管理 - 基础数据 API(承运商、车辆、人员)""" from datetime import datetime -from typing import Optional +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel, Field from sqlalchemy import case, func, or_ from sqlalchemy.orm import Session +from app.utils.excel_export import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + export_rows_to_xlsx_response, + filter_export_columns, +) + from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context @@ -114,6 +123,101 @@ def list_carriers( ) + + +CARRIER_EXPORT_COLUMNS = [ + ExportColumn(title="承运商名称", field="carrierName"), + ExportColumn(title="联系人", field="contactName"), + ExportColumn(title="联系电话", field="contactPhone"), + ExportColumn(title="合作状态", field="statusLabel"), + ExportColumn(title="资质提示", field="qualificationHint"), + ExportColumn(title="协议有效期", field="transportAgreementExpireDate"), + ExportColumn(title="备注", field="remark"), +] + + +class CarrierExportBody(BaseModel): + q: Optional[str] = None + status: Optional[str] = None + audit_status: Optional[str] = Field(default=None, alias="auditStatus") + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + model_config = {"populate_by_name": True} + + +@router.post("/carrier/export") +def export_carriers( + body: CarrierExportBody, + db: Session = Depends(get_db), + user: DbUserContext = Depends(get_db_current_user), +): + """导出承运商列表(当前筛选全量,或 ids 已选)。""" + try: + tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + query = db.query(PlCarrier).filter( + PlCarrier.tenant_no == tenant_no, + PlCarrier.del_flag == 0, + ) + if body.status: + query = query.filter(PlCarrier.status == body.status) + if body.audit_status: + query = query.filter(PlCarrier.audit_status == body.audit_status) + if body.q: + query = query.filter( + or_( + func.lower(PlCarrier.carrier_name).contains(body.q.lower()), + func.lower(PlCarrier.contact_name).contains(body.q.lower()), + ) + ) + if body.ids: + query = query.filter(PlCarrier.id.in_(body.ids)) + query = query.order_by( + case((PlCarrier.order_num.is_(None), 1), else_=0).asc(), + PlCarrier.create_time.desc(), + ) + rows = query.limit(EXPORT_MAX_ROWS + 1).all() + items = [] + for row in rows: + data = carrier_to_read(row).model_dump(by_alias=True) + data["statusLabel"] = "合作中" if str(data.get("status")) == "1" else "已终止" + if data.get("transportAgreementExpired"): + data["qualificationHint"] = "协议已过期" + elif data.get("qualificationComplete") is False: + data["qualificationHint"] = "资质不全" + else: + data["qualificationHint"] = "资质齐全" + items.append(data) + requested = None + if body.columns: + requested = [] + for key in body.columns: + if key in ( + "qualificationAttachments", + "qualificationAttachment", + "photoUrls", + ): + continue + if key == "status": + requested.append("statusLabel") + elif key == "qualificationHint": + requested.append("qualificationHint") + else: + requested.append(key) + cols = filter_export_columns(CARRIER_EXPORT_COLUMNS, requested) + date_str = datetime.now().strftime("%Y%m%d") + return export_rows_to_xlsx_response( + db, + tenant_no=tenant_no, + sheet_title="承运商列表", + columns=cols, + rows=items, + filename_stem=f"承运商列表_{date_str}", + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + @router.post("/carrier", response_model=CarrierRead, status_code=status.HTTP_201_CREATED) def create_carrier( payload: CarrierCreate, diff --git a/web-admin/src/api/pl/index.ts b/web-admin/src/api/pl/index.ts index b6fd7ece0..396594c1c 100644 --- a/web-admin/src/api/pl/index.ts +++ b/web-admin/src/api/pl/index.ts @@ -46,6 +46,8 @@ export interface CarrierQuery { export const carrierApi = { list: (params?: CarrierQuery) => request.get>('/transport/carrier', { params }), + export: (data: Record) => + request.post('/transport/carrier/export', data, { responseType: 'blob' }), get: (id: string) => request.get(`/transport/carrier/${id}`), create: (data: Partial) => request.post('/transport/carrier', data), update: (id: string, data: Partial) => request.put(`/transport/carrier/${id}`, data), diff --git a/web-admin/src/views/pl/carrier/index.vue b/web-admin/src/views/pl/carrier/index.vue index d42b070f2..3c47f78a6 100644 --- a/web-admin/src/views/pl/carrier/index.vue +++ b/web-admin/src/views/pl/carrier/index.vue @@ -63,6 +63,15 @@ 删除 + + ([]) const selectedRows = ref([]) + +function getCarrierExportFilterParams() { + const quick = quickSearchKeyword.value?.trim() + return { + q: quick || searchForm.q || undefined, + status: searchForm.status || undefined + } +} + +const carrierExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('carrierName')) cols.unshift('carrierName') + return cols +}) + const dialogVisible = ref(false) const dialogTitle = ref('') const formRef = ref()