haiwei-ui 1.0.96 → 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,392 +1,542 @@
|
|
|
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
|
-
name: `col${index}`,
|
|
284
|
-
label: header.name,
|
|
285
|
-
width: 120
|
|
286
|
-
})),
|
|
287
|
-
previewData: response.data.data.map(row => {
|
|
288
|
-
const obj = {}
|
|
289
|
-
response.data.headers.forEach((header, colIndex) => {
|
|
290
|
-
obj[`col${colIndex}`] = row[colIndex]
|
|
291
|
-
})
|
|
292
|
-
return obj
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
this.initColumnMapping()
|
|
296
|
-
this._success('文件上传成功')
|
|
297
|
-
} else {
|
|
298
|
-
this._error(response.msg || '文件上传失败')
|
|
299
|
-
this.$refs.upload.clearFiles()
|
|
300
|
-
}
|
|
301
|
-
},
|
|
449
|
+
if (!isExcel) {
|
|
450
|
+
this._error('只能上传Excel文件!')
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
453
|
+
if (!isLt10M) {
|
|
454
|
+
this._error('文件大小不能超过10MB!')
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
302
457
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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 || '文件上传失败')
|
|
306
468
|
this.$refs.upload.clearFiles()
|
|
307
|
-
}
|
|
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
|
+
}
|
|
308
490
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.fileInfo = null
|
|
313
|
-
this.parseResult = null
|
|
314
|
-
this.columnMapping = []
|
|
315
|
-
},
|
|
491
|
+
this.parsing = true
|
|
492
|
+
const formData = new FormData()
|
|
493
|
+
formData.append('file', this.model.file)
|
|
316
494
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.columnMapping = this.parseResult.columns.map(col => ({
|
|
322
|
-
excelColumn: col,
|
|
323
|
-
targetColumn: this.findBestMatch(col),
|
|
324
|
-
required: false,
|
|
325
|
-
defaultValue: ''
|
|
326
|
-
}))
|
|
327
|
-
},
|
|
495
|
+
// 获取选中的工作表名称
|
|
496
|
+
const selectedSheet = this.fileInfo.sheets.find(sheet => sheet.index === this.model.selectedSheet)
|
|
497
|
+
const sheetName = selectedSheet ? selectedSheet.name : null
|
|
328
498
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
let match = this.targetColumns.find(col => col.label.toLowerCase() === excelLabel)
|
|
338
|
-
if (match) return match.name
|
|
339
|
-
|
|
340
|
-
// 2. 尝试完全匹配名称
|
|
341
|
-
match = this.targetColumns.find(col => col.name.toLowerCase() === excelName)
|
|
342
|
-
if (match) return match.name
|
|
343
|
-
|
|
344
|
-
// 3. 尝试包含匹配
|
|
345
|
-
match = this.targetColumns.find(col =>
|
|
346
|
-
excelLabel.includes(col.label.toLowerCase()) ||
|
|
347
|
-
col.label.toLowerCase().includes(excelLabel)
|
|
348
|
-
)
|
|
349
|
-
if (match) return match.name
|
|
350
|
-
|
|
351
|
-
return null
|
|
352
|
-
},
|
|
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
|
+
}
|
|
353
507
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.parsing = true
|
|
362
|
-
const formData = new FormData()
|
|
363
|
-
formData.append('file', this.model.file)
|
|
364
|
-
formData.append('hasHeader', this.model.hasHeader)
|
|
365
|
-
formData.append('maxPreviewRows', this.model.maxPreviewRows)
|
|
366
|
-
formData.append('skipEmptyRows', this.model.skipEmptyRows)
|
|
367
|
-
|
|
368
|
-
// 使用axios直接发送请求,避免$api可能为undefined的问题
|
|
369
|
-
// 从token模块获取token
|
|
370
|
-
const t = token.get()
|
|
371
|
-
const accessToken = t && t.accessToken ? t.accessToken : ''
|
|
372
|
-
const config = {
|
|
373
|
-
headers: {
|
|
374
|
-
'Content-Type': 'multipart/form-data',
|
|
375
|
-
'Authorization': `Bearer ${accessToken}`
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 使用与uploadUrl相同的逻辑获取API基础URL
|
|
380
|
-
let baseURL = this.$http?.axios?.defaults?.baseURL || window.__HAIWEI_API_BASE_URL__ || '/api'
|
|
381
|
-
// 确保baseURL以斜杠结尾,但不要有双斜杠
|
|
382
|
-
if (!baseURL.endsWith('/')) {
|
|
383
|
-
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}`
|
|
384
515
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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)
|
|
388
539
|
.then(response => {
|
|
389
|
-
// 检查响应格式
|
|
390
540
|
if (response.data && response.data.code === 1) {
|
|
391
541
|
this.fileInfo = response.data.data.basicInfo
|
|
392
542
|
this.parseResult = {
|
|
@@ -405,6 +555,7 @@
|
|
|
405
555
|
})
|
|
406
556
|
}
|
|
407
557
|
this.initColumnMapping()
|
|
558
|
+
this.currentStep = 3
|
|
408
559
|
this._success('文件解析成功')
|
|
409
560
|
} else {
|
|
410
561
|
this._error(response.data.msg || '文件解析失败')
|
|
@@ -416,121 +567,362 @@
|
|
|
416
567
|
.finally(() => {
|
|
417
568
|
this.parsing = false
|
|
418
569
|
})
|
|
419
|
-
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
initColumnMapping() {
|
|
573
|
+
if (!this.parseResult || !this.parseResult.columns) return
|
|
420
574
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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()
|
|
432
628
|
|
|
433
|
-
if (
|
|
434
|
-
this._error(
|
|
629
|
+
if (addModels.length === 0) {
|
|
630
|
+
this._error('没有可导入的数据')
|
|
631
|
+
this.importing = false
|
|
435
632
|
return
|
|
436
633
|
}
|
|
437
634
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this._success(`成功导入${addModels.length}条数据`)
|
|
455
|
-
this.hide()
|
|
456
|
-
this.$emit('import-success')
|
|
457
|
-
})
|
|
458
|
-
.catch(error => {
|
|
459
|
-
this._error('数据导入失败:' + (error.message || '未知错误'))
|
|
460
|
-
})
|
|
461
|
-
.finally(() => {
|
|
462
|
-
this.importing = false
|
|
463
|
-
})
|
|
464
|
-
} else {
|
|
465
|
-
this._error('未配置导入方法')
|
|
466
|
-
this.importing = false
|
|
467
|
-
}
|
|
468
|
-
} catch (error) {
|
|
469
|
-
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('未配置导入方法')
|
|
470
651
|
this.importing = false
|
|
471
652
|
}
|
|
472
|
-
}
|
|
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
|
+
}
|
|
473
663
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// 获取有效的列映射(有目标字段的映射)
|
|
481
|
-
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 = {}
|
|
482
670
|
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
const
|
|
671
|
+
// 根据列映射配置设置字段值
|
|
672
|
+
validMappings.forEach(mapping => {
|
|
673
|
+
const excelColumnName = mapping.excelColumn.name
|
|
674
|
+
const targetColumn = mapping.targetColumn
|
|
675
|
+
let value = row[excelColumnName]
|
|
486
676
|
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
let value = row[excelColumnName]
|
|
492
|
-
|
|
493
|
-
// 如果值为空且设置了默认值,使用默认值
|
|
494
|
-
if ((value === null || value === undefined || value === '') && mapping.defaultValue) {
|
|
495
|
-
value = mapping.defaultValue
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// 设置到AddModel中
|
|
499
|
-
addModel[targetColumn] = value
|
|
500
|
-
})
|
|
677
|
+
// 如果值为空且设置了默认值,使用默认值
|
|
678
|
+
if ((value === null || value === undefined || value === '') && mapping.defaultValue) {
|
|
679
|
+
value = mapping.defaultValue
|
|
680
|
+
}
|
|
501
681
|
|
|
502
|
-
|
|
682
|
+
// 设置到AddModel中
|
|
683
|
+
addModel[targetColumn] = value
|
|
503
684
|
})
|
|
504
|
-
|
|
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
|
+
}
|
|
505
701
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
513
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
|
|
743
|
+
|
|
744
|
+
if (removedCount > 0) {
|
|
745
|
+
this._info(`数据去重:原始 ${originalCount} 条,去重后 ${deduplicatedCount} 条,移除 ${removedCount} 条重复数据`)
|
|
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 : ''
|
|
514
763
|
},
|
|
515
|
-
|
|
516
|
-
|
|
764
|
+
|
|
765
|
+
getMappedColumnsCount() {
|
|
766
|
+
if (!this.columnMapping) return 0
|
|
767
|
+
return this.columnMapping.filter(mapping => mapping.targetColumn).length
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
getUnmappedColumnsCount() {
|
|
771
|
+
if (!this.columnMapping) return 0
|
|
772
|
+
return this.columnMapping.filter(mapping => !mapping.targetColumn).length
|
|
517
773
|
}
|
|
774
|
+
},
|
|
775
|
+
created() {
|
|
776
|
+
this.init()
|
|
518
777
|
}
|
|
778
|
+
}
|
|
519
779
|
</script>
|
|
520
780
|
|
|
521
|
-
<style lang="scss">
|
|
522
|
-
.
|
|
523
|
-
.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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 {
|
|
529
815
|
display: flex;
|
|
530
816
|
flex-direction: column;
|
|
531
|
-
justify-content: center;
|
|
532
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
|
+
}
|
|
848
|
+
}
|
|
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;
|
|
533
863
|
}
|
|
534
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
|
+
}
|
|
535
927
|
}
|
|
536
928
|
</style>
|