From c3835fb28d1c8c4cfce4ba3f24745211d9d85224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E7=9D=BF=20=E5=BC=A0?= Date: Sat, 10 Oct 2026 16:24:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=A9=E6=96=99=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=BC=B9=E7=AA=97=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 摘要条按成败两种文案;失败时展示逐行错误表格、分页与导出按钮 - 分页复用现成的 BasePagination,每页 20 行 - 导出走 exportCsv,文件名带本地时间后缀 - downloadCsv 经 inject 注入,测试里可替换掉真实下载动作 - 测试等两轮 nextTick 再断言表格内容:el-table 先注册列、下一轮才填单元格, 只等一个 tick 拿到的是空 tr,断言会以看不出原因的方式失败 --- .../business/MaterialImportResult.test.js | 63 ++++++++++ .../business/MaterialImportResult.vue | 117 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 frontend/src/components/business/MaterialImportResult.test.js create mode 100644 frontend/src/components/business/MaterialImportResult.vue diff --git a/frontend/src/components/business/MaterialImportResult.test.js b/frontend/src/components/business/MaterialImportResult.test.js new file mode 100644 index 00000000..7a93658b --- /dev/null +++ b/frontend/src/components/business/MaterialImportResult.test.js @@ -0,0 +1,63 @@ +import { mount } from '@vue/test-utils' +import ElementPlus from 'element-plus' +import { nextTick } from 'vue' +import { describe, expect, test, vi } from 'vitest' +import MaterialImportResult from './MaterialImportResult.vue' + +const failedResult = { + total_rows: 3, + success_rows: 0, + failed_rows: 2, + errors: [ + { row: 2, data: { name: '', spec: 'M8x30', unit: '个', major: '机械结构类', minor: '标准件' }, reason: '物料名称不能为空' }, + { row: 4, data: { name: '螺栓', spec: '', unit: '吨', major: '机械结构类', minor: '标准件' }, reason: '单位「吨」不存在或已停用' } + ] +} + +// el-table 分两轮渲染:先注册列,下一轮才把单元格填上。只等一个 tick 的话 +// 拿到的是空 ,断言文本会莫名其妙地失败(实测过一次),所以统一等两轮。 +async function mountResult(result, options = {}) { + const wrapper = mount(MaterialImportResult, { + props: { result }, + global: { plugins: [ElementPlus], stubs: { teleport: true }, ...options } + }) + await nextTick() + await nextTick() + return wrapper +} + +describe('MaterialImportResult', () => { + test('失败时展示行数摘要与每行错误', async () => { + const wrapper = await mountResult(failedResult) + + expect(wrapper.text()).toContain('共 3 行') + expect(wrapper.text()).toContain('2 行未通过') + const rows = wrapper.findAll('tbody tr') + expect(rows.length).toBe(2) + expect(rows[0].text()).toContain('2') + expect(rows[0].text()).toContain('物料名称不能为空') + wrapper.unmount() + }) + + test('导出错误明细时把行号与原因写进 CSV', async () => { + const download = vi.fn() + const wrapper = await mountResult(failedResult, { provide: { downloadCsv: download } }) + + await wrapper.find('[data-test="export-errors"]').trigger('click') + + expect(download).toHaveBeenCalledTimes(1) + const [fileName, csv] = download.mock.calls[0] + expect(fileName).toContain('导入错误明细') + expect(csv).toContain('物料名称不能为空') + expect(csv).toContain('行号') + wrapper.unmount() + }) + + test('全部成功时只展示成功摘要,不显示错误表格', async () => { + const wrapper = await mountResult({ total_rows: 2, success_rows: 2, failed_rows: 0, errors: [] }) + + expect(wrapper.text()).toContain('成功导入 2 条') + expect(wrapper.find('[data-test="error-table"]').exists()).toBe(false) + wrapper.unmount() + }) +}) diff --git a/frontend/src/components/business/MaterialImportResult.vue b/frontend/src/components/business/MaterialImportResult.vue new file mode 100644 index 00000000..3fb48884 --- /dev/null +++ b/frontend/src/components/business/MaterialImportResult.vue @@ -0,0 +1,117 @@ + + + + +