From d0776f1036fa43c7990432a05206a134ed564c3d Mon Sep 17 00:00:00 2001 From: yueli Date: Mon, 3 Aug 2026 15:53:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(import):=20=E5=89=8D=E7=AB=AF=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E7=AD=96=E7=95=A5=E9=80=89=E6=8B=A9=E5=99=A8=20+=20?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=AF=BC=E5=85=A5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增导入策略单选:跳过重复行 / 覆盖更新 - 预览状态新增「将被更新」标签(橙色),区分「通过」「失败」 - 支持部分导入:有错误行时显示「忽略错误,仅导入正确的 N 条」按钮 - 预览时传递 mode 参数给后端 - 导入结果展示新增/更新/跳过数量 --- inventory-web/src/components/ImportDialog.vue | 103 ++++++++++++++++-- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/inventory-web/src/components/ImportDialog.vue b/inventory-web/src/components/ImportDialog.vue index 6078c19..2e05b27 100644 --- a/inventory-web/src/components/ImportDialog.vue +++ b/inventory-web/src/components/ImportDialog.vue @@ -42,6 +42,22 @@ + +
+ + 3. 导入策略 + + + 跳过重复行 + 仅新增不重复的数据,已存在的直接跳过 + + + 覆盖更新 + 用 Excel 中的新数据覆盖更新数据库已有记录 + + +
+
正在解析和验证数据... @@ -60,11 +76,11 @@ - + @@ -91,12 +107,24 @@ @@ -138,12 +166,21 @@ const fileList = ref([]) const fileReady = ref(false) const rawFile = ref(null) +// ★ 新增:导入策略(仅 material 类型生效) +const importMode = ref<'skip' | 'update'>('skip') + const previewRows = ref([]) const successCount = computed(() => previewRows.value.filter(r => r.status === 'success').length) +const updateCount = computed(() => previewRows.value.filter(r => r.status === 'update').length) const errorCount = computed(() => previewRows.value.filter(r => r.status === 'error').length) -const previewSummary = computed(() => - `共 ${previewRows.value.length} 条: ${successCount.value} 条通过, ${errorCount.value} 条失败` -) +const importableCount = computed(() => successCount.value + updateCount.value) +const previewSummary = computed(() => { + const parts = [`共 ${previewRows.value.length} 条`] + if (successCount.value > 0) parts.push(`${successCount.value} 条通过`) + if (updateCount.value > 0) parts.push(`${updateCount.value} 条将被更新`) + if (errorCount.value > 0) parts.push(`${errorCount.value} 条失败`) + return parts.join(',') +}) // ═══ Preview Columns (depends on type) ═══ const materialColumns = [ @@ -218,6 +255,7 @@ const handlePreview = async () => { const formData = new FormData() formData.append('file', rawFile.value) formData.append('type', props.importType) + formData.append('mode', importMode.value) // ★ 预览时传递导入策略 const res: any = await request({ url: '/v1/import/preview', @@ -240,7 +278,7 @@ const handlePreview = async () => { } const handleExecute = async () => { - if (errorCount.value > 0) return + if (importableCount.value === 0) return executing.value = true try { const res: any = await request({ @@ -248,12 +286,19 @@ const handleExecute = async () => { method: 'post', data: { type: props.importType, - rows: previewRows.value + rows: previewRows.value, + mode: importMode.value, } }) if (res.code === 200) { - ElMessage.success(res.msg || '导入成功') + const detail = res.data || {} + // ★ 构建详细结果提示 + const parts: string[] = [] + if (detail.inserted > 0) parts.push(`新增 ${detail.inserted} 条`) + if (detail.updated > 0) parts.push(`更新 ${detail.updated} 条`) + if (detail.skipped > 0) parts.push(`跳过 ${detail.skipped} 条重复`) + ElMessage.success(parts.length > 0 ? parts.join(',') : (res.msg || '导入完成')) emit('success') handleClose() } else { @@ -272,6 +317,7 @@ const handleClose = () => { rawFile.value = null fileReady.value = false previewRows.value = [] + importMode.value = 'skip' visible.value = false } @@ -283,6 +329,7 @@ watch(visible, (val) => { rawFile.value = null fileReady.value = false previewRows.value = [] + importMode.value = 'skip' } }) @@ -309,6 +356,38 @@ watch(visible, (val) => { color: #909399; font-size: 14px; } +.import-strategy { + margin-top: 8px; +} +.strategy-group { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +} +.strategy-group .el-radio { + margin-right: 0; + height: auto; + padding: 10px 14px; + border: 1px solid #DCDFE6; + border-radius: 6px; + transition: border-color 0.2s; +} +.strategy-group .el-radio.is-checked { + border-color: #409EFF; + background: #ECF5FF; +} +.strategy-label { + font-weight: 600; + font-size: 14px; + display: block; +} +.strategy-desc { + font-size: 12px; + color: #909399; + display: block; + margin-top: 2px; +} :deep(.el-upload-dragger) { width: 100%; }