t20-common-lib 0.13.2 → 0.14.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "t20-common-lib",
3
- "version": "0.13.2",
3
+ "version": "0.14.1",
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,8 @@
1
+ import FileImport from './src/main'
2
+
3
+ /* istanbul ignore next */
4
+ FileImport.install = function(Vue) {
5
+ Vue.component(FileImport.name, FileImport)
6
+ }
7
+
8
+ export default FileImport
@@ -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,330 @@
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
+ // `xlsx` 是 CommonJS 模块,默认导入在某些构建环境下可能为 undefined
35
+ // 使用 `* as` 可兼容不同打包器的导出形态。
36
+ import * as XLSX from 'xlsx'
37
+ import uploadMsg from './Upload/uploadMsg.vue'
38
+ import writeImportErrorExcel from '@/utils/writeImportErrorExcel'
39
+
40
+ export default {
41
+ name: 'FileImport',
42
+ components: { uploadMsg },
43
+ props: {
44
+ templateUrl: { type: String, default: '' },
45
+ templateOpt: { type: [Object, Array], default: undefined },
46
+ width: { type: String, default: '60%' },
47
+ fileType: { type: String, default: '.xlsx,.xls,.csv' },
48
+ fileName: { type: String, default: '下载.xlsx' },
49
+ uploadHttpRequest: { type: Function, default: undefined, required: true },
50
+ customTemplateDownload: { type: Function, default: undefined },
51
+ validateResult: { type: Object, default: undefined },
52
+ pagination: { type: Boolean, default: false },
53
+ showErrorExport: { type: Boolean, default: false },
54
+ validateDialog: { type: Boolean, default: false },
55
+ validateConfirm: { type: Function, default: undefined },
56
+ footer: { type: Object, default: undefined },
57
+ uploadOn: { type: Object, default: undefined },
58
+ hidePercent: { type: Boolean, default: false },
59
+ placement: { type: String, default: 'bottom-start' },
60
+ percentType: { type: String, default: 'loading' },
61
+ percent: { type: Number, default: 0 },
62
+ errorExportMode: { type: String, default: 'frontend' },
63
+ errorExportOptions: { type: Object, default: () => ({}) }
64
+ },
65
+ data() {
66
+ return {
67
+ currentFile: null
68
+ }
69
+ },
70
+ computed: {
71
+ errorV: {
72
+ get() {
73
+ return this.validateDialog
74
+ },
75
+ set(v) {
76
+ this.$emit('update:validateDialog', v)
77
+ return v
78
+ }
79
+ }
80
+ },
81
+ methods: {
82
+ getDefaultErrorExportFileName() {
83
+ const baseName = String(this.fileName || '导入文件')
84
+ const pureName = baseName.replace(/\.(xlsx|xls|csv)$/i, '')
85
+ return `${pureName}-错误数据.xlsx`
86
+ },
87
+ getErrorExportParams() {
88
+ return {
89
+ validateResult: this.validateResult,
90
+ file: this.currentFile,
91
+ startRow: 3,
92
+ errorValSuffix: '',
93
+ rowHeight: 15,
94
+ downloadFileName: this.getDefaultErrorExportFileName(),
95
+ noteTextFormatter: ({ label, prop, row }) => `${label}: ${row[prop]}`,
96
+ ...(this.errorExportOptions || {})
97
+ }
98
+ },
99
+ toInt(value, fallback) {
100
+ const num = Number(value)
101
+ return Number.isInteger(num) ? num : fallback
102
+ },
103
+ getTemplateSheetConfigs() {
104
+ const opt = this.templateOpt
105
+ if (!opt) return []
106
+ if (Array.isArray(opt)) {
107
+ return opt.filter(item => item && item.sheetName && item.columns)
108
+ }
109
+ if (opt.sheetName && opt.columns) {
110
+ return [opt]
111
+ }
112
+ if (opt.sheets && typeof opt.sheets === 'object') {
113
+ return Object.keys(opt.sheets)
114
+ .map(name => ({ sheetName: name, ...(opt.sheets[name] || {}) }))
115
+ .filter(item => item.columns)
116
+ }
117
+ return []
118
+ },
119
+ getSheetParseOption(sheetName) {
120
+ const option = this.templateOpt || {}
121
+ const configs = this.getTemplateSheetConfigs()
122
+ const fromConfig = configs.find(c => c.sheetName === sheetName) || {}
123
+ const sheetOption = {
124
+ ...fromConfig,
125
+ ...((option.sheets && option.sheets[sheetName]) || {}),
126
+ ...(option[sheetName] || {})
127
+ }
128
+ const headerRows = this.toInt(
129
+ sheetOption.headerRows ?? sheetOption.headerDepth ?? sheetOption.headerLevel ?? option.headerRows ?? option.headerDepth ?? option.headerLevel,
130
+ 1
131
+ )
132
+ const headerRowIndex = this.toInt(sheetOption.headerRowIndex ?? option.headerRowIndex, headerRows - 1)
133
+ const dataStartRow = this.toInt(
134
+ sheetOption.dataStartRow ?? sheetOption.startDataRow ?? option.dataStartRow ?? option.startDataRow,
135
+ headerRowIndex + 1
136
+ )
137
+ return {
138
+ headerRowIndex: Math.max(headerRowIndex, 0),
139
+ dataStartRow: Math.max(dataStartRow, 0)
140
+ }
141
+ },
142
+ parseSheetToLeafHeaderData(worksheet, parseOption) {
143
+ const matrix = XLSX.utils.sheet_to_json(worksheet, {
144
+ header: 1,
145
+ raw: true,
146
+ defval: ''
147
+ })
148
+ if (!matrix.length) {
149
+ return { headers: [], rows: [] }
150
+ }
151
+ const { headerRowIndex, dataStartRow } = parseOption
152
+ const maxColLen = matrix.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), 0)
153
+ const headers = []
154
+
155
+ for (let colIndex = 0; colIndex < maxColLen; colIndex++) {
156
+ let leafHeader = ''
157
+ for (let rowIndex = Math.min(headerRowIndex, matrix.length - 1); rowIndex >= 0; rowIndex--) {
158
+ const cell = matrix[rowIndex] && matrix[rowIndex][colIndex]
159
+ const normalized = cell === undefined || cell === null ? '' : String(cell).trim()
160
+ if (normalized) {
161
+ leafHeader = normalized
162
+ break
163
+ }
164
+ }
165
+ headers.push(leafHeader)
166
+ }
167
+
168
+ const rows = matrix
169
+ .slice(Math.min(dataStartRow, matrix.length))
170
+ .map(row => headers.map((_, colIndex) => (Array.isArray(row) ? row[colIndex] : '')))
171
+ .filter(row => row.some(cell => cell !== '' && cell !== null && cell !== undefined))
172
+
173
+ return { headers, rows }
174
+ },
175
+ normalizeHeaderLabel(text) {
176
+ return String(text == null ? '' : text)
177
+ .replace(/\u00a0/g, ' ')
178
+ .trim()
179
+ .replace(/^\*\s*/, '')
180
+ .trim()
181
+ },
182
+ flattenLeafColumns(columns) {
183
+ if (!Array.isArray(columns) || !columns.length) {
184
+ return []
185
+ }
186
+ const out = []
187
+ columns.forEach(col => {
188
+ if (!col) return
189
+ if (col.children && col.children.length) {
190
+ out.push(...this.flattenLeafColumns(col.children))
191
+ } else if (col.prop) {
192
+ out.push(col)
193
+ }
194
+ })
195
+ return out
196
+ },
197
+ expectedHeaderForColumn(col) {
198
+ const label = String(col.label == null ? '' : col.label).trim()
199
+ const required = col.required === true
200
+ return required ? `* ${label}` : label
201
+ },
202
+ buildSheetPayload(headers, rows, columnTree) {
203
+ const leafCols = this.flattenLeafColumns(columnTree)
204
+ const indexByProp = {}
205
+ const templateKeyByProp = {}
206
+ leafCols.forEach(col => {
207
+ const target = this.normalizeHeaderLabel(this.expectedHeaderForColumn(col))
208
+ const idx = headers.findIndex(h => this.normalizeHeaderLabel(h) === target)
209
+ if (idx !== -1) {
210
+ indexByProp[col.prop] = idx
211
+ templateKeyByProp[col.prop] = headers[idx]
212
+ }
213
+ })
214
+ const templateData = []
215
+ const tableData = []
216
+ rows.forEach(row => {
217
+ const tRow = {}
218
+ const pRow = {}
219
+ leafCols.forEach(col => {
220
+ const idx = indexByProp[col.prop]
221
+ if (idx === undefined) return
222
+ const val = Array.isArray(row) ? row[idx] : ''
223
+ const tk = templateKeyByProp[col.prop]
224
+ if (tk !== undefined && tk !== '') {
225
+ tRow[tk] = val
226
+ }
227
+ pRow[col.prop] = val
228
+ })
229
+ templateData.push(tRow)
230
+ tableData.push(pRow)
231
+ })
232
+ return {
233
+ headers,
234
+ templateData,
235
+ tableData
236
+ }
237
+ },
238
+ importError() {
239
+ if (this.errorExportMode === 'backend') {
240
+ this.$emit('importError', {
241
+ validateResult: this.validateResult,
242
+ file: this.currentFile
243
+ })
244
+ return
245
+ }
246
+ writeImportErrorExcel(this.getErrorExportParams()).catch(error => {
247
+ console.error('writeImportErrorExcel failed:', error)
248
+ this.$message.error(this.$l('导入模板解析失败,请检查模板内容后重试'))
249
+ })
250
+ },
251
+ commandFn(val) {
252
+ if (val === 'import') this.importFn()
253
+ if (val === 'down') this.downFn()
254
+ },
255
+ importFn() {
256
+ this.$emit('update:validateResult', undefined)
257
+ let input = document.createElement('input')
258
+ input.type = 'file'
259
+ input.accept = this.fileType
260
+ input.addEventListener('change', event => {
261
+ this.generateData(event.target.files[0])
262
+ })
263
+
264
+ input.click()
265
+ this.$nextTick(() => {
266
+ input = undefined
267
+ })
268
+ },
269
+ generateData(rawFile) {
270
+ if (!rawFile) return
271
+ this.currentFile = rawFile
272
+ const uploadFileName = rawFile.name || ''
273
+ const reader = new FileReader()
274
+ reader.onload = async e => {
275
+ const data = e.target.result
276
+ const workbook = XLSX.read(data, {
277
+ type: 'array',
278
+ raw: true,
279
+ cellHTML: true,
280
+ cellText: true,
281
+ bookFiles: true,
282
+ cellFormula: true,
283
+ cellDates: true
284
+ })
285
+ const worksheets = workbook.Sheets
286
+ const sheetsMap = {}
287
+ Object.keys(worksheets).forEach(sheet => {
288
+ const parseOption = this.getSheetParseOption(sheet)
289
+ sheetsMap[sheet] = this.parseSheetToLeafHeaderData(worksheets[sheet], parseOption)
290
+ })
291
+ const configs = this.getTemplateSheetConfigs()
292
+ if (configs.length) {
293
+ const res = {}
294
+ configs.forEach(cfg => {
295
+ const parsed = sheetsMap[cfg.sheetName]
296
+ if (!parsed) {
297
+ res[cfg.sheetName] = { headers: [], templateData: [], tableData: [] }
298
+ } else {
299
+ res[cfg.sheetName] = this.buildSheetPayload(parsed.headers, parsed.rows, cfg.columns)
300
+ }
301
+ })
302
+ this.uploadHttpRequest({ ...res, fileName: uploadFileName, file: rawFile })
303
+ } else {
304
+ this.uploadHttpRequest({ ...sheetsMap, fileName: uploadFileName, file: rawFile })
305
+ }
306
+ }
307
+ reader.readAsArrayBuffer(rawFile)
308
+ },
309
+ downFn() {
310
+ if (this.customTemplateDownload) {
311
+ this.customTemplateDownload()
312
+ } else {
313
+ this.$axios
314
+ .get(this.templateUrl, null, {
315
+ responseType: 'blob'
316
+ })
317
+ .then(blob => {
318
+ this.$downloadBlob(blob, this.fileName)
319
+ })
320
+ }
321
+ }
322
+ }
323
+ }
324
+ </script>
325
+
326
+ <style scoped>
327
+ .file-import-dropdown ::v-deep .el-dropdown-menu__item {
328
+ padding: 0 10px;
329
+ }
330
+ </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
+ }