From 4645d17348491180b0610a4ac80f935c436510bf Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 13 Mar 2026 10:39:27 +0800 Subject: [PATCH] feat(frontend): add vehicle replacement pages and enhance delivery/return/prepare with inspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create vehicle-replacement module (data.ts, index.vue, form.vue) with full CRUD, BPM approval actions, and conditional row actions - Enhance vehicle-prepare form with InspectionForm component (backwards compatible with old hardcoded checklist) - Enhance delivery-order with "还车" and "替换车" action buttons on completed orders, plus InspectionForm integration - Enhance return-order with BPM approval submit/withdraw actions and per-vehicle inspection start/view capability - Add inspectionRecordId to vehicle-prepare and delivery-order API types Co-Authored-By: Claude Opus 4.6 --- apps/web-antd/src/api/asset/delivery-order.ts | 71 ++ .../web-antd/src/api/asset/vehicle-prepare.ts | 62 ++ .../src/views/asset/delivery-order/data.ts | 174 +++++ .../src/views/asset/delivery-order/index.vue | 201 ++++++ .../asset/delivery-order/modules/form.vue | 428 ++++++++++++ .../src/views/asset/return-order/index.vue | 36 + .../views/asset/return-order/modules/form.vue | 566 ++++++++++++++++ .../asset/vehicle-prepare/modules/form.vue | 630 ++++++++++++++++++ .../views/asset/vehicle-replacement/data.ts | 247 +++++++ .../views/asset/vehicle-replacement/index.vue | 233 +++++++ .../vehicle-replacement/modules/form.vue | 138 ++++ 11 files changed, 2786 insertions(+) create mode 100644 apps/web-antd/src/api/asset/delivery-order.ts create mode 100644 apps/web-antd/src/api/asset/vehicle-prepare.ts create mode 100644 apps/web-antd/src/views/asset/delivery-order/data.ts create mode 100644 apps/web-antd/src/views/asset/delivery-order/index.vue create mode 100644 apps/web-antd/src/views/asset/delivery-order/modules/form.vue create mode 100644 apps/web-antd/src/views/asset/return-order/modules/form.vue create mode 100644 apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue create mode 100644 apps/web-antd/src/views/asset/vehicle-replacement/data.ts create mode 100644 apps/web-antd/src/views/asset/vehicle-replacement/index.vue create mode 100644 apps/web-antd/src/views/asset/vehicle-replacement/modules/form.vue diff --git a/apps/web-antd/src/api/asset/delivery-order.ts b/apps/web-antd/src/api/asset/delivery-order.ts new file mode 100644 index 0000000..7315ae6 --- /dev/null +++ b/apps/web-antd/src/api/asset/delivery-order.ts @@ -0,0 +1,71 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace AssetDeliveryOrderApi { + export interface DeliveryOrderVehicle { + id?: number; + taskVehicleId?: number; + vehicleId?: number; + plateNo?: string; + vin?: string; + brand?: string; + model?: string; + mileage?: number; + hydrogenLevel?: number; + } + + export interface DeliveryOrder { + id?: number; + orderCode?: string; + taskId: number; + taskCode?: string; + contractId?: number; + contractCode?: string; + projectName?: string; + customerId?: number; + customerName?: string; + deliveryDate: string; + deliveryPerson: string; + deliveryLocation?: string; + authorizedPersonId?: number; + authorizedPersonName?: string; + authorizedPersonPhone?: string; + authorizedPersonIdCard?: string; + esignStatus?: number; + deliveryPhotos?: string; + inspectionRecordId?: number; + status?: number; + vehicles?: DeliveryOrderVehicle[]; + createTime?: Date; + } +} + +export function getDeliveryOrderPage(params: PageParam) { + return requestClient.get>( + '/asset/delivery-order/page', + { params }, + ); +} + +export function getDeliveryOrder(id: number) { + return requestClient.get( + `/asset/delivery-order/get?id=${id}`, + ); +} + +export function createDeliveryOrder(data: AssetDeliveryOrderApi.DeliveryOrder) { + return requestClient.post('/asset/delivery-order/create', data); +} + +export function updateDeliveryOrder(data: AssetDeliveryOrderApi.DeliveryOrder) { + return requestClient.put('/asset/delivery-order/update', data); +} + +export function deleteDeliveryOrder(id: number) { + return requestClient.delete(`/asset/delivery-order/delete?id=${id}`); +} + +export function completeDeliveryOrder(id: number) { + return requestClient.put(`/asset/delivery-order/complete?id=${id}`); +} diff --git a/apps/web-antd/src/api/asset/vehicle-prepare.ts b/apps/web-antd/src/api/asset/vehicle-prepare.ts new file mode 100644 index 0000000..06d7e83 --- /dev/null +++ b/apps/web-antd/src/api/asset/vehicle-prepare.ts @@ -0,0 +1,62 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace AssetVehiclePrepareApi { + export interface VehiclePrepare { + id?: number; + vehicleId: number; + plateNo?: string; + vin?: string; + vehicleType?: string; + vehicleModelId?: number; + brand?: string; + model?: string; + contractId?: number; + contractCode?: string; + parkingLot?: string; + hasBodyAd?: boolean; + bodyAdPhoto?: string; + enlargedTextPhoto?: string; + hasTailLift?: boolean; + spareTireDepth?: number; + spareTirePhoto?: string; + defectPhotos?: string; + trailerPlateNo?: string; + checkList?: string; + inspectionRecordId?: number; + status?: number; + completeTime?: Date; + creator?: string; + createTime?: Date; + } +} + +export function getVehiclePreparePage(params: PageParam) { + return requestClient.get>( + '/asset/vehicle-prepare/page', + { params }, + ); +} + +export function getVehiclePrepare(id: number) { + return requestClient.get( + `/asset/vehicle-prepare/get?id=${id}`, + ); +} + +export function createVehiclePrepare(data: AssetVehiclePrepareApi.VehiclePrepare) { + return requestClient.post('/asset/vehicle-prepare/create', data); +} + +export function updateVehiclePrepare(data: AssetVehiclePrepareApi.VehiclePrepare) { + return requestClient.put('/asset/vehicle-prepare/update', data); +} + +export function deleteVehiclePrepare(id: number) { + return requestClient.delete(`/asset/vehicle-prepare/delete?id=${id}`); +} + +export function completeVehiclePrepare(id: number) { + return requestClient.put(`/asset/vehicle-prepare/complete?id=${id}`); +} diff --git a/apps/web-antd/src/views/asset/delivery-order/data.ts b/apps/web-antd/src/views/asset/delivery-order/data.ts new file mode 100644 index 0000000..41a2a94 --- /dev/null +++ b/apps/web-antd/src/views/asset/delivery-order/data.ts @@ -0,0 +1,174 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { getRangePickerDefaultProps } from '#/utils'; + +// ========== 新增/编辑表单 ========== +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { triggerFields: [''], show: () => false }, + }, + { + fieldName: 'taskId', + label: '交车任务', + component: 'ApiSelect', + componentProps: { + placeholder: '请选择交车任务', + api: () => + import('#/api/asset/delivery-task').then((m) => + m.getSimpleDeliveryTaskList(), + ), + labelField: 'taskCode', + valueField: 'id', + showSearch: true, + filterOption: (input: string, option: any) => + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()), + }, + rules: z.number().min(1, { message: '请选择交车任务' }), + }, + { + fieldName: 'deliveryDate', + label: '交车日期', + component: 'DatePicker', + componentProps: { + placeholder: '请选择交车日期', + showTime: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + style: { width: '100%' }, + }, + rules: z.string().min(1, { message: '请选择交车日期' }), + }, + { + fieldName: 'deliveryPerson', + label: '交车人', + component: 'Input', + componentProps: { placeholder: '请输入交车人' }, + rules: z.string().min(1, { message: '请输入交车人' }), + }, + { + fieldName: 'deliveryLocation', + label: '交车地点', + component: 'Input', + componentProps: { placeholder: '请输入交车地点' }, + }, + { + fieldName: 'authorizedPersonName', + label: '被授权人姓名', + component: 'Input', + componentProps: { placeholder: '请输入被授权人姓名' }, + }, + { + fieldName: 'authorizedPersonPhone', + label: '被授权人电话', + component: 'Input', + componentProps: { placeholder: '请输入被授权人电话' }, + }, + { + fieldName: 'authorizedPersonIdCard', + label: '被授权人身份证', + component: 'Input', + componentProps: { placeholder: '请输入被授权人身份证' }, + }, + ]; +} + +// ========== 搜索表单 ========== +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'contractCode', + label: '合同编码', + component: 'Input', + componentProps: { placeholder: '请输入合同编码' }, + }, + { + fieldName: 'projectName', + label: '项目名称', + component: 'Input', + componentProps: { placeholder: '请输入项目名称' }, + }, + { + fieldName: 'customerName', + label: '客户名称', + component: 'Input', + componentProps: { placeholder: '请输入客户名称' }, + }, + { + fieldName: 'deliveryRegion', + label: '交车区域', + component: 'Input', + componentProps: { placeholder: '请输入交车区域' }, + }, + { + fieldName: 'deliveryDate', + label: '交车时间', + component: 'RangePicker', + componentProps: getRangePickerDefaultProps(), + }, + { + fieldName: 'deliveryPerson', + label: '交车人', + component: 'Input', + componentProps: { placeholder: '请输入交车人' }, + }, + ]; +} + +// ========== 待处理列表列 ========== +export function usePendingColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'expectedDeliveryDate', + title: '预计交车时间', + minWidth: 200, + formatter: ({ row }: { row: any }) => { + const start = row.expectedDeliveryDateStart || ''; + const end = row.expectedDeliveryDateEnd || ''; + return end ? `${start} 至 ${end}` : start; + }, + }, + { field: 'createTime', title: '任务发布时间', minWidth: 160, formatter: 'formatDateTime' }, + { field: 'contractCode', title: '合同编码', minWidth: 220 }, + { field: 'projectName', title: '项目名称', minWidth: 150 }, + { field: 'customerName', title: '客户名称', minWidth: 150 }, + { field: 'vehicleCount', title: '交车数量', minWidth: 100, align: 'center' }, + { field: 'deliveryRegion', title: '交车区域', minWidth: 120 }, + { field: 'deliveryLocation', title: '交车地点', minWidth: 150 }, + { title: '操作', width: 160, fixed: 'right', slots: { default: 'actions' } }, + ]; +} + +// ========== 历史记录列表列 ========== +export function useHistoryColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'expectedDeliveryDate', + title: '预计交车时间', + minWidth: 200, + formatter: ({ row }: { row: any }) => { + const start = row.expectedDeliveryDateStart || ''; + const end = row.expectedDeliveryDateEnd || ''; + return end ? `${start} 至 ${end}` : start; + }, + }, + { field: 'createTime', title: '任务发布时间', minWidth: 160, formatter: 'formatDateTime' }, + { field: 'completeTime', title: '交车完成时间', minWidth: 160, formatter: 'formatDateTime' }, + { field: 'deliveryPerson', title: '交车人', minWidth: 100 }, + { field: 'contractCode', title: '合同编码', minWidth: 220 }, + { field: 'projectName', title: '项目名称', minWidth: 150 }, + { field: 'customerName', title: '客户名称', minWidth: 150 }, + { field: 'vehicleCount', title: '交车数量', minWidth: 100, align: 'center' }, + { field: 'deliveryRegion', title: '交车区域', minWidth: 120 }, + { field: 'deliveryLocation', title: '交车地点', minWidth: 150 }, + { title: '操作', width: 100, fixed: 'right', slots: { default: 'actions' } }, + ]; +} + +// ========== 兼容旧引用 ========== +export function useGridColumns(): VxeTableGridOptions['columns'] { + return usePendingColumns(); +} diff --git a/apps/web-antd/src/views/asset/delivery-order/index.vue b/apps/web-antd/src/views/asset/delivery-order/index.vue new file mode 100644 index 0000000..3bbb135 --- /dev/null +++ b/apps/web-antd/src/views/asset/delivery-order/index.vue @@ -0,0 +1,201 @@ + + + diff --git a/apps/web-antd/src/views/asset/delivery-order/modules/form.vue b/apps/web-antd/src/views/asset/delivery-order/modules/form.vue new file mode 100644 index 0000000..7268941 --- /dev/null +++ b/apps/web-antd/src/views/asset/delivery-order/modules/form.vue @@ -0,0 +1,428 @@ + + + diff --git a/apps/web-antd/src/views/asset/return-order/index.vue b/apps/web-antd/src/views/asset/return-order/index.vue index be4fc69..f00c4d9 100644 --- a/apps/web-antd/src/views/asset/return-order/index.vue +++ b/apps/web-antd/src/views/asset/return-order/index.vue @@ -14,6 +14,8 @@ import { deleteReturnOrder, getReturnOrderPage, settleReturnOrder, + submitReturnOrderApproval, + withdrawReturnOrderApproval, } from '#/api/asset/return-order'; import { $t } from '#/locales'; @@ -65,6 +67,20 @@ async function handleSettle(row: AssetReturnOrderApi.ReturnOrder) { handleRefresh(); } +async function handleSubmitApproval(row: AssetReturnOrderApi.ReturnOrder) { + await confirm('确认提交审批?'); + await submitReturnOrderApproval(row.id!); + message.success('已提交审批'); + handleRefresh(); +} + +async function handleWithdrawApproval(row: AssetReturnOrderApi.ReturnOrder) { + await confirm('确认撤回审批?'); + await withdrawReturnOrderApproval(row.id!); + message.success('已撤回审批'); + handleRefresh(); +} + /** 根据状态动态生成操作菜单 */ function getRowActions(row: AssetReturnOrderApi.ReturnOrder) { const actions: any[] = []; @@ -99,6 +115,26 @@ function getRowActions(row: AssetReturnOrderApi.ReturnOrder) { }); } + // 验车完成 → 提交审批 + if (row.status === 1 && row.approvalStatus !== 1) { + actions.push({ + auth: ['asset:return-order:update'], + label: '提交审批', + onClick: () => handleSubmitApproval(row), + type: 'link', + }); + } + + // 审批中 → 撤回 + if (row.approvalStatus === 1) { + actions.push({ + auth: ['asset:return-order:update'], + label: '撤回审批', + onClick: () => handleWithdrawApproval(row), + type: 'link', + }); + } + // 验车完成 → 结算 if (row.status === 1) { actions.push({ diff --git a/apps/web-antd/src/views/asset/return-order/modules/form.vue b/apps/web-antd/src/views/asset/return-order/modules/form.vue new file mode 100644 index 0000000..b8ace73 --- /dev/null +++ b/apps/web-antd/src/views/asset/return-order/modules/form.vue @@ -0,0 +1,566 @@ + + + diff --git a/apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue b/apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue new file mode 100644 index 0000000..a3a6e71 --- /dev/null +++ b/apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue @@ -0,0 +1,630 @@ + + + diff --git a/apps/web-antd/src/views/asset/vehicle-replacement/data.ts b/apps/web-antd/src/views/asset/vehicle-replacement/data.ts new file mode 100644 index 0000000..6159e9d --- /dev/null +++ b/apps/web-antd/src/views/asset/vehicle-replacement/data.ts @@ -0,0 +1,247 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; + +export const REPLACEMENT_TYPE_OPTIONS = [ + { label: '临时替换', value: 1 }, + { label: '永久替换', value: 2 }, +]; + +export const REPLACEMENT_STATUS_OPTIONS = [ + { label: '草稿', value: 0 }, + { label: '审批中', value: 1 }, + { label: '已通过', value: 2 }, + { label: '执行中', value: 3 }, + { label: '已完成', value: 4 }, + { label: '已驳回', value: 5 }, + { label: '已撤回', value: 6 }, +]; + +/** 搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'replacementCode', + label: '替换单号', + component: 'Input', + componentProps: { + placeholder: '请输入替换单号', + }, + }, + { + fieldName: 'replacementType', + label: '替换类型', + component: 'Select', + componentProps: { + placeholder: '请选择替换类型', + options: REPLACEMENT_TYPE_OPTIONS, + }, + }, + { + fieldName: 'customerName', + label: '客户名称', + component: 'Input', + componentProps: { + placeholder: '请输入客户名称', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + options: REPLACEMENT_STATUS_OPTIONS, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 60, fixed: 'left' }, + { + field: 'replacementCode', + title: '替换单号', + minWidth: 160, + fixed: 'left', + }, + { + field: 'replacementType', + title: '替换类型', + minWidth: 100, + formatter({ cellValue }: { cellValue: number }) { + const option = REPLACEMENT_TYPE_OPTIONS.find( + (item) => item.value === cellValue, + ); + return option?.label ?? ''; + }, + }, + { + field: 'contractCode', + title: '合同编号', + minWidth: 160, + }, + { + field: 'customerName', + title: '客户名称', + minWidth: 150, + }, + { + field: 'originalPlateNo', + title: '原车牌号', + minWidth: 120, + }, + { + field: 'newPlateNo', + title: '新车牌号', + minWidth: 120, + }, + { + field: 'status', + title: '状态', + minWidth: 100, + formatter({ cellValue }: { cellValue: number }) { + const option = REPLACEMENT_STATUS_OPTIONS.find( + (item) => item.value === cellValue, + ); + return option?.label ?? ''; + }, + }, + { + field: 'expectedDate', + title: '预计替换日期', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 280, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'replacementType', + label: '替换类型', + component: 'Select', + componentProps: { + placeholder: '请选择替换类型', + options: REPLACEMENT_TYPE_OPTIONS, + }, + rules: z.number({ message: '请选择替换类型' }), + }, + { + fieldName: 'contractId', + label: '关联合同', + component: 'ApiSelect', + componentProps: { + placeholder: '请选择关联合同', + api: () => + import('#/api/asset/contract').then((m) => + m.getSimpleContractList(), + ), + labelField: 'contractCode', + valueField: 'id', + showSearch: true, + filterOption: (input: string, option: any) => + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()), + }, + }, + { + fieldName: 'originalVehicleId', + label: '原车辆ID', + component: 'InputNumber', + componentProps: { + placeholder: '请输入原车辆ID', + class: 'w-full', + }, + rules: z.number({ message: '请输入原车辆ID' }), + }, + { + fieldName: 'newVehicleId', + label: '新车辆ID', + component: 'InputNumber', + componentProps: { + placeholder: '请输入新车辆ID', + class: 'w-full', + }, + rules: z.number({ message: '请输入新车辆ID' }), + }, + { + fieldName: 'deliveryOrderId', + label: '交车单ID', + component: 'InputNumber', + componentProps: { + placeholder: '请输入交车单ID', + class: 'w-full', + }, + }, + { + fieldName: 'replacementReason', + label: '替换原因', + component: 'Textarea', + componentProps: { + placeholder: '请输入替换原因', + rows: 3, + }, + rules: z.string().min(1, { message: '请输入替换原因' }), + }, + { + fieldName: 'expectedDate', + label: '预计替换日期', + component: 'DatePicker', + componentProps: { + placeholder: '请选择预计替换日期', + class: 'w-full', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + { + fieldName: 'returnDate', + label: '预计归还日期', + component: 'DatePicker', + componentProps: { + placeholder: '请选择预计归还日期', + class: 'w-full', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + dependencies: { + triggerFields: ['replacementType'], + show(values) { + return values.replacementType === 1; + }, + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} diff --git a/apps/web-antd/src/views/asset/vehicle-replacement/index.vue b/apps/web-antd/src/views/asset/vehicle-replacement/index.vue new file mode 100644 index 0000000..bc3c677 --- /dev/null +++ b/apps/web-antd/src/views/asset/vehicle-replacement/index.vue @@ -0,0 +1,233 @@ + + + diff --git a/apps/web-antd/src/views/asset/vehicle-replacement/modules/form.vue b/apps/web-antd/src/views/asset/vehicle-replacement/modules/form.vue new file mode 100644 index 0000000..65c1a72 --- /dev/null +++ b/apps/web-antd/src/views/asset/vehicle-replacement/modules/form.vue @@ -0,0 +1,138 @@ + + +