t20-common-lib 0.13.2 → 0.14.0
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 +4 -2
- package/packages/file-import/index.js +8 -0
- package/packages/file-import/src/Upload/uploadMsg.vue +212 -0
- package/packages/file-import/src/main.vue +328 -0
- package/src/index.js +4 -1
- package/src/utils/importGlobal.js +16 -0
- package/src/utils/writeImportErrorExcel.js +133 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "t20-common-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "T20",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"core-js": "^2.6.12",
|
|
26
26
|
"dayjs": "^1.11.18",
|
|
27
|
+
"exceljs": "^4.4.0",
|
|
27
28
|
"lodash-es": "^4.17.21",
|
|
28
|
-
"normalize.css": "^8.0.1"
|
|
29
|
+
"normalize.css": "^8.0.1",
|
|
30
|
+
"xlsx": "^0.18.5"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@babel/core": "^7.28.4",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
v-drag
|
|
4
|
+
style="margin-bottom: 0; margin-top: 30px"
|
|
5
|
+
:width="width"
|
|
6
|
+
:visible.sync="progressV"
|
|
7
|
+
:title="title"
|
|
8
|
+
append-to-body
|
|
9
|
+
:close-on-click-modal="false"
|
|
10
|
+
:close-on-press-escape="false"
|
|
11
|
+
@close="handleClose"
|
|
12
|
+
>
|
|
13
|
+
<div style="min-height: 28px">
|
|
14
|
+
<template v-if="!hidePercent">
|
|
15
|
+
<p v-if="!percentType">系统处理中,请稍等......</p>
|
|
16
|
+
<div v-else>
|
|
17
|
+
<div v-if="validateResult" class="m-b">
|
|
18
|
+
<span class="m-r-m">导入文件</span>
|
|
19
|
+
{{ validateResult.name }}
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex-box">
|
|
22
|
+
<div class="m-r-m">导入进度</div>
|
|
23
|
+
<div class="flex-item">
|
|
24
|
+
<el-progress
|
|
25
|
+
class="m-b"
|
|
26
|
+
:percentage="percent"
|
|
27
|
+
:status="percentType !== 'loading' ? percentType : undefined"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
<template v-if="validateResult">
|
|
34
|
+
<div class="m-b">
|
|
35
|
+
<span class="m-r">导入统计</span>
|
|
36
|
+
<span>共计导入{{ validateResult.totalNum }}条,</span>
|
|
37
|
+
<span class="m-r-s">
|
|
38
|
+
其中有效数据
|
|
39
|
+
<span class="color-success">{{ validateResult.successNum }}</span>
|
|
40
|
+
条,
|
|
41
|
+
</span>
|
|
42
|
+
<span>
|
|
43
|
+
无效数据
|
|
44
|
+
<span class="color-danger">{{ validateResult.errorList.length }}</span>
|
|
45
|
+
条。
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
<template v-if="validateResult.errorList.length">
|
|
49
|
+
<div class="bd-a">
|
|
50
|
+
<div class="flex-box flex-lr flex-v m-t m-b p-l p-r">
|
|
51
|
+
<span>无效数据详情</span>
|
|
52
|
+
<el-button v-if="showErrorExport" type="text" @click="importError">导出错误数据</el-button>
|
|
53
|
+
</div>
|
|
54
|
+
<N20-table-pro
|
|
55
|
+
ref="vTable"
|
|
56
|
+
:data="errorListC"
|
|
57
|
+
:columns="columnsList"
|
|
58
|
+
auto-resize
|
|
59
|
+
:height="'300px'"
|
|
60
|
+
:clearSelect="false"
|
|
61
|
+
:rowConfig="{ keyField: 'id' }"
|
|
62
|
+
:checkboxConfig="{ reserve: true }"
|
|
63
|
+
:resizable-config="{ isAllColumnDrag: true, minWidth: '0px' }"
|
|
64
|
+
headerAlign="center"
|
|
65
|
+
/>
|
|
66
|
+
<div v-if="pagination" class="flex-box flex-r m-t-ss m-b-ss">
|
|
67
|
+
<Pagination
|
|
68
|
+
:page-obj="page"
|
|
69
|
+
:page-key="{ no: 'current', size: 'pageSize', total: 'total' }"
|
|
70
|
+
@change="getList"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="color-warning m-t-s">上述数据输入有误,请修改导入文件中相关信息!</div>
|
|
75
|
+
</template>
|
|
76
|
+
</template>
|
|
77
|
+
</div>
|
|
78
|
+
<div slot="footer" style="height: 32px">
|
|
79
|
+
<template v-if="footerBtn">
|
|
80
|
+
<el-button
|
|
81
|
+
v-if="validateResult && validateResult.successNum > 0"
|
|
82
|
+
type="primary"
|
|
83
|
+
@click="
|
|
84
|
+
() => {
|
|
85
|
+
footerBtn.confirm.click()
|
|
86
|
+
progressV = false
|
|
87
|
+
}
|
|
88
|
+
"
|
|
89
|
+
>
|
|
90
|
+
{{ footerBtn.confirm.text | $lc }}
|
|
91
|
+
</el-button>
|
|
92
|
+
<el-button
|
|
93
|
+
plain
|
|
94
|
+
@click="
|
|
95
|
+
() => {
|
|
96
|
+
footerBtn.cancel.click()
|
|
97
|
+
progressV = false
|
|
98
|
+
}
|
|
99
|
+
"
|
|
100
|
+
>
|
|
101
|
+
{{ footerBtn.cancel.text | $lc }}
|
|
102
|
+
</el-button>
|
|
103
|
+
</template>
|
|
104
|
+
<el-button v-else-if="validateResult || percentType === 'success'" type="primary" @click="confirmFn">
|
|
105
|
+
确认
|
|
106
|
+
</el-button>
|
|
107
|
+
</div>
|
|
108
|
+
</el-dialog>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
import cloneDeep from 'lodash-es/cloneDeep'
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
name: 'UploadMsg',
|
|
116
|
+
props: {
|
|
117
|
+
title: { type: String, default: '提示' },
|
|
118
|
+
footerBtn: { type: Object, default: () => ({}) },
|
|
119
|
+
visible: { type: Boolean, default: false },
|
|
120
|
+
width: { type: String, default: '60%' },
|
|
121
|
+
percentType: { type: String, default: undefined },
|
|
122
|
+
percentMsg: { type: String, default: undefined },
|
|
123
|
+
pagination: { type: Boolean, default: true },
|
|
124
|
+
percent: { type: Number, default: undefined },
|
|
125
|
+
validateResult: { type: Object, default: undefined },
|
|
126
|
+
validateConfirm: { type: Function, default: undefined },
|
|
127
|
+
hidePercent: { type: Boolean, default: false },
|
|
128
|
+
showErrorExport: { type: Boolean, default: false }
|
|
129
|
+
},
|
|
130
|
+
data() {
|
|
131
|
+
return {
|
|
132
|
+
tableData: [],
|
|
133
|
+
errorListC: [],
|
|
134
|
+
page: {
|
|
135
|
+
current: 1,
|
|
136
|
+
pageSize: 20,
|
|
137
|
+
total: 0
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
watch: {
|
|
142
|
+
errorList: {
|
|
143
|
+
handler() {
|
|
144
|
+
this.errorListC = this.errorList
|
|
145
|
+
this.page.total = this.tableData.length
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
page: {
|
|
149
|
+
handler(v) {
|
|
150
|
+
this.pageSize = v
|
|
151
|
+
},
|
|
152
|
+
deep: true
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
computed: {
|
|
156
|
+
progressV: {
|
|
157
|
+
get() {
|
|
158
|
+
return this.visible
|
|
159
|
+
},
|
|
160
|
+
set(v) {
|
|
161
|
+
this.$emit('update:visible', v)
|
|
162
|
+
return v
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
columnsList() {
|
|
166
|
+
return (this.validateResult && this.validateResult.columnsList) || []
|
|
167
|
+
},
|
|
168
|
+
errorList: {
|
|
169
|
+
get() {
|
|
170
|
+
const errorList = this.validateResult && this.validateResult.errorList
|
|
171
|
+
if (errorList) {
|
|
172
|
+
this.tableData = cloneDeep(errorList)
|
|
173
|
+
const startIndex = (this.page.current - 1) * this.page.pageSize
|
|
174
|
+
const endIndex = startIndex + this.page.pageSize
|
|
175
|
+
return this.pagination ? this.tableData.slice(startIndex, endIndex) : this.tableData
|
|
176
|
+
}
|
|
177
|
+
return undefined
|
|
178
|
+
},
|
|
179
|
+
set(value) {
|
|
180
|
+
return value
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
methods: {
|
|
185
|
+
importError() {
|
|
186
|
+
this.$emit('importError')
|
|
187
|
+
},
|
|
188
|
+
confirmFn() {
|
|
189
|
+
if (this.validateConfirm) {
|
|
190
|
+
this.validateConfirm(() => {
|
|
191
|
+
this.progressV = false
|
|
192
|
+
})
|
|
193
|
+
} else {
|
|
194
|
+
this.progressV = false
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
handleClose() {
|
|
198
|
+
this.progressV = false
|
|
199
|
+
},
|
|
200
|
+
getList() {
|
|
201
|
+
const startIndex = (this.page.current - 1) * this.page.pageSize
|
|
202
|
+
const endIndex = startIndex + this.page.pageSize
|
|
203
|
+
this.errorListC = this.tableData.slice(startIndex, endIndex)
|
|
204
|
+
this.$nextTick(() => {
|
|
205
|
+
if (this.$refs.vTable && this.$refs.vTable.doLayout) {
|
|
206
|
+
this.$refs.vTable.doLayout()
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
</script>
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dropdown class="file-import-dropdown" trigger="click" :placement="placement" @command="commandFn">
|
|
3
|
+
<slot name="import-btn">
|
|
4
|
+
<el-button size="mini" type="primary">{{ $t('gbebit_w_c_0103') }}</el-button>
|
|
5
|
+
</slot>
|
|
6
|
+
<el-dropdown-menu slot="dropdown">
|
|
7
|
+
<el-dropdown-item command="import">模版导入</el-dropdown-item>
|
|
8
|
+
<el-dropdown-item command="down">模版下载</el-dropdown-item>
|
|
9
|
+
</el-dropdown-menu>
|
|
10
|
+
<div style="display: none">
|
|
11
|
+
<uploadMsg
|
|
12
|
+
:pagination="pagination"
|
|
13
|
+
:title="$t('导入文件')"
|
|
14
|
+
:visible.sync="errorV"
|
|
15
|
+
:width="width"
|
|
16
|
+
:show-error-export="showErrorExport"
|
|
17
|
+
:validate-result="validateResult"
|
|
18
|
+
:hide-percent="hidePercent"
|
|
19
|
+
:percentType="percentType"
|
|
20
|
+
:percent="percent"
|
|
21
|
+
:footer-btn="footer"
|
|
22
|
+
:validate-confirm="validateConfirm"
|
|
23
|
+
@importError="importError"
|
|
24
|
+
>
|
|
25
|
+
<template #error>
|
|
26
|
+
<slot name="error"></slot>
|
|
27
|
+
</template>
|
|
28
|
+
</uploadMsg>
|
|
29
|
+
</div>
|
|
30
|
+
</el-dropdown>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
import XLSX from 'xlsx'
|
|
35
|
+
import uploadMsg from './Upload/uploadMsg.vue'
|
|
36
|
+
import writeImportErrorExcel from '@/utils/writeImportErrorExcel'
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
name: 'FileImport',
|
|
40
|
+
components: { uploadMsg },
|
|
41
|
+
props: {
|
|
42
|
+
templateUrl: { type: String, default: '' },
|
|
43
|
+
templateOpt: { type: [Object, Array], default: undefined },
|
|
44
|
+
width: { type: String, default: '60%' },
|
|
45
|
+
fileType: { type: String, default: '.xlsx,.xls,.csv' },
|
|
46
|
+
fileName: { type: String, default: '下载.xlsx' },
|
|
47
|
+
uploadHttpRequest: { type: Function, default: undefined, required: true },
|
|
48
|
+
customTemplateDownload: { type: Function, default: undefined },
|
|
49
|
+
validateResult: { type: Object, default: undefined },
|
|
50
|
+
pagination: { type: Boolean, default: false },
|
|
51
|
+
showErrorExport: { type: Boolean, default: false },
|
|
52
|
+
validateDialog: { type: Boolean, default: false },
|
|
53
|
+
validateConfirm: { type: Function, default: undefined },
|
|
54
|
+
footer: { type: Object, default: undefined },
|
|
55
|
+
uploadOn: { type: Object, default: undefined },
|
|
56
|
+
hidePercent: { type: Boolean, default: false },
|
|
57
|
+
placement: { type: String, default: 'bottom-start' },
|
|
58
|
+
percentType: { type: String, default: 'loading' },
|
|
59
|
+
percent: { type: Number, default: 0 },
|
|
60
|
+
errorExportMode: { type: String, default: 'frontend' },
|
|
61
|
+
errorExportOptions: { type: Object, default: () => ({}) }
|
|
62
|
+
},
|
|
63
|
+
data() {
|
|
64
|
+
return {
|
|
65
|
+
currentFile: null
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
computed: {
|
|
69
|
+
errorV: {
|
|
70
|
+
get() {
|
|
71
|
+
return this.validateDialog
|
|
72
|
+
},
|
|
73
|
+
set(v) {
|
|
74
|
+
this.$emit('update:validateDialog', v)
|
|
75
|
+
return v
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
methods: {
|
|
80
|
+
getDefaultErrorExportFileName() {
|
|
81
|
+
const baseName = String(this.fileName || '导入文件')
|
|
82
|
+
const pureName = baseName.replace(/\.(xlsx|xls|csv)$/i, '')
|
|
83
|
+
return `${pureName}-错误数据.xlsx`
|
|
84
|
+
},
|
|
85
|
+
getErrorExportParams() {
|
|
86
|
+
return {
|
|
87
|
+
validateResult: this.validateResult,
|
|
88
|
+
file: this.currentFile,
|
|
89
|
+
startRow: 3,
|
|
90
|
+
errorValSuffix: '',
|
|
91
|
+
rowHeight: 15,
|
|
92
|
+
downloadFileName: this.getDefaultErrorExportFileName(),
|
|
93
|
+
noteTextFormatter: ({ label, prop, row }) => `${label}: ${row[prop]}`,
|
|
94
|
+
...(this.errorExportOptions || {})
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
toInt(value, fallback) {
|
|
98
|
+
const num = Number(value)
|
|
99
|
+
return Number.isInteger(num) ? num : fallback
|
|
100
|
+
},
|
|
101
|
+
getTemplateSheetConfigs() {
|
|
102
|
+
const opt = this.templateOpt
|
|
103
|
+
if (!opt) return []
|
|
104
|
+
if (Array.isArray(opt)) {
|
|
105
|
+
return opt.filter(item => item && item.sheetName && item.columns)
|
|
106
|
+
}
|
|
107
|
+
if (opt.sheetName && opt.columns) {
|
|
108
|
+
return [opt]
|
|
109
|
+
}
|
|
110
|
+
if (opt.sheets && typeof opt.sheets === 'object') {
|
|
111
|
+
return Object.keys(opt.sheets)
|
|
112
|
+
.map(name => ({ sheetName: name, ...(opt.sheets[name] || {}) }))
|
|
113
|
+
.filter(item => item.columns)
|
|
114
|
+
}
|
|
115
|
+
return []
|
|
116
|
+
},
|
|
117
|
+
getSheetParseOption(sheetName) {
|
|
118
|
+
const option = this.templateOpt || {}
|
|
119
|
+
const configs = this.getTemplateSheetConfigs()
|
|
120
|
+
const fromConfig = configs.find(c => c.sheetName === sheetName) || {}
|
|
121
|
+
const sheetOption = {
|
|
122
|
+
...fromConfig,
|
|
123
|
+
...((option.sheets && option.sheets[sheetName]) || {}),
|
|
124
|
+
...(option[sheetName] || {})
|
|
125
|
+
}
|
|
126
|
+
const headerRows = this.toInt(
|
|
127
|
+
sheetOption.headerRows ?? sheetOption.headerDepth ?? sheetOption.headerLevel ?? option.headerRows ?? option.headerDepth ?? option.headerLevel,
|
|
128
|
+
1
|
|
129
|
+
)
|
|
130
|
+
const headerRowIndex = this.toInt(sheetOption.headerRowIndex ?? option.headerRowIndex, headerRows - 1)
|
|
131
|
+
const dataStartRow = this.toInt(
|
|
132
|
+
sheetOption.dataStartRow ?? sheetOption.startDataRow ?? option.dataStartRow ?? option.startDataRow,
|
|
133
|
+
headerRowIndex + 1
|
|
134
|
+
)
|
|
135
|
+
return {
|
|
136
|
+
headerRowIndex: Math.max(headerRowIndex, 0),
|
|
137
|
+
dataStartRow: Math.max(dataStartRow, 0)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
parseSheetToLeafHeaderData(worksheet, parseOption) {
|
|
141
|
+
const matrix = XLSX.utils.sheet_to_json(worksheet, {
|
|
142
|
+
header: 1,
|
|
143
|
+
raw: true,
|
|
144
|
+
defval: ''
|
|
145
|
+
})
|
|
146
|
+
if (!matrix.length) {
|
|
147
|
+
return { headers: [], rows: [] }
|
|
148
|
+
}
|
|
149
|
+
const { headerRowIndex, dataStartRow } = parseOption
|
|
150
|
+
const maxColLen = matrix.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0)
|
|
151
|
+
const headers = []
|
|
152
|
+
|
|
153
|
+
for (let colIndex = 0; colIndex < maxColLen; colIndex++) {
|
|
154
|
+
let leafHeader = ''
|
|
155
|
+
for (let rowIndex = Math.min(headerRowIndex, matrix.length - 1); rowIndex >= 0; rowIndex--) {
|
|
156
|
+
const cell = matrix[rowIndex] && matrix[rowIndex][colIndex]
|
|
157
|
+
const normalized = cell === undefined || cell === null ? '' : String(cell).trim()
|
|
158
|
+
if (normalized) {
|
|
159
|
+
leafHeader = normalized
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
headers.push(leafHeader)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const rows = matrix
|
|
167
|
+
.slice(Math.min(dataStartRow, matrix.length))
|
|
168
|
+
.map(row => headers.map((_, colIndex) => (Array.isArray(row) ? row[colIndex] : '')))
|
|
169
|
+
.filter(row => row.some(cell => cell !== '' && cell !== null && cell !== undefined))
|
|
170
|
+
|
|
171
|
+
return { headers, rows }
|
|
172
|
+
},
|
|
173
|
+
normalizeHeaderLabel(text) {
|
|
174
|
+
return String(text == null ? '' : text)
|
|
175
|
+
.replace(/\u00a0/g, ' ')
|
|
176
|
+
.trim()
|
|
177
|
+
.replace(/^\*\s*/, '')
|
|
178
|
+
.trim()
|
|
179
|
+
},
|
|
180
|
+
flattenLeafColumns(columns) {
|
|
181
|
+
if (!Array.isArray(columns) || !columns.length) {
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
184
|
+
const out = []
|
|
185
|
+
columns.forEach(col => {
|
|
186
|
+
if (!col) return
|
|
187
|
+
if (col.children && col.children.length) {
|
|
188
|
+
out.push(...this.flattenLeafColumns(col.children))
|
|
189
|
+
} else if (col.prop) {
|
|
190
|
+
out.push(col)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
return out
|
|
194
|
+
},
|
|
195
|
+
expectedHeaderForColumn(col) {
|
|
196
|
+
const label = String(col.label == null ? '' : col.label).trim()
|
|
197
|
+
const required = col.required === true
|
|
198
|
+
return required ? `* ${label}` : label
|
|
199
|
+
},
|
|
200
|
+
buildSheetPayload(headers, rows, columnTree) {
|
|
201
|
+
const leafCols = this.flattenLeafColumns(columnTree)
|
|
202
|
+
const indexByProp = {}
|
|
203
|
+
const templateKeyByProp = {}
|
|
204
|
+
leafCols.forEach(col => {
|
|
205
|
+
const target = this.normalizeHeaderLabel(this.expectedHeaderForColumn(col))
|
|
206
|
+
const idx = headers.findIndex(h => this.normalizeHeaderLabel(h) === target)
|
|
207
|
+
if (idx !== -1) {
|
|
208
|
+
indexByProp[col.prop] = idx
|
|
209
|
+
templateKeyByProp[col.prop] = headers[idx]
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
const templateData = []
|
|
213
|
+
const tableData = []
|
|
214
|
+
rows.forEach(row => {
|
|
215
|
+
const tRow = {}
|
|
216
|
+
const pRow = {}
|
|
217
|
+
leafCols.forEach(col => {
|
|
218
|
+
const idx = indexByProp[col.prop]
|
|
219
|
+
if (idx === undefined) return
|
|
220
|
+
const val = Array.isArray(row) ? row[idx] : ''
|
|
221
|
+
const tk = templateKeyByProp[col.prop]
|
|
222
|
+
if (tk !== undefined && tk !== '') {
|
|
223
|
+
tRow[tk] = val
|
|
224
|
+
}
|
|
225
|
+
pRow[col.prop] = val
|
|
226
|
+
})
|
|
227
|
+
templateData.push(tRow)
|
|
228
|
+
tableData.push(pRow)
|
|
229
|
+
})
|
|
230
|
+
return {
|
|
231
|
+
headers,
|
|
232
|
+
templateData,
|
|
233
|
+
tableData
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
importError() {
|
|
237
|
+
if (this.errorExportMode === 'backend') {
|
|
238
|
+
this.$emit('importError', {
|
|
239
|
+
validateResult: this.validateResult,
|
|
240
|
+
file: this.currentFile
|
|
241
|
+
})
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
writeImportErrorExcel(this.getErrorExportParams()).catch(error => {
|
|
245
|
+
console.error('writeImportErrorExcel failed:', error)
|
|
246
|
+
this.$message.error(this.$l('导入模板解析失败,请检查模板内容后重试'))
|
|
247
|
+
})
|
|
248
|
+
},
|
|
249
|
+
commandFn(val) {
|
|
250
|
+
if (val === 'import') this.importFn()
|
|
251
|
+
if (val === 'down') this.downFn()
|
|
252
|
+
},
|
|
253
|
+
importFn() {
|
|
254
|
+
this.$emit('update:validateResult', undefined)
|
|
255
|
+
let input = document.createElement('input')
|
|
256
|
+
input.type = 'file'
|
|
257
|
+
input.accept = this.fileType
|
|
258
|
+
input.addEventListener('change', event => {
|
|
259
|
+
this.generateData(event.target.files[0])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
input.click()
|
|
263
|
+
this.$nextTick(() => {
|
|
264
|
+
input = undefined
|
|
265
|
+
})
|
|
266
|
+
},
|
|
267
|
+
generateData(rawFile) {
|
|
268
|
+
if (!rawFile) return
|
|
269
|
+
this.currentFile = rawFile
|
|
270
|
+
const uploadFileName = rawFile.name || ''
|
|
271
|
+
const reader = new FileReader()
|
|
272
|
+
reader.onload = async e => {
|
|
273
|
+
const data = e.target.result
|
|
274
|
+
const workbook = XLSX.read(data, {
|
|
275
|
+
type: 'array',
|
|
276
|
+
raw: true,
|
|
277
|
+
cellHTML: true,
|
|
278
|
+
cellText: true,
|
|
279
|
+
bookFiles: true,
|
|
280
|
+
cellFormula: true,
|
|
281
|
+
cellDates: true
|
|
282
|
+
})
|
|
283
|
+
const worksheets = workbook.Sheets
|
|
284
|
+
const sheetsMap = {}
|
|
285
|
+
Object.keys(worksheets).forEach(sheet => {
|
|
286
|
+
const parseOption = this.getSheetParseOption(sheet)
|
|
287
|
+
sheetsMap[sheet] = this.parseSheetToLeafHeaderData(worksheets[sheet], parseOption)
|
|
288
|
+
})
|
|
289
|
+
const configs = this.getTemplateSheetConfigs()
|
|
290
|
+
if (configs.length) {
|
|
291
|
+
const res = {}
|
|
292
|
+
configs.forEach(cfg => {
|
|
293
|
+
const parsed = sheetsMap[cfg.sheetName]
|
|
294
|
+
if (!parsed) {
|
|
295
|
+
res[cfg.sheetName] = { headers: [], templateData: [], tableData: [] }
|
|
296
|
+
} else {
|
|
297
|
+
res[cfg.sheetName] = this.buildSheetPayload(parsed.headers, parsed.rows, cfg.columns)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
this.uploadHttpRequest({ ...res, fileName: uploadFileName, file: rawFile })
|
|
301
|
+
} else {
|
|
302
|
+
this.uploadHttpRequest({ ...sheetsMap, fileName: uploadFileName, file: rawFile })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
reader.readAsArrayBuffer(rawFile)
|
|
306
|
+
},
|
|
307
|
+
downFn() {
|
|
308
|
+
if (this.customTemplateDownload) {
|
|
309
|
+
this.customTemplateDownload()
|
|
310
|
+
} else {
|
|
311
|
+
this.$axios
|
|
312
|
+
.get(this.templateUrl, null, {
|
|
313
|
+
responseType: 'blob'
|
|
314
|
+
})
|
|
315
|
+
.then(blob => {
|
|
316
|
+
this.$downloadBlob(blob, this.fileName)
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<style scoped>
|
|
325
|
+
.file-import-dropdown ::v-deep .el-dropdown-menu__item {
|
|
326
|
+
padding: 0 10px;
|
|
327
|
+
}
|
|
328
|
+
</style>
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import DynamicDescriptions from '../packages/dynamic-descriptions/index.js'
|
|
|
20
20
|
import SelectTreeUnit from '../packages/select-tree-unit/index.js'
|
|
21
21
|
import SelectTreeUnitForm from '../packages/select-tree-unit-form/index.js'
|
|
22
22
|
import ScrollLoadSelect from '../packages/scroll-load-select/index.js'
|
|
23
|
+
import FileImport from '../packages/file-import/index.js'
|
|
23
24
|
|
|
24
25
|
// 存储组件列表
|
|
25
26
|
const components = [
|
|
@@ -38,7 +39,8 @@ const components = [
|
|
|
38
39
|
DatePickerPor,
|
|
39
40
|
DynamicDescriptions,
|
|
40
41
|
SelectTreeUnit,
|
|
41
|
-
SelectTreeUnitForm
|
|
42
|
+
SelectTreeUnitForm,
|
|
43
|
+
FileImport
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
// 定义 install 方法,接收 Vue 作为参数
|
|
@@ -83,6 +85,7 @@ export {
|
|
|
83
85
|
DynamicDescriptions,
|
|
84
86
|
SelectTreeUnit,
|
|
85
87
|
SelectTreeUnitForm,
|
|
88
|
+
FileImport,
|
|
86
89
|
// 工具方法
|
|
87
90
|
repairEl,
|
|
88
91
|
getColumnWidth,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
window._g_import_g_ || (window._g_import_g_ = {})
|
|
2
|
+
|
|
3
|
+
function importG(name, ipt) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
if (window._g_import_g_[name]) {
|
|
6
|
+
resolve(window._g_import_g_[name])
|
|
7
|
+
} else {
|
|
8
|
+
ipt().then((_module) => {
|
|
9
|
+
window._g_import_g_[name] = _module
|
|
10
|
+
resolve(_module)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default importG
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import importG from './importGlobal'
|
|
2
|
+
|
|
3
|
+
function columnNumberToName(columnNumber) {
|
|
4
|
+
let num = columnNumber
|
|
5
|
+
let name = ''
|
|
6
|
+
while (num > 0) {
|
|
7
|
+
const rem = (num - 1) % 26
|
|
8
|
+
name = String.fromCharCode(65 + rem) + name
|
|
9
|
+
num = Math.floor((num - 1) / 26)
|
|
10
|
+
}
|
|
11
|
+
return name
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readAsUint8Array(file) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const reader = new FileReader()
|
|
17
|
+
reader.onload = (e) => resolve(new Uint8Array(e.target.result))
|
|
18
|
+
reader.onerror = () => reject(new Error('读取文件失败'))
|
|
19
|
+
reader.readAsArrayBuffer(file)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getLeafColumns(columns = []) {
|
|
24
|
+
const leafColumns = []
|
|
25
|
+
columns.forEach((col) => {
|
|
26
|
+
if (!col) return
|
|
27
|
+
if (Array.isArray(col.children) && col.children.length) {
|
|
28
|
+
leafColumns.push(...getLeafColumns(col.children))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (col.prop) {
|
|
32
|
+
leafColumns.push(col)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
return leafColumns
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function writeImportErrorExcel({
|
|
39
|
+
validateResult,
|
|
40
|
+
file,
|
|
41
|
+
startRow = 3,
|
|
42
|
+
errorValSuffix = '',
|
|
43
|
+
rowHeight = 15,
|
|
44
|
+
worksheetIndex = 0,
|
|
45
|
+
ignoreNodes = ['dataValidations', 'conditionalFormatting', 'extLst'],
|
|
46
|
+
downloadFileName = '导入错误数据.xlsx',
|
|
47
|
+
noteTextFormatter
|
|
48
|
+
}) {
|
|
49
|
+
if (!validateResult || !file) {
|
|
50
|
+
throw new Error('参数缺失: validateResult 或 file')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rows = JSON.parse(JSON.stringify(validateResult.errorList || []))
|
|
54
|
+
const columns = getLeafColumns(validateResult.columnsList || []).filter(
|
|
55
|
+
(col) => col.prop !== 'seq'
|
|
56
|
+
)
|
|
57
|
+
const getErrorValKey = (prop) => `${prop}${errorValSuffix || ''}`
|
|
58
|
+
|
|
59
|
+
rows.forEach((row, index) => {
|
|
60
|
+
row.position = []
|
|
61
|
+
const currentErrorProps = row.errorList || []
|
|
62
|
+
currentErrorProps.forEach((prop) => {
|
|
63
|
+
const colIndex = columns.findIndex((col) => col.prop === prop)
|
|
64
|
+
if (colIndex < 0) return
|
|
65
|
+
row.position.push([index + 1, colIndex])
|
|
66
|
+
row[getErrorValKey(prop)] = row[prop]
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const data = await readAsUint8Array(file)
|
|
71
|
+
const { default: ExcelJS } = await importG('exceljs', () =>
|
|
72
|
+
import(/* webpackChunkName: "exceljs" */ 'exceljs')
|
|
73
|
+
)
|
|
74
|
+
const workbook = new ExcelJS.Workbook()
|
|
75
|
+
await workbook.xlsx.load(data, { ignoreNodes })
|
|
76
|
+
|
|
77
|
+
const worksheet = workbook.worksheets[worksheetIndex]
|
|
78
|
+
if (!worksheet) {
|
|
79
|
+
throw new Error('未找到工作表')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
worksheet.properties.defaultRowHeight = rowHeight
|
|
83
|
+
|
|
84
|
+
for (let rowNumber = startRow; rowNumber <= worksheet.rowCount; rowNumber++) {
|
|
85
|
+
const row = worksheet.getRow(rowNumber)
|
|
86
|
+
row.hidden = false
|
|
87
|
+
row.height = rowHeight
|
|
88
|
+
row.eachCell((cell) => {
|
|
89
|
+
cell.value = ''
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
rows.forEach((row, index) => {
|
|
94
|
+
const targetRow = worksheet.getRow(startRow + index)
|
|
95
|
+
targetRow.hidden = false
|
|
96
|
+
targetRow.height = rowHeight
|
|
97
|
+
columns.forEach((col, colIndex) => {
|
|
98
|
+
const value = row[col.prop] !== undefined && row[col.prop] !== null ? row[col.prop] : ''
|
|
99
|
+
targetRow.getCell(colIndex + 1).value = value
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
rows.forEach((row) => {
|
|
104
|
+
(row.position || []).forEach((pos) => {
|
|
105
|
+
const rowIndex = pos[0] + startRow - 1
|
|
106
|
+
const colIndex = pos[1]
|
|
107
|
+
const x = `${columnNumberToName(colIndex + 1)}${rowIndex}`
|
|
108
|
+
const cell = worksheet.getCell(x)
|
|
109
|
+
const prop = columns[colIndex].prop
|
|
110
|
+
const label = columns[colIndex].label
|
|
111
|
+
const noteText = noteTextFormatter
|
|
112
|
+
? noteTextFormatter({ label, prop, row, errorValSuffix, errorValKey: getErrorValKey(prop) })
|
|
113
|
+
: `${label}: ${row[prop] || ''}`
|
|
114
|
+
cell.note = {
|
|
115
|
+
texts: [{ text: noteText }]
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const newFileBuffer = await workbook.xlsx.writeBuffer()
|
|
121
|
+
const blob = new Blob([newFileBuffer], { type: 'application/octet-stream' })
|
|
122
|
+
const url = URL.createObjectURL(blob)
|
|
123
|
+
const a = document.createElement('a')
|
|
124
|
+
a.href = url
|
|
125
|
+
a.download =
|
|
126
|
+
typeof downloadFileName === 'function'
|
|
127
|
+
? downloadFileName()
|
|
128
|
+
: downloadFileName || '导入错误数据.xlsx'
|
|
129
|
+
document.body.appendChild(a)
|
|
130
|
+
a.click()
|
|
131
|
+
document.body.removeChild(a)
|
|
132
|
+
URL.revokeObjectURL(url)
|
|
133
|
+
}
|