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 @@
-
+
-
- {{ row.status === 'success' ? '通过' : '失败' }}
-
+ 通过
+ 将被更新
+ 失败
@@ -91,12 +107,24 @@
取消
+
+
预览验证
-
- {{ executing ? '导入中...' : `确认导入 (${successCount} 条)` }}
+
+
+
+ {{ executing ? '导入中...' : `全部导入 (${importableCount} 条)` }}
+
+
+
+ {{ executing ? '导入中...' : `忽略错误,仅导入正确的 ${importableCount} 条` }}
+
+
+
+ 无可导入数据
@@ -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%;
}