haiwei-ui 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "haiwei-ui",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "HaiWei前端组件库",
5
5
  "author": "Eric",
6
6
  "license": "ISC",
7
7
  "main": "packages/index.js",
8
8
  "scripts": {
9
- "serve": "vue-cli-service serve",
10
- "build": "vue-cli-service build",
9
+ "serve": "vue-cli-service lint && vue-cli-service serve",
10
+ "build": "vue-cli-service lint && vue-cli-service build",
11
11
  "lint": "vue-cli-service lint",
12
12
  "cm": "rimraf node_modules && rimraf dist",
13
13
  "cc": "rimraf node_modules/.cache",
@@ -0,0 +1,477 @@
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>
26
+
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>
45
+
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>
55
+
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>
65
+
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>
76
+ </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>
88
+
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>
120
+
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>
129
+
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>
138
+
139
+ <!--底部-->
140
+ <template v-slot:footer>
141
+ <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" />
145
+ </el-button-group>
146
+ </template>
147
+ </component>
148
+ </template>
149
+ <script>
150
+ import dialog from '../../../../mixins/components/dialog'
151
+ import { mapState } from 'vuex'
152
+ export default {
153
+ mixins: [dialog],
154
+ data() {
155
+ return {
156
+ title: '导入数据',
157
+ icon: 'import',
158
+ width: '900px',
159
+ height: '700px',
160
+ model: {
161
+ file: null,
162
+ hasHeader: true,
163
+ skipEmptyRows: false,
164
+ maxPreviewRows: 100,
165
+ selectedSheet: 0,
166
+ importMode: 0,
167
+ errorHandling: 0
168
+ },
169
+ rules: {
170
+ file: [{ required: true, message: '请选择要导入的文件', trigger: 'change' }],
171
+ selectedSheet: [{ required: true, message: '请选择工作表', trigger: 'change' }]
172
+ },
173
+ fileList: [],
174
+ fileInfo: null,
175
+ parseResult: null,
176
+ parsing: false,
177
+ importing: false,
178
+ columnMapping: [],
179
+ targetColumns: []
180
+ }
181
+ },
182
+ props: {
183
+ /** 目标列配置 */
184
+ cols: Array,
185
+ /** 导入配置 */
186
+ options: Object,
187
+ /** 导入方法 */
188
+ action: Function
189
+ },
190
+ computed: {
191
+ ...mapState('app/user', { token: 'token' }),
192
+ uploadUrl() {
193
+ return `${this.$api.baseURL}/api/admin/excel/parseexcel`
194
+ },
195
+ headers() {
196
+ return {
197
+ 'Authorization': `Bearer ${this.token}`
198
+ }
199
+ },
200
+ uploadData() {
201
+ return {
202
+ hasHeader: this.model.hasHeader,
203
+ maxPreviewRows: this.model.maxPreviewRows,
204
+ skipEmptyRows: this.model.skipEmptyRows
205
+ }
206
+ }
207
+ },
208
+ methods: {
209
+ /**初始化 */
210
+ init() {
211
+ // 重置状态
212
+ this.fileList = []
213
+ this.fileInfo = null
214
+ this.parseResult = null
215
+ this.parsing = false
216
+ this.importing = false
217
+ this.columnMapping = []
218
+
219
+ // 初始化目标列
220
+ this.initTargetColumns()
221
+
222
+ // 应用选项配置
223
+ const { hasHeader = true, maxPreviewRows = 100, skipEmptyRows = false } = this.options
224
+ this.model.hasHeader = hasHeader
225
+ this.model.maxPreviewRows = maxPreviewRows
226
+ this.model.skipEmptyRows = skipEmptyRows
227
+ },
228
+
229
+ /**初始化目标列 */
230
+ initTargetColumns() {
231
+ if (this.cols && this.cols.length > 0) {
232
+ this.targetColumns = this.cols
233
+ .filter(col => col.show && col.name)
234
+ .map(col => ({
235
+ name: col.name,
236
+ label: col.label || col.name,
237
+ type: col.type || 'string'
238
+ }))
239
+ } else {
240
+ this.targetColumns = []
241
+ }
242
+ },
243
+
244
+ /**上传前验证 */
245
+ beforeUpload(file) {
246
+ const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
247
+ file.type === 'application/vnd.ms-excel'
248
+ const isLt100M = file.size / 1024 / 1024 < 100
249
+
250
+ if (!isExcel) {
251
+ this._error('只能上传Excel文件!')
252
+ return false
253
+ }
254
+ if (!isLt100M) {
255
+ this._error('文件大小不能超过100MB!')
256
+ return false
257
+ }
258
+
259
+ this.model.file = file
260
+ return true
261
+ },
262
+
263
+ /**上传成功 */
264
+ onUploadSuccess(response, file, fileList) {
265
+ if (response.successful) {
266
+ this.fileInfo = response.data.basicInfo
267
+ this.parseResult = response.data
268
+ this.initColumnMapping()
269
+ this._success('文件上传成功')
270
+ } else {
271
+ this._error(response.msg || '文件上传失败')
272
+ this.$refs.upload.clearFiles()
273
+ }
274
+ },
275
+
276
+ /**上传失败 */
277
+ onUploadError(err, file, fileList) {
278
+ this._error('文件上传失败')
279
+ this.$refs.upload.clearFiles()
280
+ },
281
+
282
+ /**文件移除 */
283
+ onFileRemove(file, fileList) {
284
+ this.model.file = null
285
+ this.fileInfo = null
286
+ this.parseResult = null
287
+ this.columnMapping = []
288
+ },
289
+
290
+ /**初始化列映射 */
291
+ initColumnMapping() {
292
+ if (!this.parseResult || !this.parseResult.columns) return
293
+
294
+ this.columnMapping = this.parseResult.columns.map(col => ({
295
+ excelColumn: col,
296
+ targetColumn: this.findBestMatch(col),
297
+ required: false,
298
+ defaultValue: ''
299
+ }))
300
+ },
301
+
302
+ /**查找最佳匹配的目标列 */
303
+ findBestMatch(excelColumn) {
304
+ if (!this.targetColumns || this.targetColumns.length === 0) return null
305
+
306
+ const excelLabel = excelColumn.label.toLowerCase()
307
+ const excelName = excelColumn.name.toLowerCase()
308
+
309
+ // 1. 尝试完全匹配标签
310
+ let match = this.targetColumns.find(col => col.label.toLowerCase() === excelLabel)
311
+ if (match) return match.name
312
+
313
+ // 2. 尝试完全匹配名称
314
+ match = this.targetColumns.find(col => col.name.toLowerCase() === excelName)
315
+ if (match) return match.name
316
+
317
+ // 3. 尝试包含匹配
318
+ match = this.targetColumns.find(col =>
319
+ excelLabel.includes(col.label.toLowerCase()) ||
320
+ col.label.toLowerCase().includes(excelLabel)
321
+ )
322
+ if (match) return match.name
323
+
324
+ return null
325
+ },
326
+
327
+ /**解析文件 */
328
+ onParse() {
329
+ if (!this.model.file) {
330
+ this._error('请先选择文件')
331
+ return
332
+ }
333
+
334
+ this.parsing = true
335
+ const formData = new FormData()
336
+ formData.append('file', this.model.file)
337
+ formData.append('hasHeader', this.model.hasHeader)
338
+ formData.append('maxPreviewRows', this.model.maxPreviewRows)
339
+ formData.append('skipEmptyRows', this.model.skipEmptyRows)
340
+
341
+ this.$api.post('/api/admin/excel/parseexcel', formData, {
342
+ headers: { 'Content-Type': 'multipart/form-data' }
343
+ })
344
+ .then(response => {
345
+ if (response.successful) {
346
+ this.fileInfo = response.data.basicInfo
347
+ this.parseResult = response.data
348
+ this.initColumnMapping()
349
+ this._success('文件解析成功')
350
+ } else {
351
+ this._error(response.msg || '文件解析失败')
352
+ }
353
+ })
354
+ .catch(error => {
355
+ this._error('文件解析失败:' + (error.message || '未知错误'))
356
+ })
357
+ .finally(() => {
358
+ this.parsing = false
359
+ })
360
+ },
361
+
362
+ /**开始导入 */
363
+ onImport() {
364
+ if (!this.parseResult) {
365
+ this._error('请先解析文件')
366
+ return
367
+ }
368
+
369
+ // 验证必填字段
370
+ const missingRequired = this.columnMapping
371
+ .filter(mapping => mapping.required && !mapping.targetColumn)
372
+ .map(mapping => mapping.excelColumn.label)
373
+
374
+ if (missingRequired.length > 0) {
375
+ this._error(`以下必填字段未配置映射:${missingRequired.join('、')}`)
376
+ return
377
+ }
378
+
379
+ this.importing = true
380
+
381
+ try {
382
+ // 根据列映射配置生成AddModel列表
383
+ const addModels = this.generateAddModels()
384
+
385
+ if (addModels.length === 0) {
386
+ this._error('没有可导入的数据')
387
+ this.importing = false
388
+ return
389
+ }
390
+
391
+ // 调用导入方法,传递AddModel列表
392
+ if (this.action && typeof this.action === 'function') {
393
+ this.action(addModels)
394
+ .then(() => {
395
+ this._success(`成功导入${addModels.length}条数据`)
396
+ this.hide()
397
+ this.$emit('import-success')
398
+ })
399
+ .catch(error => {
400
+ this._error('数据导入失败:' + (error.message || '未知错误'))
401
+ })
402
+ .finally(() => {
403
+ this.importing = false
404
+ })
405
+ } else {
406
+ this._error('未配置导入方法')
407
+ this.importing = false
408
+ }
409
+ } catch (error) {
410
+ this._error('数据转换失败:' + error.message)
411
+ this.importing = false
412
+ }
413
+ },
414
+
415
+ /**根据列映射配置生成AddModel列表 */
416
+ generateAddModels() {
417
+ if (!this.parseResult || !this.parseResult.previewData) {
418
+ return []
419
+ }
420
+
421
+ // 获取有效的列映射(有目标字段的映射)
422
+ const validMappings = this.columnMapping.filter(mapping => mapping.targetColumn)
423
+
424
+ // 转换每一行数据为AddModel
425
+ return this.parseResult.previewData.map((row, rowIndex) => {
426
+ const addModel = {}
427
+
428
+ // 根据列映射配置设置字段值
429
+ validMappings.forEach(mapping => {
430
+ const excelColumnName = mapping.excelColumn.name
431
+ const targetColumn = mapping.targetColumn
432
+ let value = row[excelColumnName]
433
+
434
+ // 如果值为空且设置了默认值,使用默认值
435
+ if ((value === null || value === undefined || value === '') && mapping.defaultValue) {
436
+ value = mapping.defaultValue
437
+ }
438
+
439
+ // 设置到AddModel中
440
+ addModel[targetColumn] = value
441
+ })
442
+
443
+ return addModel
444
+ })
445
+ },
446
+
447
+ /**格式化文件大小 */
448
+ formatFileSize(bytes) {
449
+ if (bytes === 0) return '0 B'
450
+ const k = 1024
451
+ const sizes = ['B', 'KB', 'MB', 'GB']
452
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
453
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
454
+ }
455
+ },
456
+ created() {
457
+ this.init()
458
+ }
459
+ }
460
+ </script>
461
+
462
+ <style lang="scss">
463
+ .nm-upload {
464
+ .el-upload {
465
+ width: 100%;
466
+
467
+ .el-upload-dragger {
468
+ width: 100%;
469
+ height: 180px;
470
+ display: flex;
471
+ flex-direction: column;
472
+ justify-content: center;
473
+ align-items: center;
474
+ }
475
+ }
476
+ }
477
+ </style>
@@ -57,7 +57,34 @@ const exportOptions = {
57
57
  showExportPeople: false
58
58
  }
59
59
 
60
+ //默认导入配置
61
+ const importOptions = {
62
+ /**启用导入功能 */
63
+ enabled: false,
64
+ /**导入数据的方法 */
65
+ action: null,
66
+ /**开启高级选项 */
67
+ advanced: false,
68
+ /**高级选项显示模式,drawer/dialog */
69
+ showMode: 'drawer',
70
+ /**按钮位置,tool:右上角工具栏 querybar:查询栏里面 */
71
+ btnLocation: 'tool',
72
+ /**按钮编码,用于控制按钮权限 */
73
+ btnCode: '',
74
+ /**默认是否包含表头 */
75
+ hasHeader: true,
76
+ /**默认最大预览行数 */
77
+ maxPreviewRows: 100,
78
+ /**默认是否跳过空行 */
79
+ skipEmptyRows: false,
80
+ /**默认导入模式:0-新增,1-更新,2-新增或更新 */
81
+ importMode: 0,
82
+ /**默认错误处理:0-遇到错误停止,1-跳过错误继续 */
83
+ errorHandling: 0
84
+ }
85
+
60
86
  export default {
61
87
  columnInfo,
62
- exportOptions
88
+ exportOptions,
89
+ importOptions
63
90
  }
@@ -76,20 +76,24 @@
76
76
 
77
77
  <!--导出-->
78
78
  <query-export v-if="exportAdvancedEnabled" :options="exportOptions_" :list-title="title" :cols="columns" :visible.sync="showExport" />
79
+
80
+ <!--导入-->
81
+ <query-import v-if="importAdvancedEnabled" :options="importOptions_" :cols="columns" :action="importOptions_.action" :visible.sync="showImport" />
79
82
  </section>
80
83
  </template>
81
84
  <script>
82
85
  import { mapState } from 'vuex'
83
86
  import def from './default.js'
84
- import QueryHeader from './components/header'
85
- import Querybar from './components/querybar'
86
- import QueryTable from './components/table'
87
- import QueryFooter from './components/footer'
88
- import QueryExport from './components/export'
87
+ import QueryHeader from './components/header'
88
+ import Querybar from './components/querybar'
89
+ import QueryTable from './components/table'
90
+ import QueryFooter from './components/footer'
91
+ import QueryExport from './components/export'
92
+ import QueryImport from './components/import'
89
93
 
90
94
  export default {
91
95
  name: 'List',
92
- components: { QueryHeader, Querybar, QueryTable, QueryFooter, QueryExport },
96
+ components: { QueryHeader, Querybar, QueryTable, QueryFooter, QueryExport, QueryImport },
93
97
  data() {
94
98
  return {
95
99
  loading_: false,
@@ -108,6 +112,7 @@
108
112
  total: 0,
109
113
  selection: [],
110
114
  showExport: false,
115
+ showImport: false,
111
116
  columns: []
112
117
  }
113
118
  },
@@ -180,6 +185,8 @@
180
185
  },
181
186
  /**导出配置 */
182
187
  exportOptions: Object,
188
+ /**导入配置 */
189
+ importOptions: Object,
183
190
  /** 页数选择项 */
184
191
  pageSizes: {
185
192
  type: Array,
@@ -222,6 +229,12 @@
222
229
  },
223
230
  exportAdvancedEnabled() {
224
231
  return this.exportOptions_.enabled && this.exportOptions_.advanced
232
+ },
233
+ importOptions_() {
234
+ return this.$_.assignIn({ title: this.title }, def.importOptions, this.importOptions)
235
+ },
236
+ importAdvancedEnabled() {
237
+ return this.importOptions_.enabled && this.importOptions_.advanced
225
238
  }
226
239
  },
227
240
  methods: {
@@ -360,6 +373,27 @@
360
373
  this.showExport = false
361
374
  this.$emit('export-change', this.showExport)
362
375
  },
376
+ /** 切换导入对话框显示状态 */
377
+ triggerImport() {
378
+ let imp = this.importOptions_
379
+ //未启用高级,直接执行导入操作
380
+ if (!imp.advanced) {
381
+ // 简单导入模式:直接打开文件选择
382
+ this._info('请使用高级导入功能进行文件选择和配置')
383
+ return
384
+ }
385
+
386
+ this.showImport ? this.closeImport() : this.openImport()
387
+ },
388
+ /** 打开导入对话框 */
389
+ openImport() {
390
+ this.showImport = true
391
+ this.$emit('import-change', this.showImport)
392
+ },
393
+ closeImport() {
394
+ this.showImport = false
395
+ this.$emit('import-change', this.showImport)
396
+ },
363
397
  /** 列表的列转导出的列 */
364
398
  listCol2ExportCol(m) {
365
399
  let col = {