From 441e1fdae1d8226aa86f260a41a9f6acbde34146 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 21 Aug 2026 18:01:00 +0800 Subject: [PATCH] feat(pl): add Excel export for vehicle maintenance apply list Support filtered and selected rows via backend export and ListExportDropdown. Co-authored-by: Cursor --- backend/app/api/pl/vehicle_maintenance.py | 83 ++++++++++++++++++- .../pl_vehicle_maintenance_service.py | 7 ++ web-admin/src/api/pl/index.ts | 2 + .../pl/vehicle-maintenance-apply/index.vue | 27 ++++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/backend/app/api/pl/vehicle_maintenance.py b/backend/app/api/pl/vehicle_maintenance.py index 52f25723a..7da2cd083 100644 --- a/backend/app/api/pl/vehicle_maintenance.py +++ b/backend/app/api/pl/vehicle_maintenance.py @@ -1,13 +1,22 @@ """车辆维保 API""" from datetime import date -from typing import Optional +from datetime import datetime +from typing import List, Optional -from fastapi import APIRouter, Depends, Query, status +from pydantic import BaseModel, Field +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user from app.core.tenant_context import get_tenant_no +from app.utils.excel_export import ( + EXPORT_MAX_ROWS, + ExportColumn, + ExportValidationError, + export_rows_to_xlsx_response, + filter_export_columns, +) from app.schemas.pl.vehicle_maintenance import ( VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyListResponse, @@ -74,6 +83,76 @@ def list_reminders( ) + +MAINT_APPLY_EXPORT_COLUMNS = [ + ExportColumn(title="申请单号", field="applyNo"), + ExportColumn(title="车牌号", field="licensePlate"), + ExportColumn(title="维保类型", field="maintenanceType"), + ExportColumn(title="申请日期", field="applyDate"), + ExportColumn(title="开始日期", field="startDate"), + ExportColumn(title="预计取车", field="expectedPickupDate"), + ExportColumn(title="申请人", field="applicantName"), + ExportColumn(title="状态", field="status"), + ExportColumn(title="创建时间", field="createTime"), +] + + +class MaintenanceApplyExportBody(BaseModel): + q: Optional[str] = None + maintenance_type: Optional[str] = Field(default=None, alias="maintenanceType") + status: Optional[str] = None + apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom") + apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo") + ids: Optional[List[str]] = None + columns: Optional[List[str]] = None + + model_config = {"populate_by_name": True} + + +@apply_router.post("/export") +def export_applies( + body: MaintenanceApplyExportBody, + 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()) + items, _ = svc.list_applies( + db, + tenant_no, + user, + q=body.q, + maintenance_type=body.maintenance_type, + status=body.status, + apply_date_from=body.apply_date_from, + apply_date_to=body.apply_date_to, + ids=body.ids, + page=1, + page_size=EXPORT_MAX_ROWS, + max_page_size=EXPORT_MAX_ROWS, + ) + rows = [] + for item in items: + data = item.model_dump(by_alias=True) + for k in ("applyDate", "startDate", "expectedPickupDate", "createTime"): + if data.get(k) is not None: + data[k] = str(data[k])[:19] + rows.append(data) + cols = filter_export_columns(MAINT_APPLY_EXPORT_COLUMNS, body.columns) + date_str = datetime.now().strftime("%Y%m%d") + return export_rows_to_xlsx_response( + db, + tenant_no=tenant_no, + sheet_title="维保申请", + columns=cols, + rows=rows, + filename_stem=f"维保申请_{date_str}", + ) + except ExportValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + @apply_router.get("/{id}", response_model=VehicleMaintenanceApplyRead) def get_apply( id: str, diff --git a/backend/app/services/pl_vehicle_maintenance_service.py b/backend/app/services/pl_vehicle_maintenance_service.py index db47b2785..63d6a54fd 100644 --- a/backend/app/services/pl_vehicle_maintenance_service.py +++ b/backend/app/services/pl_vehicle_maintenance_service.py @@ -268,8 +268,10 @@ def list_applies( apply_date_from: Optional[date] = None, apply_date_to: Optional[date] = None, applicant_id: Optional[str] = None, + ids: Optional[list[str]] = None, page: int = 1, page_size: int = 20, + max_page_size: Optional[int] = None, ) -> tuple[list[VehicleMaintenanceApplyRead], int]: query = db.query(PlVehicleMaintenanceApply).filter( PlVehicleMaintenanceApply.tenant_no == tenant_no, @@ -294,9 +296,14 @@ def list_applies( query = query.filter(PlVehicleMaintenanceApply.apply_date <= apply_date_to) if applicant_id: query = query.filter(PlVehicleMaintenanceApply.applicant_id == applicant_id) + if ids: + query = query.filter(PlVehicleMaintenanceApply.id.in_(ids)) query = apply_pl_scope(query, PlVehicleMaintenanceApply, user, db, tenant_no) total = query.count() + page = max(1, page) + cap = int(max_page_size) if max_page_size else 500 + page_size = min(max(1, page_size), cap) rows = ( query.order_by(PlVehicleMaintenanceApply.create_time.desc()) .offset((page - 1) * page_size) diff --git a/web-admin/src/api/pl/index.ts b/web-admin/src/api/pl/index.ts index 234f58d19..c6dd3624d 100644 --- a/web-admin/src/api/pl/index.ts +++ b/web-admin/src/api/pl/index.ts @@ -1530,6 +1530,8 @@ export interface VehicleMaintenanceProcessBoardListResponse { export const vehicleMaintenanceApplyApi = { list: (params?: Record) => + export: (data: Record) => + request.post('/transport/vehicle-maintenance-apply/export', data, { responseType: 'blob' }), request.get('/transport/vehicle-maintenance-apply/', { params }), diff --git a/web-admin/src/views/pl/vehicle-maintenance-apply/index.vue b/web-admin/src/views/pl/vehicle-maintenance-apply/index.vue index 58263052b..ff16e147d 100644 --- a/web-admin/src/views/pl/vehicle-maintenance-apply/index.vue +++ b/web-admin/src/views/pl/vehicle-maintenance-apply/index.vue @@ -86,6 +86,15 @@ 作废 + + import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue' import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue' +import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue' import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue' import type { StatsRowColumn } from '@/utils/list-column-stats' import { useDbInstantSearch } from '@/composables/useDbInstantSearch' @@ -472,6 +482,23 @@ const submitLoading = ref(false) const cancelLoadingMap = reactive>({}) const tableData = ref([]) const selectedRows = ref([]) + +function getMaintApplyExportFilterParams() { + return { + q: quickSearchKeyword.value.trim() || searchForm.q || undefined, + maintenanceType: searchForm.maintenanceType || undefined, + status: searchForm.status || undefined, + applyDateFrom: dateRange.value?.[0], + applyDateTo: dateRange.value?.[1] + } +} + +const maintApplyExportColumns = computed(() => { + const cols = [...visibleColumns.value] + if (!cols.includes('applyNo')) cols.unshift('applyNo') + return cols +}) + const vehicleOptions = ref([]) const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value)) const dateRange = ref([])