Просмотр исходного кода

feat(pl): add Excel export for recharge, urea and maintenance process lists

Support filtered and selected export on apply and disburse/process board pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
89e8d6e65c

+ 62 - 0
backend/app/api/pl/vehicle_maintenance.py

@@ -239,6 +239,68 @@ def list_process_board(
     )
 
 
+
+MAINT_PROCESS_EXPORT_COLUMNS = [
+    ExportColumn(title="申请单号", field="applyNo"),
+    ExportColumn(title="申请日期", field="applyDate"),
+    ExportColumn(title="申请人", field="applicantName"),
+    ExportColumn(title="申请状态", field="applyStatus"),
+    ExportColumn(title="车牌号", field="licensePlate"),
+    ExportColumn(title="维保类型", field="maintenanceType"),
+    ExportColumn(title="处理单号", field="processNo"),
+    ExportColumn(title="处理日期", field="processDate"),
+    ExportColumn(title="处理人", field="handlerName"),
+    ExportColumn(title="金额(元)", field="amount"),
+]
+
+
+class MaintProcessExportBody(BaseModel):
+    q: Optional[str] = None
+    maintenance_type: Optional[str] = Field(default=None, alias="maintenanceType")
+    apply_status: Optional[str] = Field(default=None, alias="applyStatus")
+    apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
+    apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
+    process_date_from: Optional[date] = Field(default=None, alias="processDateFrom")
+    process_date_to: Optional[date] = Field(default=None, alias="processDateTo")
+    ids: Optional[List[str]] = None
+    columns: Optional[List[str]] = None
+    model_config = {"populate_by_name": True}
+
+
+@process_router.post("/process-board/export")
+def export_process_board(
+    body: MaintProcessExportBody,
+    db: Session = Depends(get_db),
+    user: DbUserContext = Depends(get_db_current_user),
+):
+    try:
+        tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
+        items, _ = svc.list_process_board(
+            db, tenant_no, user,
+            q=body.q, maintenance_type=body.maintenance_type, apply_status=body.apply_status,
+            apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to,
+            process_date_from=body.process_date_from, process_date_to=body.process_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, v in list(data.items()):
+                if hasattr(v, 'isoformat'):
+                    data[k] = str(v)[:19]
+            rows.append(data)
+
+        cols = filter_export_columns(MAINT_PROCESS_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
+
+
 @process_router.get("/{id}", response_model=VehicleMaintenanceProcessRead)
 def get_process(
     id: str,

+ 130 - 3
backend/app/api/pl/vehicle_recharge.py

@@ -1,13 +1,21 @@
 """车辆 ETC/加油充值 API"""
-from datetime import date
-from typing import Optional
+from datetime import date, 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_recharge import (
     VehicleRechargeApplyCreate,
     VehicleRechargeApplyListResponse,
@@ -68,6 +76,63 @@ def pending_apply_options(
     return [VehicleRechargeApplyOption.model_validate(row) for row in rows]
 
 
+
+RECHARGE_APPLY_EXPORT_COLUMNS = [
+    ExportColumn(title="申请单号", field="applyNo"),
+    ExportColumn(title="车牌号", field="licensePlate"),
+    ExportColumn(title="充值类型", field="rechargeType"),
+    ExportColumn(title="申请日期", field="applyDate"),
+    ExportColumn(title="期初公里数", field="odometerKm"),
+    ExportColumn(title="申请人", field="applicantName"),
+    ExportColumn(title="状态", field="status"),
+    ExportColumn(title="创建时间", field="createTime"),
+]
+
+
+class RechargeApplyExportBody(BaseModel):
+    q: Optional[str] = None
+    recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
+    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_recharge_applies(
+    body: RechargeApplyExportBody,
+    db: Session = Depends(get_db),
+    user: DbUserContext = Depends(get_db_current_user),
+):
+    try:
+        tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
+        items, _ = svc.list_applies(
+            db, tenant_no, user,
+            q=body.q, recharge_type=body.recharge_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, v in list(data.items()):
+                if hasattr(v, 'isoformat'):
+                    data[k] = str(v)[:19]
+            rows.append(data)
+
+        cols = filter_export_columns(RECHARGE_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=VehicleRechargeApplyRead)
 def get_apply(
     id: str,
@@ -186,6 +251,68 @@ def list_disburse_board(
     )
 
 
+
+RECHARGE_DISBURSE_EXPORT_COLUMNS = [
+    ExportColumn(title="申请单号", field="applyNo"),
+    ExportColumn(title="车牌号", field="licensePlate"),
+    ExportColumn(title="充值类型", field="rechargeType"),
+    ExportColumn(title="申请日期", field="applyDate"),
+    ExportColumn(title="申请人", field="applicantName"),
+    ExportColumn(title="申请状态", field="applyStatus"),
+    ExportColumn(title="期初公里数", field="odometerKm"),
+    ExportColumn(title="充值日期", field="rechargeDate"),
+    ExportColumn(title="充值金额", field="amount"),
+    ExportColumn(title="加油站", field="gasStation"),
+]
+
+
+class RechargeDisburseExportBody(BaseModel):
+    q: Optional[str] = None
+    recharge_type: Optional[str] = Field(default=None, alias="rechargeType")
+    apply_status: Optional[str] = Field(default=None, alias="applyStatus")
+    apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
+    apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
+    recharge_date_from: Optional[date] = Field(default=None, alias="rechargeDateFrom")
+    recharge_date_to: Optional[date] = Field(default=None, alias="rechargeDateTo")
+    ids: Optional[List[str]] = None
+    columns: Optional[List[str]] = None
+    model_config = {"populate_by_name": True}
+
+
+@record_router.post("/disburse-board/export")
+def export_recharge_disburse_board(
+    body: RechargeDisburseExportBody,
+    db: Session = Depends(get_db),
+    user: DbUserContext = Depends(get_db_current_user),
+):
+    try:
+        tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
+        items, _ = svc.list_disburse_board(
+            db, tenant_no, user,
+            q=body.q, recharge_type=body.recharge_type, apply_status=body.apply_status,
+            apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to,
+            recharge_date_from=body.recharge_date_from, recharge_date_to=body.recharge_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, v in list(data.items()):
+                if hasattr(v, 'isoformat'):
+                    data[k] = str(v)[:19]
+            rows.append(data)
+
+        cols = filter_export_columns(RECHARGE_DISBURSE_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
+
+
 @record_router.get("/{id}", response_model=VehicleRechargeRecordRead)
 def get_record(
     id: str,

+ 128 - 3
backend/app/api/pl/vehicle_urea.py

@@ -1,13 +1,21 @@
 """车辆尿素 API"""
-from datetime import date
-from typing import Optional
+from datetime import date, 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_urea import (
     VehicleUreaApplyCreate,
     VehicleUreaApplyListResponse,
@@ -54,6 +62,62 @@ def list_applies(
     )
 
 
+
+UREA_APPLY_EXPORT_COLUMNS = [
+    ExportColumn(title="申请单号", field="applyNo"),
+    ExportColumn(title="车牌号", field="licensePlate"),
+    ExportColumn(title="申请时间", field="applyDate"),
+    ExportColumn(title="入库时间", field="warehouseInDate"),
+    ExportColumn(title="单位", field="unit"),
+    ExportColumn(title="入库数量", field="quantity"),
+    ExportColumn(title="申请人", field="applicantName"),
+    ExportColumn(title="状态", field="status"),
+]
+
+
+class UreaApplyExportBody(BaseModel):
+    q: Optional[str] = None
+    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_urea_applies(
+    body: UreaApplyExportBody,
+    db: Session = Depends(get_db),
+    user: DbUserContext = Depends(get_db_current_user),
+):
+    try:
+        tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
+        items, _ = svc.list_applies(
+            db, tenant_no, user,
+            q=body.q, 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, v in list(data.items()):
+                if hasattr(v, 'isoformat'):
+                    data[k] = str(v)[:19]
+            rows.append(data)
+
+        cols = filter_export_columns(UREA_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=VehicleUreaApplyRead)
 def get_apply(
     id: str,
@@ -138,6 +202,67 @@ def list_disburse_board(
     )
 
 
+
+UREA_DISBURSE_EXPORT_COLUMNS = [
+    ExportColumn(title="申请单号", field="applyNo"),
+    ExportColumn(title="车牌号", field="licensePlate"),
+    ExportColumn(title="申请时间", field="applyDate"),
+    ExportColumn(title="入库时间", field="warehouseInDate"),
+    ExportColumn(title="申请人", field="applicantName"),
+    ExportColumn(title="单位", field="unit"),
+    ExportColumn(title="入库数量", field="quantity"),
+    ExportColumn(title="申请状态", field="applyStatus"),
+    ExportColumn(title="发放日期", field="disburseDate"),
+    ExportColumn(title="发放金额", field="amount"),
+]
+
+
+class UreaDisburseExportBody(BaseModel):
+    q: Optional[str] = None
+    apply_status: Optional[str] = Field(default=None, alias="applyStatus")
+    apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
+    apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
+    disburse_date_from: Optional[date] = Field(default=None, alias="disburseDateFrom")
+    disburse_date_to: Optional[date] = Field(default=None, alias="disburseDateTo")
+    ids: Optional[List[str]] = None
+    columns: Optional[List[str]] = None
+    model_config = {"populate_by_name": True}
+
+
+@disburse_router.post("/disburse-board/export")
+def export_urea_disburse_board(
+    body: UreaDisburseExportBody,
+    db: Session = Depends(get_db),
+    user: DbUserContext = Depends(get_db_current_user),
+):
+    try:
+        tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
+        items, _ = svc.list_disburse_board(
+            db, tenant_no, user,
+            q=body.q, apply_status=body.apply_status,
+            apply_date_from=body.apply_date_from, apply_date_to=body.apply_date_to,
+            disburse_date_from=body.disburse_date_from, disburse_date_to=body.disburse_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, v in list(data.items()):
+                if hasattr(v, 'isoformat'):
+                    data[k] = str(v)[:19]
+            rows.append(data)
+
+        cols = filter_export_columns(UREA_DISBURSE_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
+
+
 @disburse_router.get("/{id}", response_model=VehicleUreaDisburseRead)
 def get_disburse(
     id: str,

+ 8 - 0
backend/app/services/pl_vehicle_maintenance_service.py

@@ -514,8 +514,10 @@ def list_process_board(
     apply_date_to: Optional[date] = None,
     process_date_from: Optional[date] = None,
     process_date_to: Optional[date] = None,
+    ids: Optional[list[str]] = None,
     page: int = 1,
     page_size: int = 20,
+    max_page_size: Optional[int] = None,
 ) -> tuple[list[VehicleMaintenanceProcessBoardItem], int]:
     query = (
         db.query(PlVehicleMaintenanceApply, PlVehicleMaintenanceProcess)
@@ -557,8 +559,14 @@ def list_process_board(
     if process_date_to:
         query = query.filter(PlVehicleMaintenanceProcess.process_date <= process_date_to)
 
+    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)

+ 16 - 0
backend/app/services/pl_vehicle_recharge_service.py

@@ -255,8 +255,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[VehicleRechargeApplyRead], int]:
     query = db.query(PlVehicleRechargeApply).filter(
         PlVehicleRechargeApply.tenant_no == tenant_no,
@@ -280,8 +282,14 @@ def list_applies(
     if applicant_id:
         query = query.filter(PlVehicleRechargeApply.applicant_id == applicant_id)
 
+    if ids:
+        query = query.filter(PlVehicleRechargeApply.id.in_(ids))
+
     query = apply_pl_scope(query, PlVehicleRechargeApply, 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(PlVehicleRechargeApply.create_time.desc())
         .offset((page - 1) * page_size)
@@ -500,8 +508,10 @@ def list_disburse_board(
     apply_date_to: Optional[date] = None,
     recharge_date_from: Optional[date] = None,
     recharge_date_to: Optional[date] = None,
+    ids: Optional[list[str]] = None,
     page: int = 1,
     page_size: int = 20,
+    max_page_size: Optional[int] = None,
 ) -> tuple[list[VehicleRechargeDisburseBoardItem], int]:
     query = (
         db.query(PlVehicleRechargeApply, PlVehicleRechargeRecord)
@@ -540,8 +550,14 @@ def list_disburse_board(
     if recharge_date_to:
         query = query.filter(PlVehicleRechargeRecord.recharge_date <= recharge_date_to)
 
+    if ids:
+        query = query.filter(PlVehicleRechargeApply.id.in_(ids))
+
     query = apply_pl_scope(query, PlVehicleRechargeApply, 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(PlVehicleRechargeApply.create_time.desc())
         .offset((page - 1) * page_size)

+ 16 - 0
backend/app/services/pl_vehicle_urea_service.py

@@ -246,8 +246,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[VehicleUreaApplyRead], int]:
     query = db.query(PlVehicleUreaApply).filter(
         PlVehicleUreaApply.tenant_no == tenant_no,
@@ -269,8 +271,14 @@ def list_applies(
     if applicant_id:
         query = query.filter(PlVehicleUreaApply.applicant_id == applicant_id)
 
+    if ids:
+        query = query.filter(PlVehicleUreaApply.id.in_(ids))
+
     query = apply_pl_scope(query, PlVehicleUreaApply, 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(PlVehicleUreaApply.create_time.desc())
         .offset((page - 1) * page_size)
@@ -422,8 +430,10 @@ def list_disburse_board(
     apply_date_to: Optional[date] = None,
     disburse_date_from: Optional[date] = None,
     disburse_date_to: Optional[date] = None,
+    ids: Optional[list[str]] = None,
     page: int = 1,
     page_size: int = 20,
+    max_page_size: Optional[int] = None,
 ) -> tuple[list[VehicleUreaDisburseBoardItem], int]:
     query = (
         db.query(PlVehicleUreaApply, PlVehicleUreaDisburse)
@@ -459,8 +469,14 @@ def list_disburse_board(
     if disburse_date_to:
         query = query.filter(PlVehicleUreaDisburse.disburse_date <= disburse_date_to)
 
+    if ids:
+        query = query.filter(PlVehicleUreaApply.id.in_(ids))
+
     query = apply_pl_scope(query, PlVehicleUreaApply, 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(PlVehicleUreaApply.create_time.desc())
         .offset((page - 1) * page_size)

+ 12 - 2
web-admin/src/api/pl/index.ts

@@ -1412,6 +1412,8 @@ export interface VehicleRechargeDisburseBoardListResponse {
 export const vehicleRechargeApplyApi = {
   list: (params?: Record<string, unknown>) =>
     request.get<VehicleRechargeApplyListResponse>('/transport/vehicle-recharge-apply/', { params }),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-recharge-apply/export', data, { responseType: 'blob' }),
   get: (id: string) => request.get<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`),
   create: (data: Partial<VehicleRechargeApply>) =>
     request.post<VehicleRechargeApply>('/transport/vehicle-recharge-apply/', data),
@@ -1430,6 +1432,8 @@ export const vehicleRechargeRecordApi = {
       '/transport/vehicle-recharge-record/disburse-board',
       { params }
     ),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-recharge-record/disburse-board/export', data, { responseType: 'blob' }),
   list: (params?: Record<string, unknown>) =>
     request.get<VehicleRechargeRecordListResponse>('/transport/vehicle-recharge-record/', { params }),
   get: (id: string) => request.get<VehicleRechargeRecord>(`/transport/vehicle-recharge-record/${id}`),
@@ -1530,11 +1534,11 @@ export interface VehicleMaintenanceProcessBoardListResponse {
 
 export const vehicleMaintenanceApplyApi = {
   list: (params?: Record<string, unknown>) =>
-  export: (data: Record<string, any>) =>
-    request.post('/transport/vehicle-maintenance-apply/export', data, { responseType: 'blob' }),
     request.get<VehicleMaintenanceApplyListResponse>('/transport/vehicle-maintenance-apply/', {
       params
     }),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-maintenance-apply/export', data, { responseType: 'blob' }),
   get: (id: string) =>
     request.get<VehicleMaintenanceApply>(`/transport/vehicle-maintenance-apply/${id}`),
   create: (data: Partial<VehicleMaintenanceApply>) =>
@@ -1556,6 +1560,8 @@ export const vehicleMaintenanceProcessApi = {
       '/transport/vehicle-maintenance-process/process-board',
       { params }
     ),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-maintenance-process/process-board/export', data, { responseType: 'blob' }),
   get: (id: string) =>
     request.get<VehicleMaintenanceProcess>(`/transport/vehicle-maintenance-process/${id}`),
   create: (data: Partial<VehicleMaintenanceProcess>) =>
@@ -1642,6 +1648,8 @@ export interface VehicleUreaDisburseBoardListResponse {
 export const vehicleUreaApplyApi = {
   list: (params?: Record<string, unknown>) =>
     request.get<VehicleUreaApplyListResponse>('/transport/vehicle-urea-apply/', { params }),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-urea-apply/export', data, { responseType: 'blob' }),
   get: (id: string) => request.get<VehicleUreaApply>(`/transport/vehicle-urea-apply/${id}`),
   create: (data: Partial<VehicleUreaApply>) =>
     request.post<VehicleUreaApply>('/transport/vehicle-urea-apply/', data),
@@ -1658,6 +1666,8 @@ export const vehicleUreaDisburseApi = {
       '/transport/vehicle-urea-disburse/disburse-board',
       { params }
     ),
+  export: (data: Record<string, any>) =>
+    request.post('/transport/vehicle-urea-disburse/disburse-board/export', data, { responseType: 'blob' }),
   get: (id: string) => request.get<VehicleUreaDisburse>(`/transport/vehicle-urea-disburse/${id}`),
   create: (data: Partial<VehicleUreaDisburse>) =>
     request.post<VehicleUreaDisburse>('/transport/vehicle-urea-disburse/', data),

+ 29 - 0
web-admin/src/views/pl/vehicle-maintenance-process/index.vue

@@ -101,6 +101,15 @@
             <el-icon><Plus /></el-icon>无申请补录
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="vehicleMaintenanceProcessApi.export"
+            :get-filter-params="getMaintProcessExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.applyId)"
+            :visible-columns="maintProcessExportColumns"
+            filename="维保过程.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -414,6 +423,7 @@
 <script setup lang="ts">
 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'
@@ -518,6 +528,25 @@ function getMaintProcessStatCellValue(row: VehicleMaintenanceProcessBoardItem, p
 const submitLoading = ref(false)
 const deleteLoadingMap = reactive<Record<string, boolean>>({})
 const selectedRows = ref<VehicleMaintenanceProcessBoardItem[]>([])
+
+function getMaintProcessExportFilterParams() {
+  return {
+    q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
+    maintenanceType: searchForm.maintenanceType || undefined,
+    applyStatus: searchForm.applyStatus || undefined,
+    applyDateFrom: applyDateRange.value?.[0],
+    applyDateTo: applyDateRange.value?.[1],
+    processDateFrom: processDateRange.value?.[0],
+    processDateTo: processDateRange.value?.[1]
+  }
+}
+
+const maintProcessExportColumns = computed(() => {
+  const cols = [...visibleColumns.value]
+  if (!cols.includes('applyNo')) cols.unshift('applyNo')
+  return cols
+})
+
 const tableData = ref<VehicleMaintenanceProcessBoardItem[]>([])
 const vehicleOptions = ref<Vehicle[]>([])
 const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))

+ 27 - 0
web-admin/src/views/pl/vehicle-recharge-apply/index.vue

@@ -74,6 +74,15 @@
             <el-icon><CircleClose /></el-icon>作废
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="vehicleRechargeApplyApi.export"
+            :get-filter-params="getRechargeApplyExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="rechargeApplyExportColumns"
+            filename="充值申请.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -308,6 +317,7 @@
 <script setup lang="ts">
 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 { useDbInstantSearch } from '@/composables/useDbInstantSearch'
 import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
 import {
@@ -376,6 +386,23 @@ const submitLoading = ref(false)
 const cancelLoadingMap = reactive<Record<string, boolean>>({})
 const tableData = ref<VehicleRechargeApply[]>([])
 const selectedRows = ref<VehicleRechargeApply[]>([])
+
+function getRechargeApplyExportFilterParams() {
+  return {
+    q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
+    rechargeType: searchForm.rechargeType || undefined,
+    status: searchForm.status || undefined,
+    applyDateFrom: dateRange.value?.[0],
+    applyDateTo: dateRange.value?.[1]
+  }
+}
+
+const rechargeApplyExportColumns = computed(() => {
+  const cols = [...visibleColumns.value]
+  if (!cols.includes('applyNo')) cols.unshift('applyNo')
+  return cols
+})
+
 const vehicleOptions = ref<Vehicle[]>([])
 const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
 const dateRange = ref<string[]>([])

+ 29 - 0
web-admin/src/views/pl/vehicle-recharge-record/index.vue

@@ -104,6 +104,15 @@
             <el-icon><Delete /></el-icon>撤销发放
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="vehicleRechargeRecordApi.export"
+            :get-filter-params="getRechargeDisburseExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.applyId)"
+            :visible-columns="rechargeDisburseExportColumns"
+            filename="充值发放.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -417,6 +426,7 @@
 <script setup lang="ts">
 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 { useDbInstantSearch } from '@/composables/useDbInstantSearch'
 import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
 import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
@@ -476,6 +486,25 @@ const submitLoading = ref(false)
 const deleteLoadingMap = reactive<Record<string, boolean>>({})
 const tableData = ref<VehicleRechargeDisburseBoardItem[]>([])
 const selectedRows = ref<VehicleRechargeDisburseBoardItem[]>([])
+
+function getRechargeDisburseExportFilterParams() {
+  return {
+    q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
+    rechargeType: searchForm.rechargeType || undefined,
+    applyStatus: searchForm.applyStatus || undefined,
+    applyDateFrom: applyDateRange.value?.[0],
+    applyDateTo: applyDateRange.value?.[1],
+    rechargeDateFrom: rechargeDateRange.value?.[0],
+    rechargeDateTo: rechargeDateRange.value?.[1]
+  }
+}
+
+const rechargeDisburseExportColumns = computed(() => {
+  const cols = [...visibleColumns.value]
+  if (!cols.includes('applyNo')) cols.unshift('applyNo')
+  return cols
+})
+
 const vehicleOptions = ref<Vehicle[]>([])
 const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
 const applyDateRange = ref<string[]>([])

+ 26 - 0
web-admin/src/views/pl/vehicle-urea-apply/index.vue

@@ -68,6 +68,15 @@
             <el-icon><CircleClose /></el-icon>作废
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="vehicleUreaApplyApi.export"
+            :get-filter-params="getUreaApplyExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.id)"
+            :visible-columns="ureaApplyExportColumns"
+            filename="尿素申请.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -335,6 +344,7 @@
 <script setup lang="ts">
 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'
@@ -436,6 +446,22 @@ const submitLoading = ref(false)
 const cancelLoadingMap = reactive<Record<string, boolean>>({})
 const tableData = ref<VehicleUreaApply[]>([])
 const selectedRows = ref<VehicleUreaApply[]>([])
+
+function getUreaApplyExportFilterParams() {
+  return {
+    q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
+    status: searchForm.status || undefined,
+    applyDateFrom: dateRange.value?.[0],
+    applyDateTo: dateRange.value?.[1]
+  }
+}
+
+const ureaApplyExportColumns = computed(() => {
+  const cols = [...visibleColumns.value]
+  if (!cols.includes('applyNo')) cols.unshift('applyNo')
+  return cols
+})
+
 const vehicleOptions = ref<Vehicle[]>([])
 const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
 const dateRange = ref<string[]>([])

+ 28 - 0
web-admin/src/views/pl/vehicle-urea-disburse/index.vue

@@ -83,6 +83,15 @@
             <el-icon><Plus /></el-icon>无申请补录
           </el-button>
 
+          <ListExportDropdown
+            :export-fn="vehicleUreaDisburseApi.export"
+            :get-filter-params="getUreaDisburseExportFilterParams"
+            :has-selection="true"
+            :selected-ids="selectedRows.map((r) => r.applyId)"
+            :visible-columns="ureaDisburseExportColumns"
+            filename="尿素发放.xlsx"
+          />
+
           <ListToolbarEndActions
             :loading="loading"
             :column-defs="listColumnDefs"
@@ -385,6 +394,7 @@
 <script setup lang="ts">
 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'
@@ -484,6 +494,24 @@ function getUreaDisburseStatCellValue(row: VehicleUreaDisburseBoardItem, prop: s
 const submitLoading = ref(false)
 const deleteLoadingMap = reactive<Record<string, boolean>>({})
 const selectedRows = ref<VehicleUreaDisburseBoardItem[]>([])
+
+function getUreaDisburseExportFilterParams() {
+  return {
+    q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
+    applyStatus: searchForm.applyStatus || undefined,
+    applyDateFrom: applyDateRange.value?.[0],
+    applyDateTo: applyDateRange.value?.[1],
+    disburseDateFrom: disburseDateRange.value?.[0],
+    disburseDateTo: disburseDateRange.value?.[1]
+  }
+}
+
+const ureaDisburseExportColumns = computed(() => {
+  const cols = [...visibleColumns.value]
+  if (!cols.includes('applyNo')) cols.unshift('applyNo')
+  return cols
+})
+
 const tableData = ref<VehicleUreaDisburseBoardItem[]>([])
 const vehicleOptions = ref<Vehicle[]>([])
 const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))