前端时间使用Java做了此功能,另一个使用Node.js开发的服务也需要此功能,所以使用TypeScript做了类似的封装,后来发现,TS做这些功能,代码看起来更简洁,嘿嘿。
直接上代码吧。
CsvUtils.ts
import { Response } from "express"; import { DateUtils, FXResponse } from "nodejs-fx"; import { GenderType } from "../model/GenderType"; const uuid = require('node-uuid'); const _reg1: RegExp = new RegExp(""", 'g'); const _reg2: RegExp = new RegExp("\"", 'g'); /** * CSV 下载辅助类 */ export class CsvUtils { private static charset: String = "utf-8"; /** * 导出 CSV * @param res Http请求Response * @param fileName 可选,文件名,用户下载的文件名 * @param onLoadData 获取分页数据 */ static async writeCsv<T>(res: Response, _constructor: { new (...args: Array<any>): T }, onLoadData: (page: number) => Promise<PageDTO<T>>, fileName: string = undefined ): Promise<any> { try { let cls = (new _constructor()).constructor.name; let items: T[] = []; let pageIndex: number = 1; let count: number = undefined; while (true) { let result:PageDTO<T> = await onLoadData(pageIndex); if (!result || !result.items || result.items.length == 0) break; if (pageIndex == 1) { count = result.count; } if (pageIndex == 1 && count != undefined && count == result.items.length) { return await this.writeCsvByItems(res, result.items, fileName, cls); } // items.push(...result.items); result.items.forEach(item => { items.push(item); }); pageIndex++; if (result.hasNext === true) continue; if (result.hasNext === false) break; if (count != undefined && items.length >= count) break; } return await this.writeCsvByItems(res, items, fileName, cls); } catch (e) { return e; } } /** * 导出列表 CSV * @param res Http请求Response * @param items 数据列表 * @param fileName 可选,文件名,用户下载的文件名 */ static async writeCsvByItems<T>(res: Response, items: Array<T>, fileName: string, className: string): Promise<any> { this.setHttpHeader(res, fileName); if (!items || items.length == 0) return ""; // 筛选出拥有注解的字段 let fields = new Array<any>(); for (var o in items[0]) { let rKey = className + "." + o.toLowerCase(); let reg = this.regMap.get(rKey); if (reg && reg.ingore === true) continue; if (!reg || !reg.name) { fields.push({v: o, t: o, conv: undefined}); } else { fields.push({v: o, t: reg.name, conv: reg.converter}) } } if (fields.length == 0) return ""; let result: string = ""; // 写入utf-8 BOM xef xbb xbf result += "uFEFF"; // 写入标题行 let strs = new Array<string>(); fields.forEach(v => { strs.push(JSON.stringify(v.t)); }); let text = this.stringToCsvLines(strs) + " "; result += text; // 写入内容 items.forEach(item => { text = this.itemToString(item, fields); if (!text) return; result += text + " "; }); return result; } /** 设置下载用的 Http 响应头部 */ private static setHttpHeader(res: Response, fileName: string) { if (!fileName) fileName = this.generateRandomFileName() + ".csv"; res.set({ "Content-Type": "application/octet-stream; charset=" + this.charset, "Content-Disposition": "attachment;filename=" + encodeURIComponent(fileName), "Pragma": "no-cache", "Expires": 0 }); } private static itemToString(item: any, fields: Array<any>): string { let result = new Array<string>(); fields.forEach(data => { let v = undefined; if (data.conv) { data.conv.data = item; v = data.conv.execute(item[data.v]); } else v = item[data.v]; if (v == undefined || v === "") { result.push(""); } else { let txt = JSON.stringify(v); if (txt.startsWith("{") || txt.startsWith("[")) { txt = """ + txt.replace(_reg1, """") + """; } result.push(txt); } }); return this.stringToCsvLines(result); } private static generateRandomFileName(): string { return uuid.v4().replace(new RegExp("-", 'g'), ''); } private static stringToCsvLines(strs: Array<string>): string { if (!strs || strs.length == 0) return ""; return strs.join(","); } // 注册的注解参数 static regMap: Map<string, CsvParams> = new Map<string, CsvParams>(); } export class PageDTO<T> { count: number = 0; hasNext: boolean = true; items: T[]; static load<T>(data: FXResponse<T[]>, pageSize: number) { let result = new PageDTO<T>(); if (data && data.code == 0 && data.data) { if (Array.isArray(data.data)) { result.items = data.data; } else if (data.data.list && Array.isArray(data.data.list)) { result.items = data.data.list; } else if (data.data.items && Array.isArray(data.data.items)) { result.items = data.data.items; } if (result.items) result.hasNext = result.items.length >= pageSize; else result.hasNext = false; } else throw data; return result; } } /** * csv 注解 * @param name 字段名称(导出后显示的名称) * @param ingore 是否忽略这个字段 * @param _constructor 转换器 * @param args 转换器构造参数(依次写) */ export function csv<T>(name: string, ingore: boolean = false, _constructor: { new (...args: Array<any>): CsvConverterBase } = undefined, ...args: any ) { return function(target:any, propertyName:string){ let p = new CsvParams(); p.name = name; p.ingore = ingore; if (_constructor) { p.converter = new _constructor(...args); } CsvUtils.regMap.set(target.constructor.name + "." + propertyName.toLowerCase(), p); } } export class CsvParams { /** 字段名称 */ name: string; /** 是否忽略 */ ingore: boolean; /** 转换器 */ converter: CsvConverterBase; } export abstract class CsvConverterBase { data: any; abstract execute(value: any): string; } /** * 时间戳转字符串 CSV转换器 */ export class TimestampCsvConverter extends CsvConverterBase { execute(value: any): string { if (value == undefined) return ""; if (!Number.isNaN(value)) { return DateUtils.formatDateTime(value); } else return value; } } /** * 性别类型CSV转换器 * @description @csv("会员标签", undefined, GenderTypeCsvConverter) */ export class GenderTypeCsvConverter extends CsvConverterBase { execute(value: GenderType): string { if (value == GenderType.female) return "女"; if (value == GenderType.male) return "男"; return "未知" } } /** * 字符串数组 CSV转换器 * @description @csv("会员标签", undefined, StringArrayCsvConverter) */ export class StringArrayCsvConverter extends CsvConverterBase { field: string; constructor(field: string) { super(); this.field = field; } execute(value: any): string { if (Array.isArray(value) && value.length > 0) { if (typeof(value[0]) == 'string') return value.join(","); if (this.field) { let items = []; value.forEach(item => items.push(item[this.field])); return items.join(","); } } return value; } } /** * 布尔值 CSV转换器 * @description @csv("允许登录APP", undefined, BoolCsvConverter, "是", "否") */ export class BoolCsvConverter extends CsvConverterBase { p1: string; p2: string; p3: string; constructor(p1: string, p2: string, p3: string = "") { super(); this.p1 = p1; this.p2 = p2; this.p3 = p3; } execute(value: any): string { if (value === true) return this.p1; if (value === false) return this.p2; return this.p3 == undefined ? "" : this.p3; } } /** * 枚举值 CSV 转换器 * @description @csv("登录角色", undefined, EnumCsvConverter, {1: "管理员", 2: "普通员工", 3: "创建者"}) */ export class EnumCsvConverter extends CsvConverterBase { enumValue: Object; constructor(enumValue: Object) { super(); this.enumValue = enumValue; } execute(value: any): string { if (value == undefined) return ""; let v = this.enumValue[value]; return v ? v : ""; } } /** * 对象字段值 CSV 转换器 * @description @csv("图像地址", undefined, ObjectCsvConverter, "url") */ export class ObjectCsvConverter extends CsvConverterBase { field: string; constructor(field: string) { super(); this.field = field; } execute(value: any): string { if (!value || !this.field) return ""; if (Array.isArray(value)) { // 数组取出每项的字段值后,用","分隔连接 let values = []; value.forEach(item => { values.push(item[this.field]); }); return values.join(","); } else return value[this.field]; } }
PageDTO 声明, 仅作参考: (主要是作分页用)
export class PageDTO<T> { count: number = 0; hasNext: boolean = true; items: T[]; static load<T>(data: Response<T[]>, pageSize: number) { let result = new PageDTO<T>(); if (data && data.code == 0 && data.data) { if (Array.isArray(data.data)) { result.items = data.data; } else if (data.data.list && Array.isArray(data.data.list)) { result.items = data.data.list; } else if (data.data.items && Array.isArray(data.data.items)) { result.items = data.data.items; } if (result.items) result.hasNext = result.items.length >= pageSize; else result.hasNext = false; } else throw data; return result; } }
调用举例:
@get("/list/pc/csv") @validate async getXXXListCsv( @query('a') a: string, @query('b') b: string, @query('c') c: string ) { return await CsvUtils.writeCsv(this.res, TestDTO, async (page): Promise<PageDTO<any>> => { let data = await this.getList(page, 20, a, b, c); return PageDTO.load(data, 20); }); }
TestDTO 声明:
export class TestDTO { /** * 会员名称 */ @csv("会员名称") name:string; /** * 头像 */ @csv("", true) memberImage:MediaModel; /** * 性别 */ @csv("性别", undefined, GenderTypeCsvConverter) gender:GenderType; /** * 会员标签名称数组 */ @csv("会员标签", undefined, StringArrayCsvConverter, "name") tags:string[]|TagsDetail[]; /** * 加入时间 */ @csv("加入时间") jointime?: string; /** * 会员在该店铺的启用状态 */ @csv("启用状态", undefined, BoolCsvConverter, "启用", "未启用") enable?: boolean; }
可以看到,使用 @csv 注解非常简单。