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