通用导出函数

193 阅读9分钟
/**
 * 通用文件导出方法
 * @param params - 导出的查询参数(会自动合并 page=1 和 pageSize=9999)
 * @param exportApi - 调用的导出API函数(需返回 Promise)
 * @param fileNamePrefix - 导出文件名的前缀(会自动附加时间戳)
 * @param loadingRef - 可选,控制加载状态的 ref 对象
 * @param formParams - 可选,表单校验参数 { formArr: 表单数组, callBack: 校验回调 }
 */
const genericExportFile = async (
  params: Record<string, any>,
  exportApi: (params: any) => Promise<any>,
  fileNamePrefix: string,
  loadingRef?: Ref<boolean>,
  formParams?: {
    formArr: any[],
    callBack?: Function | null
  }
) => {
  try {
    if (loadingRef) loadingRef.value = true;

    // 合并基础分页参数(强制全量导出)
    const exportParams = {
      page: 1,
      pageSize: 9999,
      ...params
    };

    // 如果传入了表单参数,执行校验
    if (formParams) {
      checkFormParams({ 
        formArr: formParams.formArr, 
        obj: exportParams, 
        callBack: formParams.callBack || null 
      });
    }

    // 调用导出接口
    const res = await exportApi(exportParams);
    
    // 生成带时间戳的文件名并触发下载
    const fileName = `${fileNamePrefix}_${new Date().getTime()}`;
    upLoadXlsx(res, fileName);

  } catch (error) {
    console.error('导出失败:', error);
  } finally {
    if (loadingRef) loadingRef.value = false;
  }
};

form表单请求参数校验

// form表单请求参数校验
export const checkFormParams = (data) => {
  const { formArr, obj = {}, callBack } = data
  formArr.forEach((item) => {
    if (item.formKeys && item.formKeys.length && item.value) {
      item.formKeys.forEach((v, i) => {
        obj[v] = item.value[i] || null
      })
    } else obj[item.key] = item.value
    if (item.stringArray && item.value) {
      obj[item.key] = item.value.split(',')
    }
    if (item.multiple && !item.array) {
      obj[item.key] = Array.isArray(obj[item.key]) ? item.value.join(',') : item.value
    }
  })
  for (const key in obj) {
    if ((!obj[key] && obj[key] != 0) || obj[key].length < 1) {
      obj[key] = null
    }
  }
  if (callBack) {
    callBack(obj)
  }
  return obj
}

文件下载

export const upLoadXlsx = (res, fileName = 'table') => {
  const { data, headers } = res
  const blob = new Blob([data], { type: `${headers['content-type']};charset=utf-8` })
  const dom = document.createElement('a')
  const url = window.URL.createObjectURL(blob)
  dom.href = url
  dom.download = decodeURI(`${fileName}.xlsx`)
  dom.style.display = 'none'
  document.body.appendChild(dom)
  dom.click()
  dom.parentNode.removeChild(dom)
  window.URL.revokeObjectURL(url)
}

使用示例

1. 原请款单导出场景

typescript

复制

下载

const exportFile = () => {
  genericExportFile(
    {}, // 额外参数(可覆盖默认的 page/pageSize)
    Export, // 请款单导出API
    '请款单申请列表', // 文件名前缀
    exportLoading, // 绑定加载状态
    { formArr: formArr.value } // 表单校验参数
  );
};

2. 简单场景(无需表单校验)

typescript

复制

下载

const exportUsers = () => {
  genericExportFile(
    { department: '财务部' }, // 按部门筛选
    ExportUserList, // 用户列表导出API
    '用户数据', 
    usersLoading // 独立的加载状态
  );
};

优势说明

  1. 参数灵活

    • 支持动态合并查询参数
    • 可选表单校验和加载状态控制
  2. 类型安全
    使用 TypeScript 类型约束,避免参数传递错误。

  3. 复用性强
    适用于所有导出场景,只需更换 API 和文件名前缀。

  4. 逻辑解耦
    将导出逻辑与业务组件分离,便于统一维护。

  5. 错误处理
    自动捕获异常并重置加载状态。

// 导出 export const Export = (data) => { return http.post('/api/PaymentRequest/Export', data, { responseType: 'blob' }) }

以下是针对 Export 方法的通用封装和优化建议,结合之前封装的 genericExportFile 函数,形成一个完整的导出解决方案:


1. 保持基础导出 API 不变

// api.ts
export const Export = (data: Record<string, any>) => {
  return http.post('/api/PaymentRequest/Export', data, { 
    responseType: 'blob' 
  });
};

2. 增强通用导出函数(支持自定义配置)

// export-utils.ts
interface ExportOptions {
  params?: Record<string, any>;      // 扩展参数
  api: (data: any) => Promise<any>;  // 必传:导出API函数
  fileName: string;                  // 必传:文件名(自动加时间戳)
  loadingRef?: Ref<boolean>;         // 可选:加载状态
  formValidation?: {                // 可选:表单验证
    formData: any[];                // 表单数据数组
    validator?: (params: any) => void; // 自定义验证方法
  };
  config?: {                        // 可选:HTTP配置
    responseType?: 'blob' | 'json';  // 默认 blob
    timeout?: number;
  };
}

/**
 * 通用文件导出方法
 */
export const genericExport = async (options: ExportOptions) => {
  const { 
    params = {}, 
    api, 
    fileName, 
    loadingRef, 
    formValidation, 
    config = { responseType: 'blob' } 
  } = options;

  try {
    if (loadingRef) loadingRef.value = true;

    // 合并参数(强制全量导出)
    const exportParams = {
      page: 1,
      pageSize: 9999,
      ...params
    };

    // 表单验证(支持自定义 validator)
    if (formValidation) {
      formValidation.validator 
        ? formValidation.validator(exportParams)
        : checkFormParams({
            formArr: formValidation.formData,
            obj: exportParams
          });
    }

    // 调用API
    const res = await api({ ...exportParams, ...config });

    // 下载文件
    downloadFile(res, `${fileName}_${Date.now()}`);

  } catch (error) {
    console.error('[Export Error]', error);
    throw error; // 抛出错误供外部处理
  } finally {
    if (loadingRef) loadingRef.value = false;
  }
};

/**
 * 浏览器下载文件
 */
const downloadFile = (blob: Blob, fileName: string) => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `${fileName}.xlsx`;
  a.click();
  URL.revokeObjectURL(url);
};

3. 使用示例

场景1:直接使用原始 Export API

// 保持原用法不变
Export({ status: 1 }).then(blob => {
  downloadFile(blob, '请款单');
});

场景2:通过通用方法调用

// 简单调用
genericExport({
  api: Export,
  fileName: '请款单',
  loadingRef: exportLoading
});

// 带参数和表单验证
genericExport({
  api: Export,
  params: { status: 2 }, // 覆盖默认参数
  fileName: '待审核请款单',
  loadingRef: exportLoading,
  formValidation: {
    formData: formArr.value,
    validator: (params) => { // 自定义验证
      if (!params.status) throw new Error('请选择状态');
    }
  }
});

场景3:其他模块导出

// user-api.ts
export const exportUsers = (params) => {
  return http.post('/api/User/Export', params, { 
    responseType: 'blob' 
  });
};

// 在组件中
genericExport({
  api: exportUsers,
  params: { role: 'admin' },
  fileName: '管理员列表'
});

4. 优势总结

特性说明
统一入口所有导出操作通过 genericExport 处理
类型提示完善的 TypeScript 类型定义
灵活扩展支持自定义参数、验证和配置
下载解耦独立的 downloadFile 方法
错误处理自动捕获异常并支持向上抛出

5. 扩展建议

如果需要更复杂的导出逻辑(如动态URL、多格式导出),可以扩展 ExportOptions

interface ExportOptions {
  // 新增动态URL支持
  url?: string;
  // 支持多种文件格式
  fileType?: 'xlsx' | 'csv' | 'pdf';
  // 请求方法配置
  method?: 'post' | 'get';
}

参数怎么传递

在通用导出函数中,参数传递可以分为 多个层次,根据不同的需求灵活组合。以下是具体说明和示例:


1. 基础参数传递

直接通过 params 属性传递查询参数:

// 示例:导出状态为2的请款单
genericExport({
  api: Export,
  params: { 
    status: 2,          // 覆盖默认参数
    department: '财务部' // 追加新参数
  },
  fileName: '待审核请款单'
});

2. 分页参数控制

默认强制全量导出(page=1, pageSize=9999),但可手动覆盖:

// 示例:只导出前100条
genericExport({
  api: Export,
  params: {
    page: 1,
    pageSize: 100, // 覆盖默认的9999
    status: 1
  },
  fileName: '部分请款单'
});

3. 表单参数验证

通过 formValidation 传递表单数据和校验逻辑:

genericExport({
  api: Export,
  fileName: '请款单',
  formValidation: {
    formData: formArr.value, // 表单数据数组
    validator: (params) => { // 自定义校验(可选)
      if (!params.startDate) {
        throw new Error('请选择开始日期');
      }
    }
  }
});

4. 请求配置参数

通过 config 传递HTTP请求配置:

genericExport({
  api: Export,
  fileName: '请款单',
  config: {
    responseType: 'blob', // 默认值,可省略
    timeout: 30000       // 超时时间
  }
});

5. 动态URL和请求方法

扩展场景(需调整通用函数实现):

genericExport({
  api: (data) => http.post('/api/custom-export', data), // 动态API
  fileName: '自定义导出'
});

参数传递层级总结

参数层级作用域示例
params业务查询参数{ status: 1, type: 'urgent' }
formValidation表单校验相关{ formData: [...], validator }
configHTTP请求配置{ responseType: 'blob' }
api动态决定请求目标和方式(data) => http.get(...)

完整类型定义参考

interface ExportOptions<T = any> {
  // 核心参数
  api: (data: any) => Promise<T>;
  fileName: string;
  
  // 业务参数
  params?: Record<string, any>;
  
  // 表单校验
  formValidation?: {
    formData: any[];
    validator?: (params: any) => void;
  };
  
  // 请求配置
  config?: {
    responseType?: 'blob' | 'json';
    headers?: Record<string, string>;
    timeout?: number;
  };
  
  // 状态控制
  loadingRef?: Ref<boolean>;
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
}

formValidation

formValidation 参数详解

formValidation 是通用导出函数中用于 表单校验 的参数,它允许你在发起导出请求前对参数进行验证或处理。以下是具体用法和实现逻辑:


1. 参数结构

interface FormValidation {
  formData: any[];          // 表单数据(通常来自表单绑定的数组或对象)
  validator?: (params: any) => void; // 自定义校验函数(可选)
}

2. 使用场景

(1) 基本表单校验

如果只是需要将表单数据合并到导出参数中:

genericExport({
  api: Export,
  fileName: '请款单',
  formValidation: {
    formData: formData.value // 直接传递表单数据(如 el-form 的数据)
  }
});

内部行为:自动将 formData 合并到导出参数 params 中。


(2) 自定义校验逻辑

如果需要额外校验(如必填字段、数据格式等):

genericExport({
  api: Export,
  fileName: '请款单',
  formValidation: {
    formData: formData.value,
    validator: (params) => {
      // 校验示例
      if (!params.startDate || !params.endDate) {
        throw new Error('请选择日期范围');
      }
      if (params.amount > 10000) {
        throw new Error('金额不能超过1万');
      }
    }
  }
});

特点

  • 校验失败时会抛出错误,终止导出流程。
  • 可在 validator 内修改 params(如格式化日期)。

(3) 结合 UI 框架(如 Element Plus)

如果使用类似 el-form 的校验规则:

typescript

复制

下载

const formRef = ref(); // el-form 的 ref

genericExport({
  api: Export,
  fileName: '请款单',
  formValidation: {
    formData: formData.value,
    validator: async (params) => {
      // 先触发 UI 表单校验
      await formRef.value.validate();
      // 再执行自定义逻辑
      if (!params.approver) {
        throw new Error('请选择审批人');
      }
    }
  }
});

3. 底层实现逻辑

在 genericExport 函数内部,formValidation 的处理流程如下:

if (formValidation) {
  // 1. 合并表单数据到导出参数
  Object.assign(exportParams, formValidation.formData);
  
  // 2. 执行自定义校验
  if (formValidation.validator) {
    formValidation.validator(exportParams); // 校验失败会抛出错误
  }
}

4. 常见问题

Q1: 如果不需要表单校验,只是传递参数怎么办?

直接使用 params 即可,无需传递 formValidation

genericExport({
  api: Export,
  params: { status: 1 }, // 手动控制参数
  fileName: '请款单'
});

Q2: 如何避免表单数据和 params 的字段冲突?

在 validator 中手动处理:

validator: (params) => {
  // 明确覆盖字段
  params.status = formData.value.forceStatus; 
}

Q3: 能否完全替代 UI 表单校验?

不建议。formValidation 更适合 后端参数校验 或 补充校验,UI 校验仍应由 el-form 等组件负责。


5. 完整示例

typescript

复制

下载

// 组件内
const formData = ref({ status: 1, dateRange: [] });
const exportLoading = ref(false);

const handleExport = () => {
  genericExport({
    api: Export,
    fileName: '请款单',
    loadingRef: exportLoading,
    formValidation: {
      formData: formData.value,
      validator: (params) => {
        // 校验日期范围
        if (params.dateRange?.length !== 2) {
          throw new Error('请选择完整的日期范围');
        }
        // 格式化日期字段
        params.startDate = params.dateRange[0];
        params.endDate = params.dateRange[1];
        delete params.dateRange; // 清理原始字段
      }
    }
  });
};

通过 formValidation,你可以灵活地 组合表单数据添加校验逻辑,同时保持代码的可读性和复用性。

exportLoading.value = false 这个呢

在通用导出函数中,exportLoading.value = false 是用于 重置加载状态 的关键操作。以下是详细说明和最佳实践:


1. 作用说明

  • 加载状态管理:在导出开始时设为 true(显示加载动画),完成后必须重置为 false(无论成功/失败)。
  • 防止UI卡死:避免因导出失败导致按钮一直处于禁用状态。

2. 在通用函数中的实现

在封装的 genericExport 函数中,通过 finally 确保始终重置:

export const genericExport = async (options: ExportOptions) => {
  const { loadingRef } = options;

  try {
    if (loadingRef) loadingRef.value = true; // 开始加载
    // ...执行导出逻辑
  } catch (error) {
    console.error('导出失败:', error);
  } finally {
    if (loadingRef) loadingRef.value = false; // 确保结束加载
  }
};

3. 使用示例

(1) 组件中声明加载状态

import { ref } from 'vue';

const exportLoading = ref(false); // 定义加载状态

(2) 调用时传递 loadingRef

genericExport({
  api: Export,
  fileName: '请款单',
  loadingRef: exportLoading, // 传入加载状态引用
  params: { status: 1 }
});

(3) 模板中绑定状态

<el-button 
  :loading="exportLoading" 
  @click="handleExport"
>
  导出Excel
</el-button>

4. 为什么用 finally

  • 确保可靠性:无论导出成功还是抛出错误,finally 中的代码都会执行。
  • 避免重复代码:不需在 try 和 catch 中分别写 loadingRef.value = false

5. 特殊情况处理

(1) 手动控制加载状态

如果某些场景需要延迟关闭加载状态:

genericExport({
  // ...其他参数,
  loadingRef: exportLoading,
  onSuccess: () => {
    // 导出成功后额外操作
    setTimeout(() => {
      exportLoading.value = false; // 延迟关闭
    }, 1000);
  }
});

(2) 多个导出函数共享状态

当页面有多个导出按钮时,可复用同一个 loadingRef

const exportLoading = ref(false);

// 导出请款单
const exportPayment = () => {
  genericExport({ api: ExportPayment, loadingRef: exportLoading });
};

// 导出用户
const exportUser = () => {
  genericExport({ api: ExportUser, loadingRef: exportLoading });
};

6. 最佳实践

  1. 始终传递 loadingRef
    即使当前不需要加载动画,保留参数以便后续扩展。

  2. 避免外部手动修改
    不要在调用 genericExport 后额外写 exportLoading.value = false,应由函数内部统一管理。

  3. TypeScript 类型安全
    定义明确的类型约束:

    interface ExportOptions {
      loadingRef?: Ref<boolean>;
      // ...其他参数
    }
    

7. 完整代码示例

<script setup>
import { ref } from 'vue';
import { genericExport, Export } from '@/api/export';

const exportLoading = ref(false);
const formData = ref({ status: 1 });

const handleExport = () => {
  genericExport({
    api: Export,
    fileName: '请款单',
    loadingRef: exportLoading,
    params: formData.value
  });
};
</script>

<template>
  <el-button 
    :loading="exportLoading" 
    @click="handleExport"
  >
    导出Excel
  </el-button>
</template>

通过规范化的加载状态管理,可以显著提升用户体验和代码健壮性。