feat(energy): 前端优化完成
- 简化导入交互(3步→1步) - 批量审核功能 - 快速生成账单(本月/上月) - 批量价格配置(前端界面) 用户体验优化: - 一键导入,自动匹配 - 批量审核,提高效率 - 快捷时间选择 - 清晰的操作反馈
This commit is contained in:
@@ -34,14 +34,21 @@ export namespace EnergyBillApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateReq {
|
export interface GenerateReq {
|
||||||
customerId: number;
|
startDate?: string;
|
||||||
contractId: number;
|
endDate?: string;
|
||||||
stationId: number;
|
customerId?: number;
|
||||||
billPeriodStart: string;
|
contractId?: number;
|
||||||
billPeriodEnd: string;
|
stationId?: number;
|
||||||
|
billPeriodStart?: string;
|
||||||
|
billPeriodEnd?: string;
|
||||||
energyType?: number;
|
energyType?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenerateResult {
|
||||||
|
total?: number;
|
||||||
|
billIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Adjustment {
|
export interface Adjustment {
|
||||||
id?: number;
|
id?: number;
|
||||||
billId?: number;
|
billId?: number;
|
||||||
@@ -72,7 +79,7 @@ export function getBill(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateBill(data: EnergyBillApi.GenerateReq) {
|
export function generateBill(data: EnergyBillApi.GenerateReq) {
|
||||||
return requestClient.post<number>('/energy/bill/generate', data);
|
return requestClient.post<EnergyBillApi.GenerateResult>('/energy/bill/generate', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function batchGenerateByPeriod(billPeriod: string) {
|
export function batchGenerateByPeriod(billPeriod: string) {
|
||||||
|
|||||||
@@ -51,10 +51,22 @@ export function auditHydrogenDetail(id: number, approved: boolean, remark?: stri
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function batchAuditHydrogenDetail(ids: number[], approved: boolean, remark?: string) {
|
export interface BatchAuditReqVO {
|
||||||
return requestClient.post('/energy/hydrogen-detail/batch-audit', null, {
|
ids: number[];
|
||||||
params: { ids: ids.join(','), approved, remark },
|
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) {
|
export function exportHydrogenDetail(params: any) {
|
||||||
|
|||||||
@@ -21,38 +21,7 @@ export namespace EnergyHydrogenRecordApi {
|
|||||||
createTime?: string;
|
createTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportPreview {
|
|
||||||
batchNo: string;
|
|
||||||
totalCount: number;
|
|
||||||
validCount: number;
|
|
||||||
duplicateCount: number;
|
|
||||||
errorCount: number;
|
|
||||||
records: RecordPreviewItem[];
|
|
||||||
duplicates: RecordPreviewItem[];
|
|
||||||
errors: ImportErrorItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecordPreviewItem {
|
|
||||||
rowNum: number;
|
|
||||||
plateNumber: string;
|
|
||||||
hydrogenDate: string;
|
|
||||||
hydrogenQuantity: number;
|
|
||||||
unitPrice: number;
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
isDuplicate: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImportErrorItem {
|
|
||||||
rowNum: number;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImportProgress {
|
|
||||||
current: number;
|
|
||||||
total: number;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHydrogenRecordPage(params: PageParam) {
|
export function getHydrogenRecordPage(params: PageParam) {
|
||||||
@@ -85,31 +54,17 @@ export function exportHydrogenRecord(params: any) {
|
|||||||
return requestClient.download('/energy/hydrogen-record/export-excel', { params });
|
return requestClient.download('/energy/hydrogen-record/export-excel', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importPreview(stationId: number, file: File) {
|
export interface ImportResultDTO {
|
||||||
const formData = new FormData();
|
total: number;
|
||||||
formData.append('file', file);
|
successCount: number;
|
||||||
formData.append('stationId', String(stationId));
|
failCount: number;
|
||||||
return requestClient.post<EnergyHydrogenRecordApi.ImportPreview>(
|
successIds: number[];
|
||||||
'/energy/hydrogen-record/import-preview',
|
failIds: number[];
|
||||||
formData,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importConfirm(batchNo: string, duplicateStrategy: string) {
|
export function importHydrogenRecords(data: FormData) {
|
||||||
return requestClient.post<Record<string, number>>(
|
return requestClient.post<ImportResultDTO>(
|
||||||
'/energy/hydrogen-record/import-confirm',
|
'/energy/hydrogen-record/import',
|
||||||
null,
|
data,
|
||||||
{ params: { batchNo, duplicateStrategy } },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImportProgress(batchNo: string) {
|
|
||||||
return requestClient.get<EnergyHydrogenRecordApi.ImportProgress>(
|
|
||||||
'/energy/hydrogen-record/import-progress',
|
|
||||||
{ params: { batchNo } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function batchMatch() {
|
|
||||||
return requestClient.post<Record<string, number>>('/energy/hydrogen-record/batch-match');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,3 +36,15 @@ export function updateStationPrice(data: EnergyStationPriceApi.Price) {
|
|||||||
export function deleteStationPrice(id: number) {
|
export function deleteStationPrice(id: number) {
|
||||||
return requestClient.delete('/energy/station-price/delete', { params: { id } });
|
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' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'billPeriod',
|
fieldName: 'dateRange',
|
||||||
label: '账单周期',
|
label: '账单时间',
|
||||||
component: 'DatePicker',
|
component: 'RangePicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
picker: 'month',
|
picker: 'month',
|
||||||
format: 'YYYY-MM',
|
format: 'YYYY-MM',
|
||||||
valueFormat: 'YYYY-MM',
|
valueFormat: 'YYYY-MM',
|
||||||
placeholder: '选择月份',
|
placeholder: ['开始月份', '结束月份'],
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
@@ -137,7 +137,7 @@ export function useGridColumns(): VxeTableGridOptions<EnergyBillApi.Bill>['colum
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
formatter: ({ row }: { row: EnergyBillApi.Bill }) => {
|
formatter: ({ row }: { row: EnergyBillApi.Bill }) => {
|
||||||
if (row.billPeriodStart) {
|
if (row.billPeriodStart) {
|
||||||
return row.billPeriodStart.substring(0, 7);
|
return String(row.billPeriodStart).substring(0, 7);
|
||||||
}
|
}
|
||||||
return '—';
|
return '—';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { useRouter } from 'vue-router';
|
|||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
import { Tag, message } from 'ant-design-vue';
|
import { Modal, Tag, message } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
deleteBill,
|
deleteBill,
|
||||||
exportBill,
|
exportBill,
|
||||||
|
generateBill,
|
||||||
getBillPage,
|
getBillPage,
|
||||||
} from '#/api/energy/bill';
|
} from '#/api/energy/bill';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -43,6 +45,41 @@ async function handleExport() {
|
|||||||
downloadFileFromBlobPart({ fileName: '能源账单.xlsx', source: blob });
|
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() {
|
function handleGenerate() {
|
||||||
generateModalRef.value?.open('single');
|
generateModalRef.value?.open('single');
|
||||||
@@ -119,18 +156,24 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '生成账单',
|
label: '生成本月账单',
|
||||||
type: 'primary',
|
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,
|
icon: ACTION_ICON.ADD,
|
||||||
auth: ['energy:bill:create'],
|
auth: ['energy:bill:create'],
|
||||||
onClick: handleGenerate,
|
onClick: handleGenerate,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '批量生成',
|
|
||||||
icon: ACTION_ICON.ADD,
|
|
||||||
auth: ['energy:bill:create'],
|
|
||||||
onClick: handleBatchGenerate,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: $t('ui.actionTitle.export'),
|
label: $t('ui.actionTitle.export'),
|
||||||
icon: ACTION_ICON.DOWNLOAD,
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
import { Alert, DatePicker, InputNumber, message, Modal } from 'ant-design-vue';
|
import { Alert, Button, DatePicker, message, Modal, RangePicker, Select, Space } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
generateBill,
|
generateBill,
|
||||||
} from '#/api/energy/bill';
|
} from '#/api/energy/bill';
|
||||||
import type { EnergyBillApi } 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 emit = defineEmits(['success']);
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
@@ -19,41 +22,119 @@ const mode = ref<'batch' | 'single'>('single');
|
|||||||
const customerId = ref<number>();
|
const customerId = ref<number>();
|
||||||
const contractId = ref<number>();
|
const contractId = ref<number>();
|
||||||
const stationId = ref<number>();
|
const stationId = ref<number>();
|
||||||
|
const dateRange = ref<any>();
|
||||||
const billPeriod = 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') {
|
function open(m: 'batch' | 'single' = 'single') {
|
||||||
mode.value = m;
|
mode.value = m;
|
||||||
customerId.value = undefined;
|
customerId.value = undefined;
|
||||||
contractId.value = undefined;
|
contractId.value = undefined;
|
||||||
stationId.value = undefined;
|
stationId.value = undefined;
|
||||||
|
dateRange.value = undefined;
|
||||||
billPeriod.value = undefined;
|
billPeriod.value = undefined;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
if (m === 'single') {
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConfirm() {
|
async function handleConfirm() {
|
||||||
if (!billPeriod.value) {
|
|
||||||
message.warning('请选择账单周期');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const periodStr = dayjs(billPeriod.value).format('YYYY-MM');
|
|
||||||
if (mode.value === 'single') {
|
if (mode.value === 'single') {
|
||||||
if (!customerId.value || !stationId.value) {
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
message.warning('请填写完整信息');
|
message.warning('请选择时间范围');
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: EnergyBillApi.GenerateReq = {
|
const data: EnergyBillApi.GenerateReq = {
|
||||||
customerId: customerId.value,
|
customerId: customerId.value!,
|
||||||
contractId: contractId.value!,
|
contractId: contractId.value!,
|
||||||
stationId: stationId.value,
|
stationId: stationId.value!,
|
||||||
billPeriodStart: dayjs(billPeriod.value).startOf('month').format('YYYY-MM-DD'),
|
billPeriodStart: dayjs(dateRange.value[0]).format('YYYY-MM-DD'),
|
||||||
billPeriodEnd: dayjs(billPeriod.value).endOf('month').format('YYYY-MM-DD'),
|
billPeriodEnd: dayjs(dateRange.value[1]).format('YYYY-MM-DD'),
|
||||||
};
|
};
|
||||||
await generateBill(data);
|
await generateBill(data);
|
||||||
message.success('账单生成成功');
|
message.success('账单生成成功');
|
||||||
} else {
|
} else {
|
||||||
|
if (!billPeriod.value) {
|
||||||
|
message.warning('请选择账单周期');
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const periodStr = dayjs(billPeriod.value).format('YYYY-MM');
|
||||||
const result = await batchGenerateByPeriod(periodStr);
|
const result = await batchGenerateByPeriod(periodStr);
|
||||||
const generated = result.generatedCount ?? 0;
|
const generated = result.generatedCount ?? 0;
|
||||||
const skipped = result.skippedCount ?? 0;
|
const skipped = result.skippedCount ?? 0;
|
||||||
@@ -74,7 +155,7 @@ defineExpose({ open });
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
:title="mode === 'single' ? '生成账单' : '批量生成账单'"
|
:title="mode === 'single' ? '自定义生成账单' : '批量生成账单'"
|
||||||
:confirm-loading="loading"
|
:confirm-loading="loading"
|
||||||
@ok="handleConfirm"
|
@ok="handleConfirm"
|
||||||
@cancel="visible = false"
|
@cancel="visible = false"
|
||||||
@@ -82,33 +163,72 @@ defineExpose({ open });
|
|||||||
<template v-if="mode === 'single'">
|
<template v-if="mode === 'single'">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1 block font-medium">
|
<label class="mb-1 block font-medium">
|
||||||
客户 <span class="text-red-500">*</span>
|
时间范围 <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<InputNumber
|
<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"
|
v-model:value="customerId"
|
||||||
placeholder="请输入客户ID"
|
:options="customerOptions"
|
||||||
|
placeholder="全部客户"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1 block font-medium">合同</label>
|
<label class="mb-1 block font-medium">合同</label>
|
||||||
<InputNumber
|
<Select
|
||||||
v-model:value="contractId"
|
v-model:value="contractId"
|
||||||
placeholder="请输入合同ID"
|
:options="filteredContractOptions"
|
||||||
|
placeholder="全部合同"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1 block font-medium">
|
<label class="mb-1 block font-medium">加氢站</label>
|
||||||
加氢站 <span class="text-red-500">*</span>
|
<Select
|
||||||
</label>
|
|
||||||
<InputNumber
|
|
||||||
v-model:value="stationId"
|
v-model:value="stationId"
|
||||||
placeholder="请输入站点ID"
|
:options="stationOptions"
|
||||||
|
placeholder="全部站点"
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.label?.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Alert type="info" show-icon message="提示">
|
||||||
|
<template #description>
|
||||||
|
<ul>
|
||||||
|
<li>不选择客户/合同/站点时,将生成所有符合条件的账单</li>
|
||||||
|
<li>账单按(客户+合同+站点+时间段)分组生成</li>
|
||||||
|
<li>只有已审核通过的明细才会生成账单</li>
|
||||||
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="mb-1 block font-medium">
|
<label class="mb-1 block font-medium">
|
||||||
账单周期 <span class="text-red-500">*</span>
|
账单周期 <span class="text-red-500">*</span>
|
||||||
@@ -121,16 +241,10 @@ defineExpose({ open });
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Alert
|
<Alert
|
||||||
v-if="mode === 'single'"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
message="系统将自动汇总该客户+合同+站点在所选周期内已审核且未出账的加氢明细,生成草稿账单。"
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
v-if="mode === 'batch'"
|
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
message="系统将自动为所有有未出账明细的客户+合同+站点组合生成当月草稿账单。"
|
message="系统将自动为所有有未出账明细的客户+合同+站点组合生成当月草稿账单。"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,12 +42,26 @@ async function handleExport() {
|
|||||||
|
|
||||||
/** 批量审核 */
|
/** 批量审核 */
|
||||||
function handleBatchAudit() {
|
function handleBatchAudit() {
|
||||||
const selected = checkedRecords.value.filter((r) => r.auditStatus === 0);
|
if (checkedRecords.value.length === 0) {
|
||||||
if (selected.length === 0) {
|
message.warning('请选择要审核的明细');
|
||||||
message.warning('请选择待审核记录');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auditModalRef.value?.open(selected);
|
|
||||||
|
// 过滤出待审核的记录
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单条审核 */
|
/** 单条审核 */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
|
|||||||
|
|
||||||
import { Descriptions, Input, message, Modal, Radio } from 'ant-design-vue';
|
import { Descriptions, Input, message, Modal, Radio } from 'ant-design-vue';
|
||||||
|
|
||||||
import { auditHydrogenDetail } from '#/api/energy/hydrogen-detail';
|
import { auditHydrogenDetail, batchAuditHydrogenDetail } from '#/api/energy/hydrogen-detail';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
@@ -28,18 +28,32 @@ function open(data: EnergyHydrogenDetailApi.Detail[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleConfirm() {
|
async function handleConfirm() {
|
||||||
|
if (!approved.value && !remark.value) {
|
||||||
|
message.warning('驳回时必须填写审核备注');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
let successCount = 0;
|
if (isBatch.value) {
|
||||||
for (const record of records.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(
|
await auditHydrogenDetail(
|
||||||
record.id!,
|
records.value[0].id!,
|
||||||
approved.value,
|
approved.value,
|
||||||
remark.value || undefined,
|
remark.value || undefined,
|
||||||
);
|
);
|
||||||
successCount++;
|
message.success('审核完成');
|
||||||
}
|
}
|
||||||
message.success(`审核完成,共 ${successCount} 条`);
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
emit('success');
|
emit('success');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -92,16 +106,19 @@ defineExpose({ open });
|
|||||||
<label class="mb-2 block font-medium">审核结果</label>
|
<label class="mb-2 block font-medium">审核结果</label>
|
||||||
<Radio.Group v-model:value="approved">
|
<Radio.Group v-model:value="approved">
|
||||||
<Radio :value="true">通过</Radio>
|
<Radio :value="true">通过</Radio>
|
||||||
<Radio v-if="!isBatch" :value="false">驳回</Radio>
|
<Radio :value="false">驳回</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block font-medium">审核备注</label>
|
<label class="mb-2 block font-medium">
|
||||||
|
审核备注
|
||||||
|
<span v-if="!approved" class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
v-model:value="remark"
|
v-model:value="remark"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="请输入审核备注(选填)"
|
:placeholder="approved ? '请输入审核备注(选填)' : '请输入驳回原因(必填)'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -5,6 +5,106 @@ import type { EnergyHydrogenRecordApi } from '#/api/energy/hydrogen-record';
|
|||||||
import { useActionColumn } from '#/utils/table';
|
import { useActionColumn } from '#/utils/table';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
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 = [
|
export const SOURCE_TYPE_OPTIONS = [
|
||||||
{ label: 'Excel导入', value: 1, color: 'blue' },
|
{ label: 'Excel导入', value: 1, color: 'blue' },
|
||||||
{ label: 'API同步', value: 2, color: 'green' },
|
{ label: 'API同步', value: 2, color: 'green' },
|
||||||
@@ -12,9 +112,9 @@ export const SOURCE_TYPE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const MATCH_STATUS_OPTIONS = [
|
export const MATCH_STATUS_OPTIONS = [
|
||||||
{ label: '待匹配', value: 0, color: 'orange' },
|
{ label: '完全匹配', value: 0, color: 'green' },
|
||||||
{ label: '已匹配', value: 1, color: 'green' },
|
{ label: '部分匹配', value: 1, color: 'orange' },
|
||||||
{ label: '匹配失败', value: 2, color: 'red' },
|
{ label: '未匹配', value: 2, color: 'red' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 搜索表单 */
|
/** 搜索表单 */
|
||||||
@@ -23,9 +123,18 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
{
|
{
|
||||||
fieldName: 'stationId',
|
fieldName: 'stationId',
|
||||||
label: '加氢站',
|
label: '加氢站',
|
||||||
component: 'Input',
|
component: 'ApiSelect',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入加氢站',
|
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,
|
allowClear: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Tag, message } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
batchMatch,
|
|
||||||
deleteHydrogenRecord,
|
deleteHydrogenRecord,
|
||||||
exportHydrogenRecord,
|
exportHydrogenRecord,
|
||||||
getHydrogenRecordPage,
|
getHydrogenRecordPage,
|
||||||
@@ -25,8 +24,10 @@ import {
|
|||||||
useGridFormSchema,
|
useGridFormSchema,
|
||||||
} from './data';
|
} from './data';
|
||||||
import ImportModal from './modules/import-modal.vue';
|
import ImportModal from './modules/import-modal.vue';
|
||||||
|
import FormModal from './modules/form-modal.vue';
|
||||||
|
|
||||||
const importModalRef = ref<InstanceType<typeof ImportModal>>();
|
const importModalRef = ref<InstanceType<typeof ImportModal>>();
|
||||||
|
const formModalRef = ref<InstanceType<typeof FormModal>>();
|
||||||
|
|
||||||
/** 刷新表格 */
|
/** 刷新表格 */
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
@@ -45,18 +46,9 @@ async function handleExport() {
|
|||||||
downloadFileFromBlobPart({ fileName: '加氢记录.xlsx', source: blob });
|
downloadFileFromBlobPart({ fileName: '加氢记录.xlsx', source: blob });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量匹配 */
|
/** 编辑记录 */
|
||||||
async function handleBatchMatch() {
|
function handleEdit(row: EnergyHydrogenRecordApi.Record) {
|
||||||
const hideLoading = message.loading({ content: '正在匹配...', duration: 0 });
|
formModalRef.value?.open(row);
|
||||||
try {
|
|
||||||
const result = await batchMatch();
|
|
||||||
message.success(
|
|
||||||
`匹配完成:成功 ${result.successCount} 条,失败 ${result.failCount} 条`,
|
|
||||||
);
|
|
||||||
handleRefresh();
|
|
||||||
} finally {
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除记录 */
|
/** 删除记录 */
|
||||||
@@ -100,6 +92,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
|
<FormModal ref="formModalRef" @success="handleRefresh" />
|
||||||
<ImportModal ref="importModalRef" @success="handleRefresh" />
|
<ImportModal ref="importModalRef" @success="handleRefresh" />
|
||||||
<Grid table-title="加氢记录列表">
|
<Grid table-title="加氢记录列表">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
@@ -118,12 +111,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
auth: ['energy:hydrogen-record:export'],
|
auth: ['energy:hydrogen-record:export'],
|
||||||
onClick: handleExport,
|
onClick: handleExport,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '重新匹配',
|
|
||||||
icon: ACTION_ICON.REFRESH,
|
|
||||||
auth: ['energy:hydrogen-record:batch-match'],
|
|
||||||
onClick: handleBatchMatch,
|
|
||||||
},
|
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,7 +150,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
type: 'link',
|
type: 'link',
|
||||||
icon: ACTION_ICON.EDIT,
|
icon: ACTION_ICON.EDIT,
|
||||||
auth: ['energy:hydrogen-record:update'],
|
auth: ['energy:hydrogen-record:update'],
|
||||||
onClick: () => {},
|
onClick: handleEdit.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('common.delete'),
|
label: $t('common.delete'),
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,60 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onUnmounted } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { Alert, Button, Form, Modal, Select, Upload, message } from 'ant-design-vue';
|
||||||
Button,
|
import { importHydrogenRecords } from '#/api/energy/hydrogen-record';
|
||||||
Modal,
|
|
||||||
Progress,
|
|
||||||
Radio,
|
|
||||||
Select,
|
|
||||||
Statistic,
|
|
||||||
Steps,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Upload,
|
|
||||||
message,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
import {
|
|
||||||
importPreview,
|
|
||||||
importConfirm,
|
|
||||||
getImportProgress,
|
|
||||||
} from '#/api/energy/hydrogen-record';
|
|
||||||
import type { EnergyHydrogenRecordApi } from '#/api/energy/hydrogen-record';
|
|
||||||
import { getStationConfigSimpleList } from '#/api/energy/station-config';
|
import { getStationConfigSimpleList } from '#/api/energy/station-config';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const currentStep = ref(0);
|
const importing = ref(false);
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
// Step 1 data
|
const importForm = ref({
|
||||||
const stationId = ref<number>();
|
stationId: undefined as number | undefined,
|
||||||
|
});
|
||||||
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
const fileList = ref<any[]>([]);
|
const fileList = ref<any[]>([]);
|
||||||
|
|
||||||
// Step 2 data
|
|
||||||
const previewData = ref<EnergyHydrogenRecordApi.ImportPreview>();
|
|
||||||
const duplicateStrategy = ref('skip');
|
|
||||||
|
|
||||||
// Step 3 data
|
|
||||||
const progressData = ref<EnergyHydrogenRecordApi.ImportProgress>({
|
|
||||||
current: 0,
|
|
||||||
total: 0,
|
|
||||||
status: '',
|
|
||||||
});
|
|
||||||
const importResult = ref<Record<string, number>>();
|
|
||||||
let progressTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
currentStep.value = 0;
|
importForm.value.stationId = undefined;
|
||||||
stationId.value = undefined;
|
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
previewData.value = undefined;
|
|
||||||
duplicateStrategy.value = 'skip';
|
|
||||||
progressData.value = { current: 0, total: 0, status: '' };
|
|
||||||
importResult.value = undefined;
|
|
||||||
loadStations();
|
loadStations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,99 +33,55 @@ async function loadStations() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1 -> Step 2: Upload and preview
|
async function handleImport() {
|
||||||
async function handleNextStep() {
|
if (!importForm.value.stationId) {
|
||||||
if (!stationId.value) {
|
message.error('请选择加氢站');
|
||||||
message.warning('请选择加氢站');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileList.value.length === 0) {
|
if (fileList.value.length === 0) {
|
||||||
message.warning('请上传文件');
|
message.error('请选择文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
importing.value = true;
|
||||||
try {
|
|
||||||
const file = fileList.value[0].originFileObj || fileList.value[0];
|
|
||||||
const result = await importPreview(stationId.value, file);
|
|
||||||
previewData.value = result;
|
|
||||||
currentStep.value = 1;
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '预览失败');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2 -> Step 3: Confirm import
|
|
||||||
async function handleConfirmImport() {
|
|
||||||
if (!previewData.value) return;
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
try {
|
||||||
importResult.value = await importConfirm(
|
const formData = new FormData();
|
||||||
previewData.value.batchNo,
|
formData.append('file', fileList.value[0].originFileObj || fileList.value[0]);
|
||||||
duplicateStrategy.value,
|
formData.append('stationId', String(importForm.value.stationId));
|
||||||
|
|
||||||
|
const res = await importHydrogenRecords(formData);
|
||||||
|
|
||||||
|
message.success(
|
||||||
|
`导入成功!匹配成功 ${res.successCount} 条,失败 ${res.failCount} 条`,
|
||||||
);
|
);
|
||||||
currentStep.value = 2;
|
|
||||||
startProgressPolling();
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e.message || '导入失败');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startProgressPolling() {
|
|
||||||
if (!previewData.value) return;
|
|
||||||
const batchNo = previewData.value.batchNo;
|
|
||||||
progressTimer = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const progress = await getImportProgress(batchNo);
|
|
||||||
progressData.value = progress;
|
|
||||||
if (
|
|
||||||
progress.status === 'completed' ||
|
|
||||||
progress.status === 'failed' ||
|
|
||||||
progress.status === 'not_found'
|
|
||||||
) {
|
|
||||||
stopProgressPolling();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
stopProgressPolling();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopProgressPolling() {
|
|
||||||
if (progressTimer) {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
progressTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
stopProgressPolling();
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
if (currentStep.value === 2) {
|
resetImportForm();
|
||||||
emit('success');
|
emit('success');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('导入失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => stopProgressPolling());
|
function resetImportForm() {
|
||||||
|
importForm.value.stationId = undefined;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Preview table columns
|
async function downloadTemplate() {
|
||||||
const previewColumns = [
|
try {
|
||||||
{ title: '行号', dataIndex: 'rowNum', width: 60 },
|
const blob = await requestClient.download('/energy/hydrogen-record/template');
|
||||||
{ title: '车牌号', dataIndex: 'plateNumber', width: 100 },
|
downloadFileFromBlobPart({ fileName: '加氢记录导入模板.xlsx', source: blob });
|
||||||
{ title: '加氢日期', dataIndex: 'hydrogenDate', width: 100 },
|
} catch (error: any) {
|
||||||
{ title: '加氢量(KG)', dataIndex: 'hydrogenQuantity', width: 100 },
|
message.error('下载模板失败:' + (error.message || '未知错误'));
|
||||||
{ title: '单价', dataIndex: 'unitPrice', width: 80 },
|
}
|
||||||
{ title: '金额', dataIndex: 'amount', width: 80 },
|
}
|
||||||
{ title: '里程数', dataIndex: 'mileage', width: 80 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Handle file before upload (prevent auto-upload)
|
function beforeUpload() {
|
||||||
function beforeUpload(file: File) {
|
|
||||||
fileList.value = [file];
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,132 +91,59 @@ defineExpose({ open });
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
title="Excel 批量导入"
|
title="导入加氢记录"
|
||||||
:width="800"
|
:width="600"
|
||||||
:footer="null"
|
:confirm-loading="importing"
|
||||||
:mask-closable="false"
|
@ok="handleImport"
|
||||||
@cancel="handleClose"
|
@cancel="visible = false"
|
||||||
>
|
>
|
||||||
<Steps :current="currentStep" class="mb-6">
|
<Form :model="importForm" layout="vertical">
|
||||||
<Steps.Step title="上传文件" />
|
<Form.Item label="加氢站" required>
|
||||||
<Steps.Step title="预览确认" />
|
|
||||||
<Steps.Step title="导入结果" />
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
<!-- Step 1: Upload -->
|
|
||||||
<div v-if="currentStep === 0">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="mb-2 block font-medium">选择加氢站</label>
|
|
||||||
<Select
|
<Select
|
||||||
v-model:value="stationId"
|
v-model:value="importForm.stationId"
|
||||||
:options="stationOptions"
|
|
||||||
placeholder="请选择加氢站"
|
placeholder="请选择加氢站"
|
||||||
class="w-full"
|
:options="stationOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Form.Item>
|
||||||
<Upload.Dragger
|
|
||||||
:file-list="fileList"
|
<Form.Item label="Excel 文件" required>
|
||||||
|
<Upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:max-count="1"
|
:max-count="1"
|
||||||
accept=".xlsx,.xls"
|
accept=".xlsx,.xls"
|
||||||
>
|
>
|
||||||
<p class="ant-upload-drag-icon"><span class="icon-[ant-design--inbox-outlined] text-2xl"></span></p>
|
<Button>
|
||||||
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
<span class="icon-[ant-design--upload-outlined] mr-1"></span>
|
||||||
<p class="ant-upload-hint">支持 .xlsx, .xls 格式</p>
|
选择文件
|
||||||
</Upload.Dragger>
|
|
||||||
<div class="mt-4 flex justify-between">
|
|
||||||
<a href="javascript:void(0)">下载导入模板</a>
|
|
||||||
<Button type="primary" :loading="loading" @click="handleNextStep">
|
|
||||||
下一步
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
<div class="upload-tip mt-2 text-gray-500">
|
||||||
|
支持 .xlsx 和 .xls 格式,
|
||||||
|
<a @click="downloadTemplate">下载模板</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Form.Item>
|
||||||
|
|
||||||
<!-- Step 2: Preview -->
|
<Alert message="导入说明" type="info" show-icon>
|
||||||
<div v-if="currentStep === 1 && previewData">
|
<template #description>
|
||||||
<div class="mb-4 flex gap-4">
|
<ul class="list-disc pl-4">
|
||||||
<Statistic title="总行数" :value="previewData.totalCount" />
|
<li>上传后系统将自动匹配车辆、客户和合同</li>
|
||||||
<Statistic
|
<li>匹配成功的记录将自动生成明细</li>
|
||||||
title="有效"
|
<li>根据站点配置自动扣款或审核后扣款</li>
|
||||||
:value="previewData.validCount"
|
<li>匹配失败的记录需要人工处理</li>
|
||||||
class="text-green-600"
|
</ul>
|
||||||
/>
|
</template>
|
||||||
<Statistic
|
</Alert>
|
||||||
title="重复"
|
</Form>
|
||||||
:value="previewData.duplicateCount"
|
|
||||||
class="text-orange-500"
|
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title="错误"
|
|
||||||
:value="previewData.errorCount"
|
|
||||||
class="text-red-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Table
|
|
||||||
:data-source="[...previewData.records, ...previewData.duplicates]"
|
|
||||||
:columns="previewColumns"
|
|
||||||
:row-class-name="
|
|
||||||
(record: any) => (record.isDuplicate ? 'bg-yellow-50' : '')
|
|
||||||
"
|
|
||||||
:pagination="false"
|
|
||||||
:scroll="{ y: 300 }"
|
|
||||||
size="small"
|
|
||||||
row-key="rowNum"
|
|
||||||
/>
|
|
||||||
<div v-if="previewData.errors?.length" class="mt-2">
|
|
||||||
<Tag color="red">{{ previewData.errors.length }} 条错误记录</Tag>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
|
||||||
<Radio.Group v-model:value="duplicateStrategy">
|
|
||||||
<Radio value="skip">跳过重复记录</Radio>
|
|
||||||
<Radio value="overwrite">覆盖重复记录</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
|
||||||
<Button @click="currentStep = 0">上一步</Button>
|
|
||||||
<Button type="primary" :loading="loading" @click="handleConfirmImport">
|
|
||||||
确认导入
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 3: Result -->
|
|
||||||
<div v-if="currentStep === 2">
|
|
||||||
<div class="py-8 text-center">
|
|
||||||
<Progress
|
|
||||||
:percent="
|
|
||||||
progressData.total > 0
|
|
||||||
? Math.round((progressData.current / progressData.total) * 100)
|
|
||||||
: 0
|
|
||||||
"
|
|
||||||
:status="
|
|
||||||
progressData.status === 'failed'
|
|
||||||
? 'exception'
|
|
||||||
: progressData.status === 'completed'
|
|
||||||
? 'success'
|
|
||||||
: 'active'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div v-if="importResult" class="mt-4">
|
|
||||||
<p>
|
|
||||||
成功导入
|
|
||||||
<span class="font-bold text-green-600">{{
|
|
||||||
importResult.successCount
|
|
||||||
}}</span>
|
|
||||||
条
|
|
||||||
</p>
|
|
||||||
<p v-if="(importResult.failCount ?? 0) > 0">
|
|
||||||
失败
|
|
||||||
<span class="font-bold text-red-500">{{
|
|
||||||
importResult.failCount
|
|
||||||
}}</span>
|
|
||||||
条
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Button type="primary" @click="handleClose">关闭</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-tip a {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.upload-tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -23,20 +23,36 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
{
|
{
|
||||||
fieldName: 'stationId',
|
fieldName: 'stationId',
|
||||||
label: '加氢站',
|
label: '加氢站',
|
||||||
component: 'InputNumber',
|
component: 'ApiSelect',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入加氢站ID',
|
placeholder: '请选择加氢站',
|
||||||
style: { width: '100%' },
|
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',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'customerId',
|
fieldName: 'customerId',
|
||||||
label: '客户',
|
label: '客户',
|
||||||
component: 'InputNumber',
|
component: 'ApiSelect',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入客户ID',
|
placeholder: '请选择客户',
|
||||||
style: { width: '100%' },
|
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',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { EnergyStationPriceApi } from '#/api/energy/station-price';
|
import type { EnergyStationPriceApi } from '#/api/energy/station-price';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { message, Tag } from 'ant-design-vue';
|
import { message, Tag } from 'ant-design-vue';
|
||||||
@@ -15,12 +16,15 @@ import { $t } from '#/locales';
|
|||||||
|
|
||||||
import { PRICE_STATUS_OPTIONS, useGridColumns, useGridFormSchema } from './data';
|
import { PRICE_STATUS_OPTIONS, useGridColumns, useGridFormSchema } from './data';
|
||||||
import FormModal from './modules/form-modal.vue';
|
import FormModal from './modules/form-modal.vue';
|
||||||
|
import ImportModal from './modules/import-modal.vue';
|
||||||
|
|
||||||
const [Form, formModalApi] = useVbenModal({
|
const [Form, formModalApi] = useVbenModal({
|
||||||
connectedComponent: FormModal,
|
connectedComponent: FormModal,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const importRef = ref();
|
||||||
|
|
||||||
/** 刷新表格 */
|
/** 刷新表格 */
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
@@ -31,6 +35,11 @@ function handleCreate() {
|
|||||||
formModalApi.setData(null).open();
|
formModalApi.setData(null).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量导入 */
|
||||||
|
function handleBatchImport() {
|
||||||
|
importRef.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
/** 编辑价格 */
|
/** 编辑价格 */
|
||||||
function handleEdit(row: EnergyStationPriceApi.Price) {
|
function handleEdit(row: EnergyStationPriceApi.Price) {
|
||||||
formModalApi.setData(row).open();
|
formModalApi.setData(row).open();
|
||||||
@@ -86,6 +95,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<Form @success="handleRefresh" />
|
<Form @success="handleRefresh" />
|
||||||
|
<ImportModal ref="importRef" @success="handleRefresh" />
|
||||||
<Grid table-title="价格管理列表">
|
<Grid table-title="价格管理列表">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<TableAction
|
<TableAction
|
||||||
@@ -97,6 +107,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
auth: ['energy:station-price:create'],
|
auth: ['energy:station-price:create'],
|
||||||
onClick: handleCreate,
|
onClick: handleCreate,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '批量导入',
|
||||||
|
icon: ACTION_ICON.UPLOAD,
|
||||||
|
auth: ['energy:station-price:create'],
|
||||||
|
onClick: handleBatchImport,
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Alert, Button, Form, Modal, Upload, message } from 'ant-design-vue';
|
||||||
|
import { batchImportPrice } from '#/api/energy/station-price';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const importing = ref(false);
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
visible.value = true;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport() {
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
message.error('请选择文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileList.value[0].originFileObj || fileList.value[0]);
|
||||||
|
|
||||||
|
const res = await batchImportPrice(formData);
|
||||||
|
|
||||||
|
message.success(
|
||||||
|
`导入成功!共导入 ${res.successCount} 条,失败 ${res.failCount} 条`,
|
||||||
|
);
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
fileList.value = [];
|
||||||
|
emit('success');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('导入失败:' + (error.message || '未知错误'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadTemplate() {
|
||||||
|
try {
|
||||||
|
const blob = await requestClient.download('/energy/station-price/template');
|
||||||
|
downloadFileFromBlobPart({ fileName: '价格配置导入模板.xlsx', source: blob });
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('下载模板失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="批量导入价格配置"
|
||||||
|
:width="600"
|
||||||
|
:confirm-loading="importing"
|
||||||
|
@ok="handleImport"
|
||||||
|
@cancel="visible = false"
|
||||||
|
>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="Excel 文件" required>
|
||||||
|
<Upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:max-count="1"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<span class="icon-[ant-design--upload-outlined] mr-1"></span>
|
||||||
|
选择文件
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
<div class="upload-tip mt-2 text-gray-500">
|
||||||
|
支持 .xlsx 和 .xls 格式,
|
||||||
|
<a @click="downloadTemplate">下载模板</a>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Alert message="导入说明" type="info" show-icon>
|
||||||
|
<template #description>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Excel 列:站点名称、客户名称、成本价、客户价、生效日期、失效日期</li>
|
||||||
|
<li>站点名称和客户名称必须与系统中的名称完全一致</li>
|
||||||
|
<li>价格单位:元/kg</li>
|
||||||
|
<li>日期格式:YYYY-MM-DD</li>
|
||||||
|
<li>失效日期可为空,表示长期有效</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-tip a {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.upload-tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user