feat(pl): add Excel export for carrier list

Add backend /transport/carrier/export and wire ListExportDropdown on the carrier page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-21 17:47:57 +08:00
1 parent e72bcc67bc
commit 85c96f0a5d
3 files changed
+132 -1

No files matched your search

+105 -1
View File
@@ -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,
+2
View File
@@ -46,6 +46,8 @@ export interface CarrierQuery {
export const carrierApi = {
list: (params?: CarrierQuery) =>
request.get<PlListResponse<Carrier>>('/transport/carrier', { params }),
export: (data: Record<string, any>) =>
request.post('/transport/carrier/export', data, { responseType: 'blob' }),
get: (id: string) => request.get<Carrier>(`/transport/carrier/${id}`),
create: (data: Partial<Carrier>) => request.post<Carrier>('/transport/carrier', data),
update: (id: string, data: Partial<Carrier>) => request.put<Carrier>(`/transport/carrier/${id}`, data),
+25
View File
@@ -63,6 +63,15 @@
<el-icon><Delete /></el-icon>删除
</el-button>
<ListExportDropdown
:export-fn="carrierApi.export"
:get-filter-params="getCarrierExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => r.id)"
:visible-columns="carrierExportColumns"
filename="承运商列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -320,6 +329,7 @@ import AttachmentUploadField from '@/components/business/attachment-upload-field
import PlCarrierListAttachmentCell from '@/components/business/pl-carrier-list-attachment-cell/index.vue'
import PlCarrierViewDialog from '@/components/business/pl-carrier-view-dialog/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
@@ -352,6 +362,21 @@ const {
const submitLoading = ref(false)
const carrierList = ref<Carrier[]>([])
const selectedRows = ref<Carrier[]>([])
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()