前端時間使用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 \0xef\0xbb\0xbf
result += "\uFEFF";
// 寫入標題行
let strs = new Array<string>();
fields.forEach(v => {
strs.push(JSON.stringify(v.t));
});
let text = this.stringToCsvLines(strs) + "\n";
result += text;
// 寫入內容
items.forEach(item => {
text = this.itemToString(item, fields);
if (!text) return;
result += text + "\n";
});
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 注解非常簡單。
