开发需求
- 表头单元格自动合并(类似 EasyExcel)
- 单元格样式自动设置(带斑马纹样式)
- 根据单元格数据自动设置表格列宽
- 读取表格数据自动转换为 Map 列表
- 表数据异步处理(多个 Sheet 同时执行)
- 表头数据(行)及行标题(列)单元格冻结
最终效果
- 导出 Excel 数据(指定列宽)
- 导出 Excel 数据(自动列宽)
- 导出 Excel 数据(单元格冻结)
- Excel 数据读取
依赖模块
xlsx-js-style, xlsx-style
xlsx-js-style 最新版: https://www.npmjs.com/package/xlsx-js-style
xlsx-style 最新版:https://pan.baidu.com/s/1y6vaPHVMy8U_pT2_5xzLsw 提取码: uny2
测试类
import {LogUtil} from "./LogUtil";
import {Log} from "./Log";
import {ExcelUtil} from "./ExcelUtil";
import {AsyncUtil} from "./AsyncUtil";
export class ApplicationTest {
//导出Excel数据测试四:单元格数据冻结二
public test7(): void {
let lstHeader = [
["序号"],
["书名"],
["作者"],
["年代"],
["字数"],
];
let lstData = [
["《水浒传》", "施耐庵", "宋朝", "96 万字"],
["《三国演义》", "罗贯中", "元朝", "73.4 万字"],
["《西游记》", "吴承恩", "明代", "82 万字"],
["《红楼梦》", "曹雪芹", "清代", "107.5 万字"],
["《聊斋志异》", "蒲松龄", "清代", "70.8 万字"],
];
let lstSheet = [];
for (let num = 1; num <= 5; num++) {
ExcelUtil.writeHeader(lstHeader, undefined, 1);
for (let i = 0, rowIndex = 1; i < lstData.length; i++, rowIndex++) {
let colIndex = 0;
ExcelUtil.writeCellData(rowIndex, colIndex++, (i + 1) + "");
for (let j = 0; j < lstData[i].length; j++) {
ExcelUtil.writeCellData(rowIndex, colIndex++, lstData[i][j]);
}
}
let sheet = ExcelUtil.getDataSheet();
lstSheet.push({sheet: sheet, name: "Sheet" + num});
}
ExcelUtil.write(lstSheet, "C:\\Users\\admin\\Desktop\\demo-width-auto-" + Date.now() + ".xlsx");
}
//导出Excel数据测试四:单元格数据冻结一
public async test6(): Promise<void> {
let lstHeader = [
["a", "a", "e"],
["b", "b", "e"],
["b", "b", "f"],
["c", "a", "a"],
["c", "a", "a"]
];
let lstNum = [1, 2, 3, 4, 5];
let lstData = new Array<Record<string, any>>();
await AsyncUtil.execWithListDataByAsync(lstNum, num => {
ExcelUtil.writeHeader(lstHeader, 22, 1);
for (let i = 0, rowIndex = 3; i < num * 50 + num; i++, rowIndex += 2) {
for (let j = 0; j < 5; j++) {
ExcelUtil.writeCellData(rowIndex, j, "test");
ExcelUtil.writeCellData(rowIndex + 1, j, "demo");
}
}
let sheet = ExcelUtil.getDataSheet();
lstData.push({sheet: sheet, name: "Sheet" + num});
});
ExcelUtil.write(lstData, "C:\\Users\\admin\\Desktop\\demo-" + Date.now() + ".xlsx");
}
//导出Excel数据测试三:多个 Sheet 异步处理
public async test5(): Promise<void> {
let lstHeader = [
["a", "a", "e"],
["b", "b", "e"],
["b", "b", "f"],
["c", "a", "a"],
["c", "a", "a"]
];
let lstNum = [1, 2, 3, 4, 5];
let lstData = new Array<Record<string, any>>();
await AsyncUtil.execWithListDataByAsync(lstNum, num => {
ExcelUtil.writeHeader(lstHeader, 22);
for (let i = 0, rowIndex = 3; i < num * 50 + num; i++, rowIndex += 2) {
for (let j = 0; j < 5; j++) {
ExcelUtil.writeCellData(rowIndex, j, "test");
ExcelUtil.writeCellData(rowIndex + 1, j, "demo");
}
}
let sheet = ExcelUtil.getDataSheet();
lstData.push({sheet: sheet, name: "Sheet" + num});
});
ExcelUtil.write(lstData, "C:\\Users\\admin\\Desktop\\demo-" + Date.now() + ".xlsx");
}
//Excel数据读取测试二
public test4(): void {
let lstData = ExcelUtil.toMap(
"C:\\Users\\admin\\Desktop\\demo-width-auto-1659158251861.xlsx",
0
);
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "lstData", lstData));
for (let data of lstData) {
let num = data.get("序号");
let name = data.get("书名");
let author = data.get("作者");
let age = data.get("年代");
let words = data.get("字数");
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "num", num));
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "name", name));
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "author", author));
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "age", age));
LogUtil.loggerLine(Log.of("ApplicationTest", "test4", "words", words));
console.log("---------------------------------------------------------------------------");
}
}
//导出Excel数据测试二:单元格列宽自动设置
public test3(): void {
let lstHeader = [
["序号"],
["书名"],
["作者"],
["年代"],
["字数"],
];
let lstData = [
["《水浒传》", "施耐庵", "宋朝", "96 万字"],
["《三国演义》", "罗贯中", "元朝", "73.4 万字"],
["《西游记》", "吴承恩", "明代", "82 万字"],
["《红楼梦》", "曹雪芹", "清代", "107.5 万字"],
["《聊斋志异》", "蒲松龄", "清代", "70.8 万字"],
];
let lstSheet = [];
for (let num = 1; num <= 5; num++) {
ExcelUtil.writeHeader(lstHeader);
for (let i = 0, rowIndex = 1; i < lstData.length; i++, rowIndex++) {
let colIndex = 0;
ExcelUtil.writeCellData(rowIndex, colIndex++, (i + 1) + "");
for (let j = 0; j < lstData[i].length; j++) {
ExcelUtil.writeCellData(rowIndex, colIndex++, lstData[i][j]);
}
}
let sheet = ExcelUtil.getDataSheet();
lstSheet.push({sheet: sheet, name: "Sheet" + num});
}
ExcelUtil.write(lstSheet, "C:\\Users\\admin\\Desktop\\demo-width-auto-" + Date.now() + ".xlsx");
}
//Excel读取测试一
public test2(): void {
let lstData = ExcelUtil.toMap(
"C:\\Users\\admin\\Desktop\\新建 XLSX 工作表.xlsx",
0
);
LogUtil.loggerLine(Log.of("ApplicationTest", "test2", "lstData", lstData));
for (let data of lstData) {
let num = data.get("序号");
let animal = data.get("动物");
let food = data.get("食物");
LogUtil.loggerLine(Log.of("ApplicationTest", "test2", "num", num));
LogUtil.loggerLine(Log.of("ApplicationTest", "test2", "animal", animal));
LogUtil.loggerLine(Log.of("ApplicationTest", "test2", "food", food));
console.log("---------------------------------------------------------------------------");
}
}
//导出Excel数据测试一:表头单元格自动合并
public test1(): void {
let lstHeader = [
["a", "a", "e"],
["b", "b", "e"],
["b", "b", "f"],
["c", "a", "a"],
["c", "a", "a"]
];
let lstData = [];
for (let num = 1; num <= 5; num++) {
ExcelUtil.writeHeader(lstHeader, 22);
for (let i = 0, rowIndex = 3; i < num * 50 + num; i++, rowIndex += 2) {
for (let j = 0; j < 5; j++) {
ExcelUtil.writeCellData(rowIndex, j, "test");
ExcelUtil.writeCellData(rowIndex + 1, j, "demo");
}
}
let sheet = ExcelUtil.getDataSheet();
lstData.push({sheet: sheet, name: "Sheet" + num});
}
ExcelUtil.write(lstData, "C:\\Users\\admin\\Desktop\\demo-" + Date.now() + ".xlsx");
}
}
实体类
- 单元格坐标实体
export class Coords {
private _x: number;
private _y: number;
private _value: string;
private constructor(lstHeader: Array<Array<string>>, row: number, col: number) {
this._x = row;
this._y = col;
this._value = lstHeader[col][row];
}
public static of(lstHeader: Array<Array<string>>, row: number, col: number): Coords {
return new Coords(lstHeader, row, col);
}
get x(): number {
return this._x;
}
set x(value: number) {
this._x = value;
}
get y(): number {
return this._y;
}
set y(value: number) {
this._y = value;
}
get value(): string {
return this._value;
}
set value(value: string) {
this._value = value;
}
}
- 日志记录实体
export class Log {
private _className: string;
private _methodName: string;
private _paramName: string;
private _value: any;
private constructor(className: string, methodName: string, paramName: string, value: any) {
this._className = className;
this._methodName = methodName;
this._paramName = paramName;
this._value = value;
}
public static of(className: string, methodName: string, paramName: string, value: any): Log {
return new Log(className, methodName, paramName, value);
}
get className(): string {
return this._className;
}
set className(value: string) {
this._className = value;
}
get methodName(): string {
return this._methodName;
}
set methodName(value: string) {
this._methodName = value;
}
get paramName(): string {
return this._paramName;
}
set paramName(value: string) {
this._paramName = value;
}
get value(): any {
return this._value;
}
set value(value: any) {
this._value = value;
}
}
工具类
- Excel 文件读取、导出
import * as path from "path";
import {Coords} from "./Coords";
import * as XLSX from "xlsx-js-style";
import {CellObject, CellStyle, ColInfo, Range, RowInfo, WorkBook, WorkSheet} from "xlsx-js-style";
export class ExcelUtil {
//行高
private static height: number = 36;
//数据行
private static dataRow: number = -1;
//自动设置列宽
private static widthAuto: boolean = false;
//单元格合并列表
private static lstRange: Array<Range> = [];
//列宽列表
private static lstColInfo: Array<ColInfo> = [];
//行高列表
private static lstRowInfo: Array<RowInfo> = [];
//列表冻结
private static freezeInfo: Record<string, any> = {};
//单元格数据列表
private static lstValue: Array<Array<CellObject>> = [];
//样式列表
private static lstCellStyle: Array<CellStyle> = [];
//移动方向:上、右、下、左
private static readonly MOVE: Array<Array<number>> = [[0, 1], [1, 0], [0, -1], [-1, 0]];
//获取excel表数据
public static toMap(excelName: string, sheetName: string | number, headerRow?: number, headerCol?: number, headerLastCol?: number, dataRow?: number, dataLastRow?: number, extraData?: Map<string, string>): Array<Map<string, string>> {
let workbook = XLSX.readFile(excelName);
if (typeof sheetName === "number") {
sheetName = workbook.SheetNames[sheetName];
}
let sheet = workbook.Sheets[sheetName];
//表数据转数组数据
let sheetJson = <Array<Array<any>>>XLSX.utils.sheet_to_json(sheet, {header: 1});
//数据初始化
headerRow = headerRow || 0;
headerCol = headerCol || 0;
dataRow = dataRow || headerRow + 1;
this.lstRange = sheet["!merges"] || [];
dataLastRow = dataLastRow || sheetJson.length;
extraData = extraData || new Map<string, string>();
headerLastCol = headerLastCol || sheetJson[headerRow].length;
//列号列表
let lstHeaderCol: Array<number> = [];
for (let i = headerCol; i < headerLastCol; i++) {
lstHeaderCol.push(i);
}
//行号列表
let lstDataRow: Array<number> = [];
for (let i = dataRow; i < dataLastRow; i++) {
lstDataRow.push(i);
}
return this.getSheetData(sheetJson, headerRow, lstHeaderCol, lstDataRow, extraData);
}
//excel表数据解析
private static getSheetData(sheet: Array<Array<any>>, headerRow: number, headerCol: Array<number>, dataRow: Array<number>, extraData: Map<string, string>): Array<Map<string, string>> {
//获取列头与列号的对应关系
let mapHeader = new Map<string, number>();
for (let col of headerCol) {
this.getSheetHeader(sheet, mapHeader, headerRow, col);
}
//根据列头与列号关系解析表格数据
let lstData = new Array<Map<string, string>>;
for (let row of dataRow) {
let map = this.getSheetBody(sheet, mapHeader, row);
if (map.size == 0) continue;
//添加外部数据
extraData.forEach((v, k) => map.set(k, v));
lstData.push(map);
}
return lstData;
}
//列头与列号对应关系
private static getSheetHeader(sheet: Array<Array<any>>, mapHeader: Map<string, number>, row: number, col: number) {
//格式化字符串数据
let value: string = (sheet[row][col] + "").trim();
mapHeader.set(value, col);
}
//获取与列头对应的列数据
private static getSheetBody(sheet: Array<Array<any>>, mapHeader: Map<string, number>, row: number): Map<string, string> {
let map = new Map<string, string>;
if (row > sheet.length) {
return map;
}
for (let [header, col] of mapHeader) {
//格式化字符串数据
let value = (sheet[row][col] + "").trim();
if (value.length == 0) {
//尝试获取单元格数据
value = this.getMergedCellValue(sheet, row, col);
}
if (value.length == 0) continue;
map.set(header, value);
}
return map;
}
//获取合并单元格数据
private static getMergedCellValue(sheet: Array<Array<any>>, row: number, col: number): string {
for (let range of this.lstRange) {
let minCell = range.s;
let maxCell = range.e;
if (minCell.r <= row && row <= maxCell.r &&
minCell.c <= col && col <= maxCell.c) {
//格式化字符串数据
return (sheet[minCell.r][minCell.c] + "").trim();
}
}
return "";
}
//行高、列宽初始化
private static init(rowSize: number, colSize: number): void {
this.dataRow = -1;
this.lstRange = [];
this.lstValue = [];
this.lstColInfo = [];
this.lstRowInfo = [];
this.lstCellStyle = [];
for (let i = 0; i < rowSize; i++) {
this.lstValue.push([]);
this.lstRowInfo.push({});
for (let j = 0; j < colSize; j++) {
if (i == 0) {
this.lstColInfo.push({});
}
this.lstValue[i].push({v: "", t: "s"});
}
}
this.initCellStyle();
}
//样式列表初始化
private static initCellStyle(): void {
let lstColor = ["F2F2F2", "FAFAFA", "D4D4D4"];
this.lstCellStyle.push(this.chooseCellStyle(lstColor, 0));
this.lstCellStyle.push(this.chooseCellStyle(lstColor, 1));
this.lstCellStyle.push(this.chooseCellStyle(lstColor, 2));
}
//单元格样式选择
private static chooseCellStyle(lstColor: Array<string>, choose: number): CellStyle {
let cellStyle: CellStyle = {};
cellStyle.alignment = {};
cellStyle.border = {};
cellStyle.font = {};
if (choose == 0) {
//设置表头字体样式:粗体、大小
cellStyle.font = {bold: true, sz: 11};
//设置背景色
cellStyle.fill = {fgColor: {rgb: lstColor[0]}, patternType: "solid"};
} else if (choose == 1) {
//设置数据显示字体样式:大小
cellStyle.font = {sz: 10};
//设置背景色
cellStyle.fill = {fgColor: {rgb: lstColor[1]}, patternType: "solid"};
} else if (choose == 2) {
//设置数据显示字体样式:大小
cellStyle.font = {sz: 10};
}
//设置字体: 微软雅黑
cellStyle.font.name = "Microsoft YaHei";
//设置居中:垂直居中、水平居中
cellStyle.alignment.vertical = "center";
cellStyle.alignment.horizontal = "center";
//设置边框宽度、颜色
cellStyle.border.top = {style: "thin", color: {rgb: lstColor[2]}};
cellStyle.border.bottom = {style: "thin", color: {rgb: lstColor[2]}};
cellStyle.border.left = {style: "thin", color: {rgb: lstColor[2]}};
cellStyle.border.right = {style: "thin", color: {rgb: lstColor[2]}};
return cellStyle;
}
//表格数据写入
public static writeCellData(rowIndex: number, colIndex: number, cellData: string): void {
//写入单元格数据并设置单元格样式
let cellStyle = this.getCellStyle(this.dataRow, rowIndex);
if (this.widthAuto) {
//自动设置列宽
this.lstColInfo[colIndex] = {wch: this.getWidthColByAuto(colIndex, cellData)};
}
this.saveCellValue(rowIndex, cellStyle, cellData);
if (colIndex == 0) {
//设置行高
this.lstRowInfo[rowIndex] = {hpt: this.height};
}
}
//表头数据写入
public static writeHeader(lstHeader: Array<Array<string>>, widthCol?: number, dataCol?: number, lstExcludeRow?: Array<number>): void {
let colSize = lstHeader.length;
let rowSize = lstHeader[0].length;
this.init(rowSize, colSize);
this.dataRow = rowSize;
this.widthAuto = widthCol == null;
let lstFlag = this.getFlagArray(rowSize, colSize);
for (let row = 0; row < rowSize; row++) {
for (let col = 0; col < colSize; col++) {
if (!lstFlag[row][col]) {
lstFlag[row][col] = true;
let lstCoords = new Array<Coords>();
lstCoords.push(Coords.of(lstHeader, row, col));
if (lstExcludeRow == null || lstExcludeRow.length == 0 || !lstExcludeRow.includes(row)) {
this.checkMergeRange(lstHeader, lstFlag, lstCoords, row, col, lstHeader[col][row]);
}
this.merge(lstCoords, widthCol);
}
}
}
//单元格冻结:从上往下,冻结 dataRow 行;从左往右,冻结 dataCol 列
this.freezeInfo = {xSplit: dataCol || 0 + "", ySplit: this.dataRow + "", activePane: "bottomRight", state: "frozen"};
}
//单元格合并范围递归检查
private static checkMergeRange(lstHeader: Array<Array<string>>, lstFlag: Array<Array<boolean>>, lstCoords: Array<Coords>, x: number, y: number, value: string): void {
let colSize = lstHeader.length;
let rowSize = lstHeader[0].length;
for (let move of this.MOVE) {
let moveX = x + move[0];
let moveY = y + move[1];
if (0 <= moveX && moveX < rowSize && 0 <= moveY && moveY < colSize && !lstFlag[moveX][moveY]) {
if (lstHeader[moveY][moveX] === value) {
lstFlag[moveX][moveY] = true;
lstCoords.push(Coords.of(lstHeader, moveX, moveY));
this.checkMergeRange(lstHeader, lstFlag, lstCoords, moveX, moveY, value);
}
}
}
}
//单元格合并
private static merge(lstCoords: Array<Coords>, widthCol?: number): void {
//单元格坐标排序
lstCoords.sort((c1, c2) => {
if (c1.x > c2.x || c1.y > c2.y) {
return 1;
} else if (c1.x < c2.x || c1.y < c2.y) {
return -1;
} else {
return 0;
}
});
for (let coords of lstCoords) {
//设置列宽
this.lstColInfo[coords.y] = {wch: widthCol || this.getWidthColByAuto(coords.y, coords.value)};
//设置行高
this.lstRowInfo[coords.x] = {hpt: this.height};
//写入单元格数据并设置单元格样式
let cellStyle = this.getCellStyle(this.dataRow, coords.x);
this.saveHeaderCellValue(coords.x, coords.y, cellStyle, coords.value);
}
//合并单元格
if (lstCoords.length > 1) {
let minCoords = lstCoords[0];
let maxCoords = lstCoords[lstCoords.length - 1];
this.lstRange.push({s: {r: minCoords.x, c: minCoords.y}, e: {r: maxCoords.x, c: maxCoords.y}})
}
}
//自动设置列宽
private static getWidthColByAuto(col: number, value: string): number {
let widthCol = this.lstColInfo[col].wch || 0;
let tempWidthCol = this.getWidthCol(value);
if (widthCol < tempWidthCol) {
widthCol = tempWidthCol;
}
return widthCol;
}
//根据字符串获取列宽
private static getWidthCol(value: string): number {
let chineseSum = 0;
let englishSum = 0;
let charSum = 0;
for (let i = 0; i < value.length; i++) {
if (this.isChineseChar(value.charCodeAt(i))) {
chineseSum += 2;
charSum += 2;
} else {
englishSum += 1;
charSum += 1;
}
}
charSum = charSum > 1 ? 13 + charSum - 1 : 13;
if (chineseSum == 0 && englishSum > 0) {
charSum += 4;
} else if (chineseSum > 0 && englishSum > 0) {
let percent = englishSum / chineseSum;
if (percent < 0.2) {
charSum -= 2;
} else if (percent > 0.8) {
charSum += 2;
}
}
return charSum;
}
//判断是否为中文字符
private static isChineseChar(charCode: number): boolean {
return !(0 <= charCode && charCode <= 128);
}
//表头单元格数据写入
private static saveHeaderCellValue(rowIndex: number, colIndex: number, cellStyle: CellStyle, value: string): void {
this.lstValue[rowIndex][colIndex] = {v: value, t: "s", s: cellStyle};
}
//表格单元格数据写入
private static saveCellValue(rowIndex: number, cellStyle: CellStyle, value: string): void {
if (this.lstValue.length === rowIndex) {
this.lstValue.push([]);
}
this.lstValue[rowIndex].push({v: value, t: "s", s: cellStyle});
}
//表头标记矩阵初始化
private static getFlagArray(rowSize: number, colSize: number): Array<Array<boolean>> {
let lstFlag = new Array<Array<boolean>>();
for (let i = 0; i < rowSize; i++) {
let flags = new Array<boolean>();
for (let j = 0; j < colSize; j++) {
flags.push(false);
}
lstFlag.push(flags);
}
return lstFlag;
}
//获取单元格样式
private static getCellStyle(dataRow: number, rowIndex: number): CellStyle {
//单元格样式:带斑马纹表格
let cellStyle;
if (dataRow === -1 || rowIndex < dataRow) {
//表头样式
cellStyle = this.lstCellStyle[0];
} else {
//数据样式
if (dataRow % 2 == 0) {
if (rowIndex % 2 == 0) {
cellStyle = this.lstCellStyle[2];
} else {
cellStyle = this.lstCellStyle[1];
}
} else {
if (rowIndex % 2 != 0) {
cellStyle = this.lstCellStyle[2];
} else {
cellStyle = this.lstCellStyle[1];
}
}
}
return cellStyle;
}
//数据写入到表中
public static getDataSheet(): WorkSheet {
let sheet = XLSX.utils.aoa_to_sheet(this.lstValue);
sheet["!rows"] = this.lstRowInfo;
sheet["!cols"] = this.lstColInfo;
sheet["!merges"] = this.lstRange;
sheet["!freeze"] = this.freezeInfo;
return sheet;
}
//导出excel文件
public static write(lstData: Array<Record<string, any>>, fileName: string): void {
let workbook: WorkBook = XLSX.utils.book_new();
for (let data of lstData) {
let name = <string>data.name;
let sheet = <WorkSheet>data.sheet;
XLSX.utils.book_append_sheet(workbook, sheet, name);
}
// XLSX.writeFile(workbook, fileName);
let XLSX_STYLE = require(path.join(
__dirname,
"./xlsx-style/xlsx.js"
));
XLSX_STYLE.writeFile(workbook, fileName);
}
}
- 日志打印
import {Log} from "./Log";
export class LogUtil {
private static readonly enable = true;
private constructor() {
}
public static loggerLine(log: Log): void {
if (this.enable) {
console.log("[" + log.className + "] " + log.methodName + " -> " + log.paramName + ": ", log.value);
}
}
public static logger(log: Log): void {
if (this.enable) {
process.stdout.write("[" + log.className + "] " + log.methodName + " -> " + log.paramName + ": " + log.value);
}
}
}
- 异步执行
export class AsyncUtil {
private constructor() {
}
public static execWithListDataByAsync<T>(lstData: Array<T>, consumer: (data: T) => void): Promise<void> {
return new Promise(resolve => {
let lstPromise: Array<Promise<void>> = [];
for (let data of lstData) {
lstPromise.push(new Promise(resolve => {
consumer(data);
resolve();
}));
}
Promise.all(lstPromise).then(() => {
resolve();
});
});
}
}
编码感想
- 要贪玩
- 要开心