liu
2026-10-27 b76169f3ea622e1b4636a9d047fc12ea53fed2a3
fix(import): 兼容各业务导入结果并恢复租户导出
已修改2个文件
106 ■■■■■ 文件已修改
src/api/system/tenant/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/import-modal/import-modal.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant/index.ts
@@ -110,5 +110,7 @@
}
export function exportTenant(params: any) {
  return Promise.resolve(new Blob());
  return requestClient.download('/system/tenant/export-excel', {
    params,
  });
}
src/components/import-modal/import-modal.vue
@@ -14,22 +14,26 @@
/**
 * 导入接口返回结构。
 *
 * 后端各模块的导入返回值不统一,这里只描述「可展示的导入结果」的公共部分:
 * 成功的条数、失败的集合、被跳过的行。识别不出的返回值按「全部成功」处理。
 * 后端各模块的导入返回值不统一,这里描述可展示结果中约定明确的字段;
 * 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;
}
@@ -89,27 +93,79 @@
 * 这些行不会抛异常,必须把跳过/失败的数量和行号明确告诉用户,否则数据无声丢失。
 */
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][] */
@@ -135,14 +191,16 @@
  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}`;
@@ -256,4 +314,4 @@
      </div>
    </template>
  </Modal>
</template>
</template>