haiwei-ui 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haiwei-ui",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "HaiWei前端组件库",
5
5
  "author": "Eric",
6
6
  "license": "ISC",
@@ -1,5 +1,6 @@
1
1
  <template>
2
- <component v-bind:is="`nm-${options.showMode || 'drawer'}`" header footer draggable :padding="10" :title="title" :icon="icon" :width="width" :height="height" :visible.sync="visible_">
2
+ <component v-bind:is="`nm-${options.showMode || 'drawer'}`" header footer draggable :padding="10" :title="title"
3
+ :icon="icon" :width="width" :height="height" :visible.sync="visible_">
3
4
  <div class="import-container">
4
5
  <!-- 步骤指示器 -->
5
6
  <el-steps :active="currentStep" align-center class="import-steps" simple>
@@ -18,24 +19,14 @@
18
19
  <span class="step-subtitle">支持 .xlsx 和 .xls 格式,最大10MB</span>
19
20
  </div>
20
21
  <div class="header-right">
21
- <el-upload
22
- ref="upload"
23
- :action="uploadUrl"
24
- :headers="headers"
25
- :before-upload="beforeUpload"
26
- :on-success="onUploadSuccess"
27
- :on-error="onUploadError"
28
- :on-remove="onFileRemove"
29
- :file-list="fileList"
30
- :limit="1"
31
- accept=".xlsx,.xls"
32
- :show-file-list="false"
33
- >
22
+ <el-upload ref="upload" :action="uploadUrl" :headers="headers" :before-upload="beforeUpload"
23
+ :on-success="onUploadSuccess" :on-error="onUploadError" :on-remove="onFileRemove" :file-list="fileList"
24
+ :limit="1" accept=".xlsx,.xls" :show-file-list="false">
34
25
  <nm-button type="primary" icon="upload" text="选择文件" />
35
26
  </el-upload>
36
27
  </div>
37
28
  </div>
38
-
29
+
39
30
  <div v-if="fileInfo" class="file-info">
40
31
  <el-descriptions :column="2" border size="small">
41
32
  <el-descriptions-item label="文件名">
@@ -53,7 +44,8 @@
53
44
  <el-form :model="model" label-width="120px" size="small">
54
45
  <el-form-item label="选择工作表:">
55
46
  <el-select v-model="model.selectedSheet" placeholder="请选择要导入的工作表" style="width: 100%">
56
- <el-option v-for="sheet in fileInfo.sheets" :key="sheet.index" :label="sheet.name" :value="sheet.index">
47
+ <el-option v-for="sheet in fileInfo.sheets" :key="sheet.index" :label="sheet.name"
48
+ :value="sheet.index">
57
49
  <span style="float: left">{{ sheet.name }}</span>
58
50
  <span style="float: right; color: #8492a6; font-size: 12px">
59
51
  {{ sheet.rowCount }}行 × {{ sheet.columnCount }}列
@@ -64,7 +56,7 @@
64
56
  </el-form>
65
57
  </div>
66
58
  </div>
67
-
59
+
68
60
  <div v-else class="upload-placeholder">
69
61
  <div class="placeholder-content">
70
62
  <i class="el-icon-document placeholder-icon"></i>
@@ -87,41 +79,33 @@
87
79
  <el-form-item label="是否包含表头:">
88
80
  <el-switch v-model="model.hasHeader" />
89
81
  </el-form-item>
90
-
82
+
91
83
  <el-form-item label="跳过空行:">
92
84
  <el-switch v-model="model.skipEmptyRows" />
93
85
  </el-form-item>
94
-
86
+
95
87
  <el-form-item label="表头行行号:">
96
- <el-input-number
97
- v-model="model.headerRowIndex"
98
- :min="0"
99
- :max="100"
100
- controls-position="right"
101
- style="width: 100%"
102
- size="small"
103
- />
88
+ <el-input-number v-model="model.headerRowIndex" :min="0" :max="100" controls-position="right"
89
+ style="width: 100%" size="small" />
104
90
  </el-form-item>
105
-
91
+
106
92
  <el-form-item label="最大预览行数:">
107
- <el-input-number
108
- v-model="model.maxPreviewRows"
109
- :min="1"
110
- :max="1000"
111
- controls-position="right"
112
- style="width: 100%"
113
- size="small"
114
- />
93
+ <div style="display: flex; align-items: center; gap: 8px">
94
+ <el-input-number v-model="model.maxPreviewRows" :min="1" :max="50000" controls-position="right"
95
+ style="flex: 1" size="small" />
96
+ <el-button type="primary" size="small" @click="model.maxPreviewRows = 50000" plain>
97
+ 选择全部
98
+ </el-button>
99
+ </div>
100
+ <div style="font-size: 12px; color: #909399; margin-top: 4px">
101
+ 最大可预览50000行数据
102
+ </div>
115
103
  </el-form-item>
116
-
104
+
117
105
  <el-form-item label="数据去重:">
118
106
  <el-switch v-model="model.deduplicate" />
119
107
  </el-form-item>
120
108
 
121
- <div class="form-actions">
122
- <nm-button type="primary" :loading="parsing" @click="onParse" text="解析文件" />
123
- <nm-button @click="currentStep = 1" text="返回上一步" />
124
- </div>
125
109
  </el-form>
126
110
  </el-card>
127
111
  </div>
@@ -137,31 +121,13 @@
137
121
 
138
122
  <div v-if="parseResult" class="mapping-container">
139
123
  <div class="preview-section">
140
- <el-alert
141
- :title="`已解析 ${parseResult.previewData.length} 行数据,${parseResult.columns.length} 列`"
142
- type="info"
143
- show-icon
144
- :closable="false"
145
- class="preview-alert"
146
- />
147
-
148
- <el-table
149
- :data="parseResult.previewData"
150
- border
151
- stripe
152
- size="mini"
153
- max-height="200"
154
- v-loading="parsing"
155
- class="preview-table"
156
- >
157
- <el-table-column
158
- v-for="(col, index) in parseResult.columns"
159
- :key="index"
160
- :prop="col.name"
161
- :label="col.label"
162
- :width="col.width"
163
- show-overflow-tooltip
164
- />
124
+ <el-alert :title="`已解析 ${parseResult.previewData.length} 行数据,${parseResult.columns.length} 列`" type="info"
125
+ show-icon :closable="false" class="preview-alert" />
126
+
127
+ <el-table :data="parseResult.previewData" border stripe size="mini" max-height="200" v-loading="parsing"
128
+ class="preview-table">
129
+ <el-table-column v-for="(col, index) in parseResult.columns" :key="index" :prop="col.name"
130
+ :label="col.label" :width="col.width" show-overflow-tooltip />
165
131
  </el-table>
166
132
  </div>
167
133
 
@@ -176,21 +142,10 @@
176
142
  </el-table-column>
177
143
  <el-table-column prop="targetColumn" label="目标字段" width="180">
178
144
  <template v-slot="{ row }">
179
- <el-select
180
- v-model="row.targetColumn"
181
- placeholder="请选择目标字段"
182
- style="width: 100%"
183
- clearable
184
- filterable
185
- size="mini"
186
- >
145
+ <el-select v-model="row.targetColumn" placeholder="请选择目标字段" style="width: 100%" clearable filterable
146
+ size="mini">
187
147
  <el-option label="不导入此列" :value="null"></el-option>
188
- <el-option
189
- v-for="col in targetColumns"
190
- :key="col.name"
191
- :label="col.label"
192
- :value="col.name"
193
- />
148
+ <el-option v-for="col in targetColumns" :key="col.name" :label="col.label" :value="col.name" />
194
149
  </el-select>
195
150
  </template>
196
151
  </el-table-column>
@@ -202,11 +157,6 @@
202
157
  </el-table>
203
158
  </div>
204
159
 
205
- <div class="form-actions">
206
- <nm-button type="primary" @click="currentStep = 4" text="下一步:确认导入" />
207
- <nm-button @click="currentStep = 2" text="返回上一步" />
208
- <nm-button type="info" @click="onParse" :loading="parsing" text="重新解析" />
209
- </div>
210
160
  </div>
211
161
  </el-card>
212
162
  </div>
@@ -221,13 +171,9 @@
221
171
  </div>
222
172
 
223
173
  <div class="import-summary">
224
- <el-alert
225
- :title="`准备导入 ${parseResult ? parseResult.previewData.length : 0} 条数据`"
226
- type="warning"
227
- show-icon
228
- :closable="false"
229
- />
230
-
174
+ <el-alert :title="`准备导入 ${confirmImportTotalRows} 条数据(${model.deduplicate ? '已去重' : '未去重'})`" type="warning"
175
+ show-icon :closable="false" />
176
+
231
177
  <div class="mapping-summary">
232
178
  <el-descriptions :column="2" border size="small">
233
179
  <el-descriptions-item label="Excel文件">
@@ -236,29 +182,72 @@
236
182
  <el-descriptions-item label="工作表">
237
183
  <el-tag size="small">{{ getSelectedSheetName() }}</el-tag>
238
184
  </el-descriptions-item>
239
- <el-descriptions-item label="总行数">
185
+ <el-descriptions-item label="原始总行数">
240
186
  <el-tag type="info" size="small">{{ parseResult.basicInfo.totalRows }}</el-tag>
241
187
  </el-descriptions-item>
188
+ <el-descriptions-item label="确认导入总行数">
189
+ <el-tag type="success" size="small">{{ confirmImportTotalRows }}</el-tag>
190
+ </el-descriptions-item>
242
191
  <el-descriptions-item label="已配置映射">
243
192
  <el-tag type="success" size="small">{{ getMappedColumnsCount() }}列</el-tag>
244
193
  </el-descriptions-item>
194
+ <el-descriptions-item label="数据去重">
195
+ <el-tag :type="model.deduplicate ? 'success' : 'info'" size="small">
196
+ {{ model.deduplicate ? '已启用' : '未启用' }}
197
+ </el-tag>
198
+ </el-descriptions-item>
245
199
  </el-descriptions>
246
200
  </div>
247
- </div>
248
201
 
249
- <div class="form-actions">
250
- <nm-button type="success" :loading="importing" @click="onImport" text="开始导入" />
251
- <nm-button @click="currentStep = 3" text="返回上一步" />
202
+ <!-- 去重后的数据预览 -->
203
+ <div v-if="deduplicatedPreviewData.length > 0" class="confirm-preview-section">
204
+ <el-alert :title="`去重后数据预览(仅显示已配置映射的字段,共 ${confirmImportColumns.length} 列)`" type="info" show-icon
205
+ :closable="false" class="preview-alert" />
206
+
207
+ <el-table :data="deduplicatedPreviewData" border stripe size="mini" max-height="200"
208
+ class="preview-table">
209
+ <el-table-column v-for="(col, index) in confirmImportColumns" :key="index" :prop="col.name"
210
+ :label="col.label" :width="col.width" show-overflow-tooltip />
211
+ </el-table>
212
+ </div>
213
+ <div v-else class="no-data-preview">
214
+ <el-alert title="没有可导入的数据,请检查字段映射配置" type="warning" show-icon :closable="false" />
215
+ </div>
252
216
  </div>
217
+
253
218
  </el-card>
254
219
  </div>
255
220
  </div>
256
221
 
257
222
  <!--底部-->
258
223
  <template v-slot:footer>
259
- <nm-button v-if="currentStep > 1" @click="currentStep--" text="上一步" />
260
- <nm-button v-if="currentStep < 4 && fileInfo" type="primary" @click="currentStep++" text="下一步" />
261
- <nm-button type="info" @click="hide" text="取消" />
224
+ <!-- 步骤1:选择文件 -->
225
+ <template v-if="currentStep === 1">
226
+ <nm-button v-if="fileInfo" type="primary" @click="currentStep++" text="下一步" />
227
+ <nm-button type="info" @click="hide" text="取消" />
228
+ </template>
229
+
230
+ <!-- 步骤2:配置选项 -->
231
+ <template v-else-if="currentStep === 2">
232
+ <nm-button @click="currentStep--" text="返回上一步" />
233
+ <nm-button type="primary" :loading="parsing" @click="onParse" text="解析文件" />
234
+ <nm-button type="info" @click="hide" text="取消" />
235
+ </template>
236
+
237
+ <!-- 步骤3:数据映射 -->
238
+ <template v-else-if="currentStep === 3">
239
+ <nm-button @click="currentStep--" text="返回上一步" />
240
+ <nm-button type="primary" @click="currentStep++" text="下一步:确认导入" />
241
+ <nm-button type="info" @click="onParse" :loading="parsing" text="重新解析" />
242
+ <nm-button type="info" @click="hide" text="取消" />
243
+ </template>
244
+
245
+ <!-- 步骤4:确认导入 -->
246
+ <template v-else-if="currentStep === 4">
247
+ <nm-button @click="currentStep--" text="返回上一步" />
248
+ <nm-button type="success" :loading="importing" @click="onImport" text="开始导入" />
249
+ <nm-button type="info" @click="hide" text="取消" />
250
+ </template>
262
251
  </template>
263
252
  </component>
264
253
  </template>
@@ -316,6 +305,50 @@ export default {
316
305
  return {
317
306
  'Authorization': `Bearer ${accessToken}`
318
307
  }
308
+ },
309
+ // 去重后的数据(用于确认导入步骤显示)
310
+ deduplicatedPreviewData() {
311
+ if (!this.parseResult || !this.parseResult.previewData) {
312
+ return []
313
+ }
314
+
315
+ // 获取有效的列映射(有目标字段的映射)
316
+ const validMappings = this.columnMapping.filter(mapping => mapping.targetColumn)
317
+
318
+ if (validMappings.length === 0) {
319
+ return []
320
+ }
321
+
322
+ // 转换每一行数据,只包含已配置映射的字段
323
+ const allData = this.parseResult.previewData.map(row => {
324
+ const item = {}
325
+ validMappings.forEach(mapping => {
326
+ const excelColumnName = mapping.excelColumn.name
327
+ const targetColumn = mapping.targetColumn
328
+ item[targetColumn] = row[excelColumnName]
329
+ })
330
+ return item
331
+ })
332
+
333
+ // 如果启用了去重,进行去重处理
334
+ if (this.model.deduplicate) {
335
+ return this.deduplicateModels(allData)
336
+ }
337
+
338
+ return allData
339
+ },
340
+ // 确认导入的总行数
341
+ confirmImportTotalRows() {
342
+ return this.deduplicatedPreviewData.length
343
+ },
344
+ // 确认导入的列(只显示已配置映射的字段)
345
+ confirmImportColumns() {
346
+ const validMappings = this.columnMapping.filter(mapping => mapping.targetColumn)
347
+ return validMappings.map(mapping => ({
348
+ name: mapping.targetColumn,
349
+ label: this.getTargetColumnLabel(mapping.targetColumn),
350
+ width: 120
351
+ }))
319
352
  }
320
353
  },
321
354
  methods: {
@@ -327,15 +360,15 @@ export default {
327
360
  this.parsing = false
328
361
  this.importing = false
329
362
  this.columnMapping = []
330
-
363
+
331
364
  this.initTargetColumns()
332
-
365
+
333
366
  const { hasHeader = true, maxPreviewRows = 100, skipEmptyRows = false } = this.options
334
367
  this.model.hasHeader = hasHeader
335
368
  this.model.maxPreviewRows = maxPreviewRows
336
369
  this.model.skipEmptyRows = skipEmptyRows
337
370
  },
338
-
371
+
339
372
  initTargetColumns() {
340
373
  if (this.cols && this.cols.length > 0) {
341
374
  this.targetColumns = this.cols
@@ -349,12 +382,12 @@ export default {
349
382
  this.targetColumns = []
350
383
  }
351
384
  },
352
-
385
+
353
386
  beforeUpload(file) {
354
- const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
355
- file.type === 'application/vnd.ms-excel'
387
+ const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
388
+ file.type === 'application/vnd.ms-excel'
356
389
  const isLt10M = file.size / 1024 / 1024 < 10
357
-
390
+
358
391
  if (!isExcel) {
359
392
  this._error('只能上传Excel文件!')
360
393
  return false
@@ -363,11 +396,11 @@ export default {
363
396
  this._error('文件大小不能超过10MB!')
364
397
  return false
365
398
  }
366
-
399
+
367
400
  this.model.file = file
368
401
  return true
369
402
  },
370
-
403
+
371
404
  onUploadSuccess(response, file, fileList) {
372
405
  if (response && response.code === 1) {
373
406
  this.fileInfo = response.data
@@ -377,12 +410,12 @@ export default {
377
410
  this.$refs.upload.clearFiles()
378
411
  }
379
412
  },
380
-
413
+
381
414
  onUploadError(err, file, fileList) {
382
415
  this._error('文件上传失败')
383
416
  this.$refs.upload.clearFiles()
384
417
  },
385
-
418
+
386
419
  onFileRemove(file, fileList) {
387
420
  this.model.file = null
388
421
  this.fileInfo = null
@@ -390,25 +423,25 @@ export default {
390
423
  this.columnMapping = []
391
424
  this.currentStep = 1
392
425
  },
393
-
426
+
394
427
  onParse() {
395
428
  if (!this.model.file) {
396
429
  this._error('请先选择文件')
397
430
  return
398
431
  }
399
-
432
+
400
433
  this.parsing = true
401
434
  const formData = new FormData()
402
435
  formData.append('file', this.model.file)
403
-
436
+
404
437
  // 获取选中的工作表名称
405
438
  const selectedSheet = this.fileInfo.sheets.find(sheet => sheet.index === this.model.selectedSheet)
406
439
  const sheetName = selectedSheet ? selectedSheet.name : null
407
-
440
+
408
441
  // 从token模块获取token
409
442
  const t = token.get()
410
443
  const accessToken = t && t.accessToken ? t.accessToken : ''
411
-
444
+
412
445
  // 构建解析URL - 使用与uploadUrl相同的方式
413
446
  let baseURL = this.$http?.axios?.defaults?.baseURL || window.__HAIWEI_API_BASE_URL__ || '/api'
414
447
  if (!baseURL.endsWith('/')) {
@@ -416,7 +449,7 @@ export default {
416
449
  }
417
450
  const path = 'admin/excel/parseexcel'.replace(/^\//, '')
418
451
  const parseUrl = baseURL + path
419
-
452
+
420
453
  // 添加查询参数
421
454
  const queryParams = new URLSearchParams()
422
455
  queryParams.append('hasHeader', this.model.hasHeader)
@@ -426,9 +459,9 @@ export default {
426
459
  if (sheetName) {
427
460
  queryParams.append('sheetName', sheetName)
428
461
  }
429
-
462
+
430
463
  const fullUrl = `${parseUrl}?${queryParams.toString()}`
431
-
464
+
432
465
  // 使用fetch API发送请求,避免axios依赖问题
433
466
  fetch(fullUrl, {
434
467
  method: 'POST',
@@ -438,97 +471,97 @@ export default {
438
471
  // 注意:不要设置Content-Type,让浏览器自动设置multipart/form-data的boundary
439
472
  }
440
473
  })
441
- .then(response => response.json())
442
- .then(data => {
443
- if (data && data.code === 1) {
444
- this.fileInfo = data.data.basicInfo
445
- this.parseResult = {
446
- basicInfo: data.data.basicInfo,
447
- columns: data.data.headers.map((header, index) => ({
448
- name: `col${index}`,
449
- label: header.name,
450
- width: 120
451
- })),
452
- previewData: data.data.data.map(row => {
453
- const obj = {}
454
- data.data.headers.forEach((header, colIndex) => {
455
- obj[`col${colIndex}`] = row[colIndex]
474
+ .then(response => response.json())
475
+ .then(data => {
476
+ if (data && data.code === 1) {
477
+ this.fileInfo = data.data.basicInfo
478
+ this.parseResult = {
479
+ basicInfo: data.data.basicInfo,
480
+ columns: data.data.headers.map((header, index) => ({
481
+ name: `col${index}`,
482
+ label: header.name,
483
+ width: 120
484
+ })),
485
+ previewData: data.data.data.map(row => {
486
+ const obj = {}
487
+ data.data.headers.forEach((header, colIndex) => {
488
+ obj[`col${colIndex}`] = row[colIndex]
489
+ })
490
+ return obj
456
491
  })
457
- return obj
458
- })
492
+ }
493
+ this.initColumnMapping()
494
+ this.currentStep = 3
495
+ this._success('文件解析成功')
496
+ } else {
497
+ this._error(data.msg || '文件解析失败')
459
498
  }
460
- this.initColumnMapping()
461
- this.currentStep = 3
462
- this._success('文件解析成功')
463
- } else {
464
- this._error(data.msg || '文件解析失败')
465
- }
466
- })
467
- .catch(error => {
468
- this._error('文件解析失败:' + (error.message || '未知错误'))
469
- })
470
- .finally(() => {
471
- this.parsing = false
472
- })
499
+ })
500
+ .catch(error => {
501
+ this._error('文件解析失败:' + (error.message || '未知错误'))
502
+ })
503
+ .finally(() => {
504
+ this.parsing = false
505
+ })
473
506
  },
474
-
507
+
475
508
  initColumnMapping() {
476
509
  if (!this.parseResult || !this.parseResult.columns) return
477
-
510
+
478
511
  this.columnMapping = this.parseResult.columns.map(col => ({
479
512
  excelColumn: col,
480
513
  targetColumn: this.findBestMatch(col),
481
514
  required: false
482
515
  }))
483
516
  },
484
-
517
+
485
518
  findBestMatch(excelColumn) {
486
519
  if (!this.targetColumns || this.targetColumns.length === 0) return null
487
-
520
+
488
521
  const excelLabel = excelColumn.label.toLowerCase()
489
-
522
+
490
523
  // 尝试完全匹配标签
491
524
  let match = this.targetColumns.find(col => col.label.toLowerCase() === excelLabel)
492
525
  if (match) return match.name
493
-
526
+
494
527
  // 尝试包含匹配
495
- match = this.targetColumns.find(col =>
496
- excelLabel.includes(col.label.toLowerCase()) ||
528
+ match = this.targetColumns.find(col =>
529
+ excelLabel.includes(col.label.toLowerCase()) ||
497
530
  col.label.toLowerCase().includes(excelLabel)
498
531
  )
499
532
  if (match) return match.name
500
-
533
+
501
534
  return null
502
535
  },
503
-
536
+
504
537
  onImport() {
505
538
  if (!this.parseResult) {
506
539
  this._error('请先解析文件')
507
540
  return
508
541
  }
509
-
542
+
510
543
  // 验证必填字段
511
544
  const missingRequired = this.columnMapping
512
545
  .filter(mapping => mapping.required && !mapping.targetColumn)
513
546
  .map(mapping => mapping.excelColumn.label)
514
-
547
+
515
548
  if (missingRequired.length > 0) {
516
549
  this._error(`以下必填字段未配置映射:${missingRequired.join('、')}`)
517
550
  return
518
551
  }
519
-
552
+
520
553
  this.importing = true
521
-
554
+
522
555
  try {
523
556
  // 根据列映射配置生成AddModel列表
524
557
  const addModels = this.generateAddModels()
525
-
558
+
526
559
  if (addModels.length === 0) {
527
560
  this._error('没有可导入的数据')
528
561
  this.importing = false
529
562
  return
530
563
  }
531
-
564
+
532
565
  // 调用导入方法,传递AddModel列表
533
566
  if (this.action && typeof this.action === 'function') {
534
567
  this.action(addModels)
@@ -552,42 +585,40 @@ export default {
552
585
  this.importing = false
553
586
  }
554
587
  },
555
-
588
+
556
589
  generateAddModels() {
557
590
  if (!this.parseResult || !this.parseResult.previewData) {
558
591
  return []
559
592
  }
560
-
593
+
561
594
  // 获取有效的列映射(有目标字段的映射)
562
595
  const validMappings = this.columnMapping.filter(mapping => mapping.targetColumn)
563
-
596
+
564
597
  // 转换每一行数据为AddModel
565
- const allAddModels = this.parseResult.previewData.map((row, rowIndex) => {
598
+ const allAddModels = this.parseResult.previewData.map(row => {
566
599
  const addModel = {}
567
-
600
+
568
601
  // 根据列映射配置设置字段值
569
602
  validMappings.forEach(mapping => {
570
603
  const excelColumnName = mapping.excelColumn.name
571
604
  const targetColumn = mapping.targetColumn
572
- let value = row[excelColumnName]
573
-
574
- // 设置到AddModel中
575
- addModel[targetColumn] = value
605
+ addModel[targetColumn] = row[excelColumnName]
576
606
  })
577
-
607
+
578
608
  return addModel
579
609
  })
580
-
610
+
581
611
  // 如果启用了去重,进行去重处理
582
612
  if (this.model.deduplicate && allAddModels.length > 0) {
583
- return this.deduplicateAddModels(allAddModels)
613
+ return this.deduplicateModels(allAddModels)
584
614
  }
585
-
615
+
586
616
  return allAddModels
587
617
  },
588
-
589
- deduplicateAddModels(addModels) {
590
- if (!addModels || addModels.length === 0) {
618
+
619
+ // 通用的去重方法,可用于数据和模型 - 按所有已配置映射的字段组合去重
620
+ deduplicateModels(models) {
621
+ if (!models || models.length === 0) {
591
622
  return []
592
623
  }
593
624
 
@@ -598,44 +629,56 @@ export default {
598
629
 
599
630
  // 如果没有目标字段,无法去重
600
631
  if (targetFields.length === 0) {
601
- return addModels
632
+ return models
602
633
  }
603
634
 
604
- // 使用第一个目标字段作为去重依据
605
- const deduplicateField = targetFields[0]
606
-
607
635
  // 使用Map进行去重,保留第一次出现的数据
608
636
  const uniqueMap = new Map()
609
- const deduplicatedModels = []
637
+ const deduplicatedItems = []
610
638
 
611
- for (const model of addModels) {
612
- const key = model[deduplicateField]
639
+ for (const item of models) {
640
+ // 生成组合键:将所有目标字段的值拼接成一个字符串
641
+ const keyParts = []
642
+ let hasEmptyKey = false
643
+
644
+ for (const field of targetFields) {
645
+ const value = item[field]
646
+ // 如果任何一个关键字段为空,跳过该行的去重检查
647
+ if (value === null || value === undefined || value === '') {
648
+ hasEmptyKey = true
649
+ break
650
+ }
651
+ keyParts.push(`${field}:${value}`)
652
+ }
613
653
 
614
- // 如果key为空,跳过去重
615
- if (key === null || key === undefined || key === '') {
616
- deduplicatedModels.push(model)
654
+ // 如果有空的关键字段,直接添加到结果中(不参与去重)
655
+ if (hasEmptyKey) {
656
+ deduplicatedItems.push(item)
617
657
  continue
618
658
  }
619
659
 
620
- // 如果还没有这个key,添加到Map和结果列表
621
- if (!uniqueMap.has(key)) {
622
- uniqueMap.set(key, true)
623
- deduplicatedModels.push(model)
660
+ // 生成完整的组合键
661
+ const combinedKey = keyParts.join('|')
662
+
663
+ // 如果还没有这个组合键,添加到Map和结果列表
664
+ if (!uniqueMap.has(combinedKey)) {
665
+ uniqueMap.set(combinedKey, true)
666
+ deduplicatedItems.push(item)
624
667
  }
625
668
  }
626
669
 
627
670
  // 记录去重统计
628
- const originalCount = addModels.length
629
- const deduplicatedCount = deduplicatedModels.length
671
+ const originalCount = models.length
672
+ const deduplicatedCount = deduplicatedItems.length
630
673
  const removedCount = originalCount - deduplicatedCount
631
674
 
632
675
  if (removedCount > 0) {
633
- this._success(`数据去重:原始 ${originalCount} 条,去重后 ${deduplicatedCount} 条,移除 ${removedCount} 条重复数据`)
676
+ this._success(`数据去重:原始 ${originalCount} 条,去重后 ${deduplicatedCount} 条,移除 ${removedCount} 条重复数据(按所有已配置字段组合去重)`)
634
677
  }
635
678
 
636
- return deduplicatedModels
679
+ return deduplicatedItems
637
680
  },
638
-
681
+
639
682
  formatFileSize(bytes) {
640
683
  if (bytes === 0) return '0 B'
641
684
  const k = 1024
@@ -643,17 +686,25 @@ export default {
643
686
  const i = Math.floor(Math.log(bytes) / Math.log(k))
644
687
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
645
688
  },
646
-
689
+
647
690
  getSelectedSheetName() {
648
691
  if (!this.fileInfo || !this.fileInfo.sheets) return ''
649
692
  const selectedSheet = this.fileInfo.sheets.find(sheet => sheet.index === this.model.selectedSheet)
650
693
  return selectedSheet ? selectedSheet.name : ''
651
694
  },
652
-
695
+
653
696
  getMappedColumnsCount() {
654
697
  if (!this.columnMapping) return 0
655
698
  return this.columnMapping.filter(mapping => mapping.targetColumn).length
656
- }
699
+ },
700
+
701
+ // 获取目标字段的标签
702
+ getTargetColumnLabel(columnName) {
703
+ if (!columnName) return ''
704
+ const targetCol = this.targetColumns.find(col => col.name === columnName)
705
+ return targetCol ? targetCol.label : columnName
706
+ },
707
+
657
708
  },
658
709
  created() {
659
710
  this.init()
@@ -666,45 +717,49 @@ export default {
666
717
  .import-steps {
667
718
  margin-bottom: 20px;
668
719
  }
669
-
720
+
670
721
  .step-content {
671
722
  .step-card {
672
- border: 1px solid #dcdfe6;
673
- border-radius: 4px;
723
+ border: 1px solid var(--el-border-color-light);
724
+ border-radius: var(--el-border-radius-base);
674
725
 
675
- .step-header {
676
- display: flex;
677
- justify-content: space-between;
678
- align-items: center;
679
- padding: 12px 16px;
680
- border-bottom: 1px solid #dcdfe6;
681
- background-color: #f5f7fa;
726
+ ::v-deep .el-card__header {
727
+ padding: 0;
682
728
 
683
- .header-left {
729
+ .step-header {
684
730
  display: flex;
685
- flex-direction: column;
731
+ justify-content: space-between;
732
+ align-items: center;
733
+ padding: 12px 16px;
734
+ border-bottom: 1px solid var(--el-border-color-light);
735
+ background-color: var(--el-fill-color-light);
686
736
 
687
- .step-title {
688
- font-size: 14px;
689
- font-weight: 600;
690
- color: #303133;
691
- margin-bottom: 4px;
737
+ .header-left {
738
+ display: flex;
739
+ flex-direction: column;
740
+
741
+ .step-title {
742
+ font-size: var(--el-font-size-medium);
743
+ font-weight: var(--el-font-weight-primary);
744
+ color: var(--el-text-color-primary);
745
+ margin-bottom: 4px;
746
+ }
747
+
748
+ .step-subtitle {
749
+ font-size: var(--el-font-size-extra-small);
750
+ color: var(--el-text-color-placeholder);
751
+ }
692
752
  }
693
753
 
694
- .step-subtitle {
695
- font-size: 12px;
696
- color: #909399;
754
+ .header-right {
755
+ display: flex;
756
+ align-items: center;
697
757
  }
698
758
  }
699
-
700
- .header-right {
701
- display: flex;
702
- align-items: center;
703
- }
704
759
  }
705
760
  }
706
761
  }
707
-
762
+
708
763
  .upload-placeholder {
709
764
  display: flex;
710
765
  flex-direction: column;
@@ -712,68 +767,64 @@ export default {
712
767
  justify-content: center;
713
768
  padding: 40px 16px;
714
769
  text-align: center;
715
-
770
+
716
771
  .placeholder-content {
717
772
  .placeholder-icon {
718
773
  font-size: 36px;
719
- color: #c0c4cc;
774
+ color: var(--el-text-color-placeholder);
720
775
  margin-bottom: 16px;
721
776
  }
722
-
777
+
723
778
  .placeholder-text {
724
- font-size: 14px;
725
- color: #606266;
779
+ font-size: var(--el-font-size-medium);
780
+ color: var(--el-text-color-regular);
726
781
  margin-bottom: 8px;
727
- font-weight: 500;
782
+ font-weight: var(--el-font-weight-primary);
728
783
  }
729
784
  }
730
785
  }
731
-
786
+
732
787
  .file-info {
733
- margin-top: 16px;
734
- padding: 16px;
735
- background-color: #f8f9fa;
736
- border-radius: 4px;
737
-
788
+ margin-top: var(--el-margin);
789
+ padding: var(--el-padding);
790
+ background-color: var(--el-fill-color-lighter);
791
+ border-radius: var(--el-border-radius-base);
792
+
738
793
  .sheet-select {
739
- margin-top: 16px;
794
+ margin-top: var(--el-margin);
740
795
  }
741
796
  }
742
-
743
- .form-actions {
744
- display: flex;
745
- justify-content: center;
746
- margin-top: 24px;
747
- padding-top: 16px;
748
- border-top: 1px solid #dcdfe6;
749
- }
750
-
797
+
751
798
  .mapping-container {
752
799
  .preview-section {
753
800
  margin-bottom: 24px;
754
-
755
- .preview-alert {
756
- margin-bottom: 12px;
757
- }
758
-
759
- .preview-table {
760
- margin-bottom: 8px;
761
- }
762
801
  }
763
-
802
+
764
803
  .mapping-section {
765
804
  .mapping-table {
766
805
  .excel-column {
767
806
  .column-label {
768
- font-weight: 500;
807
+ font-weight: var(--el-font-weight-primary);
769
808
  }
770
809
  }
771
810
  }
772
811
  }
773
812
  }
774
-
813
+
775
814
  .import-summary {
776
- margin: 16px 0;
815
+ margin: var(--el-margin) 0;
816
+
817
+ .mapping-summary {
818
+ margin: var(--el-margin) 0;
819
+ }
820
+
821
+ .confirm-preview-section {
822
+ margin-top: 24px;
823
+ }
824
+
825
+ .no-data-preview {
826
+ margin-top: var(--el-margin);
827
+ }
777
828
  }
778
829
  }
779
830
  </style>