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