Compare commits
13 Commits
main
...
feat/energ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17b6c99b4f | ||
|
|
f62ff30c64 | ||
|
|
be145c476e | ||
|
|
1a03965c1f | ||
|
|
ad84c21e84 | ||
|
|
12d19c93e9 | ||
|
|
c3999819c9 | ||
|
|
69afb41df5 | ||
|
|
caaeb4c819 | ||
|
|
2c6056c9d0 | ||
|
|
9c412edc78 | ||
|
|
4645d17348 | ||
|
|
594912d2b8 |
71
apps/web-antd/src/api/asset/delivery-order.ts
Normal file
71
apps/web-antd/src/api/asset/delivery-order.ts
Normal file
@@ -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<PageResult<AssetDeliveryOrderApi.DeliveryOrder>>(
|
||||||
|
'/asset/delivery-order/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeliveryOrder(id: number) {
|
||||||
|
return requestClient.get<AssetDeliveryOrderApi.DeliveryOrder>(
|
||||||
|
`/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}`);
|
||||||
|
}
|
||||||
13
apps/web-antd/src/api/asset/index.ts
Normal file
13
apps/web-antd/src/api/asset/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export * from './parking';
|
||||||
|
export * from './customer';
|
||||||
|
export * from './supplier';
|
||||||
|
export * from './vehicle-model';
|
||||||
|
export * from './vehicle-registration';
|
||||||
|
export * from './contract';
|
||||||
|
export * from './vehicle-prepare';
|
||||||
|
export * from './delivery-task';
|
||||||
|
export * from './delivery-order';
|
||||||
|
export * from './return-order';
|
||||||
|
export * from './vehicle';
|
||||||
|
export * from './inspection';
|
||||||
|
export * from './vehicle-replacement';
|
||||||
100
apps/web-antd/src/api/asset/inspection.ts
Normal file
100
apps/web-antd/src/api/asset/inspection.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace InspectionApi {
|
||||||
|
export interface Template {
|
||||||
|
id?: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
bizType: number;
|
||||||
|
vehicleType?: string;
|
||||||
|
status: number;
|
||||||
|
remark?: string;
|
||||||
|
items?: TemplateItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateItem {
|
||||||
|
id?: number;
|
||||||
|
category: string;
|
||||||
|
itemName: string;
|
||||||
|
itemCode: string;
|
||||||
|
inputType: string;
|
||||||
|
sort: number;
|
||||||
|
required: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordDetail {
|
||||||
|
id: number;
|
||||||
|
recordCode: string;
|
||||||
|
templateId: number;
|
||||||
|
sourceType: number;
|
||||||
|
sourceId: number;
|
||||||
|
vehicleId: number;
|
||||||
|
inspectorName?: string;
|
||||||
|
inspectionTime?: string;
|
||||||
|
status: number;
|
||||||
|
overallResult?: number;
|
||||||
|
remark?: string;
|
||||||
|
items: RecordItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordItem {
|
||||||
|
id: number;
|
||||||
|
itemCode: string;
|
||||||
|
category: string;
|
||||||
|
itemName: string;
|
||||||
|
inputType: string;
|
||||||
|
result?: number;
|
||||||
|
value?: string;
|
||||||
|
remark?: string;
|
||||||
|
imageUrls?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInspectionTemplatePage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<InspectionApi.Template>>(
|
||||||
|
'/asset/inspection-template/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInspectionTemplate(id: number) {
|
||||||
|
return requestClient.get<InspectionApi.Template>(
|
||||||
|
`/asset/inspection-template/get?id=${id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInspectionTemplate(data: InspectionApi.Template) {
|
||||||
|
return requestClient.post('/asset/inspection-template/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInspectionTemplate(data: InspectionApi.Template) {
|
||||||
|
return requestClient.put('/asset/inspection-template/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteInspectionTemplate(id: number) {
|
||||||
|
return requestClient.delete(`/asset/inspection-template/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInspectionRecord(id: number) {
|
||||||
|
return requestClient.get<InspectionApi.RecordDetail>(
|
||||||
|
`/asset/inspection-record/get?id=${id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInspectionRecordItem(data: {
|
||||||
|
id: number;
|
||||||
|
result?: number;
|
||||||
|
value?: string;
|
||||||
|
remark?: string;
|
||||||
|
imageUrls?: string;
|
||||||
|
}) {
|
||||||
|
return requestClient.put('/asset/inspection-record/update-item', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeInspection(id: number, inspectorName: string) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/inspection-record/complete?id=${id}&inspectorName=${inspectorName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
121
apps/web-antd/src/api/asset/return-order.ts
Normal file
121
apps/web-antd/src/api/asset/return-order.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace AssetReturnOrderApi {
|
||||||
|
export interface ReturnOrderVehicle {
|
||||||
|
id?: number;
|
||||||
|
vehicleId?: number;
|
||||||
|
plateNo?: string;
|
||||||
|
vin?: string;
|
||||||
|
brand?: string;
|
||||||
|
model?: string;
|
||||||
|
returnMileage?: number;
|
||||||
|
returnHydrogenLevel?: number;
|
||||||
|
deliveryHydrogenLevel?: number;
|
||||||
|
hydrogenDiff?: number;
|
||||||
|
hydrogenUnitPrice?: number;
|
||||||
|
hydrogenRefundAmount?: number;
|
||||||
|
checkList?: string;
|
||||||
|
defectPhotos?: string;
|
||||||
|
vehicleDamageFee?: number;
|
||||||
|
toolDamageFee?: number;
|
||||||
|
unpaidMaintenanceFee?: number;
|
||||||
|
unpaidRepairFee?: number;
|
||||||
|
otherFee?: number;
|
||||||
|
inspectionRecordId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReturnOrder {
|
||||||
|
id?: number;
|
||||||
|
orderCode?: string;
|
||||||
|
contractId: number;
|
||||||
|
contractCode?: string;
|
||||||
|
projectName?: string;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
returnDate: string;
|
||||||
|
returnPerson: string;
|
||||||
|
returnLocation?: string;
|
||||||
|
returnReason?: string;
|
||||||
|
returnReasonDesc?: string;
|
||||||
|
totalRefundAmount?: number;
|
||||||
|
depositRefund?: number;
|
||||||
|
hydrogenRefund?: number;
|
||||||
|
otherCharges?: number;
|
||||||
|
returnPhotos?: string;
|
||||||
|
sourceType?: number;
|
||||||
|
sourceId?: number;
|
||||||
|
deliveryOrderId?: number;
|
||||||
|
status?: number;
|
||||||
|
approvalStatus?: number;
|
||||||
|
vehicles?: ReturnOrderVehicle[];
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReturnOrderPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<AssetReturnOrderApi.ReturnOrder>>(
|
||||||
|
'/asset/return-order/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReturnOrder(id: number) {
|
||||||
|
return requestClient.get<AssetReturnOrderApi.ReturnOrder>(
|
||||||
|
`/asset/return-order/get?id=${id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReturnOrder(data: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
return requestClient.post('/asset/return-order/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateReturnOrder(data: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
return requestClient.put('/asset/return-order/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteReturnOrder(id: number) {
|
||||||
|
return requestClient.delete(`/asset/return-order/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeReturnOrderInspection(id: number) {
|
||||||
|
return requestClient.put(`/asset/return-order/complete-inspection?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function settleReturnOrder(id: number) {
|
||||||
|
return requestClient.put(`/asset/return-order/settle?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReturnOrderFromDelivery(
|
||||||
|
deliveryOrderId: number,
|
||||||
|
vehicleIds: number[],
|
||||||
|
) {
|
||||||
|
return requestClient.post('/asset/return-order/create-from-delivery', {
|
||||||
|
deliveryOrderId,
|
||||||
|
vehicleIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startVehicleInspection(returnOrderVehicleId: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/return-order/start-vehicle-inspection?returnOrderVehicleId=${returnOrderVehicleId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completeVehicleInspection(returnOrderVehicleId: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/return-order/complete-vehicle-inspection?returnOrderVehicleId=${returnOrderVehicleId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitReturnOrderApproval(id: number) {
|
||||||
|
return requestClient.post(`/asset/return-order/submit-approval?id=${id}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withdrawReturnOrderApproval(id: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/return-order/withdraw-approval?id=${id}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
62
apps/web-antd/src/api/asset/vehicle-prepare.ts
Normal file
62
apps/web-antd/src/api/asset/vehicle-prepare.ts
Normal file
@@ -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<PageResult<AssetVehiclePrepareApi.VehiclePrepare>>(
|
||||||
|
'/asset/vehicle-prepare/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVehiclePrepare(id: number) {
|
||||||
|
return requestClient.get<AssetVehiclePrepareApi.VehiclePrepare>(
|
||||||
|
`/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}`);
|
||||||
|
}
|
||||||
82
apps/web-antd/src/api/asset/vehicle-replacement.ts
Normal file
82
apps/web-antd/src/api/asset/vehicle-replacement.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace AssetVehicleReplacementApi {
|
||||||
|
export interface VehicleReplacement {
|
||||||
|
id?: number;
|
||||||
|
replacementCode?: string;
|
||||||
|
replacementType: number;
|
||||||
|
contractId?: number;
|
||||||
|
contractCode?: string;
|
||||||
|
projectName?: string;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
originalVehicleId?: number;
|
||||||
|
originalPlateNo?: string;
|
||||||
|
originalVin?: string;
|
||||||
|
newVehicleId?: number;
|
||||||
|
newPlateNo?: string;
|
||||||
|
newVin?: string;
|
||||||
|
deliveryOrderId?: number;
|
||||||
|
replacementReason?: string;
|
||||||
|
expectedDate?: string;
|
||||||
|
actualDate?: string;
|
||||||
|
returnDate?: string;
|
||||||
|
status?: number;
|
||||||
|
approvalStatus?: number;
|
||||||
|
bpmInstanceId?: string;
|
||||||
|
remark?: string;
|
||||||
|
creator?: string;
|
||||||
|
createTime?: Date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVehicleReplacementPage(params: PageParam) {
|
||||||
|
return requestClient.get<
|
||||||
|
PageResult<AssetVehicleReplacementApi.VehicleReplacement>
|
||||||
|
>('/asset/vehicle-replacement/page', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVehicleReplacement(id: number) {
|
||||||
|
return requestClient.get<AssetVehicleReplacementApi.VehicleReplacement>(
|
||||||
|
`/asset/vehicle-replacement/get?id=${id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createVehicleReplacement(
|
||||||
|
data: AssetVehicleReplacementApi.VehicleReplacement,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/asset/vehicle-replacement/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateVehicleReplacement(
|
||||||
|
data: AssetVehicleReplacementApi.VehicleReplacement,
|
||||||
|
) {
|
||||||
|
return requestClient.put('/asset/vehicle-replacement/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteVehicleReplacement(id: number) {
|
||||||
|
return requestClient.delete(`/asset/vehicle-replacement/delete?id=${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitVehicleReplacementApproval(id: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/vehicle-replacement/submit-approval?id=${id}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withdrawVehicleReplacementApproval(id: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/vehicle-replacement/withdraw-approval?id=${id}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmVehicleReplacementReturn(id: number) {
|
||||||
|
return requestClient.post(
|
||||||
|
`/asset/vehicle-replacement/confirm-return?id=${id}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
111
apps/web-antd/src/api/energy/account.ts
Normal file
111
apps/web-antd/src/api/energy/account.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyAccountApi {
|
||||||
|
export interface Account {
|
||||||
|
id?: number;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
balance?: number;
|
||||||
|
initBalance?: number;
|
||||||
|
accumulatedRecharge?: number;
|
||||||
|
accumulatedConsume?: number;
|
||||||
|
reminderThreshold?: number;
|
||||||
|
accountStatus?: number;
|
||||||
|
lastRechargeDate?: string;
|
||||||
|
version?: number;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountSimple {
|
||||||
|
id: number;
|
||||||
|
customerId: number;
|
||||||
|
customerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Summary {
|
||||||
|
totalCount: number;
|
||||||
|
totalBalance: number;
|
||||||
|
totalRecharge: number;
|
||||||
|
warningCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectAccount {
|
||||||
|
id?: number;
|
||||||
|
accountId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
projectName?: string;
|
||||||
|
contractCode?: string;
|
||||||
|
balance?: number;
|
||||||
|
accumulatedRecharge?: number;
|
||||||
|
accumulatedConsume?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Flow {
|
||||||
|
id?: number;
|
||||||
|
accountId?: number;
|
||||||
|
flowType?: number;
|
||||||
|
bizType?: number;
|
||||||
|
amount?: number;
|
||||||
|
balanceBefore?: number;
|
||||||
|
balanceAfter?: number;
|
||||||
|
bizId?: number;
|
||||||
|
bizCode?: string;
|
||||||
|
remark?: string;
|
||||||
|
operatorId?: number;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyAccountApi.Account>>(
|
||||||
|
'/energy/account/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccount(id: number) {
|
||||||
|
return requestClient.get<EnergyAccountApi.Account>(
|
||||||
|
'/energy/account/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountSummary() {
|
||||||
|
return requestClient.get<EnergyAccountApi.Summary>('/energy/account/summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountSimpleList() {
|
||||||
|
return requestClient.get<EnergyAccountApi.AccountSimple[]>('/energy/account/simple-list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rechargeAccount(customerId: number, amount: number, remark?: string) {
|
||||||
|
return requestClient.post('/energy/account/recharge', null, {
|
||||||
|
params: { customerId, amount, remark },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateThreshold(id: number, threshold: number) {
|
||||||
|
return requestClient.put('/energy/account/update-threshold', null, {
|
||||||
|
params: { id, threshold },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectAccountList(accountId: number) {
|
||||||
|
return requestClient.get<EnergyAccountApi.ProjectAccount[]>(
|
||||||
|
'/energy/account/project/list',
|
||||||
|
{ params: { accountId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFlowPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyAccountApi.Flow>>(
|
||||||
|
'/energy/account/flow/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportAccount(params: any) {
|
||||||
|
return requestClient.download('/energy/account/export-excel', { params });
|
||||||
|
}
|
||||||
131
apps/web-antd/src/api/energy/bill.ts
Normal file
131
apps/web-antd/src/api/energy/bill.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import type { EnergyHydrogenDetailApi } from './hydrogen-detail';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyBillApi {
|
||||||
|
export interface Bill {
|
||||||
|
id?: number;
|
||||||
|
billCode?: string;
|
||||||
|
energyType?: number;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
contractId?: number;
|
||||||
|
stationId?: number;
|
||||||
|
stationName?: string;
|
||||||
|
cooperationType?: number;
|
||||||
|
billPeriodStart?: string;
|
||||||
|
billPeriodEnd?: string;
|
||||||
|
receivableAmount?: number;
|
||||||
|
actualAmount?: number;
|
||||||
|
adjustmentAmount?: number;
|
||||||
|
paidAmount?: number;
|
||||||
|
totalQuantity?: number;
|
||||||
|
detailCount?: number;
|
||||||
|
status?: number;
|
||||||
|
auditStatus?: number;
|
||||||
|
submitStatus?: number;
|
||||||
|
paymentStatus?: number;
|
||||||
|
auditRemark?: string;
|
||||||
|
auditTime?: string;
|
||||||
|
generateTime?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateReq {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
customerId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
stationId?: number;
|
||||||
|
billPeriodStart?: string;
|
||||||
|
billPeriodEnd?: string;
|
||||||
|
energyType?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateResult {
|
||||||
|
total?: number;
|
||||||
|
billIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Adjustment {
|
||||||
|
id?: number;
|
||||||
|
billId?: number;
|
||||||
|
detailId?: number;
|
||||||
|
adjustmentType?: number;
|
||||||
|
amount?: number;
|
||||||
|
reason?: string;
|
||||||
|
attachmentUrls?: string;
|
||||||
|
operatorId?: number;
|
||||||
|
operatorName?: string;
|
||||||
|
operateTime?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBillPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyBillApi.Bill>>(
|
||||||
|
'/energy/bill/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBill(id: number) {
|
||||||
|
return requestClient.get<EnergyBillApi.Bill>(
|
||||||
|
'/energy/bill/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateBill(data: EnergyBillApi.GenerateReq) {
|
||||||
|
return requestClient.post<EnergyBillApi.GenerateResult>('/energy/bill/generate', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchGenerateByPeriod(billPeriod: string) {
|
||||||
|
return requestClient.post<Record<string, number>>(
|
||||||
|
'/energy/bill/batch-generate-by-period',
|
||||||
|
null,
|
||||||
|
{ params: { billPeriod } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBill(data: any) {
|
||||||
|
return requestClient.put('/energy/bill/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditBill(id: number, approved: boolean, remark?: string) {
|
||||||
|
return requestClient.post('/energy/bill/audit', null, {
|
||||||
|
params: { id, approved, remark },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteBill(id: number) {
|
||||||
|
return requestClient.delete('/energy/bill/delete', { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportBill(params: any) {
|
||||||
|
return requestClient.download('/energy/bill/export-excel', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBillDetailList(billId: number) {
|
||||||
|
return requestClient.get<EnergyHydrogenDetailApi.Detail[]>(
|
||||||
|
'/energy/bill/detail-list',
|
||||||
|
{ params: { billId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdjustmentList(billId: number) {
|
||||||
|
return requestClient.get<EnergyBillApi.Adjustment[]>(
|
||||||
|
'/energy/bill/adjustment/list',
|
||||||
|
{ params: { billId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAdjustment(data: EnergyBillApi.Adjustment) {
|
||||||
|
return requestClient.post<number>('/energy/bill/adjustment/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAdjustment(id: number) {
|
||||||
|
return requestClient.delete('/energy/bill/adjustment/delete', { params: { id } });
|
||||||
|
}
|
||||||
74
apps/web-antd/src/api/energy/hydrogen-detail.ts
Normal file
74
apps/web-antd/src/api/energy/hydrogen-detail.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyHydrogenDetailApi {
|
||||||
|
export interface Detail {
|
||||||
|
id?: number;
|
||||||
|
stationId?: number;
|
||||||
|
stationName?: string;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
contractId?: number;
|
||||||
|
vehicleId?: number;
|
||||||
|
plateNumber?: string;
|
||||||
|
hydrogenDate?: string;
|
||||||
|
hydrogenQuantity?: number;
|
||||||
|
costPrice?: number;
|
||||||
|
costAmount?: number;
|
||||||
|
customerPrice?: number;
|
||||||
|
customerAmount?: number;
|
||||||
|
auditStatus?: number;
|
||||||
|
deductionStatus?: number;
|
||||||
|
settlementStatus?: number;
|
||||||
|
billId?: number;
|
||||||
|
remark?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHydrogenDetailPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyHydrogenDetailApi.Detail>>(
|
||||||
|
'/energy/hydrogen-detail/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHydrogenDetail(id: number) {
|
||||||
|
return requestClient.get<EnergyHydrogenDetailApi.Detail>(
|
||||||
|
'/energy/hydrogen-detail/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateHydrogenDetail(data: EnergyHydrogenDetailApi.Detail) {
|
||||||
|
return requestClient.put('/energy/hydrogen-detail/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditHydrogenDetail(id: number, approved: boolean, remark?: string) {
|
||||||
|
return requestClient.post('/energy/hydrogen-detail/audit', null, {
|
||||||
|
params: { id, approved, remark },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchAuditReqVO {
|
||||||
|
ids: number[];
|
||||||
|
passed: boolean;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchAuditResultDTO {
|
||||||
|
total: number;
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
successIds: number[];
|
||||||
|
failIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchAuditHydrogenDetail(data: BatchAuditReqVO) {
|
||||||
|
return requestClient.post<BatchAuditResultDTO>('/energy/hydrogen-detail/batch-audit', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportHydrogenDetail(params: any) {
|
||||||
|
return requestClient.download('/energy/hydrogen-detail/export-excel', { params });
|
||||||
|
}
|
||||||
70
apps/web-antd/src/api/energy/hydrogen-record.ts
Normal file
70
apps/web-antd/src/api/energy/hydrogen-record.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyHydrogenRecordApi {
|
||||||
|
export interface Record {
|
||||||
|
id?: number;
|
||||||
|
stationId?: number;
|
||||||
|
stationName?: string;
|
||||||
|
plateNumber?: string;
|
||||||
|
hydrogenDate?: string;
|
||||||
|
hydrogenQuantity?: number;
|
||||||
|
unitPrice?: number;
|
||||||
|
amount?: number;
|
||||||
|
mileage?: number;
|
||||||
|
sourceType?: number;
|
||||||
|
matchStatus?: number;
|
||||||
|
vehicleId?: number;
|
||||||
|
customerId?: number;
|
||||||
|
uploadBatchNo?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHydrogenRecordPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyHydrogenRecordApi.Record>>(
|
||||||
|
'/energy/hydrogen-record/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHydrogenRecord(id: number) {
|
||||||
|
return requestClient.get<EnergyHydrogenRecordApi.Record>(
|
||||||
|
'/energy/hydrogen-record/get',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHydrogenRecord(data: EnergyHydrogenRecordApi.Record) {
|
||||||
|
return requestClient.post('/energy/hydrogen-record/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateHydrogenRecord(data: EnergyHydrogenRecordApi.Record) {
|
||||||
|
return requestClient.put('/energy/hydrogen-record/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteHydrogenRecord(id: number) {
|
||||||
|
return requestClient.delete('/energy/hydrogen-record/delete', { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportHydrogenRecord(params: any) {
|
||||||
|
return requestClient.download('/energy/hydrogen-record/export-excel', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportResultDTO {
|
||||||
|
total: number;
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
successIds: number[];
|
||||||
|
failIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importHydrogenRecords(data: FormData) {
|
||||||
|
return requestClient.post<ImportResultDTO>(
|
||||||
|
'/energy/hydrogen-record/import',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
43
apps/web-antd/src/api/energy/station-config.ts
Normal file
43
apps/web-antd/src/api/energy/station-config.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyStationConfigApi {
|
||||||
|
export interface Config {
|
||||||
|
id?: number;
|
||||||
|
stationId?: number;
|
||||||
|
stationName?: string;
|
||||||
|
autoDeduct?: boolean;
|
||||||
|
autoMatch?: boolean;
|
||||||
|
cooperationType?: number;
|
||||||
|
remark?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigSimple {
|
||||||
|
id: number;
|
||||||
|
stationId: number;
|
||||||
|
stationName: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationConfigPage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyStationConfigApi.Config>>(
|
||||||
|
'/energy/station-config/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationConfigSimpleList() {
|
||||||
|
return requestClient.get<EnergyStationConfigApi.ConfigSimple[]>(
|
||||||
|
'/energy/station-config/simple-list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStationConfig(data: EnergyStationConfigApi.Config) {
|
||||||
|
return requestClient.post('/energy/station-config/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateStationConfig(data: EnergyStationConfigApi.Config) {
|
||||||
|
return requestClient.put('/energy/station-config/update', data);
|
||||||
|
}
|
||||||
50
apps/web-antd/src/api/energy/station-price.ts
Normal file
50
apps/web-antd/src/api/energy/station-price.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace EnergyStationPriceApi {
|
||||||
|
export interface Price {
|
||||||
|
id?: number;
|
||||||
|
stationId?: number;
|
||||||
|
stationName?: string;
|
||||||
|
customerId?: number;
|
||||||
|
customerName?: string;
|
||||||
|
costPrice?: number;
|
||||||
|
customerPrice?: number;
|
||||||
|
effectiveDate?: string;
|
||||||
|
expiryDate?: string;
|
||||||
|
status?: number;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationPricePage(params: PageParam) {
|
||||||
|
return requestClient.get<PageResult<EnergyStationPriceApi.Price>>(
|
||||||
|
'/energy/station-price/page',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStationPrice(data: EnergyStationPriceApi.Price) {
|
||||||
|
return requestClient.post('/energy/station-price/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateStationPrice(data: EnergyStationPriceApi.Price) {
|
||||||
|
return requestClient.put('/energy/station-price/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteStationPrice(id: number) {
|
||||||
|
return requestClient.delete('/energy/station-price/delete', { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportResult {
|
||||||
|
total: number;
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchImportPrice(data: FormData) {
|
||||||
|
return requestClient.post<ImportResult>('/energy/station-price/batch-import', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
}
|
||||||
54
apps/web-antd/src/router/routes/modules/energy.ts
Normal file
54
apps/web-antd/src/router/routes/modules/energy.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/energy',
|
||||||
|
name: 'EnergyCenter',
|
||||||
|
meta: {
|
||||||
|
title: '能源管理',
|
||||||
|
icon: 'lucide:zap',
|
||||||
|
keepAlive: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'hydrogen-record',
|
||||||
|
name: 'EnergyHydrogenRecord',
|
||||||
|
component: () => import('#/views/energy/hydrogen-record/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'hydrogen-detail',
|
||||||
|
name: 'EnergyHydrogenDetail',
|
||||||
|
component: () => import('#/views/energy/hydrogen-detail/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bill',
|
||||||
|
name: 'EnergyBill',
|
||||||
|
component: () => import('#/views/energy/bill/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: String.raw`bill/detail/:id(\d+)`,
|
||||||
|
name: 'EnergyBillDetail',
|
||||||
|
meta: { title: '账单详情', activePath: '/energy/bill' },
|
||||||
|
component: () => import('#/views/energy/bill/detail.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'account',
|
||||||
|
name: 'EnergyAccount',
|
||||||
|
component: () => import('#/views/energy/account/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'station-price',
|
||||||
|
name: 'EnergyStationPrice',
|
||||||
|
component: () => import('#/views/energy/station-price/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'station-config',
|
||||||
|
name: 'EnergyStationConfig',
|
||||||
|
component: () => import('#/views/energy/station-config/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
23
apps/web-antd/src/utils/table.ts
Normal file
23
apps/web-antd/src/utils/table.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
type VxeColumn = NonNullable<VxeTableGridOptions['columns']>[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成操作列配置,根据内联按钮数量和是否有下拉菜单自动计算宽度
|
||||||
|
* @param inlineCount 内联按钮数量(最多 3 个)
|
||||||
|
* @param hasDropdown 是否有"更多"下拉菜单
|
||||||
|
* @param slotName 插槽名称,默认 'actions'
|
||||||
|
*/
|
||||||
|
export function useActionColumn(
|
||||||
|
inlineCount: number,
|
||||||
|
hasDropdown = false,
|
||||||
|
slotName = 'actions',
|
||||||
|
): VxeColumn {
|
||||||
|
const width = Math.max(80, inlineCount * 60 + (hasDropdown ? 60 : 0));
|
||||||
|
return {
|
||||||
|
title: '操作',
|
||||||
|
width,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: slotName },
|
||||||
|
};
|
||||||
|
}
|
||||||
290
apps/web-antd/src/views/asset/components/InspectionForm.vue
Normal file
290
apps/web-antd/src/views/asset/components/InspectionForm.vue
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { InspectionApi } from '#/api/asset/inspection';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
CollapsePanel,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
RadioButton,
|
||||||
|
RadioGroup,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Upload,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
completeInspection,
|
||||||
|
getInspectionRecord,
|
||||||
|
updateInspectionRecordItem,
|
||||||
|
} from '#/api/asset/inspection';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
recordId: number;
|
||||||
|
readonly?: boolean;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const record = ref<InspectionApi.RecordDetail>();
|
||||||
|
const inspectorName = ref('');
|
||||||
|
|
||||||
|
// Group items by category
|
||||||
|
const groupedItems = computed(() => {
|
||||||
|
if (!record.value?.items) return [];
|
||||||
|
const map = new Map<string, InspectionApi.RecordItem[]>();
|
||||||
|
for (const item of record.value.items) {
|
||||||
|
const list = map.get(item.category) || [];
|
||||||
|
list.push(item);
|
||||||
|
map.set(item.category, list);
|
||||||
|
}
|
||||||
|
return [...map.entries()].map(([category, items]) => ({
|
||||||
|
category,
|
||||||
|
items,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Active collapse keys (all open by default)
|
||||||
|
const activeKeys = computed(() =>
|
||||||
|
groupedItems.value.map((g) => g.category),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Whether inspection is already completed
|
||||||
|
const isCompleted = computed(() => record.value?.status === 2);
|
||||||
|
|
||||||
|
// Load inspection record
|
||||||
|
async function loadRecord() {
|
||||||
|
if (!props.recordId) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
record.value = await getInspectionRecord(props.recordId);
|
||||||
|
inspectorName.value = record.value.inspectorName || '';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.recordId,
|
||||||
|
() => loadRecord(),
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save single item on change
|
||||||
|
async function handleItemChange(item: InspectionApi.RecordItem) {
|
||||||
|
if (props.readonly || isCompleted.value) return;
|
||||||
|
try {
|
||||||
|
await updateInspectionRecordItem({
|
||||||
|
id: item.id,
|
||||||
|
result: item.result,
|
||||||
|
value: item.value,
|
||||||
|
remark: item.remark,
|
||||||
|
imageUrls: item.imageUrls,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
message.error('保存检查项失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete inspection
|
||||||
|
async function handleComplete() {
|
||||||
|
if (!inspectorName.value) {
|
||||||
|
message.warning('请输入验车人姓名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await completeInspection(props.recordId, inspectorName.value);
|
||||||
|
message.success('验车完成');
|
||||||
|
await loadRecord();
|
||||||
|
props.onComplete?.();
|
||||||
|
} catch {
|
||||||
|
message.error('完成验车失败');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result options for checkbox type
|
||||||
|
const resultOptions = [
|
||||||
|
{ label: '合格', value: 1 },
|
||||||
|
{ label: '不合格', value: 2 },
|
||||||
|
{ label: '不适用', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parse image URLs
|
||||||
|
function parseImageUrls(urls?: string): string[] {
|
||||||
|
if (!urls) return [];
|
||||||
|
return urls.split(',').filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image upload (simplified - assumes backend returns URL)
|
||||||
|
function handleImageUpload(item: InspectionApi.RecordItem, info: any) {
|
||||||
|
if (info.file.status === 'done' && info.file.response) {
|
||||||
|
const url = info.file.response.data;
|
||||||
|
const urls = parseImageUrls(item.imageUrls);
|
||||||
|
urls.push(url);
|
||||||
|
item.imageUrls = urls.join(',');
|
||||||
|
handleItemChange(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if inputs should be disabled
|
||||||
|
const isDisabled = computed(() => props.readonly || isCompleted.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<div class="inspection-form">
|
||||||
|
<!-- Inspector name -->
|
||||||
|
<div class="mb-4 flex items-center gap-4">
|
||||||
|
<label class="font-medium">验车人:</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="inspectorName"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
placeholder="请输入验车人姓名"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
<span v-if="record?.inspectionTime" class="text-gray-500">
|
||||||
|
验车时间:{{ record.inspectionTime }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isCompleted"
|
||||||
|
class="rounded bg-green-100 px-2 py-1 text-sm text-green-700"
|
||||||
|
>
|
||||||
|
已完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inspection items grouped by category -->
|
||||||
|
<Collapse :active-key="activeKeys" :bordered="true">
|
||||||
|
<CollapsePanel
|
||||||
|
v-for="group in groupedItems"
|
||||||
|
:key="group.category"
|
||||||
|
:header="group.category"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.id"
|
||||||
|
class="rounded border border-gray-200 p-3"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-start justify-between">
|
||||||
|
<span class="font-medium">{{ item.itemName }}</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ item.itemCode }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-start gap-4">
|
||||||
|
<!-- Checkbox type: radio group -->
|
||||||
|
<div v-if="item.inputType === 'checkbox'" class="flex-1">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="item.result"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
button-style="solid"
|
||||||
|
size="small"
|
||||||
|
@change="handleItemChange(item)"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
v-for="opt in resultOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</RadioButton>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Number type: input number -->
|
||||||
|
<div v-else-if="item.inputType === 'number'" class="flex-1">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="item.value"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
placeholder="请输入数值"
|
||||||
|
size="small"
|
||||||
|
style="width: 200px"
|
||||||
|
@change="handleItemChange(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text type: input -->
|
||||||
|
<div v-else class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model:value="item.value"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
placeholder="请输入"
|
||||||
|
size="small"
|
||||||
|
style="width: 300px"
|
||||||
|
@change="handleItemChange(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remark -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<Input
|
||||||
|
v-model:value="item.remark"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
placeholder="备注"
|
||||||
|
size="small"
|
||||||
|
@change="handleItemChange(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Images -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<Space :size="8" wrap>
|
||||||
|
<Image
|
||||||
|
v-for="(url, idx) in parseImageUrls(item.imageUrls)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
:width="60"
|
||||||
|
:height="60"
|
||||||
|
style="object-fit: cover; border-radius: 4px"
|
||||||
|
/>
|
||||||
|
<Upload
|
||||||
|
v-if="!isDisabled"
|
||||||
|
:show-upload-list="false"
|
||||||
|
action="/admin-api/infra/file/upload"
|
||||||
|
name="file"
|
||||||
|
accept="image/*"
|
||||||
|
@change="(info: any) => handleImageUpload(item, info)"
|
||||||
|
>
|
||||||
|
<Button size="small" type="dashed">上传图片</Button>
|
||||||
|
</Upload>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsePanel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<!-- Overall remark -->
|
||||||
|
<div v-if="record" class="mt-4">
|
||||||
|
<label class="mb-1 block font-medium">整体备注:</label>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="record.remark"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入整体备注"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Complete button -->
|
||||||
|
<div v-if="!isDisabled" class="mt-4 text-right">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleComplete"
|
||||||
|
>
|
||||||
|
完成验车
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</template>
|
||||||
368
apps/web-antd/src/views/asset/customer/data.ts
Normal file
368
apps/web-antd/src/views/asset/customer/data.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetCustomerApi } from '#/api/asset/customer';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 基本信息 */
|
||||||
|
export function useBasicFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerCode',
|
||||||
|
label: '客户编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户编码',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入客户编码' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'coopStatus',
|
||||||
|
label: '合作状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择合作状态',
|
||||||
|
options: [
|
||||||
|
{ label: '已合作', value: '已合作' },
|
||||||
|
{ label: '终止合作', value: '终止合作' },
|
||||||
|
{ label: '洽谈中', value: '洽谈中' },
|
||||||
|
{ label: '合约过期', value: '合约过期' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请选择合作状态' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入客户名称' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'province',
|
||||||
|
label: '省份',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入省份',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入省份' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'city',
|
||||||
|
label: '城市',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入城市',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入城市' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'address',
|
||||||
|
label: '详细地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入详细地址',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入详细地址' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择区域',
|
||||||
|
options: [
|
||||||
|
{ label: '华北', value: '华北' },
|
||||||
|
{ label: '华东', value: '华东' },
|
||||||
|
{ label: '华南', value: '华南' },
|
||||||
|
{ label: '华中', value: '华中' },
|
||||||
|
{ label: '东北', value: '东北' },
|
||||||
|
{ label: '西南', value: '西南' },
|
||||||
|
{ label: '西北', value: '西北' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contact',
|
||||||
|
label: '联系人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系人',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入联系人' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactMobile',
|
||||||
|
label: '联系手机',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系手机',
|
||||||
|
},
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: '请输入联系手机' })
|
||||||
|
.regex(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactPhone',
|
||||||
|
label: '联系电话',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系电话',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'creditCodeOrId',
|
||||||
|
label: '统一社会信用代码/身份证',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入统一社会信用代码或身份证号',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入统一社会信用代码或身份证号' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'businessManagers',
|
||||||
|
label: '业务负责人',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入业务负责人',
|
||||||
|
mode: 'tags',
|
||||||
|
},
|
||||||
|
rules: z.array(z.string()).min(1, { message: '请输入业务负责人' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 开票信息 */
|
||||||
|
export function useInvoiceFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'taxId',
|
||||||
|
label: '税号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入税号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'invoiceAddress',
|
||||||
|
label: '开票地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开票地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'invoicePhone',
|
||||||
|
label: '开票电话',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开票电话',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'account',
|
||||||
|
label: '银行账号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入银行账号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'openingBank',
|
||||||
|
label: '开户行',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开户行',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mailingAddress',
|
||||||
|
label: '邮寄地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮寄地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'customerCode',
|
||||||
|
label: '客户编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'coopStatus',
|
||||||
|
label: '合作状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择合作状态',
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ label: '已合作', value: '已合作' },
|
||||||
|
{ label: '终止合作', value: '终止合作' },
|
||||||
|
{ label: '洽谈中', value: '洽谈中' },
|
||||||
|
{ label: '合约过期', value: '合约过期' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择区域',
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ label: '华北', value: '华北' },
|
||||||
|
{ label: '华东', value: '华东' },
|
||||||
|
{ label: '华南', value: '华南' },
|
||||||
|
{ label: '华中', value: '华中' },
|
||||||
|
{ label: '东北', value: '东北' },
|
||||||
|
{ label: '西南', value: '西南' },
|
||||||
|
{ label: '西北', value: '西北' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'city',
|
||||||
|
label: '城市',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入城市',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 60, fixed: 'left' },
|
||||||
|
{
|
||||||
|
field: 'customerCode',
|
||||||
|
title: '客户编码',
|
||||||
|
minWidth: 120,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'coopStatus',
|
||||||
|
title: '合作状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
title: '客户名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'region',
|
||||||
|
title: '区域',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'city',
|
||||||
|
title: '城市',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'address',
|
||||||
|
title: '地址',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contact',
|
||||||
|
title: '联系人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactMobile',
|
||||||
|
title: '联系手机',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactPhone',
|
||||||
|
title: '联系电话',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
title: '邮箱',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creditCodeOrId',
|
||||||
|
title: '统一社会信用代码/身份证',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'businessManagers',
|
||||||
|
title: '业务负责人',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: ({ cellValue }: { cellValue: string[] }) =>
|
||||||
|
Array.isArray(cellValue) ? cellValue.join(', ') : cellValue || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creator',
|
||||||
|
title: '创建人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
176
apps/web-antd/src/views/asset/delivery-order/data.ts
Normal file
176
apps/web-antd/src/views/asset/delivery-order/data.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
// ========== 新增/编辑表单 ==========
|
||||||
|
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,
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
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 },
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 历史记录列表列 ==========
|
||||||
|
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 },
|
||||||
|
useActionColumn(3, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 兼容旧引用 ==========
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return usePendingColumns();
|
||||||
|
}
|
||||||
201
apps/web-antd/src/views/asset/delivery-order/index.vue
Normal file
201
apps/web-antd/src/views/asset/delivery-order/index.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetDeliveryOrderApi } from '#/api/asset/delivery-order';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message, Tabs } from 'ant-design-vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
completeDeliveryOrder,
|
||||||
|
deleteDeliveryOrder,
|
||||||
|
getDeliveryOrderPage,
|
||||||
|
} from '#/api/asset/delivery-order';
|
||||||
|
import { createReturnOrderFromDelivery } from '#/api/asset/return-order';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useGridFormSchema,
|
||||||
|
useHistoryColumns,
|
||||||
|
usePendingColumns,
|
||||||
|
} from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = ref('pending');
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(row: AssetDeliveryOrderApi.DeliveryOrder) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: AssetDeliveryOrderApi.DeliveryOrder) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.orderCode]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDeliveryOrder(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.orderCode]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleComplete(row: AssetDeliveryOrderApi.DeliveryOrder) {
|
||||||
|
await confirm('确认完成交车?完成后将更新任务车辆状态。');
|
||||||
|
await completeDeliveryOrder(row.id!);
|
||||||
|
message.success('交车已完成');
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 还车:从交车单创建还车单
|
||||||
|
async function handleReturn(row: AssetDeliveryOrderApi.DeliveryOrder) {
|
||||||
|
await confirm('确认为该交车单创建还车单?');
|
||||||
|
const vehicleIds = (row.vehicles || []).map((v: any) => v.vehicleId).filter(Boolean);
|
||||||
|
if (vehicleIds.length === 0) {
|
||||||
|
message.warning('该交车单没有关联车辆');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await createReturnOrderFromDelivery(row.id!, vehicleIds);
|
||||||
|
message.success('还车单已创建');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换车:跳转到替换车新增页面,带上参数
|
||||||
|
function handleReplacement(row: AssetDeliveryOrderApi.DeliveryOrder) {
|
||||||
|
router.push({
|
||||||
|
path: '/asset/vehicle-replacement',
|
||||||
|
query: {
|
||||||
|
contractId: String(row.contractId || ''),
|
||||||
|
deliveryOrderId: String(row.id || ''),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: usePendingColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getDeliveryOrderPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
status: activeTab.value === 'history' ? 1 : 0,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<AssetDeliveryOrderApi.DeliveryOrder>,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleTabChange(key: string) {
|
||||||
|
activeTab.value = key;
|
||||||
|
const columns =
|
||||||
|
key === 'history' ? useHistoryColumns() : usePendingColumns();
|
||||||
|
gridApi.grid?.reloadColumn(columns as any);
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Tabs :active-key="activeTab" @update:active-key="handleTabChange">
|
||||||
|
<Tabs.TabPane key="pending" tab="待处理" />
|
||||||
|
<Tabs.TabPane key="history" tab="历史记录" />
|
||||||
|
</Tabs>
|
||||||
|
<Grid :table-title="activeTab === 'history' ? '历史记录' : '待处理'">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
v-if="activeTab === 'pending'"
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '交车',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['asset:delivery-order:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
v-if="activeTab === 'pending'"
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '查看',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['asset:delivery-order:query'],
|
||||||
|
onClick: () => message.info('查看功能开发中'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '交车',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['asset:delivery-order:create'],
|
||||||
|
onClick: () => handleCreate(),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<TableAction
|
||||||
|
v-else
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '查看',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
auth: ['asset:delivery-order:query'],
|
||||||
|
onClick: () => handleEdit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '还车',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['asset:return-order:create'],
|
||||||
|
onClick: () => handleReturn(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '替换车',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['asset:vehicle-replacement:create'],
|
||||||
|
onClick: () => handleReplacement(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
428
apps/web-antd/src/views/asset/delivery-order/modules/form.vue
Normal file
428
apps/web-antd/src/views/asset/delivery-order/modules/form.vue
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AssetDeliveryOrderApi } from '#/api/asset/delivery-order';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import InspectionForm from '../../components/InspectionForm.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createDeliveryOrder,
|
||||||
|
getDeliveryOrder,
|
||||||
|
updateDeliveryOrder,
|
||||||
|
} from '#/api/asset/delivery-order';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<AssetDeliveryOrderApi.DeliveryOrder>();
|
||||||
|
const vehicles = ref<AssetDeliveryOrderApi.DeliveryOrderVehicle[]>([]);
|
||||||
|
|
||||||
|
// 司机信息
|
||||||
|
const driverInfo = ref({
|
||||||
|
driverName: '',
|
||||||
|
driverIdCard: '',
|
||||||
|
driverPhone: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 费用信息
|
||||||
|
const costList = ref<{ costName: string; amount: number; remark: string }[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 交检清单数据
|
||||||
|
const inspectionData = ref<
|
||||||
|
Record<string, { checked: boolean; remark: string }>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const INSPECTION_ITEMS = [
|
||||||
|
{
|
||||||
|
category: '车辆附件',
|
||||||
|
items: [
|
||||||
|
'行驶证',
|
||||||
|
'营运证',
|
||||||
|
'通行证',
|
||||||
|
'加氢证',
|
||||||
|
'登记证',
|
||||||
|
'保险卡',
|
||||||
|
'车钥匙',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '外观检查',
|
||||||
|
items: ['车身完好', '挡风玻璃', '后视镜', '车灯完好', '轮胎正常'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '车内检查',
|
||||||
|
items: [
|
||||||
|
'仪表盘正常',
|
||||||
|
'空调功能',
|
||||||
|
'安全带',
|
||||||
|
'喇叭功能',
|
||||||
|
'雨刮器',
|
||||||
|
'车窗升降',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '动力系统',
|
||||||
|
items: ['发动机正常', '燃料电池状态', '制动功能', '转向正常'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function initInspectionData() {
|
||||||
|
const data: Record<string, { checked: boolean; remark: string }> = {};
|
||||||
|
for (const cat of INSPECTION_ITEMS) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
data[`${cat.category}-${item}`] = { checked: true, remark: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inspectionData.value = data;
|
||||||
|
}
|
||||||
|
initInspectionData();
|
||||||
|
|
||||||
|
const inspectionTableData = computed(() => {
|
||||||
|
const rows: any[] = [];
|
||||||
|
for (const cat of INSPECTION_ITEMS) {
|
||||||
|
for (let i = 0; i < cat.items.length; i++) {
|
||||||
|
rows.push({
|
||||||
|
key: `${cat.category}-${cat.items[i]}`,
|
||||||
|
category: cat.category,
|
||||||
|
item: cat.items[i]!,
|
||||||
|
rowSpan: i === 0 ? cat.items.length : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inspectionColumns = [
|
||||||
|
{
|
||||||
|
title: '类别',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 120,
|
||||||
|
customCell: (_: any, index: number) => {
|
||||||
|
const row = inspectionTableData.value[index];
|
||||||
|
return { rowSpan: row?.rowSpan ?? 1 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '检查项目', dataIndex: 'item', key: 'item', width: 160 },
|
||||||
|
{ title: '交车方', key: 'deliver', width: 120 },
|
||||||
|
{ title: '接车方', key: 'receive', width: 120 },
|
||||||
|
{ title: '备注', key: 'remark', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['交车单'])
|
||||||
|
: $t('ui.actionTitle.create', ['交车单']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [BasicForm, basicFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 150,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-6',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 车辆明细列定义
|
||||||
|
const vehicleColumns = [
|
||||||
|
{ title: '序号', key: 'index', width: 60 },
|
||||||
|
{ title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 },
|
||||||
|
{ title: '型号', dataIndex: 'model', key: 'model', width: 100 },
|
||||||
|
{ title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 },
|
||||||
|
{ title: 'VIN码', dataIndex: 'vin', key: 'vin', width: 180 },
|
||||||
|
{ title: '停车场', dataIndex: 'parkingLot', key: 'parkingLot', width: 120 },
|
||||||
|
{ title: '交车里程(km)', key: 'mileage', width: 130 },
|
||||||
|
{ title: '交车氢量(%)', key: 'hydrogenLevel', width: 120 },
|
||||||
|
{ title: '交车电量(%)', key: 'batteryLevel', width: 120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 费用列定义
|
||||||
|
const costColumns = [
|
||||||
|
{ title: '序号', key: 'index', width: 60 },
|
||||||
|
{ title: '费用名称', key: 'costName', width: 200 },
|
||||||
|
{ title: '金额(元)', key: 'amount', width: 150 },
|
||||||
|
{ title: '备注', key: 'costRemark', width: 200 },
|
||||||
|
{ title: '操作', key: 'costAction', width: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleAddCost() {
|
||||||
|
costList.value.push({ costName: '', amount: 0, remark: '' });
|
||||||
|
}
|
||||||
|
function handleDeleteCost(index: number) {
|
||||||
|
costList.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const costTotal = computed(() => {
|
||||||
|
return costList.value.reduce((sum, c) => sum + (c.amount || 0), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await basicFormApi.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
const data =
|
||||||
|
(await basicFormApi.getValues()) as AssetDeliveryOrderApi.DeliveryOrder;
|
||||||
|
data.vehicles = vehicles.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDeliveryOrder(data)
|
||||||
|
: createDeliveryOrder(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
vehicles.value = [];
|
||||||
|
costList.value = [];
|
||||||
|
driverInfo.value = { driverName: '', driverIdCard: '', driverPhone: '' };
|
||||||
|
initInspectionData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<AssetDeliveryOrderApi.DeliveryOrder>();
|
||||||
|
if (!data || !data.id) return;
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getDeliveryOrder(data.id);
|
||||||
|
await basicFormApi.setValues(formData.value);
|
||||||
|
vehicles.value = formData.value.vehicles || [];
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-4/5" style="max-width: 1400px">
|
||||||
|
<div class="space-y-4" style="max-height: 75vh; overflow-y: auto">
|
||||||
|
<!-- Card 1: 交车基本信息 -->
|
||||||
|
<Card title="交车基本信息" size="small">
|
||||||
|
<BasicForm />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 2: 司机信息 -->
|
||||||
|
<Card title="司机信息" size="small">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">司机姓名</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="driverInfo.driverName"
|
||||||
|
placeholder="请输入司机姓名"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">司机身份证</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="driverInfo.driverIdCard"
|
||||||
|
placeholder="请输入司机身份证号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">司机手机号</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="driverInfo.driverPhone"
|
||||||
|
placeholder="请输入司机手机号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 3: 交车车辆明细 -->
|
||||||
|
<Card title="交车车辆明细" size="small">
|
||||||
|
<Table
|
||||||
|
:columns="vehicleColumns"
|
||||||
|
:data-source="vehicles"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ x: 1200 }"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'index'">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'mileage'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.mileage"
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="里程"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'hydrogenLevel'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.hydrogenLevel"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="氢量%"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'batteryLevel'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.batteryLevel"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="电量%"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 4: 交检清单 -->
|
||||||
|
<Card title="交检清单" size="small">
|
||||||
|
<!-- 新版:使用验车记录组件 -->
|
||||||
|
<template v-if="formData?.inspectionRecordId">
|
||||||
|
<InspectionForm :record-id="formData.inspectionRecordId" :readonly="false" />
|
||||||
|
</template>
|
||||||
|
<!-- 旧版:硬编码检查单(兼容旧数据) -->
|
||||||
|
<template v-else>
|
||||||
|
<Table
|
||||||
|
:columns="inspectionColumns"
|
||||||
|
:data-source="inspectionTableData"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'deliver'">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="inspectionData[record.key]!.checked"
|
||||||
|
checked-children="✓"
|
||||||
|
un-checked-children="×"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'receive'">
|
||||||
|
<Switch
|
||||||
|
checked-children="✓"
|
||||||
|
un-checked-children="×"
|
||||||
|
size="small"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'remark'">
|
||||||
|
<Input
|
||||||
|
v-model:value="inspectionData[record.key]!.remark"
|
||||||
|
placeholder="备注"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 5: 费用信息 -->
|
||||||
|
<Card title="费用信息" size="small">
|
||||||
|
<Space direction="vertical" style="width: 100%" :size="12">
|
||||||
|
<div
|
||||||
|
v-if="costList.length > 0"
|
||||||
|
class="rounded bg-blue-50 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
费用合计:<span class="font-semibold text-red-500"
|
||||||
|
>¥{{ costTotal.toFixed(2) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
:columns="costColumns"
|
||||||
|
:data-source="costList"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'index'">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'costName'">
|
||||||
|
<Input
|
||||||
|
v-model:value="record.costName"
|
||||||
|
placeholder="请输入费用名称"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'amount'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.amount"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
placeholder="金额"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'costRemark'">
|
||||||
|
<Input
|
||||||
|
v-model:value="record.remark"
|
||||||
|
placeholder="备注"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'costAction'">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteCost(index)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button type="dashed" block @click="handleAddCost">
|
||||||
|
+ 添加费用项
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
169
apps/web-antd/src/views/asset/delivery-task/data.ts
Normal file
169
apps/web-antd/src/views/asset/delivery-task/data.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
// ========== 新增/编辑表单 ==========
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: { triggerFields: [''], show: () => false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contractId',
|
||||||
|
label: '选择项目名称',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择或输入项目名称',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/asset/contract').then((m) =>
|
||||||
|
m.getSimpleContractList(),
|
||||||
|
),
|
||||||
|
labelField: 'projectName',
|
||||||
|
valueField: 'id',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: z.number().min(1, { message: '请选择项目名称' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contractCode',
|
||||||
|
label: '合同编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '选择项目后自动填充', disabled: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '选择项目后自动填充', disabled: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deliveryRegion',
|
||||||
|
label: '交车区域',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '选择项目后自动填充', disabled: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deliveryLocation',
|
||||||
|
label: '交车地点',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '选择项目后自动填充', disabled: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'expectedDeliveryDateStart',
|
||||||
|
label: '预计交车开始日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'expectedDeliveryDateEnd',
|
||||||
|
label: '预计交车结束日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择日期(可选)',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'billingStartDate',
|
||||||
|
label: '开始计费日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择开始计费日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'needReturn',
|
||||||
|
label: '已交车辆是否需要还车',
|
||||||
|
component: 'Checkbox',
|
||||||
|
renderComponentContent: () => ({ default: () => '不需要还车' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 搜索表单 ==========
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'taskCode',
|
||||||
|
label: '交车任务编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入交车任务编码' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contractCode',
|
||||||
|
label: '合同编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入合同编码' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入客户名称' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 主表列配置(按合同分组) ==========
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'expand', width: 60, fixed: 'left' },
|
||||||
|
{ field: 'contractCode', title: '合同编码', minWidth: 220, fixed: 'left' },
|
||||||
|
{ field: 'projectName', title: '项目名称', minWidth: 150 },
|
||||||
|
{ field: 'customerName', title: '客户名称', minWidth: 150 },
|
||||||
|
{ field: 'businessDeptName', title: '业务部门', minWidth: 120 },
|
||||||
|
{ field: 'businessManagerName', title: '业务负责人', minWidth: 100 },
|
||||||
|
{ field: 'startDate', title: '合同生效日期', minWidth: 120 },
|
||||||
|
{ field: 'endDate', title: '合同结束日期', minWidth: 120 },
|
||||||
|
useActionColumn(1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 子表列配置(交车任务明细) ==========
|
||||||
|
export function useSubGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ field: 'taskCode', title: '交车任务编码', minWidth: 260 },
|
||||||
|
{
|
||||||
|
field: 'taskStatus',
|
||||||
|
title: '任务状态',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) => cellValue === 1 ? '挂起' : '激活',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deliveryStatus',
|
||||||
|
title: '交车状态',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) => cellValue === 1 ? '已交车' : '未交车',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'expectedDeliveryDateStart',
|
||||||
|
title: '预计交车日期',
|
||||||
|
minWidth: 200,
|
||||||
|
formatter: ({ row }: { row: any }) => {
|
||||||
|
const start = row.expectedDeliveryDateStart || '';
|
||||||
|
const end = row.expectedDeliveryDateEnd || '';
|
||||||
|
return end ? `${start} 至 ${end}` : start;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'vehicleCount', title: '交车数量', minWidth: 100, align: 'center' },
|
||||||
|
{ field: 'billingStartDate', title: '开始计费日期', minWidth: 120 },
|
||||||
|
{ field: 'creator', title: '创建人', minWidth: 100 },
|
||||||
|
{ field: 'createTime', title: '创建时间', minWidth: 120, formatter: 'formatDateTime' },
|
||||||
|
useActionColumn(3, true, 'subActions'),
|
||||||
|
];
|
||||||
|
}
|
||||||
139
apps/web-antd/src/views/asset/inspection-template/data.ts
Normal file
139
apps/web-antd/src/views/asset/inspection-template/data.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
export const BIZ_TYPE_OPTIONS = [
|
||||||
|
{ label: '备车', value: 1 },
|
||||||
|
{ label: '交车', value: 2 },
|
||||||
|
{ label: '还车', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STATUS_OPTIONS = [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INPUT_TYPE_OPTIONS = [
|
||||||
|
{ label: '勾选(合格/不合格)', value: 'checkbox' },
|
||||||
|
{ label: '数值', value: 'number' },
|
||||||
|
{ label: '文本', value: 'text' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入模板名称' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'bizType',
|
||||||
|
label: '适用业务',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择',
|
||||||
|
options: BIZ_TYPE_OPTIONS,
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择',
|
||||||
|
options: STATUS_OPTIONS,
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 60, fixed: 'left' },
|
||||||
|
{ field: 'code', title: '模板编码', minWidth: 140, fixed: 'left' },
|
||||||
|
{ field: 'name', title: '模板名称', minWidth: 180 },
|
||||||
|
{
|
||||||
|
field: 'bizType',
|
||||||
|
title: '适用业务',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter({ cellValue }: any) {
|
||||||
|
return BIZ_TYPE_OPTIONS.find((o) => o.value === cellValue)?.label ?? cellValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'vehicleType', title: '适用车型', minWidth: 120 },
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter({ cellValue }: any) {
|
||||||
|
return cellValue === 1 ? '启用' : '禁用';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 150 },
|
||||||
|
{ field: 'createTime', title: '创建时间', minWidth: 180, formatter: 'formatDateTime' },
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模板基本信息表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: { triggerFields: [''], show: () => false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'code',
|
||||||
|
label: '模板编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入模板编码' },
|
||||||
|
rules: z.string().min(1, { message: '请输入模板编码' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '模板名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入模板名称' },
|
||||||
|
rules: z.string().min(1, { message: '请输入模板名称' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'bizType',
|
||||||
|
label: '适用业务',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择适用业务',
|
||||||
|
options: BIZ_TYPE_OPTIONS,
|
||||||
|
},
|
||||||
|
rules: z.number({ message: '请选择适用业务' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'vehicleType',
|
||||||
|
label: '适用车型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '留空表示通用模板' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: STATUS_OPTIONS,
|
||||||
|
},
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: { placeholder: '请输入备注', rows: 2 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
124
apps/web-antd/src/views/asset/inspection-template/index.vue
Normal file
124
apps/web-antd/src/views/asset/inspection-template/index.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { InspectionApi } from '#/api/asset/inspection';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteInspectionTemplate,
|
||||||
|
getInspectionTemplatePage,
|
||||||
|
} from '#/api/asset/inspection';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(row: InspectionApi.Template) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: InspectionApi.Template) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteInspectionTemplate(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getInspectionTemplatePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<InspectionApi.Template>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="验车模板">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['模板']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['asset:inspection-template:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['asset:inspection-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['asset:inspection-template:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { InspectionApi } from '#/api/asset/inspection';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createInspectionTemplate,
|
||||||
|
getInspectionTemplate,
|
||||||
|
updateInspectionTemplate,
|
||||||
|
} from '#/api/asset/inspection';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { INPUT_TYPE_OPTIONS, useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<InspectionApi.Template>();
|
||||||
|
const items = ref<InspectionApi.TemplateItem[]>([]);
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['验车模板'])
|
||||||
|
: $t('ui.actionTitle.create', ['验车模板']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [BasicForm, basicFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-4',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查项列定义
|
||||||
|
const itemColumns = [
|
||||||
|
{ title: '序号', key: 'index', width: 60 },
|
||||||
|
{ title: '分类', key: 'category', width: 140 },
|
||||||
|
{ title: '检查项名称', key: 'itemName', width: 180 },
|
||||||
|
{ title: '检查项编码', key: 'itemCode', width: 140 },
|
||||||
|
{ title: '输入类型', key: 'inputType', width: 160 },
|
||||||
|
{ title: '排序', key: 'sort', width: 80 },
|
||||||
|
{ title: '必填', key: 'required', width: 80 },
|
||||||
|
{ title: '操作', key: 'action', width: 80, fixed: 'right' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleAddItem() {
|
||||||
|
items.value.push({
|
||||||
|
category: '',
|
||||||
|
itemName: '',
|
||||||
|
itemCode: '',
|
||||||
|
inputType: 'checkbox',
|
||||||
|
sort: items.value.length,
|
||||||
|
required: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteItem(index: number) {
|
||||||
|
items.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await basicFormApi.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
if (items.value.length === 0) {
|
||||||
|
message.warning('请至少添加一个检查项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验检查项必填字段
|
||||||
|
for (let i = 0; i < items.value.length; i++) {
|
||||||
|
const item = items.value[i]!;
|
||||||
|
if (!item.category || !item.itemName || !item.itemCode) {
|
||||||
|
message.warning(`第 ${i + 1} 项的分类、名称、编码不能为空`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
const data = (await basicFormApi.getValues()) as InspectionApi.Template;
|
||||||
|
data.items = items.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateInspectionTemplate(data)
|
||||||
|
: createInspectionTemplate(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
items.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<InspectionApi.Template>();
|
||||||
|
if (!data || !data.id) return;
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getInspectionTemplate(data.id);
|
||||||
|
await basicFormApi.setValues(formData.value);
|
||||||
|
items.value = formData.value.items || [];
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-4/5" style="max-width: 1200px">
|
||||||
|
<div class="space-y-4" style="max-height: 75vh; overflow-y: auto">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<Card title="基本信息" size="small">
|
||||||
|
<BasicForm />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 检查项列表 -->
|
||||||
|
<Card title="检查项" size="small">
|
||||||
|
<Table
|
||||||
|
:columns="itemColumns"
|
||||||
|
:data-source="items"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ x: 900 }"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'index'">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'category'">
|
||||||
|
<Input
|
||||||
|
v-model:value="record.category"
|
||||||
|
placeholder="分类"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'itemName'">
|
||||||
|
<Input
|
||||||
|
v-model:value="record.itemName"
|
||||||
|
placeholder="检查项名称"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'itemCode'">
|
||||||
|
<Input
|
||||||
|
v-model:value="record.itemCode"
|
||||||
|
placeholder="编码"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'inputType'">
|
||||||
|
<Select
|
||||||
|
v-model:value="record.inputType"
|
||||||
|
:options="INPUT_TYPE_OPTIONS"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'sort'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.sort"
|
||||||
|
:min="0"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'required'">
|
||||||
|
<Select
|
||||||
|
v-model:value="record.required"
|
||||||
|
:options="[
|
||||||
|
{ label: '是', value: 1 },
|
||||||
|
{ label: '否', value: 0 },
|
||||||
|
]"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteItem(index)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button type="dashed" block class="mt-3" @click="handleAddItem">
|
||||||
|
+ 添加检查项
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
280
apps/web-antd/src/views/asset/parking/data.ts
Normal file
280
apps/web-antd/src/views/asset/parking/data.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetParkingApi } from '#/api/asset/parking';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '停车场名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入停车场名称',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入停车场名称' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'capacity',
|
||||||
|
label: '容量',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入容量',
|
||||||
|
min: 1,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: z.number().min(1, { message: '容量必须大于等于1' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'province',
|
||||||
|
label: '省份',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入省份',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入省份' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'city',
|
||||||
|
label: '城市',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入城市',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入城市' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'address',
|
||||||
|
label: '地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入详细地址',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入详细地址' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'managerName',
|
||||||
|
label: '负责人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入负责人姓名',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入负责人姓名' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'managerPhone',
|
||||||
|
label: '负责人联系方式',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入负责人联系方式',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入负责人联系方式' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactName',
|
||||||
|
label: '停车场联系人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入停车场联系人',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactPhone',
|
||||||
|
label: '停车场联系方式',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入停车场联系方式',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'leaseStartDate',
|
||||||
|
label: '租赁开始时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择租赁开始时间',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'leaseEndDate',
|
||||||
|
label: '租赁结束时间',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择租赁结束时间',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'rentFee',
|
||||||
|
label: '租金费用(元/月)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入租金费用',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'longitude',
|
||||||
|
label: '经度',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入经度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'latitude',
|
||||||
|
label: '纬度',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入纬度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '停车场名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入停车场名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'province',
|
||||||
|
label: '省份',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入省份',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'city',
|
||||||
|
label: '城市',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入城市',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'managerName',
|
||||||
|
label: '负责人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入负责人',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 60, fixed: 'left' },
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '停车场名称',
|
||||||
|
minWidth: 150,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'capacity',
|
||||||
|
title: '容量',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'parkedAmount',
|
||||||
|
title: '已停车辆数',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'province',
|
||||||
|
title: '省份',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'city',
|
||||||
|
title: '城市',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'address',
|
||||||
|
title: '地址',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'managerName',
|
||||||
|
title: '负责人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'managerPhone',
|
||||||
|
title: '负责人联系方式',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactName',
|
||||||
|
title: '停车场联系人',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactPhone',
|
||||||
|
title: '停车场联系方式',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'leaseStartDate',
|
||||||
|
title: '租赁开始时间',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatDate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'leaseEndDate',
|
||||||
|
title: '租赁结束时间',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatDate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'rentFee',
|
||||||
|
title: '租金费用(元/月)',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
219
apps/web-antd/src/views/asset/return-order/index.vue
Normal file
219
apps/web-antd/src/views/asset/return-order/index.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetReturnOrderApi } from '#/api/asset/return-order';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
completeReturnOrderInspection,
|
||||||
|
deleteReturnOrder,
|
||||||
|
getReturnOrderPage,
|
||||||
|
settleReturnOrder,
|
||||||
|
submitReturnOrderApproval,
|
||||||
|
withdrawReturnOrderApproval,
|
||||||
|
} from '#/api/asset/return-order';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(row: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.orderCode]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteReturnOrder(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.orderCode]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCompleteInspection(row: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
await confirm('确认验车完成?完成后状态将变更为验车完成。');
|
||||||
|
await completeReturnOrderInspection(row.id!);
|
||||||
|
message.success('验车已完成');
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSettle(row: AssetReturnOrderApi.ReturnOrder) {
|
||||||
|
await confirm('确认结算还车单?结算后不可修改。');
|
||||||
|
await settleReturnOrder(row.id!);
|
||||||
|
message.success('还车单已结算');
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
// 查看 - 始终可见
|
||||||
|
actions.push({
|
||||||
|
auth: ['asset:return-order:query'],
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
label: '查看',
|
||||||
|
onClick: () => message.info('查看功能开发中'),
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 待验车 → 可编辑
|
||||||
|
if (row.status === 0) {
|
||||||
|
actions.push({
|
||||||
|
auth: ['asset:return-order:update'],
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
label: $t('common.edit'),
|
||||||
|
onClick: () => handleEdit(row),
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待验车 → 完成验车
|
||||||
|
if (row.status === 0) {
|
||||||
|
actions.push({
|
||||||
|
auth: ['asset:return-order:update'],
|
||||||
|
label: '完成验车',
|
||||||
|
onClick: () => handleCompleteInspection(row),
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验车完成 → 提交审批
|
||||||
|
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({
|
||||||
|
auth: ['asset:return-order:update'],
|
||||||
|
label: '结算',
|
||||||
|
onClick: () => handleSettle(row),
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待验车 → 可删除
|
||||||
|
if (row.status === 0) {
|
||||||
|
actions.push({
|
||||||
|
auth: ['asset:return-order:delete'],
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
label: $t('common.delete'),
|
||||||
|
popConfirm: {
|
||||||
|
confirm: () => handleDelete(row),
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.orderCode]),
|
||||||
|
},
|
||||||
|
type: 'link',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getReturnOrderPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<AssetReturnOrderApi.ReturnOrder>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="还车管理">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['还车单']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['asset:return-order:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction :actions="getRowActions(row)" />
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
566
apps/web-antd/src/views/asset/return-order/modules/form.vue
Normal file
566
apps/web-antd/src/views/asset/return-order/modules/form.vue
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AssetReturnOrderApi } from '#/api/asset/return-order';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import InspectionForm from '../../components/InspectionForm.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createReturnOrder,
|
||||||
|
getReturnOrder,
|
||||||
|
startVehicleInspection,
|
||||||
|
updateReturnOrder,
|
||||||
|
} from '#/api/asset/return-order';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<AssetReturnOrderApi.ReturnOrder>();
|
||||||
|
const vehicles = ref<AssetReturnOrderApi.ReturnOrderVehicle[]>([]);
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['还车单'])
|
||||||
|
: $t('ui.actionTitle.create', ['还车单']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [BasicForm, basicFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 150,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-6',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 还车车辆列表 ==========
|
||||||
|
const vehicleColumns = [
|
||||||
|
{ title: '序号', key: 'index', width: 60 },
|
||||||
|
{ title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 },
|
||||||
|
{ title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 },
|
||||||
|
{ title: '型号', dataIndex: 'model', key: 'model', width: 100 },
|
||||||
|
{ title: 'VIN码', dataIndex: 'vin', key: 'vin', width: 180 },
|
||||||
|
{ title: '还车里程(km)', key: 'returnMileage', width: 130 },
|
||||||
|
{ title: '还车氢量(%)', key: 'returnHydrogenLevel', width: 120 },
|
||||||
|
{
|
||||||
|
title: '交车氢量(%)',
|
||||||
|
dataIndex: 'deliveryHydrogenLevel',
|
||||||
|
key: 'deliveryHydrogenLevel',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{ title: '氢量差值', key: 'hydrogenDiff', width: 100 },
|
||||||
|
{ title: '氢费退还(元)', key: 'hydrogenRefundAmount', width: 120 },
|
||||||
|
{ title: '验车', key: 'inspection', width: 120 },
|
||||||
|
{ title: '操作', key: 'action', width: 80, fixed: 'right' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleAddVehicle() {
|
||||||
|
vehicles.value.push({
|
||||||
|
plateNo: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
vin: '',
|
||||||
|
returnMileage: 0,
|
||||||
|
returnHydrogenLevel: 0,
|
||||||
|
deliveryHydrogenLevel: 0,
|
||||||
|
hydrogenDiff: 0,
|
||||||
|
hydrogenUnitPrice: 0,
|
||||||
|
hydrogenRefundAmount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteVehicle(index: number) {
|
||||||
|
vehicles.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 车辆验车 ==========
|
||||||
|
async function handleStartInspection(vehicle: AssetReturnOrderApi.ReturnOrderVehicle) {
|
||||||
|
if (!vehicle.id) return;
|
||||||
|
await startVehicleInspection(vehicle.id);
|
||||||
|
message.success('验车记录已创建');
|
||||||
|
// Reload the return order to get updated inspectionRecordId
|
||||||
|
if (formData.value?.id) {
|
||||||
|
formData.value = await getReturnOrder(formData.value.id);
|
||||||
|
vehicles.value = formData.value.vehicles || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 费用核算 ==========
|
||||||
|
const costBreakdown = ref({
|
||||||
|
vehicleDamageFee: 0,
|
||||||
|
toolDamageFee: 0,
|
||||||
|
unpaidMaintenanceFee: 0,
|
||||||
|
unpaidRepairFee: 0,
|
||||||
|
violationFee: 0,
|
||||||
|
otherFee: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 各项费用合计
|
||||||
|
const totalDeductions = computed(() => {
|
||||||
|
const c = costBreakdown.value;
|
||||||
|
return (
|
||||||
|
(c.vehicleDamageFee || 0) +
|
||||||
|
(c.toolDamageFee || 0) +
|
||||||
|
(c.unpaidMaintenanceFee || 0) +
|
||||||
|
(c.unpaidRepairFee || 0) +
|
||||||
|
(c.violationFee || 0) +
|
||||||
|
(c.otherFee || 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 氢费退还合计
|
||||||
|
const totalHydrogenRefund = computed(() => {
|
||||||
|
return vehicles.value.reduce(
|
||||||
|
(sum, v) => sum + (v.hydrogenRefundAmount || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 汇总
|
||||||
|
const summary = computed(() => {
|
||||||
|
const depositRefund = formData.value?.depositRefund || 0;
|
||||||
|
const totalRefund = depositRefund + totalHydrogenRefund.value;
|
||||||
|
const totalDeduct = totalDeductions.value;
|
||||||
|
const netAmount = totalRefund - totalDeduct;
|
||||||
|
return {
|
||||||
|
depositRefund,
|
||||||
|
hydrogenRefund: totalHydrogenRefund.value,
|
||||||
|
totalRefund,
|
||||||
|
totalDeductions: totalDeduct,
|
||||||
|
netAmount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 交检清单 ==========
|
||||||
|
const inspectionData = ref<
|
||||||
|
Record<string, { deliver: boolean; receive: boolean; remark: string }>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const INSPECTION_ITEMS = [
|
||||||
|
{
|
||||||
|
category: '车辆附件',
|
||||||
|
items: ['行驶证', '营运证', '通行证', '加氢证', '登记证', '保险卡', '车钥匙'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '外观检查',
|
||||||
|
items: ['车身完好', '挡风玻璃', '后视镜', '车灯完好', '轮胎正常'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '车内检查',
|
||||||
|
items: ['仪表盘正常', '空调功能', '安全带', '喇叭功能', '雨刮器'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '动力系统',
|
||||||
|
items: ['发动机正常', '燃料电池状态', '制动功能', '转向正常'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function initInspectionData() {
|
||||||
|
const data: Record<
|
||||||
|
string,
|
||||||
|
{ deliver: boolean; receive: boolean; remark: string }
|
||||||
|
> = {};
|
||||||
|
for (const cat of INSPECTION_ITEMS) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
data[`${cat.category}-${item}`] = {
|
||||||
|
deliver: true,
|
||||||
|
receive: true,
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inspectionData.value = data;
|
||||||
|
}
|
||||||
|
initInspectionData();
|
||||||
|
|
||||||
|
const inspectionTableData = computed(() => {
|
||||||
|
const rows: any[] = [];
|
||||||
|
for (const cat of INSPECTION_ITEMS) {
|
||||||
|
for (let i = 0; i < cat.items.length; i++) {
|
||||||
|
rows.push({
|
||||||
|
key: `${cat.category}-${cat.items[i]}`,
|
||||||
|
category: cat.category,
|
||||||
|
item: cat.items[i]!,
|
||||||
|
rowSpan: i === 0 ? cat.items.length : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inspectionColumns = [
|
||||||
|
{
|
||||||
|
title: '类别',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 120,
|
||||||
|
customCell: (_: any, index: number) => {
|
||||||
|
const row = inspectionTableData.value[index];
|
||||||
|
return { rowSpan: row?.rowSpan ?? 1 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '检查项目', dataIndex: 'item', key: 'item', width: 160 },
|
||||||
|
{ title: '交车方(交车时)', key: 'deliver', width: 120 },
|
||||||
|
{ title: '还车方(还车时)', key: 'receive', width: 120 },
|
||||||
|
{ title: '备注', key: 'inspRemark', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== Modal ==========
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await basicFormApi.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
if (vehicles.value.length === 0) {
|
||||||
|
message.warning('请至少添加一辆还车车辆');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
const data =
|
||||||
|
(await basicFormApi.getValues()) as AssetReturnOrderApi.ReturnOrder;
|
||||||
|
data.vehicles = vehicles.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateReturnOrder(data)
|
||||||
|
: createReturnOrder(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
vehicles.value = [];
|
||||||
|
costBreakdown.value = {
|
||||||
|
vehicleDamageFee: 0,
|
||||||
|
toolDamageFee: 0,
|
||||||
|
unpaidMaintenanceFee: 0,
|
||||||
|
unpaidRepairFee: 0,
|
||||||
|
violationFee: 0,
|
||||||
|
otherFee: 0,
|
||||||
|
};
|
||||||
|
initInspectionData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<AssetReturnOrderApi.ReturnOrder>();
|
||||||
|
if (!data || !data.id) return;
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getReturnOrder(data.id);
|
||||||
|
await basicFormApi.setValues(formData.value);
|
||||||
|
vehicles.value = formData.value.vehicles || [];
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-4/5" style="max-width: 1400px">
|
||||||
|
<div class="space-y-4" style="max-height: 75vh; overflow-y: auto">
|
||||||
|
<!-- Card 1: 还车基本信息 -->
|
||||||
|
<Card title="还车基本信息" size="small">
|
||||||
|
<BasicForm />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 2: 还车车辆 -->
|
||||||
|
<Card title="还车车辆" size="small">
|
||||||
|
<Space direction="vertical" style="width: 100%" :size="12">
|
||||||
|
<Table
|
||||||
|
:columns="vehicleColumns"
|
||||||
|
:data-source="vehicles"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ x: 1400 }"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'index'">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'returnMileage'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.returnMileage"
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="里程"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'returnHydrogenLevel'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.returnHydrogenLevel"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="氢量%"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'hydrogenDiff'">
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
(record.returnHydrogenLevel || 0) -
|
||||||
|
(record.deliveryHydrogenLevel || 0) <
|
||||||
|
0
|
||||||
|
? 'text-red-500'
|
||||||
|
: 'text-green-600'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
(record.returnHydrogenLevel || 0) -
|
||||||
|
(record.deliveryHydrogenLevel || 0)
|
||||||
|
).toFixed(0)
|
||||||
|
}}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'hydrogenRefundAmount'">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="record.hydrogenRefundAmount"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
placeholder="退还金额"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'inspection'">
|
||||||
|
<Button
|
||||||
|
v-if="!record.inspectionRecordId"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleStartInspection(record)"
|
||||||
|
>
|
||||||
|
开始验车
|
||||||
|
</Button>
|
||||||
|
<span v-else class="text-green-600">已有验车记录</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteVehicle(index)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button type="dashed" block @click="handleAddVehicle">
|
||||||
|
+ 添加车辆
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 3: 交检清单(旧版兼容) -->
|
||||||
|
<Card title="交检清单" size="small">
|
||||||
|
<Table
|
||||||
|
:columns="inspectionColumns"
|
||||||
|
:data-source="inspectionTableData"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'deliver'">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="inspectionData[record.key]!.deliver"
|
||||||
|
checked-children="✓"
|
||||||
|
un-checked-children="×"
|
||||||
|
size="small"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'receive'">
|
||||||
|
<Switch
|
||||||
|
v-model:checked="inspectionData[record.key]!.receive"
|
||||||
|
checked-children="✓"
|
||||||
|
un-checked-children="×"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'inspRemark'">
|
||||||
|
<Input
|
||||||
|
v-model:value="inspectionData[record.key]!.remark"
|
||||||
|
placeholder="备注"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 车辆验车详情 -->
|
||||||
|
<template v-for="vehicle in vehicles" :key="vehicle.id">
|
||||||
|
<Card
|
||||||
|
v-if="vehicle.inspectionRecordId"
|
||||||
|
:title="`${vehicle.plateNo || '车辆'} - 验车记录`"
|
||||||
|
size="small"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<InspectionForm :record-id="vehicle.inspectionRecordId" :readonly="false" />
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Card 4: 费用核算 -->
|
||||||
|
<Card title="费用核算" size="small">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">车辆损坏费用</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.vehicleDamageFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">工具损坏费用</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.toolDamageFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">未结保养费</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.unpaidMaintenanceFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">未结维修费</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.unpaidRepairFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">违章罚款</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.violationFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">其他费用</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="costBreakdown.otherFee"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
addon-after="元"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<!-- 费用汇总 -->
|
||||||
|
<div class="grid grid-cols-5 gap-4 rounded bg-gray-50 p-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-gray-500">退还保证金</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold">
|
||||||
|
¥{{ summary.depositRefund.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-gray-500">氢费退还</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold">
|
||||||
|
¥{{ summary.hydrogenRefund.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-gray-500">应退总额</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-green-600">
|
||||||
|
¥{{ summary.totalRefund.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-gray-500">扣款总额</div>
|
||||||
|
<div class="mt-1 text-lg font-semibold text-red-500">
|
||||||
|
¥{{ summary.totalDeductions.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ summary.netAmount >= 0 ? '应退款' : '应缴纳' }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-1 text-xl font-bold"
|
||||||
|
:class="
|
||||||
|
summary.netAmount >= 0 ? 'text-green-600' : 'text-red-500'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
¥{{ Math.abs(summary.netAmount).toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
392
apps/web-antd/src/views/asset/supplier/data.ts
Normal file
392
apps/web-antd/src/views/asset/supplier/data.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetSupplierApi } from '#/api/asset/supplier';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 基本信息 */
|
||||||
|
export function useBasicFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'supplierCode',
|
||||||
|
label: '供应商编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入供应商编码',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入供应商编码' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'coopStatus',
|
||||||
|
label: '合作状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择合作状态',
|
||||||
|
options: [
|
||||||
|
{ label: '已合作', value: '已合作' },
|
||||||
|
{ label: '终止合作', value: '终止合作' },
|
||||||
|
{ label: '洽谈中', value: '洽谈中' },
|
||||||
|
{ label: '合约过期', value: '合约过期' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请选择合作状态' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'supplierName',
|
||||||
|
label: '供应商名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入供应商名称',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入供应商名称' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '供应商类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择供应商类型',
|
||||||
|
options: [
|
||||||
|
{ label: '备件供应商', value: '备件供应商' },
|
||||||
|
{ label: '保险公司', value: '保险公司' },
|
||||||
|
{ label: '加氢站', value: '加氢站' },
|
||||||
|
{ label: '充电站', value: '充电站' },
|
||||||
|
{ label: '维修站', value: '维修站' },
|
||||||
|
{ label: '救援车队', value: '救援车队' },
|
||||||
|
{ label: '整车厂', value: '整车厂' },
|
||||||
|
{ label: '其他', value: '其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请选择供应商类型' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'province',
|
||||||
|
label: '省份',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入省份',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入省份' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'city',
|
||||||
|
label: '城市',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入城市',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入城市' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'address',
|
||||||
|
label: '详细地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入详细地址',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入详细地址' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择区域',
|
||||||
|
options: [
|
||||||
|
{ label: '华北', value: '华北' },
|
||||||
|
{ label: '华东', value: '华东' },
|
||||||
|
{ label: '华南', value: '华南' },
|
||||||
|
{ label: '华中', value: '华中' },
|
||||||
|
{ label: '东北', value: '东北' },
|
||||||
|
{ label: '西南', value: '西南' },
|
||||||
|
{ label: '西北', value: '西北' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contact',
|
||||||
|
label: '联系人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系人',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactMobile',
|
||||||
|
label: '联系手机',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系手机',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'contactPhone',
|
||||||
|
label: '联系电话',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入联系电话',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮箱',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'creditCodeOrId',
|
||||||
|
label: '统一社会信用代码/身份证',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入统一社会信用代码或身份证号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 开票信息 */
|
||||||
|
export function useInvoiceFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'taxId',
|
||||||
|
label: '税号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入税号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'invoiceAddress',
|
||||||
|
label: '开票地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开票地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'invoicePhone',
|
||||||
|
label: '开票电话',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开票电话',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'account',
|
||||||
|
label: '银行账号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入银行账号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'openingBank',
|
||||||
|
label: '开户行',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入开户行',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mailingAddress',
|
||||||
|
label: '邮寄地址',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入邮寄地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'supplierCode',
|
||||||
|
label: '供应商编码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入供应商编码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'supplierName',
|
||||||
|
label: '供应商名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入供应商名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '供应商类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择供应商类型',
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ label: '备件供应商', value: '备件供应商' },
|
||||||
|
{ label: '保险公司', value: '保险公司' },
|
||||||
|
{ label: '加氢站', value: '加氢站' },
|
||||||
|
{ label: '充电站', value: '充电站' },
|
||||||
|
{ label: '维修站', value: '维修站' },
|
||||||
|
{ label: '救援车队', value: '救援车队' },
|
||||||
|
{ label: '整车厂', value: '整车厂' },
|
||||||
|
{ label: '其他', value: '其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'coopStatus',
|
||||||
|
label: '合作状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择合作状态',
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ label: '已合作', value: '已合作' },
|
||||||
|
{ label: '终止合作', value: '终止合作' },
|
||||||
|
{ label: '洽谈中', value: '洽谈中' },
|
||||||
|
{ label: '合约过期', value: '合约过期' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'region',
|
||||||
|
label: '区域',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择区域',
|
||||||
|
mode: 'multiple',
|
||||||
|
options: [
|
||||||
|
{ label: '华北', value: '华北' },
|
||||||
|
{ label: '华东', value: '华东' },
|
||||||
|
{ label: '华南', value: '华南' },
|
||||||
|
{ label: '华中', value: '华中' },
|
||||||
|
{ label: '东北', value: '东北' },
|
||||||
|
{ label: '西南', value: '西南' },
|
||||||
|
{ label: '西北', value: '西北' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 60, fixed: 'left' },
|
||||||
|
{
|
||||||
|
field: 'supplierCode',
|
||||||
|
title: '供应商编码',
|
||||||
|
minWidth: 120,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'supplierName',
|
||||||
|
title: '供应商名称',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '供应商类型',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'coopStatus',
|
||||||
|
title: '合作状态',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'region',
|
||||||
|
title: '区域',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'province',
|
||||||
|
title: '省份',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'city',
|
||||||
|
title: '城市',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'address',
|
||||||
|
title: '地址',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contact',
|
||||||
|
title: '联系人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactMobile',
|
||||||
|
title: '联系手机',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'contactPhone',
|
||||||
|
title: '联系电话',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'email',
|
||||||
|
title: '邮箱',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creditCodeOrId',
|
||||||
|
title: '统一社会信用代码',
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creator',
|
||||||
|
title: '创建人',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
413
apps/web-antd/src/views/asset/vehicle-model/data.ts
Normal file
413
apps/web-antd/src/views/asset/vehicle-model/data.ts
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 基本信息 */
|
||||||
|
export function useBasicFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'brand',
|
||||||
|
label: '品牌',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入品牌,如:帕力安牌',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入品牌' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'model',
|
||||||
|
label: '型号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入型号,如:XDQ504LXCFCEV01',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入型号' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'vehicleType',
|
||||||
|
label: '车辆类型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车辆类型,如:轻型箱式货车',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入车辆类型' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'modelLabel',
|
||||||
|
label: '型号标签',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入型号标签,如:4.5T冷链车',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入型号标签' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'fuelType',
|
||||||
|
label: '燃料种类',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择燃料种类',
|
||||||
|
options: [
|
||||||
|
{ label: '氢', value: '氢' },
|
||||||
|
{ label: '电', value: '电' },
|
||||||
|
{ label: '柴油', value: '柴油' },
|
||||||
|
{ label: '氢电混合', value: '氢电混合' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请选择燃料种类' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateColor',
|
||||||
|
label: '车牌颜色',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择车牌颜色',
|
||||||
|
options: [
|
||||||
|
{ label: '绿牌', value: '绿牌' },
|
||||||
|
{ label: '黄牌', value: '黄牌' },
|
||||||
|
{ label: '黄绿牌', value: '黄绿牌' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请选择车牌颜色' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 车辆尺寸 */
|
||||||
|
export function useSizeFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'truckSize_x',
|
||||||
|
label: '车辆长度(mm)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车辆长度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'truckSize_y',
|
||||||
|
label: '车辆宽度(mm)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车辆宽度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'truckSize_z',
|
||||||
|
label: '车辆高度(mm)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车辆高度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'tireSize',
|
||||||
|
label: '轮胎规格',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入轮胎规格',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'tireNumber',
|
||||||
|
label: '轮胎数量',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入轮胎数量',
|
||||||
|
min: 1,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 电池信息 */
|
||||||
|
export function useBatteryFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'batteryType',
|
||||||
|
label: '电池类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择电池类型',
|
||||||
|
options: [
|
||||||
|
{ label: '磷酸铁锂', value: '磷酸铁锂' },
|
||||||
|
{ label: '三元锂', value: '三元锂' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'batteryFactory',
|
||||||
|
label: '电池厂家',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入电池厂家',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'reserveElectricity',
|
||||||
|
label: '储电量(kwh)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入储电量',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'electricityMileage',
|
||||||
|
label: '电续航里程(KM)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入电续航里程',
|
||||||
|
min: 0,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 氢能信息 */
|
||||||
|
export function useHydrogenFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenCapacity',
|
||||||
|
label: '氢瓶容量(L)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入氢瓶容量',
|
||||||
|
min: 0,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenFactory',
|
||||||
|
label: '供氢系统厂家',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入供氢系统厂家',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenUnit',
|
||||||
|
label: '仪表盘氢气单位',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择氢气单位',
|
||||||
|
options: [
|
||||||
|
{ label: '%', value: '%' },
|
||||||
|
{ label: 'MPa', value: 'MPa' },
|
||||||
|
{ label: 'Kg', value: 'Kg' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenMileage',
|
||||||
|
label: '氢续航里程(KM)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入氢续航里程',
|
||||||
|
min: 0,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 其他信息 */
|
||||||
|
export function useOtherFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'refrigeratorFactory',
|
||||||
|
label: '冷机厂家',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入冷机厂家',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'onlineSpreadEnterprise',
|
||||||
|
label: '电堆厂家',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入电堆厂家',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'brand',
|
||||||
|
label: '品牌',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入品牌',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'model',
|
||||||
|
label: '型号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入型号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'fuelType',
|
||||||
|
label: '燃料种类',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择燃料种类',
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '氢', value: '氢' },
|
||||||
|
{ label: '电', value: '电' },
|
||||||
|
{ label: '柴油', value: '柴油' },
|
||||||
|
{ label: '氢电混合', value: '氢电混合' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 60, fixed: 'left' },
|
||||||
|
{
|
||||||
|
field: 'brand',
|
||||||
|
title: '品牌',
|
||||||
|
minWidth: 120,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'model',
|
||||||
|
title: '型号',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'modelLabel',
|
||||||
|
title: '型号标签',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'vehicleType',
|
||||||
|
title: '车辆类型',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fuelType',
|
||||||
|
title: '燃料种类',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'plateColor',
|
||||||
|
title: '车牌颜色',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tireNumber',
|
||||||
|
title: '轮胎数量',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tireSize',
|
||||||
|
title: '轮胎规格',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'batteryType',
|
||||||
|
title: '电池类型',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'reserveElectricity',
|
||||||
|
title: '储电量(kWh)',
|
||||||
|
minWidth: 110,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenCapacity',
|
||||||
|
title: '氢瓶容量(L)',
|
||||||
|
minWidth: 110,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'electricityMileage',
|
||||||
|
title: '电续航(KM)',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenMileage',
|
||||||
|
title: '氢续航(KM)',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 维保项目表格列配置 */
|
||||||
|
export function useMaintainItemColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'seq', width: 60, title: '序号' },
|
||||||
|
{
|
||||||
|
field: 'maintainItem',
|
||||||
|
title: '保养项目名称',
|
||||||
|
minWidth: 150,
|
||||||
|
editRender: { name: 'input' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'kilometerCycle',
|
||||||
|
title: '保养公里周期(KM)',
|
||||||
|
minWidth: 150,
|
||||||
|
editRender: { name: 'input', props: { type: 'number' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'timeCycle',
|
||||||
|
title: '保养时间周期(月)',
|
||||||
|
minWidth: 150,
|
||||||
|
editRender: { name: 'input', props: { type: 'number' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hourFee',
|
||||||
|
title: '工时费(元)',
|
||||||
|
minWidth: 120,
|
||||||
|
editRender: { name: 'input', props: { type: 'number' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'materialFee',
|
||||||
|
title: '材料费(元)',
|
||||||
|
minWidth: 120,
|
||||||
|
editRender: { name: 'input', props: { type: 'number' } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalFee',
|
||||||
|
title: '费用合计(元)',
|
||||||
|
minWidth: 120,
|
||||||
|
editRender: { name: 'input', props: { type: 'number' } },
|
||||||
|
},
|
||||||
|
useActionColumn(1, false, 'operate'),
|
||||||
|
];
|
||||||
|
}
|
||||||
217
apps/web-antd/src/views/asset/vehicle-prepare/data.ts
Normal file
217
apps/web-antd/src/views/asset/vehicle-prepare/data.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
// ========== 车辆类型选项 ==========
|
||||||
|
export const VEHICLE_TYPE_OPTIONS = [
|
||||||
|
{ label: '轻型厢式货车', value: '轻型厢式货车' },
|
||||||
|
{ label: '重型厢式货车', value: '重型厢式货车' },
|
||||||
|
{ label: '重型半挂牵引车', value: '重型半挂牵引车' },
|
||||||
|
{ label: '重型集装箱半挂车', value: '重型集装箱半挂车' },
|
||||||
|
{ label: '小型普通客车', value: '小型普通客车' },
|
||||||
|
{ label: '重型平板半挂车', value: '重型平板半挂车' },
|
||||||
|
{ label: '叉车', value: '叉车' },
|
||||||
|
{ label: '油车', value: '油车' },
|
||||||
|
{ label: '观光车', value: '观光车' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== 氢量单位选项 ==========
|
||||||
|
export const HYDROGEN_UNIT_OPTIONS = [
|
||||||
|
{ label: '%', value: '%' },
|
||||||
|
{ label: 'MPa', value: 'MPa' },
|
||||||
|
{ label: 'kg', value: 'kg' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== 整备类型选项 ==========
|
||||||
|
export const PREP_TYPE_OPTIONS = [
|
||||||
|
{ label: '新车整备', value: '新车整备' },
|
||||||
|
{ label: '日常整备', value: '日常整备' },
|
||||||
|
{ label: '还车整备', value: '还车整备' },
|
||||||
|
{ label: '替换车整备', value: '替换车整备' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== 备车检查项 ==========
|
||||||
|
export const CHECK_CATEGORIES = [
|
||||||
|
{
|
||||||
|
category: '车灯',
|
||||||
|
items: ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '仪表盘',
|
||||||
|
items: ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '驾驶室',
|
||||||
|
items: [
|
||||||
|
'点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜',
|
||||||
|
'内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能',
|
||||||
|
'手刹', '车钥匙功能', '喇叭', '音响功能', '遮阳板',
|
||||||
|
'主副驾座椅', '方向盘', '内饰干净整洁',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '轮胎',
|
||||||
|
items: ['左前轮', '右前轮', '左后轮', '右后轮'],
|
||||||
|
type: 'input' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '液位检查',
|
||||||
|
items: ['机油', '冷却液', '制动液', '转向助力液'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '外观检查',
|
||||||
|
items: ['车漆', '车身', '挡风玻璃', '后视镜'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '车辆外观',
|
||||||
|
items: ['前保险杠', '后保险杠', '车顶', '底盘'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '其他',
|
||||||
|
items: ['灭火器', '三角警示牌', '急救包'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '随车工具',
|
||||||
|
items: ['千斤顶', '轮胎扳手', '拖车钩'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '随车证件',
|
||||||
|
items: ['行驶证', '保险卡', '年检标志'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '整车',
|
||||||
|
items: ['整车清洁', '整车功能'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '燃料电池系统',
|
||||||
|
items: ['燃料电池状态', '氢气瓶', '氢气管路'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '冷机',
|
||||||
|
items: ['制冷系统', '温控系统'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '制动系统',
|
||||||
|
items: ['制动片', '制动盘', '手刹功能'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========== 搜索表单 ==========
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'completeTime',
|
||||||
|
label: '备车日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNo',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入车牌号' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'vehicleType',
|
||||||
|
label: '车辆类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择车辆类型',
|
||||||
|
allowClear: true,
|
||||||
|
options: VEHICLE_TYPE_OPTIONS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'brand',
|
||||||
|
label: '品牌',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入品牌' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'parkingLot',
|
||||||
|
label: '停车场',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: { placeholder: '请输入停车场' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 已完成列表列 ==========
|
||||||
|
export function useCompletedColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ field: 'completeTime', title: '备车时间', minWidth: 160, formatter: 'formatDateTime' },
|
||||||
|
{ field: 'creator', title: '备车人', minWidth: 100 },
|
||||||
|
{ field: 'vehicleType', title: '车辆类型', minWidth: 120 },
|
||||||
|
{ field: 'brand', title: '品牌', minWidth: 100 },
|
||||||
|
{ field: 'model', title: '型号', minWidth: 100 },
|
||||||
|
{ field: 'plateNo', title: '车牌号', minWidth: 120 },
|
||||||
|
{ field: 'vin', title: '车辆识别代码', minWidth: 180 },
|
||||||
|
{ field: 'parkingLot', title: '停车场', minWidth: 120 },
|
||||||
|
{
|
||||||
|
field: 'hasBodyAd',
|
||||||
|
title: '车身广告及放大字',
|
||||||
|
minWidth: 130,
|
||||||
|
formatter: ({ cellValue }: { cellValue: boolean }) => cellValue ? '有' : '无',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hasTailLift',
|
||||||
|
title: '尾板',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter: ({ cellValue }: { cellValue: boolean }) => cellValue ? '有' : '无',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'spareTireDepth',
|
||||||
|
title: '备胎胎纹深度',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) => cellValue != null ? `${cellValue}mm` : '',
|
||||||
|
},
|
||||||
|
useActionColumn(1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 待提交列表列 ==========
|
||||||
|
export function usePendingColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ field: 'createTime', title: '创建时间', minWidth: 160, formatter: 'formatDateTime' },
|
||||||
|
{ field: 'creator', title: '创建人', minWidth: 100 },
|
||||||
|
{ field: 'vehicleType', title: '车辆类型', minWidth: 120 },
|
||||||
|
{ field: 'brand', title: '品牌', minWidth: 100 },
|
||||||
|
{ field: 'model', title: '型号', minWidth: 100 },
|
||||||
|
{ field: 'plateNo', title: '车牌号', minWidth: 120 },
|
||||||
|
{ field: 'vin', title: '车辆识别代码', minWidth: 180 },
|
||||||
|
{ field: 'parkingLot', title: '停车场', minWidth: 120 },
|
||||||
|
{
|
||||||
|
field: 'hasBodyAd',
|
||||||
|
title: '车身广告及放大字',
|
||||||
|
minWidth: 130,
|
||||||
|
formatter: ({ cellValue }: { cellValue: boolean }) => cellValue ? '有' : '无',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hasTailLift',
|
||||||
|
title: '尾板',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter: ({ cellValue }: { cellValue: boolean }) => cellValue ? '有' : '无',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'spareTireDepth',
|
||||||
|
title: '备胎胎纹深度',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: ({ cellValue }: { cellValue: number }) => cellValue != null ? `${cellValue}mm` : '',
|
||||||
|
},
|
||||||
|
useActionColumn(3, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 兼容旧引用 ==========
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return usePendingColumns();
|
||||||
|
}
|
||||||
630
apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue
Normal file
630
apps/web-antd/src/views/asset/vehicle-prepare/modules/form.vue
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AssetVehiclePrepareApi } from '#/api/asset/vehicle-prepare';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import InspectionForm from '../../components/InspectionForm.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Upload,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { PlusOutlined } from '@vben/icons';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createVehiclePrepare,
|
||||||
|
getVehiclePrepare,
|
||||||
|
updateVehiclePrepare,
|
||||||
|
} from '#/api/asset/vehicle-prepare';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHECK_CATEGORIES,
|
||||||
|
HYDROGEN_UNIT_OPTIONS,
|
||||||
|
PREP_TYPE_OPTIONS,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<AssetVehiclePrepareApi.VehiclePrepare>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['备车记录'])
|
||||||
|
: $t('ui.actionTitle.create', ['备车记录']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 车辆基本信息 ==========
|
||||||
|
const vehicleInfo = ref({
|
||||||
|
vehicleId: undefined as number | undefined,
|
||||||
|
plateNo: '',
|
||||||
|
vehicleType: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
vin: '',
|
||||||
|
parkingLot: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 整备信息 ==========
|
||||||
|
const prepInfo = ref({
|
||||||
|
preparationType: '日常整备',
|
||||||
|
mileage: undefined as number | undefined,
|
||||||
|
hydrogenRemaining: undefined as number | undefined,
|
||||||
|
hydrogenUnit: '%',
|
||||||
|
batteryRemaining: undefined as number | undefined,
|
||||||
|
commercialInsuranceExpiry: '',
|
||||||
|
compulsoryInsuranceExpiry: '',
|
||||||
|
hasBodyAd: false,
|
||||||
|
hasTailLift: false,
|
||||||
|
trailerPlateNo: '',
|
||||||
|
spareTireDepth: undefined as number | undefined,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 照片上传 ==========
|
||||||
|
const bodyAdPhotos = ref<any[]>([]);
|
||||||
|
const enlargedTextPhotos = ref<any[]>([]);
|
||||||
|
const spareTirePhotos = ref<any[]>([]);
|
||||||
|
const defectPhotos = ref<any[]>([]);
|
||||||
|
|
||||||
|
// ========== 备车检查单 ==========
|
||||||
|
const checkDrawerVisible = ref(false);
|
||||||
|
const checkData = ref<
|
||||||
|
Record<string, { checked: boolean; remark: string; depth?: number }>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
function initCheckData() {
|
||||||
|
const data: Record<
|
||||||
|
string,
|
||||||
|
{ checked: boolean; remark: string; depth?: number }
|
||||||
|
> = {};
|
||||||
|
for (const cat of CHECK_CATEGORIES) {
|
||||||
|
for (const item of cat.items) {
|
||||||
|
const key = `${cat.category}-${item}`;
|
||||||
|
data[key] = { checked: true, remark: '', depth: undefined };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkData.value = data;
|
||||||
|
}
|
||||||
|
initCheckData();
|
||||||
|
|
||||||
|
// 生成检查表数据源(带行合并信息)
|
||||||
|
const checkTableData = computed(() => {
|
||||||
|
const rows: any[] = [];
|
||||||
|
for (const cat of CHECK_CATEGORIES) {
|
||||||
|
for (let i = 0; i < cat.items.length; i++) {
|
||||||
|
rows.push({
|
||||||
|
key: `${cat.category}-${cat.items[i]}`,
|
||||||
|
category: cat.category,
|
||||||
|
item: cat.items[i]!,
|
||||||
|
isInput: cat.type === 'input',
|
||||||
|
rowSpan: i === 0 ? cat.items.length : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkColumns = [
|
||||||
|
{
|
||||||
|
title: '类别',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 120,
|
||||||
|
customCell: (_: any, index: number) => {
|
||||||
|
const row = checkTableData.value[index];
|
||||||
|
return { rowSpan: row?.rowSpan ?? 1 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '检查项目', dataIndex: 'item', key: 'item', width: 160 },
|
||||||
|
{ title: '检查情况', key: 'check', width: 160 },
|
||||||
|
{ title: '备注', key: 'remark', width: 200 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 已完成的检查项统计
|
||||||
|
const checkSummary = computed(() => {
|
||||||
|
const total = Object.keys(checkData.value).length;
|
||||||
|
const normal = Object.values(checkData.value).filter(
|
||||||
|
(v) => v.checked,
|
||||||
|
).length;
|
||||||
|
return { total, normal, abnormal: total - normal };
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 重置表单 ==========
|
||||||
|
function resetForm() {
|
||||||
|
vehicleInfo.value = {
|
||||||
|
vehicleId: undefined,
|
||||||
|
plateNo: '',
|
||||||
|
vehicleType: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
vin: '',
|
||||||
|
parkingLot: '',
|
||||||
|
};
|
||||||
|
prepInfo.value = {
|
||||||
|
preparationType: '日常整备',
|
||||||
|
mileage: undefined,
|
||||||
|
hydrogenRemaining: undefined,
|
||||||
|
hydrogenUnit: '%',
|
||||||
|
batteryRemaining: undefined,
|
||||||
|
commercialInsuranceExpiry: '',
|
||||||
|
compulsoryInsuranceExpiry: '',
|
||||||
|
hasBodyAd: false,
|
||||||
|
hasTailLift: false,
|
||||||
|
trailerPlateNo: '',
|
||||||
|
spareTireDepth: undefined,
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
bodyAdPhotos.value = [];
|
||||||
|
enlargedTextPhotos.value = [];
|
||||||
|
spareTirePhotos.value = [];
|
||||||
|
defectPhotos.value = [];
|
||||||
|
initCheckData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Modal ==========
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
// 基础校验
|
||||||
|
if (!vehicleInfo.value.vehicleId) {
|
||||||
|
message.warning('请选择车牌号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
const data = {
|
||||||
|
...vehicleInfo.value,
|
||||||
|
...prepInfo.value,
|
||||||
|
checkList: JSON.stringify(checkData.value),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if (formData.value?.id) {
|
||||||
|
data.id = formData.value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateVehiclePrepare(data)
|
||||||
|
: createVehiclePrepare(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<AssetVehiclePrepareApi.VehiclePrepare>();
|
||||||
|
if (!data || !data.id) return;
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getVehiclePrepare(data.id);
|
||||||
|
// 回填表单数据
|
||||||
|
vehicleInfo.value = {
|
||||||
|
vehicleId: formData.value.vehicleId,
|
||||||
|
plateNo: formData.value.plateNo || '',
|
||||||
|
vehicleType: formData.value.vehicleType || '',
|
||||||
|
brand: formData.value.brand || '',
|
||||||
|
model: formData.value.model || '',
|
||||||
|
vin: formData.value.vin || '',
|
||||||
|
parkingLot: formData.value.parkingLot || '',
|
||||||
|
};
|
||||||
|
prepInfo.value = {
|
||||||
|
...prepInfo.value,
|
||||||
|
hasBodyAd: formData.value.hasBodyAd || false,
|
||||||
|
hasTailLift: formData.value.hasTailLift || false,
|
||||||
|
spareTireDepth: formData.value.spareTireDepth,
|
||||||
|
trailerPlateNo: formData.value.trailerPlateNo || '',
|
||||||
|
};
|
||||||
|
if (formData.value.checkList) {
|
||||||
|
try {
|
||||||
|
checkData.value = JSON.parse(formData.value.checkList);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isView = computed(() => {
|
||||||
|
const mode = modalApi.getData<any>()?._mode;
|
||||||
|
return !!mode && mode === 'view';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-4/5" style="max-width: 1400px">
|
||||||
|
<div class="space-y-4" style="max-height: 75vh; overflow-y: auto">
|
||||||
|
<!-- Card 1: 车辆基本信息 -->
|
||||||
|
<Card title="车辆基本信息" size="small">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
<span class="text-red-500">*</span> 车牌号
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="vehicleInfo.vehicleId"
|
||||||
|
placeholder="请选择或输入车牌号"
|
||||||
|
show-search
|
||||||
|
:filter-option="
|
||||||
|
(input: string, option: any) =>
|
||||||
|
(option?.label ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<!-- 实际应从API获取 -->
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">车辆类型</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="vehicleInfo.vehicleType"
|
||||||
|
placeholder="选择车牌号后自动填充"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">品牌</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="vehicleInfo.brand"
|
||||||
|
placeholder="选择车牌号后自动填充"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">型号</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="vehicleInfo.model"
|
||||||
|
placeholder="选择车牌号后自动填充"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">车辆识别代码</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="vehicleInfo.vin"
|
||||||
|
placeholder="选择车牌号后自动填充"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">停车场</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="vehicleInfo.parkingLot"
|
||||||
|
placeholder="请选择停车场"
|
||||||
|
style="width: 100%"
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<!-- 从停车场API获取 -->
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 2: 整备信息 -->
|
||||||
|
<Card title="整备信息" size="small">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">整备类型</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="prepInfo.preparationType"
|
||||||
|
:options="PREP_TYPE_OPTIONS"
|
||||||
|
placeholder="请选择整备类型"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">仪表盘里程</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="prepInfo.mileage"
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
addon-after="km"
|
||||||
|
placeholder="请输入里程"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">剩余氢量</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="prepInfo.hydrogenRemaining"
|
||||||
|
:min="0"
|
||||||
|
:precision="1"
|
||||||
|
placeholder="请输入"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-model:value="prepInfo.hydrogenUnit"
|
||||||
|
:options="HYDROGEN_UNIT_OPTIONS"
|
||||||
|
style="width: 80px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">剩余电量</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="prepInfo.batteryRemaining"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:precision="0"
|
||||||
|
addon-after="%"
|
||||||
|
placeholder="请输入电量百分比"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">商业险到期时间</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="prepInfo.commercialInsuranceExpiry"
|
||||||
|
placeholder="自动获取"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">交强险到期时间</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="prepInfo.compulsoryInsuranceExpiry"
|
||||||
|
placeholder="自动获取"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<!-- 设备开关 -->
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4 flex items-center gap-3">
|
||||||
|
<label class="font-medium">车身广告及放大字</label>
|
||||||
|
<Switch v-model:checked="prepInfo.hasBodyAd" />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4 flex items-center gap-3">
|
||||||
|
<label class="font-medium">尾板</label>
|
||||||
|
<Switch v-model:checked="prepInfo.hasTailLift" />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">备胎胎纹深度</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="prepInfo.spareTireDepth"
|
||||||
|
:min="0"
|
||||||
|
:precision="1"
|
||||||
|
addon-after="mm"
|
||||||
|
placeholder="请输入"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">挂车牌号</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="prepInfo.trailerPlateNo"
|
||||||
|
placeholder="请输入挂车牌号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 3: 照片信息 -->
|
||||||
|
<Card title="照片信息" size="small">
|
||||||
|
<Row :gutter="24">
|
||||||
|
<Col :span="6" v-if="prepInfo.hasBodyAd">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block font-medium">广告照片(最多1张)</label>
|
||||||
|
<Upload
|
||||||
|
:file-list="bodyAdPhotos"
|
||||||
|
list-type="picture-card"
|
||||||
|
:max-count="1"
|
||||||
|
:before-upload="() => false"
|
||||||
|
@change="({ fileList }: any) => (bodyAdPhotos = fileList)"
|
||||||
|
>
|
||||||
|
<div v-if="bodyAdPhotos.length < 1">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="mt-2 text-xs">上传</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6" v-if="prepInfo.hasBodyAd">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block font-medium"
|
||||||
|
>放大字照片(最多1张)</label
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
:file-list="enlargedTextPhotos"
|
||||||
|
list-type="picture-card"
|
||||||
|
:max-count="1"
|
||||||
|
:before-upload="() => false"
|
||||||
|
@change="
|
||||||
|
({ fileList }: any) => (enlargedTextPhotos = fileList)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div v-if="enlargedTextPhotos.length < 1">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="mt-2 text-xs">上传</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block font-medium"
|
||||||
|
>备胎照片(最多1张)</label
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
:file-list="spareTirePhotos"
|
||||||
|
list-type="picture-card"
|
||||||
|
:max-count="1"
|
||||||
|
:before-upload="() => false"
|
||||||
|
@change="({ fileList }: any) => (spareTirePhotos = fileList)"
|
||||||
|
>
|
||||||
|
<div v-if="spareTirePhotos.length < 1">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="mt-2 text-xs">上传</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col :span="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block font-medium"
|
||||||
|
>瑕疵照片(最多4张)</label
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
:file-list="defectPhotos"
|
||||||
|
list-type="picture-card"
|
||||||
|
:max-count="4"
|
||||||
|
:before-upload="() => false"
|
||||||
|
@change="({ fileList }: any) => (defectPhotos = fileList)"
|
||||||
|
>
|
||||||
|
<div v-if="defectPhotos.length < 4">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="mt-2 text-xs">上传</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 4: 车辆检查 -->
|
||||||
|
<Card title="车辆检查" size="small">
|
||||||
|
<!-- 新版:使用验车记录组件 -->
|
||||||
|
<template v-if="formData?.inspectionRecordId">
|
||||||
|
<InspectionForm :record-id="formData.inspectionRecordId" :readonly="isView" />
|
||||||
|
</template>
|
||||||
|
<!-- 旧版:硬编码检查单(兼容旧数据) -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Button type="primary" @click="checkDrawerVisible = true">
|
||||||
|
打开备车检查单
|
||||||
|
</Button>
|
||||||
|
<span class="ml-4 text-gray-500">
|
||||||
|
共 {{ checkSummary.total }} 项,正常
|
||||||
|
<span class="text-green-600">{{ checkSummary.normal }}</span>
|
||||||
|
项,异常
|
||||||
|
<span class="text-red-500">{{ checkSummary.abnormal }}</span> 项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Card 5: 备注 -->
|
||||||
|
<Card title="备注" size="small">
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="prepInfo.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
:rows="3"
|
||||||
|
:maxlength="500"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 检查单抽屉 -->
|
||||||
|
<Drawer
|
||||||
|
v-model:open="checkDrawerVisible"
|
||||||
|
title="备车检查单"
|
||||||
|
:width="780"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<span class="text-gray-500">
|
||||||
|
正常 {{ checkSummary.normal }}/{{ checkSummary.total }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<Table
|
||||||
|
:columns="checkColumns"
|
||||||
|
:data-source="checkTableData"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'check'">
|
||||||
|
<template v-if="record.isInput">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="checkData[record.key]!.depth"
|
||||||
|
placeholder="胎纹深度"
|
||||||
|
addon-after="mm"
|
||||||
|
:min="0"
|
||||||
|
:precision="1"
|
||||||
|
size="small"
|
||||||
|
style="width: 130px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Switch
|
||||||
|
v-model:checked="checkData[record.key]!.checked"
|
||||||
|
checked-children="正常"
|
||||||
|
un-checked-children="异常"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'remark'">
|
||||||
|
<Input
|
||||||
|
v-model:value="checkData[record.key]!.remark"
|
||||||
|
placeholder="备注"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Drawer>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
315
apps/web-antd/src/views/asset/vehicle-registration/data.ts
Normal file
315
apps/web-antd/src/views/asset/vehicle-registration/data.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetVehicleRegistrationApi } from '#/api/asset/vehicle-registration';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 基本信息 */
|
||||||
|
export function useBasicFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'vin',
|
||||||
|
label: '车辆识别代号(VIN)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入VIN码',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入VIN码' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNo',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车牌号',
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: '请输入车牌号' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateDate',
|
||||||
|
label: '上牌日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择上牌日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'operator',
|
||||||
|
label: '操作员',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入操作员',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - OCR识别信息 */
|
||||||
|
export function useOcrFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'recognizedBrand',
|
||||||
|
label: '品牌型号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'recognizedModel',
|
||||||
|
label: '车型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'vehicleType',
|
||||||
|
label: '车辆类型',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'owner',
|
||||||
|
label: '所有人',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'useCharacter',
|
||||||
|
label: '使用性质',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'engineNo',
|
||||||
|
label: '发动机号码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'registerDate',
|
||||||
|
label: '注册日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'issueDate',
|
||||||
|
label: '发证日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'inspectionRecord',
|
||||||
|
label: '检验记录',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'scrapDate',
|
||||||
|
label: '强制报废期止',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'curbWeight',
|
||||||
|
label: '整备质量(kg)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'totalMass',
|
||||||
|
label: '总质量(kg)',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'approvedPassengerCapacity',
|
||||||
|
label: '核定载人数',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'OCR自动识别',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 - 其他信息 */
|
||||||
|
export function useOtherFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'vehicleModelId',
|
||||||
|
label: '匹配车型',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择车型',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/asset/vehicle-model').then((m) =>
|
||||||
|
m.getSimpleVehicleModelList(),
|
||||||
|
),
|
||||||
|
labelField: 'modelLabel',
|
||||||
|
valueField: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'vin',
|
||||||
|
label: 'VIN码',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入VIN码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNo',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车牌号',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
options: [
|
||||||
|
{ label: '待确认', value: 0 },
|
||||||
|
{ label: '已确认', value: 1 },
|
||||||
|
{ label: '已作废', value: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateDate',
|
||||||
|
label: '上牌日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
placeholder: ['开始日期', '结束日期'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: getRangePickerDefaultProps(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 50, fixed: 'left' },
|
||||||
|
{
|
||||||
|
field: 'vin',
|
||||||
|
title: 'VIN码',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'plateNo',
|
||||||
|
title: '车牌号',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'plateDate',
|
||||||
|
title: '上牌日期',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'recognizedBrand',
|
||||||
|
title: '品牌型号',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'vehicleType',
|
||||||
|
title: '车辆类型',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'owner',
|
||||||
|
title: '所有人',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
cellRender: { name: 'CellDict', props: { type: 'tag' } },
|
||||||
|
params: {
|
||||||
|
dictType: 'asset_vehicle_registration_status',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operator',
|
||||||
|
title: '操作员',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 160,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
260
apps/web-antd/src/views/asset/vehicle-replacement/data.ts
Normal file
260
apps/web-antd/src/views/asset/vehicle-replacement/data.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
useActionColumn(3, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
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()),
|
||||||
|
},
|
||||||
|
rules: z.number({ message: '请选择关联合同' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'originalVehicleId',
|
||||||
|
label: '原车辆',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择原车辆',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/asset/vehicle').then((m) =>
|
||||||
|
m.getVehicleSimpleList(),
|
||||||
|
),
|
||||||
|
labelField: 'plateNo',
|
||||||
|
valueField: 'id',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: z.number({ message: '请选择原车辆' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'newVehicleId',
|
||||||
|
label: '新车辆',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择新车辆',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/asset/vehicle').then((m) =>
|
||||||
|
m.getVehicleSimpleList(),
|
||||||
|
),
|
||||||
|
labelField: 'plateNo',
|
||||||
|
valueField: 'id',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: z.number({ message: '请选择新车辆' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: '请选择预计替换日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'returnDate',
|
||||||
|
label: '预计归还日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择预计归还日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['replacementType'],
|
||||||
|
show(values) {
|
||||||
|
return values.replacementType === 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
233
apps/web-antd/src/views/asset/vehicle-replacement/index.vue
Normal file
233
apps/web-antd/src/views/asset/vehicle-replacement/index.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AssetVehicleReplacementApi } from '#/api/asset/vehicle-replacement';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
confirmVehicleReplacementReturn,
|
||||||
|
deleteVehicleReplacement,
|
||||||
|
getVehicleReplacementPage,
|
||||||
|
submitVehicleReplacementApproval,
|
||||||
|
withdrawVehicleReplacementApproval,
|
||||||
|
} from '#/api/asset/vehicle-replacement';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增替换车 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看替换车 */
|
||||||
|
function handleView(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
formModalApi.setData({ ...row, _mode: 'view' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑替换车 */
|
||||||
|
function handleEdit(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
formModalApi.setData({ ...row, _mode: 'edit' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除替换车 */
|
||||||
|
async function handleDelete(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.replacementCode]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteVehicleReplacement(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.replacementCode]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交审批 */
|
||||||
|
async function handleSubmitApproval(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: '提交审批中...',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await submitVehicleReplacementApproval(row.id!);
|
||||||
|
message.success('提交审批成功');
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 撤回审批 */
|
||||||
|
async function handleWithdrawApproval(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
await confirm('确认撤回审批吗?');
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: '撤回审批中...',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await withdrawVehicleReplacementApproval(row.id!);
|
||||||
|
message.success('撤回审批成功');
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认换回 */
|
||||||
|
async function handleConfirmReturn(row: AssetVehicleReplacementApi.VehicleReplacement) {
|
||||||
|
await confirm('确认换回原车吗?');
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: '确认换回中...',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await confirmVehicleReplacementReturn(row.id!);
|
||||||
|
message.success('确认换回成功');
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedIds = ref<number[]>([]);
|
||||||
|
function handleRowCheckboxChange({
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
records: AssetVehicleReplacementApi.VehicleReplacement[];
|
||||||
|
}) {
|
||||||
|
checkedIds.value = records.map((item) => item.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getVehicleReplacementPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<AssetVehicleReplacementApi.VehicleReplacement>,
|
||||||
|
gridEvents: {
|
||||||
|
checkboxAll: handleRowCheckboxChange,
|
||||||
|
checkboxChange: handleRowCheckboxChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="替换车列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['替换车']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['asset:vehicle-replacement:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.view'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
onClick: handleView.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['asset:vehicle-replacement:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
ifShow: row.status === 0 || row.status === 5 || row.status === 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['asset:vehicle-replacement:delete'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [
|
||||||
|
row.replacementCode,
|
||||||
|
]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '提交审批',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.CHECK,
|
||||||
|
auth: ['asset:vehicle-replacement:update'],
|
||||||
|
ifShow: row.status === 0 || row.status === 5 || row.status === 6,
|
||||||
|
onClick: handleSubmitApproval.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '撤回审批',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.CLOSE,
|
||||||
|
auth: ['asset:vehicle-replacement:update'],
|
||||||
|
ifShow: row.status === 1,
|
||||||
|
onClick: handleWithdrawApproval.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '确认换回',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.CHECK,
|
||||||
|
auth: ['asset:vehicle-replacement:update'],
|
||||||
|
ifShow: row.status === 3 && row.replacementType === 1,
|
||||||
|
onClick: handleConfirmReturn.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AssetVehicleReplacementApi } from '#/api/asset/vehicle-replacement';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Card, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createVehicleReplacement,
|
||||||
|
getVehicleReplacement,
|
||||||
|
updateVehicleReplacement,
|
||||||
|
} from '#/api/asset/vehicle-replacement';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
type FormMode = 'create' | 'edit' | 'view';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<AssetVehicleReplacementApi.VehicleReplacement>();
|
||||||
|
const mode = ref<FormMode>('create');
|
||||||
|
|
||||||
|
const isReadOnly = computed(() => mode.value === 'view');
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
if (mode.value === 'view') {
|
||||||
|
return '查看替换车';
|
||||||
|
}
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['替换车'])
|
||||||
|
: $t('ui.actionTitle.create', ['替换车']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 150,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
if (isReadOnly.value) {
|
||||||
|
await modalApi.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
|
||||||
|
const data =
|
||||||
|
(await formApi.getValues()) as AssetVehicleReplacementApi.VehicleReplacement;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateVehicleReplacement(data)
|
||||||
|
: createVehicleReplacement(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
mode.value = 'create';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<
|
||||||
|
AssetVehicleReplacementApi.VehicleReplacement & { _mode?: FormMode }
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Determine mode
|
||||||
|
if (data?._mode) {
|
||||||
|
mode.value = data._mode;
|
||||||
|
} else if (data?.id) {
|
||||||
|
mode.value = 'edit';
|
||||||
|
} else {
|
||||||
|
mode.value = 'create';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set readonly state on form
|
||||||
|
await formApi.setState({ commonConfig: { componentProps: { disabled: isReadOnly.value } } });
|
||||||
|
|
||||||
|
if (data?.id) {
|
||||||
|
// Edit or view mode: load data from API
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getVehicleReplacement(data.id);
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
} else if (data) {
|
||||||
|
// Create mode: check for pre-fill data (contractId, vehicleId, deliveryOrderId)
|
||||||
|
const prefillData: Record<string, any> = {};
|
||||||
|
if (data.contractId) {
|
||||||
|
prefillData.contractId = data.contractId;
|
||||||
|
}
|
||||||
|
if (data.originalVehicleId) {
|
||||||
|
prefillData.originalVehicleId = data.originalVehicleId;
|
||||||
|
}
|
||||||
|
if (data.deliveryOrderId) {
|
||||||
|
prefillData.deliveryOrderId = data.deliveryOrderId;
|
||||||
|
}
|
||||||
|
if (Object.keys(prefillData).length > 0) {
|
||||||
|
await formApi.setValues(prefillData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-3/4">
|
||||||
|
<div class="mx-4">
|
||||||
|
<Card :bordered="false">
|
||||||
|
<Form />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
103
apps/web-antd/src/views/energy/account/data.ts
Normal file
103
apps/web-antd/src/views/energy/account/data.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
export const ACCOUNT_STATUS_OPTIONS = [
|
||||||
|
{ label: '正常', value: 1, color: 'green' },
|
||||||
|
{ label: '冻结', value: 0, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FLOW_TYPE_OPTIONS = [
|
||||||
|
{ label: '充值', value: 1, color: 'green' },
|
||||||
|
{ label: '扣款', value: 2, color: 'red' },
|
||||||
|
{ label: '退款', value: 3, color: 'blue' },
|
||||||
|
{ label: '调整', value: 4, color: 'orange' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'accountStatus',
|
||||||
|
label: '账户状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: ACCOUNT_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择账户状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyAccountApi.Account>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
title: '客户名称',
|
||||||
|
minWidth: 150,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'balance',
|
||||||
|
title: '当前余额',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'right',
|
||||||
|
slots: { default: 'balance' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'initBalance',
|
||||||
|
title: '初始余额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accumulatedRecharge',
|
||||||
|
title: '累计充值',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accumulatedConsume',
|
||||||
|
title: '累计消费',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'reminderThreshold',
|
||||||
|
title: '预警阈值',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'accountStatus',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'accountStatus' },
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
130
apps/web-antd/src/views/energy/account/index.vue
Normal file
130
apps/web-antd/src/views/energy/account/index.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||||
|
|
||||||
|
import { ref, onMounted, h } from 'vue';
|
||||||
|
import { Page, SummaryCard } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
import { message, Tag, Modal, InputNumber } from 'ant-design-vue';
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getAccountPage, getAccountSummary, exportAccount, updateThreshold } from '#/api/energy/account';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { useGridColumns, useGridFormSchema, ACCOUNT_STATUS_OPTIONS } from './data';
|
||||||
|
import AccountDrawer from './modules/account-drawer.vue';
|
||||||
|
import RechargeModal from './modules/recharge-modal.vue';
|
||||||
|
|
||||||
|
const summary = ref<EnergyAccountApi.Summary>({ totalCount: 0, totalBalance: 0, totalRecharge: 0, warningCount: 0 });
|
||||||
|
const drawerRef = ref();
|
||||||
|
const rechargeRef = ref();
|
||||||
|
|
||||||
|
async function loadSummary() {
|
||||||
|
summary.value = await getAccountSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
loadSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDetail(row: EnergyAccountApi.Account) {
|
||||||
|
drawerRef.value?.open(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRecharge(row: EnergyAccountApi.Account) {
|
||||||
|
rechargeRef.value?.open(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set threshold — simple modal with InputNumber
|
||||||
|
function handleSetThreshold(row: EnergyAccountApi.Account) {
|
||||||
|
let thresholdValue = row.reminderThreshold || 0;
|
||||||
|
Modal.confirm({
|
||||||
|
title: '设置预警阈值',
|
||||||
|
content: () => {
|
||||||
|
return h(InputNumber, {
|
||||||
|
value: thresholdValue,
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
placeholder: '请输入预警阈值',
|
||||||
|
'onUpdate:value': (val: number) => { thresholdValue = val; },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async onOk() {
|
||||||
|
await updateThreshold(row.id!, thresholdValue);
|
||||||
|
message.success('阈值设置成功');
|
||||||
|
handleRefresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportAccount(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '能源账户.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: { collapsed: true, schema: useGridFormSchema() },
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getAccountPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: { keyField: 'id', isHover: true },
|
||||||
|
rowClassName: ({ row }: any) => (row.balance < 0 ? 'bg-red-50' : ''),
|
||||||
|
toolbarConfig: { refresh: true, search: true },
|
||||||
|
} as VxeTableGridOptions<EnergyAccountApi.Account>,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(loadSummary);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<div class="mb-4 grid grid-cols-4 gap-4 px-4 pt-4">
|
||||||
|
<SummaryCard title="账户总数" :value="summary.totalCount" icon="lucide:users" icon-color="#1890ff" icon-bg-color="#e6f7ff" />
|
||||||
|
<SummaryCard title="总余额" :value="summary.totalBalance" prefix="¥" :decimals="2" icon="lucide:wallet" icon-color="#52c41a" icon-bg-color="#f6ffed" />
|
||||||
|
<SummaryCard title="累计充值" :value="summary.totalRecharge" prefix="¥" :decimals="2" icon="lucide:trending-up" icon-color="#fa8c16" icon-bg-color="#fff7e6" />
|
||||||
|
<SummaryCard title="预警账户" :value="summary.warningCount" icon="lucide:alert-triangle" icon-color="#ff4d4f" icon-bg-color="#fff2f0" tooltip="余额低于阈值" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<Grid table-title="账户列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '导出', type: 'primary', icon: ACTION_ICON.DOWNLOAD, auth: ['energy:account:export'], onClick: handleExport },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #balance="{ row }">
|
||||||
|
<span :class="row.balance < 0 ? 'font-bold text-red-500' : row.balance <= row.reminderThreshold ? 'font-bold text-orange-500' : 'font-bold text-green-500'">
|
||||||
|
¥{{ row.balance?.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #accountStatus="{ row }">
|
||||||
|
<Tag :color="ACCOUNT_STATUS_OPTIONS.find(o => o.value === row.accountStatus)?.color">
|
||||||
|
{{ ACCOUNT_STATUS_OPTIONS.find(o => o.value === row.accountStatus)?.label }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', icon: ACTION_ICON.VIEW, onClick: () => handleDetail(row) },
|
||||||
|
{ label: '充值', type: 'link', auth: ['energy:account:update'], onClick: () => handleRecharge(row) },
|
||||||
|
{ label: '设置阈值', type: 'link', auth: ['energy:account:update'], onClick: () => handleSetThreshold(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<AccountDrawer ref="drawerRef" />
|
||||||
|
<RechargeModal ref="rechargeRef" @success="handleRefresh" />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Drawer, Tabs } from 'ant-design-vue';
|
||||||
|
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||||
|
import { getProjectAccountList, getFlowPage } from '#/api/energy/account';
|
||||||
|
import { FLOW_TYPE_OPTIONS } from '../data';
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const account = ref<EnergyAccountApi.Account>();
|
||||||
|
const projectAccounts = ref<EnergyAccountApi.ProjectAccount[]>([]);
|
||||||
|
const flows = ref<EnergyAccountApi.Flow[]>([]);
|
||||||
|
const activeTab = ref('project');
|
||||||
|
|
||||||
|
function open(data: EnergyAccountApi.Account) {
|
||||||
|
account.value = data;
|
||||||
|
visible.value = true;
|
||||||
|
loadProjectAccounts();
|
||||||
|
loadFlows();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjectAccounts() {
|
||||||
|
if (!account.value?.id) return;
|
||||||
|
projectAccounts.value = await getProjectAccountList(account.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFlows() {
|
||||||
|
if (!account.value?.id) return;
|
||||||
|
const result = await getFlowPage({ pageNo: 1, pageSize: 50, accountId: account.value.id } as any);
|
||||||
|
flows.value = result?.list || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project account columns for a-table
|
||||||
|
const projectColumns = [
|
||||||
|
{ title: '项目名称', dataIndex: 'projectName', width: 150 },
|
||||||
|
{ title: '合同编号', dataIndex: 'contractCode', width: 120 },
|
||||||
|
{ title: '项目余额', dataIndex: 'balance', width: 100, align: 'right' as const },
|
||||||
|
{ title: '已划账', dataIndex: 'accumulatedRecharge', width: 100, align: 'right' as const },
|
||||||
|
{ title: '已消费', dataIndex: 'accumulatedConsume', width: 100, align: 'right' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Flow columns for a-table
|
||||||
|
const flowColumns = [
|
||||||
|
{ title: '时间', dataIndex: 'createTime', width: 180 },
|
||||||
|
{ title: '流水类型', dataIndex: 'flowType', width: 80, customRender: ({ text }: any) => FLOW_TYPE_OPTIONS.find(o => o.value === text)?.label || text },
|
||||||
|
{ title: '变动金额', dataIndex: 'amount', width: 100, align: 'right' as const },
|
||||||
|
{ title: '变动前余额', dataIndex: 'balanceBefore', width: 100, align: 'right' as const },
|
||||||
|
{ title: '变动后余额', dataIndex: 'balanceAfter', width: 100, align: 'right' as const },
|
||||||
|
{ title: '关联单据号', dataIndex: 'bizCode', width: 120 },
|
||||||
|
{ title: '备注', dataIndex: 'remark', width: 150 },
|
||||||
|
];
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer v-model:open="visible" :title="account?.customerName + ' - 账户详情'" width="720">
|
||||||
|
<!-- Balance card -->
|
||||||
|
<div class="mb-4 rounded-lg p-6 text-white" style="background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%)">
|
||||||
|
<div class="text-sm opacity-80">当前余额</div>
|
||||||
|
<div class="text-3xl font-bold">¥{{ account?.balance?.toFixed(2) }}</div>
|
||||||
|
<div class="mt-2 flex gap-4 text-sm opacity-80">
|
||||||
|
<span>累计充值 ¥{{ account?.accumulatedRecharge?.toFixed(2) }}</span>
|
||||||
|
<span>累计消费 ¥{{ account?.accumulatedConsume?.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs v-model:activeKey="activeTab">
|
||||||
|
<Tabs.TabPane key="project" tab="项目账户">
|
||||||
|
<a-table :data-source="projectAccounts" :columns="projectColumns" :pagination="false" size="small" row-key="id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="flow" tab="账户流水">
|
||||||
|
<a-table :data-source="flows" :columns="flowColumns" :pagination="false" size="small" row-key="id" :scroll="{ y: 400 }" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { Modal, InputNumber, Input, Descriptions, message } from 'ant-design-vue';
|
||||||
|
import { rechargeAccount } from '#/api/energy/account';
|
||||||
|
import type { EnergyAccountApi } from '#/api/energy/account';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const account = ref<EnergyAccountApi.Account>();
|
||||||
|
const amount = ref<number>(0);
|
||||||
|
const remark = ref('');
|
||||||
|
|
||||||
|
const afterBalance = computed(() => (account.value?.balance || 0) + (amount.value || 0));
|
||||||
|
|
||||||
|
function open(data: EnergyAccountApi.Account) {
|
||||||
|
account.value = data;
|
||||||
|
amount.value = 0;
|
||||||
|
remark.value = '';
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
if (!amount.value || amount.value <= 0) { message.warning('请输入充值金额'); return; }
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await rechargeAccount(account.value!.customerId!, amount.value, remark.value || undefined);
|
||||||
|
message.success('充值成功');
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '充值失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal v-model:open="visible" title="账户充值" :confirm-loading="loading" @ok="handleConfirm">
|
||||||
|
<Descriptions :column="1" bordered size="small" class="mb-4">
|
||||||
|
<Descriptions.Item label="客户名称">{{ account?.customerName }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="当前余额">¥{{ account?.balance?.toFixed(2) }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">充值金额 <span class="text-red-500">*</span></label>
|
||||||
|
<InputNumber v-model:value="amount" :min="0.01" :precision="2" placeholder="请输入充值金额" class="w-full" addon-after="元" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">备注</label>
|
||||||
|
<Input.TextArea v-model:value="remark" :rows="2" placeholder="请输入备注(选填)" />
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-blue-50 p-3 text-center">
|
||||||
|
充值后余额:<strong class="text-blue-600">¥{{ afterBalance.toFixed(2) }}</strong>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
188
apps/web-antd/src/views/energy/bill/data.ts
Normal file
188
apps/web-antd/src/views/energy/bill/data.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyBillApi } from '#/api/energy/bill';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
export const COOPERATION_TYPE_OPTIONS = [
|
||||||
|
{ label: '预充值', value: 1, color: 'blue' },
|
||||||
|
{ label: '月结算', value: 2, color: 'pink' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BILL_STATUS_OPTIONS = [
|
||||||
|
{ label: '草稿', value: 0, color: 'orange' },
|
||||||
|
{ label: '已确认', value: 1, color: 'green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BILL_AUDIT_STATUS_OPTIONS = [
|
||||||
|
{ label: '待审核', value: 0, color: 'orange' },
|
||||||
|
{ label: '已通过', value: 1, color: 'green' },
|
||||||
|
{ label: '已驳回', value: 2, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PAYMENT_STATUS_OPTIONS = [
|
||||||
|
{ label: '未支付', value: 0, color: 'default' },
|
||||||
|
{ label: '部分支付', value: 1, color: 'orange' },
|
||||||
|
{ label: '已支付', value: 2, color: 'green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'stationName',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入加氢站',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'dateRange',
|
||||||
|
label: '账单时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
picker: 'month',
|
||||||
|
format: 'YYYY-MM',
|
||||||
|
valueFormat: 'YYYY-MM',
|
||||||
|
placeholder: ['开始月份', '结束月份'],
|
||||||
|
allowClear: true,
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'cooperationType',
|
||||||
|
label: '合作模式',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: COOPERATION_TYPE_OPTIONS,
|
||||||
|
placeholder: '请选择合作模式',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '账单状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: BILL_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择账单状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'auditStatus',
|
||||||
|
label: '审核状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: BILL_AUDIT_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择审核状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'paymentStatus',
|
||||||
|
label: '付款状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: PAYMENT_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择付款状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyBillApi.Bill>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'billCode',
|
||||||
|
title: '账单编号',
|
||||||
|
minWidth: 180,
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { default: 'billCode' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
title: '客户名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stationName',
|
||||||
|
title: '加氢站',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cooperationType',
|
||||||
|
title: '合作模式',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'cooperationType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'billPeriodStart',
|
||||||
|
title: '账单周期',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
formatter: ({ row }: { row: EnergyBillApi.Bill }) => {
|
||||||
|
if (row.billPeriodStart) {
|
||||||
|
return String(row.billPeriodStart).substring(0, 7);
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receivableAmount',
|
||||||
|
title: '应收金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'adjustmentAmount',
|
||||||
|
title: '调整金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
slots: { default: 'adjustmentAmount' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actualAmount',
|
||||||
|
title: '实收金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
slots: { default: 'actualAmount' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '账单状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'status' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'auditStatus',
|
||||||
|
title: '审核状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'auditStatus' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'paymentStatus',
|
||||||
|
title: '付款状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'paymentStatus' },
|
||||||
|
},
|
||||||
|
useActionColumn(3),
|
||||||
|
];
|
||||||
|
}
|
||||||
39
apps/web-antd/src/views/energy/bill/detail-data.ts
Normal file
39
apps/web-antd/src/views/energy/bill/detail-data.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Detail list columns (for reference / VXE usage)
|
||||||
|
export function useDetailColumns() {
|
||||||
|
return [
|
||||||
|
{ field: 'plateNumber', title: '车牌号', minWidth: 120 },
|
||||||
|
{ field: 'hydrogenDate', title: '加氢日期', minWidth: 120, formatter: 'formatDate' },
|
||||||
|
{ field: 'hydrogenQuantity', title: '加氢量(KG)', minWidth: 100, align: 'right' },
|
||||||
|
{ field: 'costPrice', title: '成本单价', minWidth: 80, align: 'right' },
|
||||||
|
{ field: 'costAmount', title: '成本金额', minWidth: 100, align: 'right' },
|
||||||
|
{ field: 'customerPrice', title: '对客单价', minWidth: 80, align: 'right' },
|
||||||
|
{ field: 'customerAmount', title: '对客金额', minWidth: 100, align: 'right' },
|
||||||
|
{
|
||||||
|
field: 'deductionStatus',
|
||||||
|
title: '扣款状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'deductionStatus' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjustment list columns (for reference / VXE usage)
|
||||||
|
export function useAdjustmentColumns() {
|
||||||
|
return [
|
||||||
|
{ field: 'adjustmentType', title: '调整类型', minWidth: 100, slots: { default: 'adjustmentType' } },
|
||||||
|
{ field: 'amount', title: '调整金额', minWidth: 100, align: 'right' },
|
||||||
|
{ field: 'reason', title: '调整原因', minWidth: 200 },
|
||||||
|
{ field: 'detailId', title: '关联明细', minWidth: 100 },
|
||||||
|
{ field: 'operatorName', title: '操作人', minWidth: 100 },
|
||||||
|
{ field: 'operateTime', title: '操作时间', minWidth: 180, formatter: 'formatDateTime' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export status constants from data.ts
|
||||||
|
export {
|
||||||
|
BILL_AUDIT_STATUS_OPTIONS,
|
||||||
|
BILL_STATUS_OPTIONS,
|
||||||
|
COOPERATION_TYPE_OPTIONS,
|
||||||
|
PAYMENT_STATUS_OPTIONS,
|
||||||
|
} from './data';
|
||||||
271
apps/web-antd/src/views/energy/bill/detail.vue
Normal file
271
apps/web-antd/src/views/energy/bill/detail.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EnergyBillApi } from '#/api/energy/bill';
|
||||||
|
import type { EnergyHydrogenDetailApi } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Descriptions,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Radio,
|
||||||
|
Tabs,
|
||||||
|
Tag,
|
||||||
|
Timeline,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
auditBill,
|
||||||
|
getAdjustmentList,
|
||||||
|
getBill,
|
||||||
|
getBillDetailList,
|
||||||
|
} from '#/api/energy/bill';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BILL_AUDIT_STATUS_OPTIONS,
|
||||||
|
BILL_STATUS_OPTIONS,
|
||||||
|
COOPERATION_TYPE_OPTIONS,
|
||||||
|
PAYMENT_STATUS_OPTIONS,
|
||||||
|
} from './detail-data';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const billId = Number(route.params.id);
|
||||||
|
|
||||||
|
const bill = ref<EnergyBillApi.Bill>({});
|
||||||
|
const details = ref<EnergyHydrogenDetailApi.Detail[]>([]);
|
||||||
|
const adjustments = ref<EnergyBillApi.Adjustment[]>([]);
|
||||||
|
const activeTab = ref('detail');
|
||||||
|
|
||||||
|
// Audit modal state
|
||||||
|
const auditVisible = ref(false);
|
||||||
|
const auditApproved = ref(true);
|
||||||
|
const auditRemark = ref('');
|
||||||
|
const auditLoading = ref(false);
|
||||||
|
|
||||||
|
// Ant Design Table column definitions
|
||||||
|
const detailTableColumns = [
|
||||||
|
{ title: '车牌号', dataIndex: 'plateNumber', width: 120 },
|
||||||
|
{ title: '加氢日期', dataIndex: 'hydrogenDate', width: 120 },
|
||||||
|
{ title: '加氢量(KG)', dataIndex: 'hydrogenQuantity', width: 100, align: 'right' as const },
|
||||||
|
{ title: '成本单价', dataIndex: 'costPrice', width: 80, align: 'right' as const },
|
||||||
|
{ title: '成本金额', dataIndex: 'costAmount', width: 100, align: 'right' as const },
|
||||||
|
{ title: '对客单价', dataIndex: 'customerPrice', width: 80, align: 'right' as const },
|
||||||
|
{ title: '对客金额', dataIndex: 'customerAmount', width: 100, align: 'right' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
const adjustmentTableColumns = [
|
||||||
|
{ title: '调整类型', dataIndex: 'adjustmentType', width: 100 },
|
||||||
|
{ title: '调整金额', dataIndex: 'amount', width: 100, align: 'right' as const },
|
||||||
|
{ title: '调整原因', dataIndex: 'reason', width: 200 },
|
||||||
|
{ title: '关联明细', dataIndex: 'detailId', width: 100 },
|
||||||
|
{ title: '操作人', dataIndex: 'operatorName', width: 100 },
|
||||||
|
{ title: '操作时间', dataIndex: 'operateTime', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
bill.value = await getBill(billId);
|
||||||
|
details.value = await getBillDetailList(billId);
|
||||||
|
adjustments.value = await getAdjustmentList(billId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAudit() {
|
||||||
|
auditLoading.value = true;
|
||||||
|
try {
|
||||||
|
await auditBill(billId, auditApproved.value, auditRemark.value || undefined);
|
||||||
|
message.success('审核完成');
|
||||||
|
auditVisible.value = false;
|
||||||
|
loadData();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '审核失败');
|
||||||
|
} finally {
|
||||||
|
auditLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
router.push({ name: 'EnergyBill' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: find option by value
|
||||||
|
function findOption(options: { value: any; label: string; color: string }[], value: any) {
|
||||||
|
return options.find((o) => o.value === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const billPeriod = computed(() => {
|
||||||
|
if (bill.value.billPeriodStart) {
|
||||||
|
return bill.value.billPeriodStart.substring(0, 7);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(loadData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-4 flex items-center justify-between px-4 pt-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h2 class="text-lg font-bold">{{ bill.billCode || '—' }}</h2>
|
||||||
|
<Tag
|
||||||
|
v-if="bill.status != null"
|
||||||
|
:color="findOption(BILL_STATUS_OPTIONS, bill.status)?.color"
|
||||||
|
>
|
||||||
|
{{ findOption(BILL_STATUS_OPTIONS, bill.status)?.label }}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
v-if="bill.auditStatus != null"
|
||||||
|
:color="findOption(BILL_AUDIT_STATUS_OPTIONS, bill.auditStatus)?.color"
|
||||||
|
>
|
||||||
|
{{ findOption(BILL_AUDIT_STATUS_OPTIONS, bill.auditStatus)?.label }}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
v-if="bill.paymentStatus != null"
|
||||||
|
:color="findOption(PAYMENT_STATUS_OPTIONS, bill.paymentStatus)?.color"
|
||||||
|
>
|
||||||
|
{{ findOption(PAYMENT_STATUS_OPTIONS, bill.paymentStatus)?.label }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="bill.status === 1 && bill.auditStatus === 0"
|
||||||
|
type="primary"
|
||||||
|
@click="auditVisible = true"
|
||||||
|
>
|
||||||
|
审核
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleBack">返回</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Cards -->
|
||||||
|
<div class="mb-4 grid grid-cols-2 gap-4 px-4">
|
||||||
|
<Card title="基本信息" size="small">
|
||||||
|
<Descriptions :column="2" bordered size="small">
|
||||||
|
<Descriptions.Item label="客户名称">{{ bill.customerName || '—' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="合同编号">{{ bill.contractId || '—' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="加氢站">{{ bill.stationName || '—' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="账单周期">{{ billPeriod || '—' }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
<Card title="金额汇总" size="small">
|
||||||
|
<!-- 预充值模式 -->
|
||||||
|
<template v-if="bill.cooperationType === 1">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>应收金额</span>
|
||||||
|
<span>¥{{ bill.receivableAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>调整金额</span>
|
||||||
|
<span class="text-red-500">¥{{ bill.adjustmentAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between font-bold">
|
||||||
|
<span>实收金额</span>
|
||||||
|
<span>¥{{ bill.actualAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>已扣款金额</span>
|
||||||
|
<span class="font-bold text-blue-500">¥{{ bill.paidAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-400">
|
||||||
|
预充值模式:加氢时已从账户实时扣款,账单仅作对账凭据
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<!-- 月结算模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>应收金额</span>
|
||||||
|
<span>¥{{ bill.receivableAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>调整金额</span>
|
||||||
|
<span class="text-red-500">¥{{ bill.adjustmentAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between font-bold">
|
||||||
|
<span>实收金额</span>
|
||||||
|
<span>¥{{ bill.actualAmount ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>已付款 / 待付款</span>
|
||||||
|
<span class="font-bold text-pink-500">
|
||||||
|
¥{{ bill.paidAmount ?? 0 }} /
|
||||||
|
¥{{ ((bill.actualAmount ?? 0) - (bill.paidAmount ?? 0)).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-400">
|
||||||
|
月结算模式:账单审核后发送客户,客户按账单付款
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="px-4">
|
||||||
|
<Tabs v-model:activeKey="activeTab">
|
||||||
|
<Tabs.TabPane key="detail" tab="加氢明细">
|
||||||
|
<a-table
|
||||||
|
:data-source="details"
|
||||||
|
:columns="detailTableColumns"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
row-key="id"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="adjustment" tab="调整记录">
|
||||||
|
<a-table
|
||||||
|
:data-source="adjustments"
|
||||||
|
:columns="adjustmentTableColumns"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
row-key="id"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="log" tab="操作日志">
|
||||||
|
<Timeline>
|
||||||
|
<Timeline.Item>账单创建于 {{ bill.createTime || '—' }}</Timeline.Item>
|
||||||
|
<Timeline.Item v-if="bill.auditTime">
|
||||||
|
账单审核于 {{ bill.auditTime }}
|
||||||
|
<span v-if="bill.auditRemark">({{ bill.auditRemark }})</span>
|
||||||
|
</Timeline.Item>
|
||||||
|
</Timeline>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audit Modal -->
|
||||||
|
<Modal
|
||||||
|
v-model:open="auditVisible"
|
||||||
|
title="账单审核"
|
||||||
|
:confirm-loading="auditLoading"
|
||||||
|
@ok="handleAudit"
|
||||||
|
@cancel="auditVisible = false"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">审核结果</label>
|
||||||
|
<Radio.Group v-model:value="auditApproved">
|
||||||
|
<Radio :value="true">通过</Radio>
|
||||||
|
<Radio :value="false">驳回</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block font-medium">审核备注</label>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="auditRemark"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入审核备注(选填)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
318
apps/web-antd/src/views/energy/bill/index.vue
Normal file
318
apps/web-antd/src/views/energy/bill/index.vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyBillApi } from '#/api/energy/bill';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Modal, Tag, message } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteBill,
|
||||||
|
exportBill,
|
||||||
|
generateBill,
|
||||||
|
getBillPage,
|
||||||
|
} from '#/api/energy/bill';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BILL_AUDIT_STATUS_OPTIONS,
|
||||||
|
BILL_STATUS_OPTIONS,
|
||||||
|
COOPERATION_TYPE_OPTIONS,
|
||||||
|
PAYMENT_STATUS_OPTIONS,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import GenerateModal from './modules/generate-modal.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const generateModalRef = ref<InstanceType<typeof GenerateModal>>();
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出 */
|
||||||
|
async function handleExport() {
|
||||||
|
const formValues = await gridApi.formApi?.getValues();
|
||||||
|
const blob = await exportBill(formValues ?? {});
|
||||||
|
downloadFileFromBlobPart({ fileName: '能源账单.xlsx', source: blob });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 快速生成账单 */
|
||||||
|
function handleQuickGenerate(type: 'current' | 'last') {
|
||||||
|
const now = dayjs();
|
||||||
|
let startDate: string;
|
||||||
|
let endDate: string;
|
||||||
|
let title: string;
|
||||||
|
|
||||||
|
if (type === 'current') {
|
||||||
|
startDate = now.startOf('month').format('YYYY-MM-DD');
|
||||||
|
endDate = now.format('YYYY-MM-DD');
|
||||||
|
title = '本月';
|
||||||
|
} else {
|
||||||
|
startDate = now.subtract(1, 'month').startOf('month').format('YYYY-MM-DD');
|
||||||
|
endDate = now.subtract(1, 'month').endOf('month').format('YYYY-MM-DD');
|
||||||
|
title = '上月';
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: `生成${title}账单`,
|
||||||
|
content: `确定要生成 ${startDate} 至 ${endDate} 的账单吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await generateBill({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
message.success(`账单生成成功!共生成 ${res.total || 0} 张账单`);
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('生成账单失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成账单 */
|
||||||
|
function handleGenerate() {
|
||||||
|
generateModalRef.value?.open('single');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量生成 */
|
||||||
|
function handleBatchGenerate() {
|
||||||
|
generateModalRef.value?.open('batch');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跳转详情 */
|
||||||
|
function handleView(row: EnergyBillApi.Bill) {
|
||||||
|
router.push({ name: 'EnergyBillDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑(占位) */
|
||||||
|
function handleEdit(_row: EnergyBillApi.Bill) {
|
||||||
|
// TODO: open edit modal
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 */
|
||||||
|
async function handleDelete(row: EnergyBillApi.Bill) {
|
||||||
|
try {
|
||||||
|
await deleteBill(row.id!);
|
||||||
|
message.success('删除成功');
|
||||||
|
handleRefresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核跳转详情 */
|
||||||
|
function handleAudit(row: EnergyBillApi.Bill) {
|
||||||
|
router.push({ name: 'EnergyBillDetail', params: { id: row.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getBillPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<EnergyBillApi.Bill>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<GenerateModal ref="generateModalRef" @success="handleRefresh" />
|
||||||
|
<Grid table-title="能源账单列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '生成本月账单',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:thunderbolt-outlined',
|
||||||
|
auth: ['energy:bill:create'],
|
||||||
|
onClick: () => handleQuickGenerate('current'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '生成上月账单',
|
||||||
|
icon: 'ant-design:calendar-outlined',
|
||||||
|
auth: ['energy:bill:create'],
|
||||||
|
onClick: () => handleQuickGenerate('last'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '自定义生成',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['energy:bill:create'],
|
||||||
|
onClick: handleGenerate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['energy:bill:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 账单编号 - router-link -->
|
||||||
|
<template #billCode="{ row }">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'EnergyBillDetail', params: { id: row.id } }"
|
||||||
|
class="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{{ row.billCode }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 合作模式 -->
|
||||||
|
<template #cooperationType="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.cooperationType != null"
|
||||||
|
:color="
|
||||||
|
COOPERATION_TYPE_OPTIONS.find(
|
||||||
|
(o) => o.value === row.cooperationType,
|
||||||
|
)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
COOPERATION_TYPE_OPTIONS.find(
|
||||||
|
(o) => o.value === row.cooperationType,
|
||||||
|
)?.label ?? row.cooperationType
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 调整金额 - 负数红色 -->
|
||||||
|
<template #adjustmentAmount="{ row }">
|
||||||
|
<span :class="{ 'text-red-500': (row.adjustmentAmount ?? 0) < 0 }">
|
||||||
|
{{ row.adjustmentAmount ?? '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 实收金额 - 加粗 -->
|
||||||
|
<template #actualAmount="{ row }">
|
||||||
|
<span class="font-bold">{{ row.actualAmount ?? '—' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 账单状态 -->
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.status != null"
|
||||||
|
:color="
|
||||||
|
BILL_STATUS_OPTIONS.find((o) => o.value === row.status)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
BILL_STATUS_OPTIONS.find((o) => o.value === row.status)?.label ??
|
||||||
|
row.status
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 审核状态 -->
|
||||||
|
<template #auditStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.auditStatus != null"
|
||||||
|
:color="
|
||||||
|
BILL_AUDIT_STATUS_OPTIONS.find((o) => o.value === row.auditStatus)
|
||||||
|
?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
BILL_AUDIT_STATUS_OPTIONS.find((o) => o.value === row.auditStatus)
|
||||||
|
?.label ?? row.auditStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 付款状态 -->
|
||||||
|
<template #paymentStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.paymentStatus != null"
|
||||||
|
:color="
|
||||||
|
PAYMENT_STATUS_OPTIONS.find((o) => o.value === row.paymentStatus)
|
||||||
|
?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
PAYMENT_STATUS_OPTIONS.find((o) => o.value === row.paymentStatus)
|
||||||
|
?.label ?? row.paymentStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '详情',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
onClick: () => handleView(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
show: row.status === 0,
|
||||||
|
auth: ['energy:bill:update'],
|
||||||
|
onClick: () => handleEdit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
show: row.status === 0,
|
||||||
|
auth: ['energy:bill:delete'],
|
||||||
|
onClick: () => handleDelete(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '审核',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.AUDIT,
|
||||||
|
show: row.status === 1 && row.auditStatus === 0,
|
||||||
|
auth: ['energy:bill:audit'],
|
||||||
|
onClick: () => handleAudit(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
250
apps/web-antd/src/views/energy/bill/modules/generate-modal.vue
Normal file
250
apps/web-antd/src/views/energy/bill/modules/generate-modal.vue
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Alert, Button, DatePicker, message, Modal, RangePicker, Select, Space } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
batchGenerateByPeriod,
|
||||||
|
generateBill,
|
||||||
|
} from '#/api/energy/bill';
|
||||||
|
import type { EnergyBillApi } from '#/api/energy/bill';
|
||||||
|
import { getStationConfigSimpleList } from '#/api/energy/station-config';
|
||||||
|
import { getSimpleCustomerList } from '#/api/asset/customer';
|
||||||
|
import { getSimpleContractList } from '#/api/asset/contract';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const mode = ref<'batch' | 'single'>('single');
|
||||||
|
|
||||||
|
// Single mode fields
|
||||||
|
const customerId = ref<number>();
|
||||||
|
const contractId = ref<number>();
|
||||||
|
const stationId = ref<number>();
|
||||||
|
const dateRange = ref<any>();
|
||||||
|
const billPeriod = ref<any>();
|
||||||
|
|
||||||
|
// Dropdown options
|
||||||
|
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
const contractOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
const filteredContractOptions = ref<{ label: string; value: number; customerId?: number }[]>([]);
|
||||||
|
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
const [customers, contracts, stations] = await Promise.all([
|
||||||
|
getSimpleCustomerList(),
|
||||||
|
getSimpleContractList(),
|
||||||
|
getStationConfigSimpleList(),
|
||||||
|
]);
|
||||||
|
customerOptions.value = customers.map((c) => ({
|
||||||
|
label: c.customerName,
|
||||||
|
value: c.id!,
|
||||||
|
}));
|
||||||
|
contractOptions.value = contracts.map((c) => ({
|
||||||
|
label: `${c.contractCode} - ${c.projectName}`,
|
||||||
|
value: c.id!,
|
||||||
|
customerId: c.customerId,
|
||||||
|
}));
|
||||||
|
filteredContractOptions.value = contractOptions.value;
|
||||||
|
stationOptions.value = stations.map((s) => ({
|
||||||
|
label: s.stationName,
|
||||||
|
value: s.stationId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择客户后,筛选该客户下的合同
|
||||||
|
watch(customerId, (val) => {
|
||||||
|
contractId.value = undefined;
|
||||||
|
if (val) {
|
||||||
|
filteredContractOptions.value = contractOptions.value.filter(
|
||||||
|
(c) => (c as any).customerId === val,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredContractOptions.value = contractOptions.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 快捷时间选择
|
||||||
|
function setDateRange(type: string) {
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'today':
|
||||||
|
dateRange.value = [now, now];
|
||||||
|
break;
|
||||||
|
case 'yesterday':
|
||||||
|
dateRange.value = [now.subtract(1, 'day'), now.subtract(1, 'day')];
|
||||||
|
break;
|
||||||
|
case 'thisWeek':
|
||||||
|
dateRange.value = [now.startOf('week'), now];
|
||||||
|
break;
|
||||||
|
case 'lastWeek':
|
||||||
|
dateRange.value = [
|
||||||
|
now.subtract(1, 'week').startOf('week'),
|
||||||
|
now.subtract(1, 'week').endOf('week'),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'thisMonth':
|
||||||
|
dateRange.value = [now.startOf('month'), now];
|
||||||
|
break;
|
||||||
|
case 'lastMonth':
|
||||||
|
dateRange.value = [
|
||||||
|
now.subtract(1, 'month').startOf('month'),
|
||||||
|
now.subtract(1, 'month').endOf('month'),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(m: 'batch' | 'single' = 'single') {
|
||||||
|
mode.value = m;
|
||||||
|
customerId.value = undefined;
|
||||||
|
contractId.value = undefined;
|
||||||
|
stationId.value = undefined;
|
||||||
|
dateRange.value = undefined;
|
||||||
|
billPeriod.value = undefined;
|
||||||
|
visible.value = true;
|
||||||
|
if (m === 'single') {
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (mode.value === 'single') {
|
||||||
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
|
message.warning('请选择时间范围');
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data: EnergyBillApi.GenerateReq = {
|
||||||
|
customerId: customerId.value!,
|
||||||
|
contractId: contractId.value!,
|
||||||
|
stationId: stationId.value!,
|
||||||
|
billPeriodStart: dayjs(dateRange.value[0]).format('YYYY-MM-DD'),
|
||||||
|
billPeriodEnd: dayjs(dateRange.value[1]).format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
await generateBill(data);
|
||||||
|
message.success('账单生成成功');
|
||||||
|
} else {
|
||||||
|
if (!billPeriod.value) {
|
||||||
|
message.warning('请选择账单周期');
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const periodStr = dayjs(billPeriod.value).format('YYYY-MM');
|
||||||
|
const result = await batchGenerateByPeriod(periodStr);
|
||||||
|
const generated = result.generatedCount ?? 0;
|
||||||
|
const skipped = result.skippedCount ?? 0;
|
||||||
|
message.success(`批量生成完成:成功 ${generated} 条,跳过 ${skipped} 条`);
|
||||||
|
}
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '生成失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="mode === 'single' ? '自定义生成账单' : '批量生成账单'"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<template v-if="mode === 'single'">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
时间范围 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="dateRange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
:placeholder="['开始日期', '结束日期']"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<Space>
|
||||||
|
<Button size="small" @click="setDateRange('today')">今天</Button>
|
||||||
|
<Button size="small" @click="setDateRange('yesterday')">昨天</Button>
|
||||||
|
<Button size="small" @click="setDateRange('thisWeek')">本周</Button>
|
||||||
|
<Button size="small" @click="setDateRange('lastWeek')">上周</Button>
|
||||||
|
<Button size="small" @click="setDateRange('thisMonth')">本月</Button>
|
||||||
|
<Button size="small" @click="setDateRange('lastMonth')">上月</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">客户</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="customerId"
|
||||||
|
:options="customerOptions"
|
||||||
|
placeholder="全部客户"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">合同</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="contractId"
|
||||||
|
:options="filteredContractOptions"
|
||||||
|
placeholder="全部合同"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">加氢站</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="stationId"
|
||||||
|
:options="stationOptions"
|
||||||
|
placeholder="全部站点"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Alert type="info" show-icon message="提示">
|
||||||
|
<template #description>
|
||||||
|
<ul>
|
||||||
|
<li>不选择客户/合同/站点时,将生成所有符合条件的账单</li>
|
||||||
|
<li>账单按(客户+合同+站点+时间段)分组生成</li>
|
||||||
|
<li>只有已审核通过的明细才会生成账单</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
账单周期 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="billPeriod"
|
||||||
|
picker="month"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="选择月份"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
message="系统将自动为所有有未出账明细的客户+合同+站点组合生成当月草稿账单。"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
181
apps/web-antd/src/views/energy/hydrogen-detail/data.ts
Normal file
181
apps/web-antd/src/views/energy/hydrogen-detail/data.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyHydrogenDetailApi } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
export const AUDIT_STATUS_OPTIONS = [
|
||||||
|
{ label: '待审核', value: 0, color: 'orange' },
|
||||||
|
{ label: '已审核', value: 1, color: 'green' },
|
||||||
|
{ label: '审核驳回', value: 2, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEDUCTION_STATUS_OPTIONS = [
|
||||||
|
{ label: '未扣款', value: 0, color: 'default' },
|
||||||
|
{ label: '已扣款', value: 1, color: 'green' },
|
||||||
|
{ label: '扣款失败', value: 2, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SETTLEMENT_STATUS_OPTIONS = [
|
||||||
|
{ label: '未结算', value: 0, color: 'default' },
|
||||||
|
{ label: '已结算', value: 1, color: 'green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'stationName',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入加氢站',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNumber',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车牌号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenDate',
|
||||||
|
label: '加氢日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'auditStatus',
|
||||||
|
label: '审核状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: AUDIT_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择审核状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'deductionStatus',
|
||||||
|
label: '扣款状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: DEDUCTION_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择扣款状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'settlementStatus',
|
||||||
|
label: '结算状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: SETTLEMENT_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择结算状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyHydrogenDetailApi.Detail>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
width: 60,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stationName',
|
||||||
|
title: '加氢站',
|
||||||
|
minWidth: 150,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
title: '客户名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'plateNumber',
|
||||||
|
title: '车牌号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenDate',
|
||||||
|
title: '加氢日期',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatDate',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenQuantity',
|
||||||
|
title: '加氢量',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'costPrice',
|
||||||
|
title: '成本单价',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'costAmount',
|
||||||
|
title: '成本金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerPrice',
|
||||||
|
title: '对客单价',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerAmount',
|
||||||
|
title: '对客金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'auditStatus',
|
||||||
|
title: '审核状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'auditStatus' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deductionStatus',
|
||||||
|
title: '扣款状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'deductionStatus' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'settlementStatus',
|
||||||
|
title: '结算状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'settlementStatus' },
|
||||||
|
},
|
||||||
|
useActionColumn(2),
|
||||||
|
];
|
||||||
|
}
|
||||||
230
apps/web-antd/src/views/energy/hydrogen-detail/index.vue
Normal file
230
apps/web-antd/src/views/energy/hydrogen-detail/index.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyHydrogenDetailApi } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Tag, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
exportHydrogenDetail,
|
||||||
|
getHydrogenDetailPage,
|
||||||
|
} from '#/api/energy/hydrogen-detail';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AUDIT_STATUS_OPTIONS,
|
||||||
|
DEDUCTION_STATUS_OPTIONS,
|
||||||
|
SETTLEMENT_STATUS_OPTIONS,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import AuditModal from './modules/audit-modal.vue';
|
||||||
|
|
||||||
|
const auditModalRef = ref<InstanceType<typeof AuditModal>>();
|
||||||
|
const checkedRecords = ref<EnergyHydrogenDetailApi.Detail[]>([]);
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出 */
|
||||||
|
async function handleExport() {
|
||||||
|
const formValues = await gridApi.formApi?.getValues();
|
||||||
|
const blob = await exportHydrogenDetail(formValues ?? {});
|
||||||
|
downloadFileFromBlobPart({ fileName: '加氢明细.xlsx', source: blob });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量审核 */
|
||||||
|
function handleBatchAudit() {
|
||||||
|
if (checkedRecords.value.length === 0) {
|
||||||
|
message.warning('请选择要审核的明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤出待审核的记录
|
||||||
|
const pendingRecords = checkedRecords.value.filter((r) => r.auditStatus === 0);
|
||||||
|
|
||||||
|
if (pendingRecords.length === 0) {
|
||||||
|
message.warning('所选记录中没有待审核的明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingRecords.length < checkedRecords.value.length) {
|
||||||
|
message.warning(
|
||||||
|
`已过滤 ${checkedRecords.value.length - pendingRecords.length} 条非待审核记录`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditModalRef.value?.open(pendingRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单条审核 */
|
||||||
|
function handleAudit(row: EnergyHydrogenDetailApi.Detail) {
|
||||||
|
auditModalRef.value?.open([row]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑(占位) */
|
||||||
|
function handleEdit(_row: EnergyHydrogenDetailApi.Detail) {
|
||||||
|
// TODO: open edit modal
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看(占位) */
|
||||||
|
function handleView(_row: EnergyHydrogenDetailApi.Detail) {
|
||||||
|
// TODO: open view modal
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
checkboxConfig: {
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getHydrogenDetailPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
onCheckboxChange: ({ records }) => {
|
||||||
|
checkedRecords.value = records;
|
||||||
|
},
|
||||||
|
onCheckboxAll: ({ records }) => {
|
||||||
|
checkedRecords.value = records;
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<EnergyHydrogenDetailApi.Detail>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<AuditModal ref="auditModalRef" @success="handleRefresh" />
|
||||||
|
<Grid table-title="加氢明细列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '批量审核',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.AUDIT,
|
||||||
|
disabled: checkedRecords.length === 0,
|
||||||
|
auth: ['energy:hydrogen-detail:update'],
|
||||||
|
onClick: handleBatchAudit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['energy:hydrogen-detail:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #auditStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.auditStatus != null"
|
||||||
|
:color="
|
||||||
|
AUDIT_STATUS_OPTIONS.find((o) => o.value === row.auditStatus)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
AUDIT_STATUS_OPTIONS.find((o) => o.value === row.auditStatus)
|
||||||
|
?.label ?? row.auditStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #deductionStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.deductionStatus != null"
|
||||||
|
:color="
|
||||||
|
DEDUCTION_STATUS_OPTIONS.find(
|
||||||
|
(o) => o.value === row.deductionStatus,
|
||||||
|
)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
DEDUCTION_STATUS_OPTIONS.find(
|
||||||
|
(o) => o.value === row.deductionStatus,
|
||||||
|
)?.label ?? row.deductionStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #settlementStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.settlementStatus != null"
|
||||||
|
:color="
|
||||||
|
SETTLEMENT_STATUS_OPTIONS.find(
|
||||||
|
(o) => o.value === row.settlementStatus,
|
||||||
|
)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
SETTLEMENT_STATUS_OPTIONS.find(
|
||||||
|
(o) => o.value === row.settlementStatus,
|
||||||
|
)?.label ?? row.settlementStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '审核',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.AUDIT,
|
||||||
|
show: row.auditStatus === 0,
|
||||||
|
auth: ['energy:hydrogen-detail:update'],
|
||||||
|
onClick: () => handleAudit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
show: row.auditStatus === 0 || row.auditStatus === 2,
|
||||||
|
auth: ['energy:hydrogen-detail:update'],
|
||||||
|
onClick: () => handleEdit(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.view'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.VIEW,
|
||||||
|
show: row.auditStatus === 1,
|
||||||
|
onClick: () => handleView(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EnergyHydrogenDetailApi } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Descriptions, Input, message, Modal, Radio } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { auditHydrogenDetail, batchAuditHydrogenDetail } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const records = ref<EnergyHydrogenDetailApi.Detail[]>([]);
|
||||||
|
const approved = ref(true);
|
||||||
|
const remark = ref('');
|
||||||
|
|
||||||
|
const isBatch = computed(() => records.value.length > 1);
|
||||||
|
const totalAmount = computed(() =>
|
||||||
|
records.value.reduce((sum, r) => sum + (r.customerAmount || 0), 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
function open(data: EnergyHydrogenDetailApi.Detail[]) {
|
||||||
|
records.value = data;
|
||||||
|
approved.value = true;
|
||||||
|
remark.value = '';
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
if (!approved.value && !remark.value) {
|
||||||
|
message.warning('驳回时必须填写审核备注');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (isBatch.value) {
|
||||||
|
// 批量审核:调用批量接口
|
||||||
|
const result = await batchAuditHydrogenDetail({
|
||||||
|
ids: records.value.map(r => r.id!),
|
||||||
|
passed: approved.value,
|
||||||
|
remark: remark.value || undefined,
|
||||||
|
});
|
||||||
|
message.success(
|
||||||
|
`审核完成!成功 ${result.successCount} 条,失败 ${result.failCount} 条`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 单条审核:调用单条接口
|
||||||
|
await auditHydrogenDetail(
|
||||||
|
records.value[0].id!,
|
||||||
|
approved.value,
|
||||||
|
remark.value || undefined,
|
||||||
|
);
|
||||||
|
message.success('审核完成');
|
||||||
|
}
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '审核失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="审核"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<!-- Single record summary -->
|
||||||
|
<Descriptions
|
||||||
|
v-if="!isBatch && records[0]"
|
||||||
|
:column="2"
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<Descriptions.Item label="车牌号">{{
|
||||||
|
records[0].plateNumber
|
||||||
|
}}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="加氢站">{{
|
||||||
|
records[0].stationName
|
||||||
|
}}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="加氢日期">{{
|
||||||
|
records[0].hydrogenDate
|
||||||
|
}}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="对客金额"
|
||||||
|
>¥{{ records[0].customerAmount }}</Descriptions.Item
|
||||||
|
>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
<!-- Batch summary -->
|
||||||
|
<div v-if="isBatch" class="mb-4 rounded bg-gray-50 p-4">
|
||||||
|
<p>已选中 <strong>{{ records.length }}</strong> 条记录</p>
|
||||||
|
<p>合计金额:<strong>¥{{ totalAmount.toFixed(2) }}</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block font-medium">审核结果</label>
|
||||||
|
<Radio.Group v-model:value="approved">
|
||||||
|
<Radio :value="true">通过</Radio>
|
||||||
|
<Radio :value="false">驳回</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block font-medium">
|
||||||
|
审核备注
|
||||||
|
<span v-if="!approved" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="remark"
|
||||||
|
:rows="3"
|
||||||
|
:placeholder="approved ? '请输入审核备注(选填)' : '请输入驳回原因(必填)'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
243
apps/web-antd/src/views/energy/hydrogen-record/data.ts
Normal file
243
apps/web-antd/src/views/energy/hydrogen-record/data.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyHydrogenRecordApi } from '#/api/energy/hydrogen-record';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择加氢站',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/energy/station-config').then(
|
||||||
|
(m) => m.getStationConfigSimpleList(),
|
||||||
|
),
|
||||||
|
labelField: 'stationName',
|
||||||
|
valueField: 'stationId',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
option.label?.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNumber',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车牌号',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenDate',
|
||||||
|
label: '加氢日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择加氢日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenQuantity',
|
||||||
|
label: '加氢量(KG)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入加氢量',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'unitPrice',
|
||||||
|
label: '单价(元/KG)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入单价',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'amount',
|
||||||
|
label: '金额',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入金额',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'mileage',
|
||||||
|
label: '里程(KM)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入里程数',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SOURCE_TYPE_OPTIONS = [
|
||||||
|
{ label: 'Excel导入', value: 1, color: 'blue' },
|
||||||
|
{ label: 'API同步', value: 2, color: 'green' },
|
||||||
|
{ label: '手动录入', value: 3, color: 'default' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MATCH_STATUS_OPTIONS = [
|
||||||
|
{ label: '完全匹配', value: 0, color: 'green' },
|
||||||
|
{ label: '部分匹配', value: 1, color: 'orange' },
|
||||||
|
{ label: '未匹配', value: 2, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择加氢站',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/energy/station-config').then(
|
||||||
|
(m) => m.getStationConfigSimpleList(),
|
||||||
|
),
|
||||||
|
labelField: 'stationName',
|
||||||
|
valueField: 'stationId',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
option.label?.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'plateNumber',
|
||||||
|
label: '车牌号',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入车牌号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'hydrogenDate',
|
||||||
|
label: '加氢日期',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'matchStatus',
|
||||||
|
label: '匹配状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: MATCH_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择匹配状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sourceType',
|
||||||
|
label: '数据来源',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: SOURCE_TYPE_OPTIONS,
|
||||||
|
placeholder: '请选择数据来源',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyHydrogenRecordApi.Record>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'stationName',
|
||||||
|
title: '站点名称',
|
||||||
|
minWidth: 150,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'plateNumber',
|
||||||
|
title: '车牌号',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenDate',
|
||||||
|
title: '加氢日期',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatDate',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'hydrogenQuantity',
|
||||||
|
title: '加氢量(KG)',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'unitPrice',
|
||||||
|
title: '单价',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'amount',
|
||||||
|
title: '金额',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'mileage',
|
||||||
|
title: '里程数',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sourceType',
|
||||||
|
title: '数据来源',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'sourceType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'matchStatus',
|
||||||
|
title: '匹配状态',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
slots: { default: 'matchStatus' },
|
||||||
|
},
|
||||||
|
useActionColumn(2),
|
||||||
|
];
|
||||||
|
}
|
||||||
171
apps/web-antd/src/views/energy/hydrogen-record/index.vue
Normal file
171
apps/web-antd/src/views/energy/hydrogen-record/index.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyHydrogenRecordApi } from '#/api/energy/hydrogen-record';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Tag, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteHydrogenRecord,
|
||||||
|
exportHydrogenRecord,
|
||||||
|
getHydrogenRecordPage,
|
||||||
|
} from '#/api/energy/hydrogen-record';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MATCH_STATUS_OPTIONS,
|
||||||
|
SOURCE_TYPE_OPTIONS,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import ImportModal from './modules/import-modal.vue';
|
||||||
|
import FormModal from './modules/form-modal.vue';
|
||||||
|
|
||||||
|
const importModalRef = ref<InstanceType<typeof ImportModal>>();
|
||||||
|
const formModalRef = ref<InstanceType<typeof FormModal>>();
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开导入弹窗 */
|
||||||
|
function handleImport() {
|
||||||
|
importModalRef.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出 */
|
||||||
|
async function handleExport() {
|
||||||
|
const formValues = await gridApi.formApi?.getValues();
|
||||||
|
const blob = await exportHydrogenRecord(formValues ?? {});
|
||||||
|
downloadFileFromBlobPart({ fileName: '加氢记录.xlsx', source: blob });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑记录 */
|
||||||
|
function handleEdit(row: EnergyHydrogenRecordApi.Record) {
|
||||||
|
formModalRef.value?.open(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除记录 */
|
||||||
|
async function handleDelete(row: EnergyHydrogenRecordApi.Record) {
|
||||||
|
await deleteHydrogenRecord(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getHydrogenRecordPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<EnergyHydrogenRecordApi.Record>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal ref="formModalRef" @success="handleRefresh" />
|
||||||
|
<ImportModal ref="importModalRef" @success="handleRefresh" />
|
||||||
|
<Grid table-title="加氢记录列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: 'Excel导入',
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.UPLOAD,
|
||||||
|
auth: ['energy:hydrogen-record:import'],
|
||||||
|
onClick: handleImport,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['energy:hydrogen-record:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #sourceType="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.sourceType != null"
|
||||||
|
:color="
|
||||||
|
SOURCE_TYPE_OPTIONS.find((o) => o.value === row.sourceType)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
SOURCE_TYPE_OPTIONS.find((o) => o.value === row.sourceType)
|
||||||
|
?.label ?? row.sourceType
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
<template #matchStatus="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.matchStatus != null"
|
||||||
|
:color="
|
||||||
|
MATCH_STATUS_OPTIONS.find((o) => o.value === row.matchStatus)?.color
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
MATCH_STATUS_OPTIONS.find((o) => o.value === row.matchStatus)
|
||||||
|
?.label ?? row.matchStatus
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['energy:hydrogen-record:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['energy:hydrogen-record:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.plateNumber]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EnergyHydrogenRecordApi } from '#/api/energy/hydrogen-record';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DatePicker, InputNumber, message, Modal, Select } from 'ant-design-vue';
|
||||||
|
import { Input } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createHydrogenRecord,
|
||||||
|
updateHydrogenRecord,
|
||||||
|
} from '#/api/energy/hydrogen-record';
|
||||||
|
import { getStationConfigSimpleList } from '#/api/energy/station-config';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formData = ref<Partial<EnergyHydrogenRecordApi.Record>>({});
|
||||||
|
const isUpdate = computed(() => !!formData.value?.id);
|
||||||
|
|
||||||
|
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
async function loadOptions() {
|
||||||
|
const stations = await getStationConfigSimpleList();
|
||||||
|
stationOptions.value = stations.map((s) => ({
|
||||||
|
label: s.stationName,
|
||||||
|
value: s.stationId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(row?: EnergyHydrogenRecordApi.Record) {
|
||||||
|
if (row && row.id) {
|
||||||
|
formData.value = { ...row };
|
||||||
|
} else {
|
||||||
|
formData.value = {};
|
||||||
|
}
|
||||||
|
visible.value = true;
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
if (
|
||||||
|
!formData.value.stationId ||
|
||||||
|
!formData.value.plateNumber ||
|
||||||
|
!formData.value.hydrogenDate
|
||||||
|
) {
|
||||||
|
message.warning('请填写必填项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (isUpdate.value) {
|
||||||
|
await updateHydrogenRecord(
|
||||||
|
formData.value as EnergyHydrogenRecordApi.Record,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await createHydrogenRecord(
|
||||||
|
formData.value as EnergyHydrogenRecordApi.Record,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
message.success('操作成功');
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message || '操作失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterOption = (input: string, option: any) =>
|
||||||
|
option.label?.toLowerCase().includes(input.toLowerCase());
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="isUpdate ? '修改加氢记录' : '新增加氢记录'"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
@ok="handleConfirm"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
加氢站 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model:value="formData.stationId"
|
||||||
|
:options="stationOptions"
|
||||||
|
placeholder="请选择加氢站"
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOption"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
车牌号 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.plateNumber"
|
||||||
|
placeholder="请输入车牌号"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
加氢日期 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="formData.hydrogenDate"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="请选择加氢日期"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
加氢量(KG) <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="formData.hydrogenQuantity"
|
||||||
|
placeholder="请输入加氢量"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">
|
||||||
|
单价(元/KG) <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="formData.unitPrice"
|
||||||
|
placeholder="请输入单价"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">金额</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="formData.amount"
|
||||||
|
placeholder="请输入金额"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-1 block font-medium">里程(KM)</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="formData.mileage"
|
||||||
|
placeholder="请输入里程数"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Alert, Button, Form, Modal, Select, Upload, message } from 'ant-design-vue';
|
||||||
|
import { importHydrogenRecords } from '#/api/energy/hydrogen-record';
|
||||||
|
import { getStationConfigSimpleList } from '#/api/energy/station-config';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
|
|
||||||
|
const importForm = ref({
|
||||||
|
stationId: undefined as number | undefined,
|
||||||
|
});
|
||||||
|
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
visible.value = true;
|
||||||
|
importForm.value.stationId = undefined;
|
||||||
|
fileList.value = [];
|
||||||
|
loadStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStations() {
|
||||||
|
const list = await getStationConfigSimpleList();
|
||||||
|
stationOptions.value = list.map((s) => ({
|
||||||
|
label: s.stationName,
|
||||||
|
value: s.stationId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport() {
|
||||||
|
if (!importForm.value.stationId) {
|
||||||
|
message.error('请选择加氢站');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
message.error('请选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileList.value[0].originFileObj || fileList.value[0]);
|
||||||
|
formData.append('stationId', String(importForm.value.stationId));
|
||||||
|
|
||||||
|
const res = await importHydrogenRecords(formData);
|
||||||
|
|
||||||
|
message.success(
|
||||||
|
`导入成功!匹配成功 ${res.successCount} 条,失败 ${res.failCount} 条`,
|
||||||
|
);
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
resetImportForm();
|
||||||
|
emit('success');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('导入失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetImportForm() {
|
||||||
|
importForm.value.stationId = undefined;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadTemplate() {
|
||||||
|
try {
|
||||||
|
const blob = await requestClient.download('/energy/hydrogen-record/template');
|
||||||
|
downloadFileFromBlobPart({ fileName: '加氢记录导入模板.xlsx', source: blob });
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('下载模板失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="导入加氢记录"
|
||||||
|
:width="600"
|
||||||
|
:confirm-loading="importing"
|
||||||
|
@ok="handleImport"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<Form :model="importForm" layout="vertical">
|
||||||
|
<Form.Item label="加氢站" required>
|
||||||
|
<Select
|
||||||
|
v-model:value="importForm.stationId"
|
||||||
|
placeholder="请选择加氢站"
|
||||||
|
:options="stationOptions"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Excel 文件" required>
|
||||||
|
<Upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:max-count="1"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<span class="icon-[ant-design--upload-outlined] mr-1"></span>
|
||||||
|
选择文件
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
<div class="upload-tip mt-2 text-gray-500">
|
||||||
|
支持 .xlsx 和 .xls 格式,
|
||||||
|
<a @click="downloadTemplate">下载模板</a>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Alert message="导入说明" type="info" show-icon>
|
||||||
|
<template #description>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>上传后系统将自动匹配车辆、客户和合同</li>
|
||||||
|
<li>匹配成功的记录将自动生成明细</li>
|
||||||
|
<li>根据站点配置自动扣款或审核后扣款</li>
|
||||||
|
<li>匹配失败的记录需要人工处理</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-tip a {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.upload-tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
apps/web-antd/src/views/energy/station-config/data.ts
Normal file
147
apps/web-antd/src/views/energy/station-config/data.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyStationConfigApi } from '#/api/energy/station-config';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
export const DEDUCT_MODE_OPTIONS = [
|
||||||
|
{ label: '审核即扣款', value: true },
|
||||||
|
{ label: '出账单后扣款', value: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AUTO_MATCH_OPTIONS = [
|
||||||
|
{ label: '开启', value: true },
|
||||||
|
{ label: '关闭', value: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const COOPERATION_TYPE_OPTIONS = [
|
||||||
|
{ label: '预充值', value: 1 },
|
||||||
|
{ label: '月结算', value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入加氢站ID',
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'cooperationType',
|
||||||
|
label: '合作模式',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: COOPERATION_TYPE_OPTIONS,
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'autoDeduct',
|
||||||
|
label: '扣款模式',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: DEDUCT_MODE_OPTIONS,
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'autoMatch',
|
||||||
|
label: '自动匹配',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: AUTO_MATCH_OPTIONS,
|
||||||
|
placeholder: '请选择是否开启自动匹配',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'stationName',
|
||||||
|
label: '站点名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入站点名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'autoDeduct',
|
||||||
|
label: '扣款模式',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: DEDUCT_MODE_OPTIONS,
|
||||||
|
placeholder: '请选择扣款模式',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyStationConfigApi.Config>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'stationName',
|
||||||
|
title: '站点名称',
|
||||||
|
minWidth: 150,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cooperationType',
|
||||||
|
title: '合作模式',
|
||||||
|
minWidth: 120,
|
||||||
|
slots: { default: 'cooperationType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'autoDeduct',
|
||||||
|
title: '扣款模式',
|
||||||
|
minWidth: 130,
|
||||||
|
slots: { default: 'autoDeduct' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'autoMatch',
|
||||||
|
title: '自动匹配',
|
||||||
|
minWidth: 100,
|
||||||
|
slots: { default: 'autoMatch' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(1),
|
||||||
|
];
|
||||||
|
}
|
||||||
133
apps/web-antd/src/views/energy/station-config/index.vue
Normal file
133
apps/web-antd/src/views/energy/station-config/index.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyStationConfigApi } from '#/api/energy/station-config';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getStationConfigPage } from '#/api/energy/station-config';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
COOPERATION_TYPE_OPTIONS,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import FormModal from './modules/form-modal.vue';
|
||||||
|
|
||||||
|
const [Form, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: FormModal,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建站点配置 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑站点配置 */
|
||||||
|
function handleEdit(row: EnergyStationConfigApi.Config) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getStationConfigPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<EnergyStationConfigApi.Config>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<Form @success="handleRefresh" />
|
||||||
|
<Grid table-title="站点配置列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['站点配置']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['energy:station-config:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #cooperationType="{ row }">
|
||||||
|
<Tag
|
||||||
|
v-if="row.cooperationType != null"
|
||||||
|
:color="
|
||||||
|
COOPERATION_TYPE_OPTIONS.find(
|
||||||
|
(o) => o.value === row.cooperationType,
|
||||||
|
)?.value === 1
|
||||||
|
? 'blue'
|
||||||
|
: 'purple'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
COOPERATION_TYPE_OPTIONS.find(
|
||||||
|
(o) => o.value === row.cooperationType,
|
||||||
|
)?.label ?? '—'
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
|
<template #autoDeduct="{ row }">
|
||||||
|
<Tag :color="row.autoDeduct ? 'orange' : 'cyan'">
|
||||||
|
{{ row.autoDeduct ? '审核即扣款' : '出账单后扣款' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #autoMatch="{ row }">
|
||||||
|
<Tag :color="row.autoMatch ? 'green' : 'default'">
|
||||||
|
{{ row.autoMatch ? '开启' : '关闭' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['energy:station-config:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EnergyStationConfigApi } from '#/api/energy/station-config';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createStationConfig,
|
||||||
|
updateStationConfig,
|
||||||
|
} from '#/api/energy/station-config';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<EnergyStationConfigApi.Config>();
|
||||||
|
const isUpdate = computed(() => !!formData.value?.id);
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return isUpdate.value
|
||||||
|
? $t('ui.actionTitle.edit', ['站点配置'])
|
||||||
|
: $t('ui.actionTitle.create', ['站点配置']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
const data = (await formApi.getValues()) as EnergyStationConfigApi.Config;
|
||||||
|
try {
|
||||||
|
await (isUpdate.value
|
||||||
|
? updateStationConfig(data)
|
||||||
|
: createStationConfig(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<EnergyStationConfigApi.Config>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
// 编辑时禁用加氢站选择
|
||||||
|
await formApi.updateSchema([
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
componentProps: { disabled: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-2/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
<div class="mx-4 mb-4 rounded bg-blue-50 px-3 py-2 text-sm text-blue-600">
|
||||||
|
此配置仅对预充值模式客户生效,月结算客户不涉及账户扣款
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
194
apps/web-antd/src/views/energy/station-price/data.ts
Normal file
194
apps/web-antd/src/views/energy/station-price/data.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyStationPriceApi } from '#/api/energy/station-price';
|
||||||
|
|
||||||
|
import { useActionColumn } from '#/utils/table';
|
||||||
|
|
||||||
|
export const PRICE_STATUS_OPTIONS = [
|
||||||
|
{ label: '生效中', value: 1, color: 'green' },
|
||||||
|
{ label: '已过期', value: 0, color: 'default' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 新增/修改的表单 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
label: '加氢站',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择加氢站',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/energy/station-config').then(
|
||||||
|
(m) => m.getStationConfigSimpleList(),
|
||||||
|
),
|
||||||
|
labelField: 'stationName',
|
||||||
|
valueField: 'stationId',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
option.label?.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerId',
|
||||||
|
label: '客户',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择客户',
|
||||||
|
api: () =>
|
||||||
|
import('#/api/asset/customer').then(
|
||||||
|
(m) => m.getSimpleCustomerList(),
|
||||||
|
),
|
||||||
|
labelField: 'customerName',
|
||||||
|
valueField: 'id',
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
option.label?.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'costPrice',
|
||||||
|
label: '成本价(元/KG)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入成本价',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerPrice',
|
||||||
|
label: '客户价(元/KG)',
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户价',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'effectiveDate',
|
||||||
|
label: '生效日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择生效日期',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'expiryDate',
|
||||||
|
label: '过期日期',
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择过期日期(可选)',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'stationName',
|
||||||
|
label: '站点名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入站点名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'customerName',
|
||||||
|
label: '客户名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入客户名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: PRICE_STATUS_OPTIONS,
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<EnergyStationPriceApi.Price>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'stationName',
|
||||||
|
title: '站点名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerName',
|
||||||
|
title: '客户名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'costPrice',
|
||||||
|
title: '成本价',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
slots: { default: 'costPrice' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'customerPrice',
|
||||||
|
title: '客户价',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'right',
|
||||||
|
slots: { default: 'customerPrice' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'effectiveDate',
|
||||||
|
title: '生效日期',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: 'formatDate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'expiryDate',
|
||||||
|
title: '过期日期',
|
||||||
|
minWidth: 120,
|
||||||
|
slots: { default: 'expiryDate' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
slots: { default: 'status' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
useActionColumn(2),
|
||||||
|
];
|
||||||
|
}
|
||||||
171
apps/web-antd/src/views/energy/station-price/index.vue
Normal file
171
apps/web-antd/src/views/energy/station-price/index.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { EnergyStationPriceApi } from '#/api/energy/station-price';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteStationPrice,
|
||||||
|
getStationPricePage,
|
||||||
|
} from '#/api/energy/station-price';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { PRICE_STATUS_OPTIONS, useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import FormModal from './modules/form-modal.vue';
|
||||||
|
import ImportModal from './modules/import-modal.vue';
|
||||||
|
|
||||||
|
const [Form, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: FormModal,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const importRef = ref();
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建价格 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量导入 */
|
||||||
|
function handleBatchImport() {
|
||||||
|
importRef.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑价格 */
|
||||||
|
function handleEdit(row: EnergyStationPriceApi.Price) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除价格 */
|
||||||
|
async function handleDelete(row: EnergyStationPriceApi.Price) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.stationName]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteStationPrice(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.stationName]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getStationPricePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<EnergyStationPriceApi.Price>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<Form @success="handleRefresh" />
|
||||||
|
<ImportModal ref="importRef" @success="handleRefresh" />
|
||||||
|
<Grid table-title="价格管理列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['价格']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['energy:station-price:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '批量导入',
|
||||||
|
icon: ACTION_ICON.UPLOAD,
|
||||||
|
auth: ['energy:station-price:create'],
|
||||||
|
onClick: handleBatchImport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #costPrice="{ row }">
|
||||||
|
<span class="font-bold text-orange-600">
|
||||||
|
{{ row.costPrice != null ? row.costPrice.toFixed(2) : '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #customerPrice="{ row }">
|
||||||
|
<span class="font-bold text-blue-600">
|
||||||
|
{{ row.customerPrice != null ? row.customerPrice.toFixed(2) : '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #expiryDate="{ row }">
|
||||||
|
{{ row.expiryDate ?? '—' }}
|
||||||
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Tag
|
||||||
|
:color="
|
||||||
|
PRICE_STATUS_OPTIONS.find((o) => o.value === row.status)?.color ??
|
||||||
|
'default'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
PRICE_STATUS_OPTIONS.find((o) => o.value === row.status)?.label ??
|
||||||
|
'—'
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['energy:station-price:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['energy:station-price:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.stationName]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EnergyStationPriceApi } from '#/api/energy/station-price';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createStationPrice,
|
||||||
|
updateStationPrice,
|
||||||
|
} from '#/api/energy/station-price';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<EnergyStationPriceApi.Price>();
|
||||||
|
const isUpdate = computed(() => !!formData.value?.id);
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return isUpdate.value
|
||||||
|
? $t('ui.actionTitle.edit', ['价格'])
|
||||||
|
: $t('ui.actionTitle.create', ['价格']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
const data = (await formApi.getValues()) as EnergyStationPriceApi.Price;
|
||||||
|
try {
|
||||||
|
await (isUpdate.value
|
||||||
|
? updateStationPrice(data)
|
||||||
|
: createStationPrice(data));
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<EnergyStationPriceApi.Price>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = data;
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle" class="w-2/3">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Alert, Button, Form, Modal, Upload, message } from 'ant-design-vue';
|
||||||
|
import { batchImportPrice } from '#/api/energy/station-price';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
visible.value = true;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport() {
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
message.error('请选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileList.value[0].originFileObj || fileList.value[0]);
|
||||||
|
|
||||||
|
const res = await batchImportPrice(formData);
|
||||||
|
|
||||||
|
message.success(
|
||||||
|
`导入成功!共导入 ${res.successCount} 条,失败 ${res.failCount} 条`,
|
||||||
|
);
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
fileList.value = [];
|
||||||
|
emit('success');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('导入失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadTemplate() {
|
||||||
|
try {
|
||||||
|
const blob = await requestClient.download('/energy/station-price/template');
|
||||||
|
downloadFileFromBlobPart({ fileName: '价格配置导入模板.xlsx', source: blob });
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('下载模板失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="批量导入价格配置"
|
||||||
|
:width="600"
|
||||||
|
:confirm-loading="importing"
|
||||||
|
@ok="handleImport"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="Excel 文件" required>
|
||||||
|
<Upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:max-count="1"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<span class="icon-[ant-design--upload-outlined] mr-1"></span>
|
||||||
|
选择文件
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
<div class="upload-tip mt-2 text-gray-500">
|
||||||
|
支持 .xlsx 和 .xls 格式,
|
||||||
|
<a @click="downloadTemplate">下载模板</a>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Alert message="导入说明" type="info" show-icon>
|
||||||
|
<template #description>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Excel 列:站点名称、客户名称、成本价、客户价、生效日期、失效日期</li>
|
||||||
|
<li>站点名称和客户名称必须与系统中的名称完全一致</li>
|
||||||
|
<li>价格单位:元/kg</li>
|
||||||
|
<li>日期格式:YYYY-MM-DD</li>
|
||||||
|
<li>失效日期可为空,表示长期有效</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-tip a {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.upload-tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user