haiwei-ui 1.1.1 → 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,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<component v-bind:is="`nm-${options.showMode || 'drawer'}`" header footer draggable :padding="10" :title="title"
|
|
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
|
-
|
|
23
|
-
:
|
|
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"
|
|
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
|
-
|
|
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
|
-
<
|
|
108
|
-
v-model="model.maxPreviewRows"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
226
|
-
|
|
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
|
-
|
|
250
|
-
<
|
|
251
|
-
|
|
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
|
-
|
|
260
|
-
<
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
458
|
-
|
|
492
|
+
}
|
|
493
|
+
this.initColumnMapping()
|
|
494
|
+
this.currentStep = 3
|
|
495
|
+
this._success('文件解析成功')
|
|
496
|
+
} else {
|
|
497
|
+
this._error(data.msg || '文件解析失败')
|
|
459
498
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
this.
|
|
463
|
-
}
|
|
464
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
613
|
+
return this.deduplicateModels(allAddModels)
|
|
584
614
|
}
|
|
585
|
-
|
|
615
|
+
|
|
586
616
|
return allAddModels
|
|
587
617
|
},
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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
|
|
610
|
-
|
|
611
|
-
for (const
|
|
612
|
-
const key =
|
|
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
|
-
|
|
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
|
-
|
|
654
|
+
deduplicatedItems.push(item)
|
|
624
655
|
}
|
|
625
656
|
}
|
|
626
|
-
|
|
657
|
+
|
|
627
658
|
// 记录去重统计
|
|
628
|
-
const originalCount =
|
|
629
|
-
const deduplicatedCount =
|
|
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
|
|
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
|
|
673
|
-
border-radius:
|
|
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
|
|
681
|
-
background-color:
|
|
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:
|
|
689
|
-
font-weight:
|
|
690
|
-
color:
|
|
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:
|
|
696
|
-
color:
|
|
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:
|
|
758
|
+
color: var(--el-text-color-placeholder);
|
|
720
759
|
margin-bottom: 16px;
|
|
721
760
|
}
|
|
722
|
-
|
|
761
|
+
|
|
723
762
|
.placeholder-text {
|
|
724
|
-
font-size:
|
|
725
|
-
color:
|
|
763
|
+
font-size: var(--el-font-size-medium);
|
|
764
|
+
color: var(--el-text-color-regular);
|
|
726
765
|
margin-bottom: 8px;
|
|
727
|
-
font-weight:
|
|
766
|
+
font-weight: var(--el-font-weight-primary);
|
|
728
767
|
}
|
|
729
768
|
}
|
|
730
769
|
}
|
|
731
|
-
|
|
770
|
+
|
|
732
771
|
.file-info {
|
|
733
|
-
margin-top:
|
|
734
|
-
padding:
|
|
735
|
-
background-color:
|
|
736
|
-
border-radius:
|
|
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:
|
|
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:
|
|
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:
|
|
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>
|
|
@@ -2,19 +2,14 @@
|
|
|
2
2
|
<nm-container>
|
|
3
3
|
<nm-box page>
|
|
4
4
|
<div class="nm-default">
|
|
5
|
-
<nm-icon name="work" class="nm-size-100" />
|
|
5
|
+
<nm-icon name="work" class="nm-size-100" />
|
|
6
6
|
<h1 class="nm-m-20 nm-size-20">欢迎使用{{ title }}管理平台</h1>
|
|
7
7
|
<p class="nm-description">
|
|
8
8
|
本系统采用前后端分离架构:<br>
|
|
9
9
|
前端基于 Vue.js + Element UI 构建<br>
|
|
10
10
|
后端采用 .NET Core 框架开发
|
|
11
11
|
</p>
|
|
12
|
-
|
|
13
|
-
<img
|
|
14
|
-
style="width:100%"
|
|
15
|
-
src="../../../public/images/motto.png"
|
|
16
|
-
alt="系统标语"
|
|
17
|
-
/>
|
|
12
|
+
<img style="width:100%" src="../../../public/images/motto.png" />
|
|
18
13
|
</div>
|
|
19
14
|
</nm-box>
|
|
20
15
|
</nm-container>
|
|
@@ -36,3 +31,4 @@
|
|
|
36
31
|
text-align: center;
|
|
37
32
|
}
|
|
38
33
|
</style>
|
|
34
|
+
|