| | |
| | | /** |
| | | * 导入接口返回结构。 |
| | | * |
| | | * 后端各模块的导入返回值不统一,这里只描述「可展示的导入结果」的公共部分: |
| | | * 成功的条数、失败的集合、被跳过的行。识别不出的返回值按「全部成功」处理。 |
| | | * 后端各模块的导入返回值不统一,这里描述可展示结果中约定明确的字段; |
| | | * create* / update* / failure* 等业务专用字段会在运行时统一识别。 |
| | | */ |
| | | export interface ImportResultLike { |
| | | /** 成功导入的单号/名称列表 */ |
| | | createPurchaseRequestNos?: string[]; |
| | | /** 失败集合:key 为单号或行号,value 为原因 */ |
| | | failurePurchaseRequestNos?: Record<string, string>; |
| | | /** 被跳过的行:key 为 Excel 行号,value 为原因 */ |
| | | skippedRows?: Record<string, string>; |
| | | /** 被跳过的行数 */ |
| | | skippedCount?: number; |
| | | /** Excel 中实际有数据的行数 */ |
| | | totalRows?: number; |
| | | /** 成功导入的行数 */ |
| | | successRows?: number; |
| | | /** 成功导入的数量 */ |
| | | successCount?: number; |
| | | /** 导入失败的数量 */ |
| | | failureCount?: number; |
| | | /** 其它模块的通用字段:失败列表 */ |
| | | failureList?: Record<string, string> | string[]; |
| | | /** 其它模块的通用字段:失败原因 */ |
| | | failureMessages?: Record<string, string> | string[]; |
| | | [key: string]: unknown; |
| | | } |
| | | |
| | |
| | | * 这些行不会抛异常,必须把跳过/失败的数量和行号明确告诉用户,否则数据无声丢失。 |
| | | */ |
| | | function summarizeImportResult(raw: unknown) { |
| | | const result = (raw ?? {}) as ImportResultLike; |
| | | const result = unwrapImportResult(raw); |
| | | |
| | | // 成功条数:优先用后端给的成功行数,其次用成功单号数组长度 |
| | | const successNos = Array.isArray(result.createPurchaseRequestNos) |
| | | ? result.createPurchaseRequestNos |
| | | : []; |
| | | const successRows = |
| | | typeof result.successRows === 'number' ? result.successRows : successNos.length; |
| | | // 各模块分别返回 createCodes、createContractNames、updateNames 等字段, |
| | | // 不能把通用组件绑定到某一个业务的字段名。 |
| | | const successRows = firstNumber(result.successRows, result.successCount) |
| | | ?? countArrayFields(result, /^(?:create|update|success)(?!Count$)/i); |
| | | |
| | | const failureEntries = toEntries(result.failurePurchaseRequestNos ?? result.failureList); |
| | | const failureEntries = collectEntries(result, /^failure(?!Count$)/i); |
| | | const skippedEntries = toEntries(result.skippedRows); |
| | | const failureRows = firstNumber(result.failureCount) ?? failureEntries.length; |
| | | const skippedRows = firstNumber(result.skippedCount) ?? skippedEntries.length; |
| | | const totalRows = firstNumber(result.totalRows); |
| | | const recognized = hasOutcomeFields(result); |
| | | |
| | | const totalRows = typeof result.totalRows === 'number' ? result.totalRows : undefined; |
| | | if (failureRows > 0 && failureEntries.length === 0) { |
| | | failureEntries.push([`失败 ${failureRows} 条`, '后端未返回失败明细']); |
| | | } |
| | | if (skippedRows > 0 && skippedEntries.length === 0) { |
| | | skippedEntries.push([`跳过 ${skippedRows} 条`, '后端未返回跳过明细']); |
| | | } |
| | | |
| | | return { |
| | | successRows, |
| | | failureRows, |
| | | skippedRows, |
| | | failureEntries, |
| | | skippedEntries, |
| | | totalRows, |
| | | hasIssue: failureEntries.length > 0 || skippedEntries.length > 0, |
| | | recognized, |
| | | hasIssue: failureRows > 0 || skippedRows > 0, |
| | | }; |
| | | } |
| | | |
| | | const OUTCOME_KEY_PATTERN = /^(?:create|update|success|failure|skipped|totalRows)/i; |
| | | |
| | | function hasOutcomeFields(result: ImportResultLike) { |
| | | return Object.keys(result).some((key) => OUTCOME_KEY_PATTERN.test(key)); |
| | | } |
| | | |
| | | /** requestClient 默认会解包 data,同时兼容 Axios / CommonResult 的嵌套 data。 */ |
| | | function unwrapImportResult(raw: unknown): ImportResultLike { |
| | | let current = raw; |
| | | for (let depth = 0; depth < 3; depth++) { |
| | | if (!current || typeof current !== 'object' || Array.isArray(current)) { |
| | | return {}; |
| | | } |
| | | const result = current as ImportResultLike; |
| | | if (hasOutcomeFields(result)) { |
| | | return result; |
| | | } |
| | | current = result.data; |
| | | } |
| | | return {}; |
| | | } |
| | | |
| | | function firstNumber(...values: unknown[]): number | undefined { |
| | | return values.find((value): value is number => typeof value === 'number'); |
| | | } |
| | | |
| | | /** 汇总 create* / update* / success* 数组的长度。 */ |
| | | function countArrayFields(result: ImportResultLike, keyPattern: RegExp) { |
| | | return Object.entries(result).reduce( |
| | | (total, [key, value]) => |
| | | total + (keyPattern.test(key) && Array.isArray(value) ? value.length : 0), |
| | | 0, |
| | | ); |
| | | } |
| | | |
| | | /** 合并 failure* 等结果字段,保留业务单号及失败原因。 */ |
| | | function collectEntries(result: ImportResultLike, keyPattern: RegExp) { |
| | | return Object.entries(result).flatMap(([key, value]) => |
| | | keyPattern.test(key) ? toEntries(value) : [], |
| | | ); |
| | | } |
| | | |
| | | /** 把 Record / string[] / undefined 统一成 [label, reason][] */ |
| | |
| | | if (summary.successRows > 0) { |
| | | parts.push(`成功 ${summary.successRows} 条`); |
| | | } |
| | | if (summary.skippedEntries.length > 0) { |
| | | parts.push(`跳过 ${summary.skippedEntries.length} 条`); |
| | | if (summary.skippedRows > 0) { |
| | | parts.push(`跳过 ${summary.skippedRows} 条`); |
| | | } |
| | | if (summary.failureEntries.length > 0) { |
| | | parts.push(`失败 ${summary.failureEntries.length} 条`); |
| | | if (summary.failureRows > 0) { |
| | | parts.push(`失败 ${summary.failureRows} 条`); |
| | | } |
| | | if (parts.length === 0) { |
| | | return '导入完成,但没有导入任何数据'; |
| | | return summary.recognized |
| | | ? '导入完成,但没有可处理的数据' |
| | | : '导入请求已完成,请刷新列表确认结果'; |
| | | } |
| | | const total = summary.totalRows === undefined ? '' : `(表格共 ${summary.totalRows} 行)`; |
| | | return `导入完成:${parts.join(',')}${total}`; |
| | |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | </template> |