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 @@
+
+
+
+
+
+
+
+ 导出错误明细
+
+
+
+
+
+ {{ row.data.name || '—' }}
+
+
+ {{ row.data.spec || '—' }}
+
+
+ {{ row.data.unit || '—' }}
+
+
+ {{ row.data.major || '—' }}
+
+
+ {{ row.data.minor || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+