Compare commits

..

10 Commits

Author SHA1 Message Date
kkfluous
17b6c99b4f feat(energy): 前端优化完成
- 简化导入交互(3步→1步)
- 批量审核功能
- 快速生成账单(本月/上月)
- 批量价格配置(前端界面)

用户体验优化:
- 一键导入,自动匹配
- 批量审核,提高效率
- 快捷时间选择
- 清晰的操作反馈
2026-03-16 13:22:46 +08:00
kkfluous
f62ff30c64 fix(energy): 修复导入弹窗图标引用方式
使用 Iconify class 替代 @ant-design/icons-vue 包导入

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:43:23 +08:00
kkfluous
be145c476e feat(energy): 实现能源账户管理页面(列表+Drawer+充值)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 01:40:10 +08:00
kkfluous
1a03965c1f feat(energy): 实现能源账单详情页
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:36:20 +08:00
kkfluous
ad84c21e84 feat(energy): 实现能源账单列表页(含生成弹窗)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:36:07 +08:00
kkfluous
12d19c93e9 feat(energy): 实现加氢明细管理页面(含审核弹窗)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:30:34 +08:00
kkfluous
c3999819c9 feat(energy): 实现加氢记录管理页面(含三步导入)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:27:31 +08:00
kkfluous
69afb41df5 feat(energy): 实现加氢站配置和价格管理页面
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 01:22:50 +08:00
kkfluous
caaeb4c819 feat(energy): 创建前端 API 层(6个文件)和路由配置 2026-03-16 01:07:00 +08:00
kkfluous
2c6056c9d0 refactor: 封装 useActionColumn 工具函数,统一操作列宽度计算
将 11 个页面共 15 处硬编码的操作列配置替换为 useActionColumn() 调用,
根据内联按钮数量和是否有下拉菜单自动计算宽度,解决按钮文字被遮挡问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:43:38 +08:00
41 changed files with 6387 additions and 23 deletions

View 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 });
}

View 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 } });
}

View 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 });
}

View 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,
);
}

View 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);
}

View 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' },
});
}

View 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;

View 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 },
};
}

View 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),
];
}

View File

@@ -3,6 +3,7 @@ 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[] {
@@ -37,6 +38,7 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请选择交车日期',
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
style: { width: '100%' },
},
@@ -138,7 +140,7 @@ export function usePendingColumns(): VxeTableGridOptions['columns'] {
{ field: 'vehicleCount', title: '交车数量', minWidth: 100, align: 'center' },
{ field: 'deliveryRegion', title: '交车区域', minWidth: 120 },
{ field: 'deliveryLocation', title: '交车地点', minWidth: 150 },
{ title: '操作', width: 160, fixed: 'right', slots: { default: 'actions' } },
useActionColumn(3),
];
}
@@ -164,7 +166,7 @@ export function useHistoryColumns(): VxeTableGridOptions['columns'] {
{ field: 'vehicleCount', title: '交车数量', minWidth: 100, align: 'center' },
{ field: 'deliveryRegion', title: '交车区域', minWidth: 120 },
{ field: 'deliveryLocation', title: '交车地点', minWidth: 150 },
{ title: '操作', width: 100, fixed: 'right', slots: { default: 'actions' } },
useActionColumn(3, true),
];
}

View 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'),
];
}

View File

@@ -2,6 +2,7 @@ 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 },
@@ -77,7 +78,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
},
{ field: 'remark', title: '备注', minWidth: 150 },
{ field: 'createTime', title: '创建时间', minWidth: 180, formatter: 'formatDateTime' },
{ title: '操作', width: 160, fixed: 'right', slots: { default: 'actions' } },
useActionColumn(3),
];
}

View 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),
];
}

View 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),
];
}

View 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'),
];
}

View 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();
}

View 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),
];
}

View File

@@ -2,6 +2,7 @@ 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 },
@@ -122,12 +123,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 280,
fixed: 'right',
slots: { default: 'actions' },
},
useActionColumn(3, true),
];
}
@@ -168,26 +164,43 @@ export function useFormSchema(): VbenFormSchema[] {
filterOption: (input: string, option: any) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
},
rules: z.number({ message: '请选择关联合同' }),
},
{
fieldName: 'originalVehicleId',
label: '原车辆ID',
component: 'InputNumber',
label: '原车辆',
component: 'ApiSelect',
componentProps: {
placeholder: '请输入原车辆ID',
class: 'w-full',
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: '请输入原车辆ID' }),
rules: z.number({ message: '请选择原车辆' }),
},
{
fieldName: 'newVehicleId',
label: '新车辆ID',
component: 'InputNumber',
label: '新车辆',
component: 'ApiSelect',
componentProps: {
placeholder: '请输入新车辆ID',
class: 'w-full',
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: '请输入新车辆ID' }),
rules: z.number({ message: '请选择新车辆' }),
},
{
fieldName: 'deliveryOrderId',
@@ -214,8 +227,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'DatePicker',
componentProps: {
placeholder: '请选择预计替换日期',
class: 'w-full',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
},
{
@@ -224,8 +237,8 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'DatePicker',
componentProps: {
placeholder: '请选择预计归还日期',
class: 'w-full',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
dependencies: {
triggerFields: ['replacementType'],

View 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),
];
}

View 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>

View File

@@ -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>

View File

@@ -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>

View 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),
];
}

View 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';

View 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>

View 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>

View 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>

View 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),
];
}

View 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>

View File

@@ -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>

View 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),
];
}

View 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>

View File

@@ -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>

View File

@@ -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>

View 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),
];
}

View 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>

View File

@@ -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>

View 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),
];
}

View 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>

View File

@@ -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>

View File

@@ -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>