feat(pl): add Excel export for vehicle maintenance apply list

Support filtered and selected rows via backend export and ListExportDropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-21 18:01:00 +08:00
1 parent 6a89452f1a
commit 441e1fdae1
4 files changed
+117 -2

No files matched your search

+81 -2
View File
@@ -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,
@@ -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)
+2
View File
@@ -1530,6 +1530,8 @@ 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
}),
@@ -86,6 +86,15 @@
<el-icon><CircleClose /></el-icon>作废
</el-button>
<ListExportDropdown
:export-fn="vehicleMaintenanceApplyApi.export"
:get-filter-params="getMaintApplyExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => r.id)"
:visible-columns="maintApplyExportColumns"
filename="维保申请.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -370,6 +379,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'
@@ -472,6 +482,23 @@ const submitLoading = ref(false)
const cancelLoadingMap = reactive<Record<string, boolean>>({})
const tableData = ref<VehicleMaintenanceApply[]>([])
const selectedRows = ref<VehicleMaintenanceApply[]>([])
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<Vehicle[]>([])
const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
const dateRange = ref<string[]>([])