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="10" :title="title" :icon="icon" :width="width" :height="height" :visible.sync="visible_">
3
- <el-form ref="form" :model="model" :rules="rules" :size="fontSize" label-width="120px">
4
- <!-- 文件上传 -->
5
- <el-form-item label="选择文件:" prop="file">
6
- <el-upload
7
- ref="upload"
8
- class="nm-upload"
9
- drag
10
- :action="uploadUrl"
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
- <el-form-item v-if="fileInfo" label="解析选项:">
29
- <el-row :gutter="20">
30
- <el-col :span="12">
31
- <el-checkbox v-model="model.hasHeader" :disabled="parsing">包含表头</el-checkbox>
32
- </el-col>
33
- <el-col :span="12">
34
- <el-checkbox v-model="model.skipEmptyRows" :disabled="parsing">跳过空行</el-checkbox>
35
- </el-col>
36
- </el-row>
37
- <el-row :gutter="20" class="nm-m-t-10">
38
- <el-col :span="12">
39
- <el-input-number v-model="model.maxPreviewRows" :min="1" :max="1000" :disabled="parsing" controls-position="right" style="width: 100%">
40
- <template slot="prepend">最大预览行数</template>
41
- </el-input-number>
42
- </el-col>
43
- </el-row>
44
- </el-form-item>
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
- <el-form-item v-if="fileInfo" label="文件信息:">
48
- <el-descriptions :column="2" border size="small">
49
- <el-descriptions-item label="文件名">{{ fileInfo.fileName }}</el-descriptions-item>
50
- <el-descriptions-item label="文件大小">{{ formatFileSize(fileInfo.fileSize) }}</el-descriptions-item>
51
- <el-descriptions-item label="工作表">{{ fileInfo.sheetCount }}个</el-descriptions-item>
52
- <el-descriptions-item label="总行数">{{ fileInfo.totalRows }}行</el-descriptions-item>
53
- </el-descriptions>
54
- </el-form-item>
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
- <el-form-item v-if="fileInfo && fileInfo.sheets && fileInfo.sheets.length > 1" label="选择工作表:" prop="selectedSheet">
58
- <el-select v-model="model.selectedSheet" placeholder="请选择工作表" :disabled="parsing" style="width: 100%">
59
- <el-option v-for="sheet in fileInfo.sheets" :key="sheet.index" :label="sheet.name" :value="sheet.index">
60
- <span style="float: left">{{ sheet.name }}</span>
61
- <span style="float: right; color: #8492a6; font-size: 13px">{{ sheet.rowCount }}行 × {{ sheet.columnCount }}列</span>
62
- </el-option>
63
- </el-select>
64
- </el-form-item>
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
- <el-form-item v-if="parseResult" label="数据预览:">
68
- <el-alert v-if="parseResult.basicInfo" :title="`已解析 ${parseResult.basicInfo.rowCount} 行数据,${parseResult.basicInfo.columnCount} 列`" type="info" show-icon :closable="false" class="nm-m-b-10" />
69
-
70
- <el-table :data="parseResult.previewData" border stripe size="mini" max-height="400" v-loading="parsing">
71
- <el-table-column v-for="(col, index) in parseResult.columns" :key="index" :prop="col.name" :label="col.label" :width="col.width">
72
- <template v-slot:header>
73
- <div class="nm-text-center">
74
- <div>{{ col.label }}</div>
75
- <div class="nm-text-xs nm-text-muted">({{ col.name }})</div>
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
- </template>
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
- <el-form-item v-if="parseResult && parseResult.columns && parseResult.columns.length > 0" label="列映射配置:">
91
- <el-table :data="columnMapping" border stripe size="mini" max-height="300">
92
- <el-table-column prop="excelColumn" label="Excel列" width="150">
93
- <template v-slot="{ row }">
94
- <span>{{ row.excelColumn.label }} <span class="nm-text-muted">({{ row.excelColumn.name }})</span></span>
95
- </template>
96
- </el-table-column>
97
- <el-table-column prop="targetColumn" label="目标字段" width="200">
98
- <template v-slot="{ row }">
99
- <el-select v-model="row.targetColumn" placeholder="请选择目标字段" style="width: 100%">
100
- <el-option label="不导入此列" :value="null"></el-option>
101
- <el-option v-for="col in targetColumns" :key="col.name" :label="col.label" :value="col.name">
102
- <span style="float: left">{{ col.label }}</span>
103
- <span style="float: right; color: #8492a6; font-size: 13px">{{ col.name }}</span>
104
- </el-option>
105
- </el-select>
106
- </template>
107
- </el-table-column>
108
- <el-table-column prop="required" label="必填" width="80" align="center">
109
- <template v-slot="{ row }">
110
- <el-checkbox v-model="row.required" :disabled="!row.targetColumn"></el-checkbox>
111
- </template>
112
- </el-table-column>
113
- <el-table-column prop="defaultValue" label="默认值">
114
- <template v-slot="{ row }">
115
- <el-input v-model="row.defaultValue" :disabled="!row.targetColumn" placeholder="当Excel为空时使用此默认值"></el-input>
116
- </template>
117
- </el-table-column>
118
- </el-table>
119
- </el-form-item>
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
- <el-form-item v-if="parseResult" label="导入模式:" prop="importMode">
123
- <el-radio-group v-model="model.importMode">
124
- <el-radio :label="0">新增数据</el-radio>
125
- <el-radio :label="1">更新数据</el-radio>
126
- <el-radio :label="2">新增或更新</el-radio>
127
- </el-radio-group>
128
- </el-form-item>
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
- <el-form-item v-if="parseResult" label="错误处理:" prop="errorHandling">
132
- <el-radio-group v-model="model.errorHandling">
133
- <el-radio :label="0">遇到错误停止导入</el-radio>
134
- <el-radio :label="1">跳过错误继续导入</el-radio>
135
- </el-radio-group>
136
- </el-form-item>
137
- </el-form>
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
- <nm-button v-if="fileInfo && !parseResult" type="primary" :loading="parsing" text="解析文件" @click="onParse" />
143
- <nm-button v-if="parseResult" type="success" :loading="importing" text="开始导入" @click="onImport" />
144
- <nm-button type="info" text="取消" @click="hide" />
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
- import dialog from '../../../../mixins/components/dialog'
151
- import { mapState } from 'vuex'
152
- import token from '../../../../utils/token'
153
- export default {
154
- mixins: [dialog],
155
- data() {
156
- return {
157
- title: '导入数据',
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: []
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
- headers() {
205
- // 从token模块获取token
206
- const t = token.get()
207
- const accessToken = t && t.accessToken ? t.accessToken : ''
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
- uploadData() {
213
- return {
214
- hasHeader: this.model.hasHeader,
215
- maxPreviewRows: this.model.maxPreviewRows,
216
- skipEmptyRows: this.model.skipEmptyRows
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
- methods: {
221
- /**初始化 */
222
- init() {
223
- // 重置状态
224
- this.fileList = []
225
- this.fileInfo = null
226
- this.parseResult = null
227
- this.parsing = false
228
- this.importing = false
229
- this.columnMapping = []
230
-
231
- // 初始化目标列
232
- this.initTargetColumns()
233
-
234
- // 应用选项配置
235
- const { hasHeader = true, maxPreviewRows = 100, skipEmptyRows = false } = this.options
236
- this.model.hasHeader = hasHeader
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
- beforeUpload(file) {
258
- const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
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
- },
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
- onUploadSuccess(response, file, fileList) {
277
- if (response.successful) {
278
- this.fileInfo = response.data.basicInfo
279
- this.parseResult = response.data
280
- this.initColumnMapping()
281
- this._success('文件上传成功')
282
- } else {
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
- onUploadError(err, file, fileList) {
290
- this._error('文件上传失败')
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
- onFileRemove(file, fileList) {
296
- this.model.file = null
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
- initColumnMapping() {
304
- if (!this.parseResult || !this.parseResult.columns) return
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
- findBestMatch(excelColumn) {
316
- if (!this.targetColumns || this.targetColumns.length === 0) return null
317
-
318
- const excelLabel = excelColumn.label.toLowerCase()
319
- const excelName = excelColumn.name.toLowerCase()
320
-
321
- // 1. 尝试完全匹配标签
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
- onParse() {
341
- if (!this.model.file) {
342
- this._error('请先选择文件')
343
- return
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
- const path = 'admin/excel/parseexcel'.replace(/^\//, '')
372
- this.$http.post(baseURL + path, formData, config)
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.successful) {
540
+ if (response.data && response.data.code === 1) {
375
541
  this.fileInfo = response.data.data.basicInfo
376
- this.parseResult = response.data.data
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
- onImport() {
393
- if (!this.parseResult) {
394
- this._error('请先解析文件')
395
- return
396
- }
397
-
398
- // 验证必填字段
399
- const missingRequired = this.columnMapping
400
- .filter(mapping => mapping.required && !mapping.targetColumn)
401
- .map(mapping => mapping.excelColumn.label)
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 (missingRequired.length > 0) {
404
- this._error(`以下必填字段未配置映射:${missingRequired.join('')}`)
629
+ if (addModels.length === 0) {
630
+ this._error('没有可导入的数据')
631
+ this.importing = false
405
632
  return
406
633
  }
407
634
 
408
- this.importing = true
409
-
410
- try {
411
- // 根据列映射配置生成AddModel列表
412
- const addModels = this.generateAddModels()
413
-
414
- if (addModels.length === 0) {
415
- this._error('没有可导入的数据')
416
- this.importing = false
417
- return
418
- }
419
-
420
- // 调用导入方法,传递AddModel列表
421
- if (this.action && typeof this.action === 'function') {
422
- this.action(addModels)
423
- .then(() => {
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
- /**根据列映射配置生成AddModel列表 */
445
- generateAddModels() {
446
- if (!this.parseResult || !this.parseResult.previewData) {
447
- return []
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
- // 转换每一行数据为AddModel
454
- return this.parseResult.previewData.map((row, rowIndex) => {
455
- const addModel = {}
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
- validMappings.forEach(mapping => {
459
- const excelColumnName = mapping.excelColumn.name
460
- const targetColumn = mapping.targetColumn
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
- return addModel
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
- formatFileSize(bytes) {
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
- created() {
486
- this.init()
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
- .nm-upload {
493
- .el-upload {
494
- width: 100%;
495
-
496
- .el-upload-dragger {
497
- width: 100%;
498
- height: 180px;
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>