/**
* 通用文件导出方法
* @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 // 独立的加载状态
);
};
优势说明
-
参数灵活
- 支持动态合并查询参数
- 可选表单校验和加载状态控制
-
类型安全
使用 TypeScript 类型约束,避免参数传递错误。 -
复用性强
适用于所有导出场景,只需更换 API 和文件名前缀。 -
逻辑解耦
将导出逻辑与业务组件分离,便于统一维护。 -
错误处理
自动捕获异常并重置加载状态。
// 导出 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 } |
config | HTTP请求配置 | { 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. 最佳实践
-
始终传递
loadingRef
即使当前不需要加载动画,保留参数以便后续扩展。 -
避免外部手动修改
不要在调用genericExport后额外写exportLoading.value = false,应由函数内部统一管理。 -
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>
通过规范化的加载状态管理,可以显著提升用户体验和代码健壮性。