haiwei-ui 1.1.2 → 1.1.3

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.3",
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,90 +585,88 @@ 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
+
594
625
  // 获取所有已配置的目标字段
595
626
  const targetFields = this.columnMapping
596
627
  .filter(mapping => mapping.targetColumn)
597
628
  .map(mapping => mapping.targetColumn)
598
-
629
+
599
630
  // 如果没有目标字段,无法去重
600
631
  if (targetFields.length === 0) {
601
- return addModels
632
+ return models
602
633
  }
603
-
634
+
604
635
  // 使用第一个目标字段作为去重依据
605
636
  const deduplicateField = targetFields[0]
606
-
637
+
607
638
  // 使用Map进行去重,保留第一次出现的数据
608
639
  const uniqueMap = new Map()
609
- const deduplicatedModels = []
610
-
611
- for (const model of addModels) {
612
- const key = model[deduplicateField]
613
-
640
+ const deduplicatedItems = []
641
+
642
+ for (const item of models) {
643
+ const key = item[deduplicateField]
644
+
614
645
  // 如果key为空,跳过去重
615
646
  if (key === null || key === undefined || key === '') {
616
- deduplicatedModels.push(model)
647
+ deduplicatedItems.push(item)
617
648
  continue
618
649
  }
619
-
650
+
620
651
  // 如果还没有这个key,添加到Map和结果列表
621
652
  if (!uniqueMap.has(key)) {
622
653
  uniqueMap.set(key, true)
623
- deduplicatedModels.push(model)
654
+ deduplicatedItems.push(item)
624
655
  }
625
656
  }
626
-
657
+
627
658
  // 记录去重统计
628
- const originalCount = addModels.length
629
- const deduplicatedCount = deduplicatedModels.length
659
+ const originalCount = models.length
660
+ const deduplicatedCount = deduplicatedItems.length
630
661
  const removedCount = originalCount - deduplicatedCount
631
-
662
+
632
663
  if (removedCount > 0) {
633
664
  this._success(`数据去重:原始 ${originalCount} 条,去重后 ${deduplicatedCount} 条,移除 ${removedCount} 条重复数据`)
634
665
  }
635
-
636
- return deduplicatedModels
666
+
667
+ return deduplicatedItems
637
668
  },
638
-
669
+
639
670
  formatFileSize(bytes) {
640
671
  if (bytes === 0) return '0 B'
641
672
  const k = 1024
@@ -643,17 +674,25 @@ export default {
643
674
  const i = Math.floor(Math.log(bytes) / Math.log(k))
644
675
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
645
676
  },
646
-
677
+
647
678
  getSelectedSheetName() {
648
679
  if (!this.fileInfo || !this.fileInfo.sheets) return ''
649
680
  const selectedSheet = this.fileInfo.sheets.find(sheet => sheet.index === this.model.selectedSheet)
650
681
  return selectedSheet ? selectedSheet.name : ''
651
682
  },
652
-
683
+
653
684
  getMappedColumnsCount() {
654
685
  if (!this.columnMapping) return 0
655
686
  return this.columnMapping.filter(mapping => mapping.targetColumn).length
656
- }
687
+ },
688
+
689
+ // 获取目标字段的标签
690
+ getTargetColumnLabel(columnName) {
691
+ if (!columnName) return ''
692
+ const targetCol = this.targetColumns.find(col => col.name === columnName)
693
+ return targetCol ? targetCol.label : columnName
694
+ },
695
+
657
696
  },
658
697
  created() {
659
698
  this.init()
@@ -666,37 +705,37 @@ export default {
666
705
  .import-steps {
667
706
  margin-bottom: 20px;
668
707
  }
669
-
708
+
670
709
  .step-content {
671
710
  .step-card {
672
- border: 1px solid #dcdfe6;
673
- border-radius: 4px;
674
-
711
+ border: 1px solid var(--el-border-color-light);
712
+ border-radius: var(--el-border-radius-base);
713
+
675
714
  .step-header {
676
715
  display: flex;
677
716
  justify-content: space-between;
678
717
  align-items: center;
679
718
  padding: 12px 16px;
680
- border-bottom: 1px solid #dcdfe6;
681
- background-color: #f5f7fa;
682
-
719
+ border-bottom: 1px solid var(--el-border-color-light);
720
+ background-color: var(--el-fill-color-light);
721
+
683
722
  .header-left {
684
723
  display: flex;
685
724
  flex-direction: column;
686
-
725
+
687
726
  .step-title {
688
- font-size: 14px;
689
- font-weight: 600;
690
- color: #303133;
727
+ font-size: var(--el-font-size-medium);
728
+ font-weight: var(--el-font-weight-primary);
729
+ color: var(--el-text-color-primary);
691
730
  margin-bottom: 4px;
692
731
  }
693
-
732
+
694
733
  .step-subtitle {
695
- font-size: 12px;
696
- color: #909399;
734
+ font-size: var(--el-font-size-extra-small);
735
+ color: var(--el-text-color-placeholder);
697
736
  }
698
737
  }
699
-
738
+
700
739
  .header-right {
701
740
  display: flex;
702
741
  align-items: center;
@@ -704,7 +743,7 @@ export default {
704
743
  }
705
744
  }
706
745
  }
707
-
746
+
708
747
  .upload-placeholder {
709
748
  display: flex;
710
749
  flex-direction: column;
@@ -712,68 +751,64 @@ export default {
712
751
  justify-content: center;
713
752
  padding: 40px 16px;
714
753
  text-align: center;
715
-
754
+
716
755
  .placeholder-content {
717
756
  .placeholder-icon {
718
757
  font-size: 36px;
719
- color: #c0c4cc;
758
+ color: var(--el-text-color-placeholder);
720
759
  margin-bottom: 16px;
721
760
  }
722
-
761
+
723
762
  .placeholder-text {
724
- font-size: 14px;
725
- color: #606266;
763
+ font-size: var(--el-font-size-medium);
764
+ color: var(--el-text-color-regular);
726
765
  margin-bottom: 8px;
727
- font-weight: 500;
766
+ font-weight: var(--el-font-weight-primary);
728
767
  }
729
768
  }
730
769
  }
731
-
770
+
732
771
  .file-info {
733
- margin-top: 16px;
734
- padding: 16px;
735
- background-color: #f8f9fa;
736
- border-radius: 4px;
737
-
772
+ margin-top: var(--el-margin);
773
+ padding: var(--el-padding);
774
+ background-color: var(--el-fill-color-lighter);
775
+ border-radius: var(--el-border-radius-base);
776
+
738
777
  .sheet-select {
739
- margin-top: 16px;
778
+ margin-top: var(--el-margin);
740
779
  }
741
780
  }
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
-
781
+
751
782
  .mapping-container {
752
783
  .preview-section {
753
784
  margin-bottom: 24px;
754
-
755
- .preview-alert {
756
- margin-bottom: 12px;
757
- }
758
-
759
- .preview-table {
760
- margin-bottom: 8px;
761
- }
762
785
  }
763
-
786
+
764
787
  .mapping-section {
765
788
  .mapping-table {
766
789
  .excel-column {
767
790
  .column-label {
768
- font-weight: 500;
791
+ font-weight: var(--el-font-weight-primary);
769
792
  }
770
793
  }
771
794
  }
772
795
  }
773
796
  }
774
-
797
+
775
798
  .import-summary {
776
- margin: 16px 0;
799
+ margin: var(--el-margin) 0;
800
+
801
+ .mapping-summary {
802
+ margin: var(--el-margin) 0;
803
+ }
804
+
805
+ .confirm-preview-section {
806
+ margin-top: 24px;
807
+ }
808
+
809
+ .no-data-preview {
810
+ margin-top: var(--el-margin);
811
+ }
777
812
  }
778
813
  }
779
814
  </style>