sh-view 2.8.1 → 2.8.2
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/.eslintrc.js +25 -20
- package/other.js +8 -8
- package/package.json +9 -6
- package/packages/components/index.js +91 -91
- package/packages/components/sh-alert/alert.ts +30 -0
- package/packages/components/sh-alert/index.vue +143 -168
- package/packages/components/sh-badge/index.vue +242 -242
- package/packages/components/sh-calendar/index.vue +650 -650
- package/packages/components/sh-card/index.vue +148 -148
- package/packages/components/sh-code-editor/index.vue +19 -19
- package/packages/components/sh-col/index.vue +92 -92
- package/packages/components/sh-corner/index.vue +230 -230
- package/packages/components/sh-count-to/index.vue +131 -131
- package/packages/components/sh-date/index.vue +301 -301
- package/packages/components/sh-drawer/index.vue +579 -579
- package/packages/components/sh-drawer/scrollbar.js +78 -78
- package/packages/components/sh-empty/index.vue +42 -42
- package/packages/components/sh-form/js/props.js +76 -76
- package/packages/components/sh-form/js/useForm.js +229 -229
- package/packages/components/sh-header/index.vue +261 -260
- package/packages/components/sh-icon/css/default/ionicons.svg +869 -869
- package/packages/components/sh-icon/css/font/iconfont.json +247 -247
- package/packages/components/sh-icon/index.vue +41 -41
- package/packages/components/sh-image/index.vue +133 -133
- package/packages/components/sh-list/index.vue +146 -146
- package/packages/components/sh-loading/index.vue +53 -53
- package/packages/components/sh-modal/index.vue +188 -188
- package/packages/components/sh-noticebar/index.vue +215 -215
- package/packages/components/sh-poptip/index.vue +597 -597
- package/packages/components/sh-progress/index.vue +276 -276
- package/packages/components/sh-pull-refresh/index.vue +289 -289
- package/packages/components/sh-result/index.vue +114 -114
- package/packages/components/sh-row/index.vue +66 -66
- package/packages/components/sh-split/components/trigger.vue +33 -33
- package/packages/components/sh-split/index.vue +342 -342
- package/packages/components/sh-table/components/importModal.vue +363 -363
- package/packages/components/sh-table/components/sh-column.vue +68 -68
- package/packages/components/sh-table/js/excel_to_json.js +313 -313
- package/packages/components/sh-table/js/props.js +305 -305
- package/packages/components/sh-table/js/tableMethods.js +167 -167
- package/packages/components/sh-table/js/useTable.js +636 -636
- package/packages/components/sh-table/table.vue +217 -217
- package/packages/components/sh-tabs/index.vue +426 -426
- package/packages/components/sh-tag/index.vue +168 -168
- package/packages/components/sh-toolbar/index.vue +182 -182
- package/packages/components/sh-tree/components/table-tree.vue +289 -289
- package/packages/components/sh-tree/mixin/treeProps.js +122 -122
- package/packages/components/sh-upload/index.vue +535 -535
- package/packages/components/sh-water-fall/index.vue +80 -80
- package/packages/components/sh-water-mark/index.vue +96 -96
- package/packages/css/index.js +4 -4
- package/packages/directive/index.js +19 -19
- package/packages/directive/module/click-out.js +14 -14
- package/packages/directive/module/draggable.js +42 -42
- package/packages/directive/module/line-clamp.js +22 -22
- package/packages/directive/module/prevent-click.js +18 -18
- package/packages/directive/module/resize.js +14 -14
- package/packages/directive/module/ripple.js +166 -166
- package/packages/index.js +39 -39
- package/packages/mixin/index.js +86 -86
- package/packages/other/sh-cron-modal/components/cron-content.vue +294 -294
- package/packages/other/sh-cron-modal/index.vue +81 -81
- package/packages/other/sh-cron-modal/mixin/cron-emits.js +1 -1
- package/packages/other/sh-cron-modal/mixin/cron-props.js +9 -9
- package/packages/other/sh-cron-modal/tabs/cron-week-box.vue +126 -126
- package/packages/other/sh-menu/index.vue +326 -326
- package/packages/other/sh-menu/menu-group-content.vue +136 -136
- package/packages/other/sh-menu/menu-item-content.vue +71 -71
- package/packages/other/sh-menu-card/index.vue +250 -250
- package/packages/other/sh-menu-card/menu-box.vue +87 -87
- package/packages/other/sh-preview/components/sh-excel.vue +163 -163
- package/packages/other/sh-preview/js/data-hook.js +41 -41
- package/packages/other/sh-preview/js/data-props.js +15 -15
- package/packages/other/sh-system-tip/index.vue +115 -115
- package/packages/utils/resize.js +69 -70
- package/packages/utils/transfer-queue.js +12 -12
- package/packages/vxeTable/index.js +193 -184
- package/packages/vxeTable/plugins/export.js +450 -450
- package/packages/vxeTable/render/cell/vxe-render-img.vue +27 -27
- package/packages/vxeTable/render/cell/vxe-render-table.vue +51 -51
- package/packages/vxeTable/render/cell/vxe-render-time.vue +44 -44
- package/packages/vxeTable/render/cell/vxe-render-tree.vue +70 -70
- package/packages/vxeTable/render/filters/vxe-filter-input.vue +26 -26
- package/packages/vxeTable/render/filters/vxe-filter-time.vue +26 -26
- package/packages/vxeTable/render/globalRenders.jsx +514 -514
- package/packages/vxeTable/render/mixin/cell-hooks.js +198 -198
- package/packages/vxeTable/render/mixin/cell-props.js +23 -23
- package/packages/vxeTable/render/mixin/filter-hooks.js +46 -46
- package/tsconfig.json +25 -0
- package/types/component.d.ts +1 -0
- package/types/index.ts +0 -0
|
@@ -1,363 +1,363 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<sh-modal v-bind="modalConfig" :loading="modalLoading" @close="onModalClose">
|
|
3
|
-
<div class="import-template-top">
|
|
4
|
-
<transition name="fade">
|
|
5
|
-
<sh-progress v-if="showProgress" :percent="progressPercent" :stroke-width="2">
|
|
6
|
-
<template v-if="progressPercent === 100"><span>上传成功</span></template>
|
|
7
|
-
</sh-progress>
|
|
8
|
-
</transition>
|
|
9
|
-
</div>
|
|
10
|
-
<div class="import-template-head">
|
|
11
|
-
<sh-button :size="size" @click="handleDownloadTemplateBtn">下载导入模板</sh-button>
|
|
12
|
-
<sh-button :size="size" status="primary" @click="handleImportFileBtn">选择导入文件</sh-button>
|
|
13
|
-
<div class="floatright">
|
|
14
|
-
<sh-poptip v-if="importErrorData.length > 0" trigger="click" title="错误信息" placement="bottom-end">
|
|
15
|
-
<sh-button :size="size" status="danger">
|
|
16
|
-
共发现 <strong>{{ importErrorData.length }}</strong> 条不符合规范数据
|
|
17
|
-
</sh-button>
|
|
18
|
-
<template #content>
|
|
19
|
-
<div class="import-template-error-list">
|
|
20
|
-
<div v-for="(errData, errDataIndex) in importErrorData" :key="errDataIndex" class="error-item">
|
|
21
|
-
第 <strong class="red">{{ errData.rowIndex }}</strong> 行,错误:<span class="red">{{ errData.errMsg }}</span>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</template>
|
|
25
|
-
</sh-poptip>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
<div class="import-template-body">
|
|
29
|
-
<sh-table ref="shtable" v-bind="tableConfig"></sh-table>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="import-template-foot">
|
|
32
|
-
<div class="foot-left"></div>
|
|
33
|
-
<div class="foot-right">
|
|
34
|
-
<sh-button :size="size" @click="onModalClose">取消</sh-button>
|
|
35
|
-
<sh-button :size="size" status="primary" @click="handleImportDataBtn('all')">全部导入</sh-button>
|
|
36
|
-
<sh-button v-if="tableGlobalConfig.selectType" :size="size" status="warning" @click="handleImportDataBtn('select')">导入选中</sh-button>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</sh-modal>
|
|
40
|
-
</template>
|
|
41
|
-
|
|
42
|
-
<script>
|
|
43
|
-
import { defineComponent, computed, getCurrentInstance, ref, onBeforeMount } from 'vue'
|
|
44
|
-
import * as ExcelJS from 'exceljs'
|
|
45
|
-
export default defineComponent({
|
|
46
|
-
name: 'TableImport',
|
|
47
|
-
props: {
|
|
48
|
-
modalConfig: {
|
|
49
|
-
type: Object,
|
|
50
|
-
default() {
|
|
51
|
-
return {}
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
columns: {
|
|
55
|
-
type: Array
|
|
56
|
-
},
|
|
57
|
-
height: {
|
|
58
|
-
type: [Number, String],
|
|
59
|
-
default: 360
|
|
60
|
-
},
|
|
61
|
-
needValidate: {
|
|
62
|
-
type: Boolean
|
|
63
|
-
},
|
|
64
|
-
importRules: {
|
|
65
|
-
type: Object,
|
|
66
|
-
default() {
|
|
67
|
-
return {}
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
importConfig: {
|
|
71
|
-
type: Object,
|
|
72
|
-
default() {
|
|
73
|
-
return {}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
globalConfig: {
|
|
77
|
-
type: Object,
|
|
78
|
-
default() {
|
|
79
|
-
return {}
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
size: {
|
|
83
|
-
type: String,
|
|
84
|
-
default: 'mini' // medium / small / mini
|
|
85
|
-
},
|
|
86
|
-
importFileCallback: {
|
|
87
|
-
type: Function
|
|
88
|
-
},
|
|
89
|
-
importFileFinished: {
|
|
90
|
-
type: Function
|
|
91
|
-
},
|
|
92
|
-
downloadTemplateCallback: {
|
|
93
|
-
type: Function
|
|
94
|
-
},
|
|
95
|
-
downloadTemplateFinished: {
|
|
96
|
-
type: Function
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
emits: ['confirm', 'cancel'],
|
|
100
|
-
setup(props, context) {
|
|
101
|
-
const { proxy } = getCurrentInstance()
|
|
102
|
-
const { $vUtils, $vTable } = proxy
|
|
103
|
-
const { emit, slots } = context
|
|
104
|
-
const shtable = ref()
|
|
105
|
-
|
|
106
|
-
const importTableData = ref([])
|
|
107
|
-
const importErrorData = ref([])
|
|
108
|
-
const modalLoading = ref(false)
|
|
109
|
-
const showProgress = ref(false)
|
|
110
|
-
const progressPercent = ref(0)
|
|
111
|
-
|
|
112
|
-
const tableGlobalConfig = computed(() => Object.assign({ seq: true, title: true, tableName: '导入预览' }, props.globalConfig))
|
|
113
|
-
const tableColumns = computed(() => props.columns.filter(item => !(['seq', 'checkbox', 'radio'].includes(item.type) || item.renderName === '$vGlobalOption')))
|
|
114
|
-
const tableConfig = computed(() => {
|
|
115
|
-
return {
|
|
116
|
-
height: props.height,
|
|
117
|
-
size: props.size,
|
|
118
|
-
editRules: props.importRules,
|
|
119
|
-
columns: tableColumns.value,
|
|
120
|
-
data: importTableData.value,
|
|
121
|
-
globalConfig: tableGlobalConfig.value
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
// 初始化
|
|
125
|
-
const initCreated = () => {
|
|
126
|
-
showProgress.value = false
|
|
127
|
-
progressPercent.value = 0
|
|
128
|
-
importTableData.value = []
|
|
129
|
-
}
|
|
130
|
-
// 弹窗关闭
|
|
131
|
-
const onModalClose = () => {
|
|
132
|
-
$vUtils.set(props.modalConfig, 'modelValue', false)
|
|
133
|
-
emit('cancel')
|
|
134
|
-
}
|
|
135
|
-
// 确认导入数据按钮
|
|
136
|
-
const handleImportDataBtn = async (type = 'all') => {
|
|
137
|
-
let importData = type === 'all' ? importTableData.value : shtable.value.getSelectionData()
|
|
138
|
-
if (!importData || !Array.isArray(importData) || importData.length < 1) {
|
|
139
|
-
proxy.msginfo('未导入数据')
|
|
140
|
-
return false
|
|
141
|
-
}
|
|
142
|
-
if (props.needValidate) {
|
|
143
|
-
let validateErrMap = await handleImportDataValidate(importData)
|
|
144
|
-
if (validateErrMap) {
|
|
145
|
-
proxy.msgerror('导入校验失败,请检查数据')
|
|
146
|
-
return validateErrMap
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
emit('confirm', importData)
|
|
150
|
-
onModalClose()
|
|
151
|
-
}
|
|
152
|
-
// 导入数据验证
|
|
153
|
-
const handleImportDataValidate = async rows => {
|
|
154
|
-
return shtable.value.validate(rows)
|
|
155
|
-
}
|
|
156
|
-
// 下载导入模板按钮
|
|
157
|
-
const handleDownloadTemplateBtn = () => {
|
|
158
|
-
const parentTable = shtable.value.tableRef.value
|
|
159
|
-
if (typeof props.downloadTemplateCallback === 'function') {
|
|
160
|
-
props.downloadTemplateCallback()
|
|
161
|
-
}
|
|
162
|
-
let defaultOption = {
|
|
163
|
-
filename: '模板',
|
|
164
|
-
sheetName: props.globalConfig?.filename || '模板',
|
|
165
|
-
type: 'xlsx',
|
|
166
|
-
original: false,
|
|
167
|
-
download: true,
|
|
168
|
-
message: false,
|
|
169
|
-
useStyle: false,
|
|
170
|
-
columns: parentTable.getColumns().filter(item => item.type !== 'seq'),
|
|
171
|
-
data: [],
|
|
172
|
-
afterExportMethod: options => {
|
|
173
|
-
handleDownloadTemplateFinished(options)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
parentTable.exportData(Object.assign(defaultOption, props.importConfig))
|
|
177
|
-
}
|
|
178
|
-
// 下载导入模板成功回调
|
|
179
|
-
const handleDownloadTemplateFinished = options => {
|
|
180
|
-
if (typeof props.downloadTemplateFinished === 'function') {
|
|
181
|
-
props.downloadTemplateFinished(options)
|
|
182
|
-
}
|
|
183
|
-
proxy.msgsuccess('下载模板完成!')
|
|
184
|
-
}
|
|
185
|
-
// 选择导入文件按钮
|
|
186
|
-
const handleImportFileBtn = async () => {
|
|
187
|
-
try {
|
|
188
|
-
const { file } = await $vTable.readFile({
|
|
189
|
-
types: ['xlsx', 'xls']
|
|
190
|
-
})
|
|
191
|
-
if (typeof props.importFileCallback === 'function') {
|
|
192
|
-
props.importFileCallback(file)
|
|
193
|
-
}
|
|
194
|
-
let fileRes = await readFile(file)
|
|
195
|
-
let { results, errDatas } = dealImportTableData(tableColumns.value, fileRes)
|
|
196
|
-
importTableData.value = results
|
|
197
|
-
importErrorData.value = errDatas
|
|
198
|
-
setTimeout(() => {
|
|
199
|
-
showProgress.value = false
|
|
200
|
-
}, 2000)
|
|
201
|
-
if (typeof props.importFileFinished === 'function') {
|
|
202
|
-
props.importFileFinished(options)
|
|
203
|
-
}
|
|
204
|
-
proxy.msgsuccess('导入完成!')
|
|
205
|
-
} catch (e) {
|
|
206
|
-
proxy.msgerror(e.message || e)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// 读取文件
|
|
210
|
-
const readFile = async file => {
|
|
211
|
-
return new Promise((resolve, reject) => {
|
|
212
|
-
const reader = new FileReader()
|
|
213
|
-
reader.readAsArrayBuffer(file)
|
|
214
|
-
reader.onloadstart = e => {
|
|
215
|
-
modalLoading.value = true
|
|
216
|
-
showProgress.value = true
|
|
217
|
-
}
|
|
218
|
-
reader.onprogress = e => {
|
|
219
|
-
progressPercent.value = Math.round((e.loaded / e.total) * 100)
|
|
220
|
-
}
|
|
221
|
-
reader.onerror = e => {
|
|
222
|
-
reject(new Error('文件读取出错'))
|
|
223
|
-
}
|
|
224
|
-
reader.onload = e => {
|
|
225
|
-
modalLoading.value = false
|
|
226
|
-
const workbook = new ExcelJS.Workbook()
|
|
227
|
-
const data = e.target.result
|
|
228
|
-
workbook.xlsx.load(data).then(wb => {
|
|
229
|
-
const firstSheet = wb.worksheets[0]
|
|
230
|
-
if (!firstSheet) {
|
|
231
|
-
reject(new Error('文件读取出错'))
|
|
232
|
-
}
|
|
233
|
-
let headerRowCount = 1
|
|
234
|
-
if (firstSheet.views?.length) {
|
|
235
|
-
const frozenViews = firstSheet.views.filter(vm => vm.state === 'frozen')
|
|
236
|
-
if (frozenViews.length) headerRowCount = frozenViews[0].ySplit
|
|
237
|
-
}
|
|
238
|
-
const sheetValues = firstSheet.getSheetValues()
|
|
239
|
-
const dataValues = sheetValues.filter(a => !!a).slice(headerRowCount)
|
|
240
|
-
resolve(dataValues)
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
// 根据表格读取数据生成表格数据
|
|
246
|
-
const dealImportTableData = (columns, records) => {
|
|
247
|
-
let fields = generateColumnsAll(columns)
|
|
248
|
-
let errDatas = []
|
|
249
|
-
let results = records.map((row, rowIndex) => {
|
|
250
|
-
const item = {}
|
|
251
|
-
row.forEach((cellValue, cIndex) => {
|
|
252
|
-
let column = fields[cIndex - 1]
|
|
253
|
-
const { field, title, renderName, renderProps } = column
|
|
254
|
-
let fieldValue = cellValue
|
|
255
|
-
let split = renderProps?.split || ','
|
|
256
|
-
let valueKey = renderName === '$vTree' ? renderProps?.nodeKey || 'id' : 'value'
|
|
257
|
-
let labelKey = renderProps?.labelField || 'label'
|
|
258
|
-
switch (renderName) {
|
|
259
|
-
case '$vSelect':
|
|
260
|
-
case '$vSwitch':
|
|
261
|
-
case '$vCheckgroup':
|
|
262
|
-
case '$vRadiogroup':
|
|
263
|
-
case '$vTree':
|
|
264
|
-
if (cellValue && renderProps.options && Array.isArray(renderProps.options)) {
|
|
265
|
-
// 如果匹配到选择项则保留,否则删除掉不规范数据
|
|
266
|
-
// 判断导入值为数据源的key or value 去匹配
|
|
267
|
-
if (renderProps.multiple || ['$vSelect', '$vCheckgroup', '$vTree'].includes(renderName)) {
|
|
268
|
-
let oriArray = cellValue ? cellValue.split(split).filter(_ => _ && _ !== 'undefined') : []
|
|
269
|
-
let values = oriArray.map(orv => {
|
|
270
|
-
let opt = renderProps.options.find(opt => String(orv) === String(opt[valueKey]) || String(orv) === opt[labelKey])
|
|
271
|
-
return opt[valueKey]
|
|
272
|
-
})
|
|
273
|
-
fieldValue = values.join(split)
|
|
274
|
-
} else {
|
|
275
|
-
let opt = renderProps.options.find(opt => String(cellValue) === String(opt[valueKey]) || String(cellValue) === opt[labelKey])
|
|
276
|
-
fieldValue = opt[valueKey]
|
|
277
|
-
}
|
|
278
|
-
if (!fieldValue || fieldValue.includes(undefined)) {
|
|
279
|
-
errDatas.push({ rowIndex: rowIndex + 1, errMsg: `字段【${title}】不符合数据源规范,已被清空` })
|
|
280
|
-
fieldValue = ''
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
break
|
|
284
|
-
default:
|
|
285
|
-
break
|
|
286
|
-
}
|
|
287
|
-
$vUtils.set(item, field, fieldValue)
|
|
288
|
-
})
|
|
289
|
-
return item
|
|
290
|
-
})
|
|
291
|
-
return { results, errDatas }
|
|
292
|
-
}
|
|
293
|
-
// 生成全表头
|
|
294
|
-
const generateColumnsAll = (columns, resultColumns = []) => {
|
|
295
|
-
columns.forEach((column, index) => {
|
|
296
|
-
if (column.children && Array.isArray(column.children) && column.children.length) {
|
|
297
|
-
generateColumnsAll(column.children, resultColumns)
|
|
298
|
-
} else {
|
|
299
|
-
if (column.field) {
|
|
300
|
-
resultColumns.push({
|
|
301
|
-
title: column.title,
|
|
302
|
-
field: column.field,
|
|
303
|
-
renderName: column.renderName,
|
|
304
|
-
renderProps: column.renderProps
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
})
|
|
309
|
-
return resultColumns
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
onBeforeMount(() => {
|
|
313
|
-
initCreated()
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
shtable,
|
|
318
|
-
modalLoading,
|
|
319
|
-
showProgress,
|
|
320
|
-
progressPercent,
|
|
321
|
-
importErrorData,
|
|
322
|
-
tableConfig,
|
|
323
|
-
tableGlobalConfig,
|
|
324
|
-
onModalClose,
|
|
325
|
-
handleDownloadTemplateBtn,
|
|
326
|
-
handleImportFileBtn,
|
|
327
|
-
handleImportDataBtn
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
</script>
|
|
332
|
-
|
|
333
|
-
<style lang="scss" scoped>
|
|
334
|
-
.import-template-head {
|
|
335
|
-
+ .import-template-body,
|
|
336
|
-
+ .import-template-foot {
|
|
337
|
-
margin-top: 10px;
|
|
338
|
-
}
|
|
339
|
-
.floatright {
|
|
340
|
-
float: right;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
.import-template-body {
|
|
344
|
-
+ .import-template-foot,
|
|
345
|
-
+ .import-template-error {
|
|
346
|
-
margin-top: 10px;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
.import-template-foot {
|
|
350
|
-
display: flex;
|
|
351
|
-
align-items: center;
|
|
352
|
-
justify-content: space-between;
|
|
353
|
-
}
|
|
354
|
-
.import-template-error-list {
|
|
355
|
-
max-height: 300px;
|
|
356
|
-
overflow: auto;
|
|
357
|
-
font-size: 13px;
|
|
358
|
-
.error-item {
|
|
359
|
-
display: block;
|
|
360
|
-
margin-bottom: 5px;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<sh-modal v-bind="modalConfig" :loading="modalLoading" @close="onModalClose">
|
|
3
|
+
<div class="import-template-top">
|
|
4
|
+
<transition name="fade">
|
|
5
|
+
<sh-progress v-if="showProgress" :percent="progressPercent" :stroke-width="2">
|
|
6
|
+
<template v-if="progressPercent === 100"><span>上传成功</span></template>
|
|
7
|
+
</sh-progress>
|
|
8
|
+
</transition>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="import-template-head">
|
|
11
|
+
<sh-button :size="size" @click="handleDownloadTemplateBtn">下载导入模板</sh-button>
|
|
12
|
+
<sh-button :size="size" status="primary" @click="handleImportFileBtn">选择导入文件</sh-button>
|
|
13
|
+
<div class="floatright">
|
|
14
|
+
<sh-poptip v-if="importErrorData.length > 0" trigger="click" title="错误信息" placement="bottom-end">
|
|
15
|
+
<sh-button :size="size" status="danger">
|
|
16
|
+
共发现 <strong>{{ importErrorData.length }}</strong> 条不符合规范数据
|
|
17
|
+
</sh-button>
|
|
18
|
+
<template #content>
|
|
19
|
+
<div class="import-template-error-list">
|
|
20
|
+
<div v-for="(errData, errDataIndex) in importErrorData" :key="errDataIndex" class="error-item">
|
|
21
|
+
第 <strong class="red">{{ errData.rowIndex }}</strong> 行,错误:<span class="red">{{ errData.errMsg }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
</sh-poptip>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="import-template-body">
|
|
29
|
+
<sh-table ref="shtable" v-bind="tableConfig"></sh-table>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="import-template-foot">
|
|
32
|
+
<div class="foot-left"></div>
|
|
33
|
+
<div class="foot-right">
|
|
34
|
+
<sh-button :size="size" @click="onModalClose">取消</sh-button>
|
|
35
|
+
<sh-button :size="size" status="primary" @click="handleImportDataBtn('all')">全部导入</sh-button>
|
|
36
|
+
<sh-button v-if="tableGlobalConfig.selectType" :size="size" status="warning" @click="handleImportDataBtn('select')">导入选中</sh-button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</sh-modal>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
import { defineComponent, computed, getCurrentInstance, ref, onBeforeMount } from 'vue'
|
|
44
|
+
import * as ExcelJS from 'exceljs'
|
|
45
|
+
export default defineComponent({
|
|
46
|
+
name: 'TableImport',
|
|
47
|
+
props: {
|
|
48
|
+
modalConfig: {
|
|
49
|
+
type: Object,
|
|
50
|
+
default() {
|
|
51
|
+
return {}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
columns: {
|
|
55
|
+
type: Array
|
|
56
|
+
},
|
|
57
|
+
height: {
|
|
58
|
+
type: [Number, String],
|
|
59
|
+
default: 360
|
|
60
|
+
},
|
|
61
|
+
needValidate: {
|
|
62
|
+
type: Boolean
|
|
63
|
+
},
|
|
64
|
+
importRules: {
|
|
65
|
+
type: Object,
|
|
66
|
+
default() {
|
|
67
|
+
return {}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
importConfig: {
|
|
71
|
+
type: Object,
|
|
72
|
+
default() {
|
|
73
|
+
return {}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
globalConfig: {
|
|
77
|
+
type: Object,
|
|
78
|
+
default() {
|
|
79
|
+
return {}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
size: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: 'mini' // medium / small / mini
|
|
85
|
+
},
|
|
86
|
+
importFileCallback: {
|
|
87
|
+
type: Function
|
|
88
|
+
},
|
|
89
|
+
importFileFinished: {
|
|
90
|
+
type: Function
|
|
91
|
+
},
|
|
92
|
+
downloadTemplateCallback: {
|
|
93
|
+
type: Function
|
|
94
|
+
},
|
|
95
|
+
downloadTemplateFinished: {
|
|
96
|
+
type: Function
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
emits: ['confirm', 'cancel'],
|
|
100
|
+
setup(props, context) {
|
|
101
|
+
const { proxy } = getCurrentInstance()
|
|
102
|
+
const { $vUtils, $vTable } = proxy
|
|
103
|
+
const { emit, slots } = context
|
|
104
|
+
const shtable = ref()
|
|
105
|
+
|
|
106
|
+
const importTableData = ref([])
|
|
107
|
+
const importErrorData = ref([])
|
|
108
|
+
const modalLoading = ref(false)
|
|
109
|
+
const showProgress = ref(false)
|
|
110
|
+
const progressPercent = ref(0)
|
|
111
|
+
|
|
112
|
+
const tableGlobalConfig = computed(() => Object.assign({ seq: true, title: true, tableName: '导入预览' }, props.globalConfig))
|
|
113
|
+
const tableColumns = computed(() => props.columns.filter(item => !(['seq', 'checkbox', 'radio'].includes(item.type) || item.renderName === '$vGlobalOption')))
|
|
114
|
+
const tableConfig = computed(() => {
|
|
115
|
+
return {
|
|
116
|
+
height: props.height,
|
|
117
|
+
size: props.size,
|
|
118
|
+
editRules: props.importRules,
|
|
119
|
+
columns: tableColumns.value,
|
|
120
|
+
data: importTableData.value,
|
|
121
|
+
globalConfig: tableGlobalConfig.value
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
// 初始化
|
|
125
|
+
const initCreated = () => {
|
|
126
|
+
showProgress.value = false
|
|
127
|
+
progressPercent.value = 0
|
|
128
|
+
importTableData.value = []
|
|
129
|
+
}
|
|
130
|
+
// 弹窗关闭
|
|
131
|
+
const onModalClose = () => {
|
|
132
|
+
$vUtils.set(props.modalConfig, 'modelValue', false)
|
|
133
|
+
emit('cancel')
|
|
134
|
+
}
|
|
135
|
+
// 确认导入数据按钮
|
|
136
|
+
const handleImportDataBtn = async (type = 'all') => {
|
|
137
|
+
let importData = type === 'all' ? importTableData.value : shtable.value.getSelectionData()
|
|
138
|
+
if (!importData || !Array.isArray(importData) || importData.length < 1) {
|
|
139
|
+
proxy.msginfo('未导入数据')
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
if (props.needValidate) {
|
|
143
|
+
let validateErrMap = await handleImportDataValidate(importData)
|
|
144
|
+
if (validateErrMap) {
|
|
145
|
+
proxy.msgerror('导入校验失败,请检查数据')
|
|
146
|
+
return validateErrMap
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
emit('confirm', importData)
|
|
150
|
+
onModalClose()
|
|
151
|
+
}
|
|
152
|
+
// 导入数据验证
|
|
153
|
+
const handleImportDataValidate = async rows => {
|
|
154
|
+
return shtable.value.validate(rows)
|
|
155
|
+
}
|
|
156
|
+
// 下载导入模板按钮
|
|
157
|
+
const handleDownloadTemplateBtn = () => {
|
|
158
|
+
const parentTable = shtable.value.tableRef.value
|
|
159
|
+
if (typeof props.downloadTemplateCallback === 'function') {
|
|
160
|
+
props.downloadTemplateCallback()
|
|
161
|
+
}
|
|
162
|
+
let defaultOption = {
|
|
163
|
+
filename: '模板',
|
|
164
|
+
sheetName: props.globalConfig?.filename || '模板',
|
|
165
|
+
type: 'xlsx',
|
|
166
|
+
original: false,
|
|
167
|
+
download: true,
|
|
168
|
+
message: false,
|
|
169
|
+
useStyle: false,
|
|
170
|
+
columns: parentTable.getColumns().filter(item => item.type !== 'seq'),
|
|
171
|
+
data: [],
|
|
172
|
+
afterExportMethod: options => {
|
|
173
|
+
handleDownloadTemplateFinished(options)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
parentTable.exportData(Object.assign(defaultOption, props.importConfig))
|
|
177
|
+
}
|
|
178
|
+
// 下载导入模板成功回调
|
|
179
|
+
const handleDownloadTemplateFinished = options => {
|
|
180
|
+
if (typeof props.downloadTemplateFinished === 'function') {
|
|
181
|
+
props.downloadTemplateFinished(options)
|
|
182
|
+
}
|
|
183
|
+
proxy.msgsuccess('下载模板完成!')
|
|
184
|
+
}
|
|
185
|
+
// 选择导入文件按钮
|
|
186
|
+
const handleImportFileBtn = async () => {
|
|
187
|
+
try {
|
|
188
|
+
const { file } = await $vTable.readFile({
|
|
189
|
+
types: ['xlsx', 'xls']
|
|
190
|
+
})
|
|
191
|
+
if (typeof props.importFileCallback === 'function') {
|
|
192
|
+
props.importFileCallback(file)
|
|
193
|
+
}
|
|
194
|
+
let fileRes = await readFile(file)
|
|
195
|
+
let { results, errDatas } = dealImportTableData(tableColumns.value, fileRes)
|
|
196
|
+
importTableData.value = results
|
|
197
|
+
importErrorData.value = errDatas
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
showProgress.value = false
|
|
200
|
+
}, 2000)
|
|
201
|
+
if (typeof props.importFileFinished === 'function') {
|
|
202
|
+
props.importFileFinished(options)
|
|
203
|
+
}
|
|
204
|
+
proxy.msgsuccess('导入完成!')
|
|
205
|
+
} catch (e) {
|
|
206
|
+
proxy.msgerror(e.message || e)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 读取文件
|
|
210
|
+
const readFile = async file => {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const reader = new FileReader()
|
|
213
|
+
reader.readAsArrayBuffer(file)
|
|
214
|
+
reader.onloadstart = e => {
|
|
215
|
+
modalLoading.value = true
|
|
216
|
+
showProgress.value = true
|
|
217
|
+
}
|
|
218
|
+
reader.onprogress = e => {
|
|
219
|
+
progressPercent.value = Math.round((e.loaded / e.total) * 100)
|
|
220
|
+
}
|
|
221
|
+
reader.onerror = e => {
|
|
222
|
+
reject(new Error('文件读取出错'))
|
|
223
|
+
}
|
|
224
|
+
reader.onload = e => {
|
|
225
|
+
modalLoading.value = false
|
|
226
|
+
const workbook = new ExcelJS.Workbook()
|
|
227
|
+
const data = e.target.result
|
|
228
|
+
workbook.xlsx.load(data).then(wb => {
|
|
229
|
+
const firstSheet = wb.worksheets[0]
|
|
230
|
+
if (!firstSheet) {
|
|
231
|
+
reject(new Error('文件读取出错'))
|
|
232
|
+
}
|
|
233
|
+
let headerRowCount = 1
|
|
234
|
+
if (firstSheet.views?.length) {
|
|
235
|
+
const frozenViews = firstSheet.views.filter(vm => vm.state === 'frozen')
|
|
236
|
+
if (frozenViews.length) headerRowCount = frozenViews[0].ySplit
|
|
237
|
+
}
|
|
238
|
+
const sheetValues = firstSheet.getSheetValues()
|
|
239
|
+
const dataValues = sheetValues.filter(a => !!a).slice(headerRowCount)
|
|
240
|
+
resolve(dataValues)
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
// 根据表格读取数据生成表格数据
|
|
246
|
+
const dealImportTableData = (columns, records) => {
|
|
247
|
+
let fields = generateColumnsAll(columns)
|
|
248
|
+
let errDatas = []
|
|
249
|
+
let results = records.map((row, rowIndex) => {
|
|
250
|
+
const item = {}
|
|
251
|
+
row.forEach((cellValue, cIndex) => {
|
|
252
|
+
let column = fields[cIndex - 1]
|
|
253
|
+
const { field, title, renderName, renderProps } = column
|
|
254
|
+
let fieldValue = cellValue
|
|
255
|
+
let split = renderProps?.split || ','
|
|
256
|
+
let valueKey = renderName === '$vTree' ? renderProps?.nodeKey || 'id' : 'value'
|
|
257
|
+
let labelKey = renderProps?.labelField || 'label'
|
|
258
|
+
switch (renderName) {
|
|
259
|
+
case '$vSelect':
|
|
260
|
+
case '$vSwitch':
|
|
261
|
+
case '$vCheckgroup':
|
|
262
|
+
case '$vRadiogroup':
|
|
263
|
+
case '$vTree':
|
|
264
|
+
if (cellValue && renderProps.options && Array.isArray(renderProps.options)) {
|
|
265
|
+
// 如果匹配到选择项则保留,否则删除掉不规范数据
|
|
266
|
+
// 判断导入值为数据源的key or value 去匹配
|
|
267
|
+
if (renderProps.multiple || ['$vSelect', '$vCheckgroup', '$vTree'].includes(renderName)) {
|
|
268
|
+
let oriArray = cellValue ? cellValue.split(split).filter(_ => _ && _ !== 'undefined') : []
|
|
269
|
+
let values = oriArray.map(orv => {
|
|
270
|
+
let opt = renderProps.options.find(opt => String(orv) === String(opt[valueKey]) || String(orv) === opt[labelKey])
|
|
271
|
+
return opt[valueKey]
|
|
272
|
+
})
|
|
273
|
+
fieldValue = values.join(split)
|
|
274
|
+
} else {
|
|
275
|
+
let opt = renderProps.options.find(opt => String(cellValue) === String(opt[valueKey]) || String(cellValue) === opt[labelKey])
|
|
276
|
+
fieldValue = opt[valueKey]
|
|
277
|
+
}
|
|
278
|
+
if (!fieldValue || fieldValue.includes(undefined)) {
|
|
279
|
+
errDatas.push({ rowIndex: rowIndex + 1, errMsg: `字段【${title}】不符合数据源规范,已被清空` })
|
|
280
|
+
fieldValue = ''
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
default:
|
|
285
|
+
break
|
|
286
|
+
}
|
|
287
|
+
$vUtils.set(item, field, fieldValue)
|
|
288
|
+
})
|
|
289
|
+
return item
|
|
290
|
+
})
|
|
291
|
+
return { results, errDatas }
|
|
292
|
+
}
|
|
293
|
+
// 生成全表头
|
|
294
|
+
const generateColumnsAll = (columns, resultColumns = []) => {
|
|
295
|
+
columns.forEach((column, index) => {
|
|
296
|
+
if (column.children && Array.isArray(column.children) && column.children.length) {
|
|
297
|
+
generateColumnsAll(column.children, resultColumns)
|
|
298
|
+
} else {
|
|
299
|
+
if (column.field) {
|
|
300
|
+
resultColumns.push({
|
|
301
|
+
title: column.title,
|
|
302
|
+
field: column.field,
|
|
303
|
+
renderName: column.renderName,
|
|
304
|
+
renderProps: column.renderProps
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
return resultColumns
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
onBeforeMount(() => {
|
|
313
|
+
initCreated()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
shtable,
|
|
318
|
+
modalLoading,
|
|
319
|
+
showProgress,
|
|
320
|
+
progressPercent,
|
|
321
|
+
importErrorData,
|
|
322
|
+
tableConfig,
|
|
323
|
+
tableGlobalConfig,
|
|
324
|
+
onModalClose,
|
|
325
|
+
handleDownloadTemplateBtn,
|
|
326
|
+
handleImportFileBtn,
|
|
327
|
+
handleImportDataBtn
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
</script>
|
|
332
|
+
|
|
333
|
+
<style lang="scss" scoped>
|
|
334
|
+
.import-template-head {
|
|
335
|
+
+ .import-template-body,
|
|
336
|
+
+ .import-template-foot {
|
|
337
|
+
margin-top: 10px;
|
|
338
|
+
}
|
|
339
|
+
.floatright {
|
|
340
|
+
float: right;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
.import-template-body {
|
|
344
|
+
+ .import-template-foot,
|
|
345
|
+
+ .import-template-error {
|
|
346
|
+
margin-top: 10px;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
.import-template-foot {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: space-between;
|
|
353
|
+
}
|
|
354
|
+
.import-template-error-list {
|
|
355
|
+
max-height: 300px;
|
|
356
|
+
overflow: auto;
|
|
357
|
+
font-size: 13px;
|
|
358
|
+
.error-item {
|
|
359
|
+
display: block;
|
|
360
|
+
margin-bottom: 5px;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
</style>
|