feat(energy): 实现加氢记录管理页面(含三步导入)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
134
apps/web-antd/src/views/energy/hydrogen-record/data.ts
Normal file
134
apps/web-antd/src/views/energy/hydrogen-record/data.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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 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: 'orange' },
|
||||||
|
{ label: '已匹配', value: 1, color: 'green' },
|
||||||
|
{ label: '匹配失败', value: 2, color: 'red' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'stationId',
|
||||||
|
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: '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),
|
||||||
|
];
|
||||||
|
}
|
||||||
184
apps/web-antd/src/views/energy/hydrogen-record/index.vue
Normal file
184
apps/web-antd/src/views/energy/hydrogen-record/index.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<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 {
|
||||||
|
batchMatch,
|
||||||
|
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';
|
||||||
|
|
||||||
|
const importModalRef = ref<InstanceType<typeof ImportModal>>();
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量匹配 */
|
||||||
|
async function handleBatchMatch() {
|
||||||
|
const hideLoading = message.loading({ content: '正在匹配...', duration: 0 });
|
||||||
|
try {
|
||||||
|
const result = await batchMatch();
|
||||||
|
message.success(
|
||||||
|
`匹配完成:成功 ${result.successCount} 条,失败 ${result.failCount} 条`,
|
||||||
|
);
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除记录 */
|
||||||
|
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>
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重新匹配',
|
||||||
|
icon: ACTION_ICON.REFRESH,
|
||||||
|
auth: ['energy:hydrogen-record:batch-match'],
|
||||||
|
onClick: handleBatchMatch,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</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: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['energy:hydrogen-record:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.plateNumber]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Progress,
|
||||||
|
Radio,
|
||||||
|
Select,
|
||||||
|
Statistic,
|
||||||
|
Steps,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Upload,
|
||||||
|
message,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { InboxOutlined } from '@ant-design/icons-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';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const currentStep = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// Step 1 data
|
||||||
|
const stationId = ref<number>();
|
||||||
|
const stationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
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() {
|
||||||
|
visible.value = true;
|
||||||
|
currentStep.value = 0;
|
||||||
|
stationId.value = undefined;
|
||||||
|
fileList.value = [];
|
||||||
|
previewData.value = undefined;
|
||||||
|
duplicateStrategy.value = 'skip';
|
||||||
|
progressData.value = { current: 0, total: 0, status: '' };
|
||||||
|
importResult.value = undefined;
|
||||||
|
loadStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStations() {
|
||||||
|
const list = await getStationConfigSimpleList();
|
||||||
|
stationOptions.value = list.map((s) => ({
|
||||||
|
label: s.stationName,
|
||||||
|
value: s.stationId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1 -> Step 2: Upload and preview
|
||||||
|
async function handleNextStep() {
|
||||||
|
if (!stationId.value) {
|
||||||
|
message.warning('请选择加氢站');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
message.warning('请上传文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.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 {
|
||||||
|
importResult.value = await importConfirm(
|
||||||
|
previewData.value.batchNo,
|
||||||
|
duplicateStrategy.value,
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
if (currentStep.value === 2) {
|
||||||
|
emit('success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => stopProgressPolling());
|
||||||
|
|
||||||
|
// Preview table columns
|
||||||
|
const previewColumns = [
|
||||||
|
{ title: '行号', dataIndex: 'rowNum', width: 60 },
|
||||||
|
{ title: '车牌号', dataIndex: 'plateNumber', width: 100 },
|
||||||
|
{ title: '加氢日期', dataIndex: 'hydrogenDate', width: 100 },
|
||||||
|
{ title: '加氢量(KG)', dataIndex: 'hydrogenQuantity', width: 100 },
|
||||||
|
{ title: '单价', dataIndex: 'unitPrice', width: 80 },
|
||||||
|
{ title: '金额', dataIndex: 'amount', width: 80 },
|
||||||
|
{ title: '里程数', dataIndex: 'mileage', width: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle file before upload (prevent auto-upload)
|
||||||
|
function beforeUpload(file: File) {
|
||||||
|
fileList.value = [file];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-model:open="visible"
|
||||||
|
title="Excel 批量导入"
|
||||||
|
:width="800"
|
||||||
|
:footer="null"
|
||||||
|
:mask-closable="false"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<Steps :current="currentStep" class="mb-6">
|
||||||
|
<Steps.Step title="上传文件" />
|
||||||
|
<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
|
||||||
|
v-model:value="stationId"
|
||||||
|
:options="stationOptions"
|
||||||
|
placeholder="请选择加氢站"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Upload.Dragger
|
||||||
|
:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:max-count="1"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
>
|
||||||
|
<p class="ant-upload-drag-icon"><InboxOutlined /></p>
|
||||||
|
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Preview -->
|
||||||
|
<div v-if="currentStep === 1 && previewData">
|
||||||
|
<div class="mb-4 flex gap-4">
|
||||||
|
<Statistic title="总行数" :value="previewData.totalCount" />
|
||||||
|
<Statistic
|
||||||
|
title="有效"
|
||||||
|
:value="previewData.validCount"
|
||||||
|
class="text-green-600"
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
title="重复"
|
||||||
|
: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>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user