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