Files
nocobase/packages/plugins/@nocobase/plugin-action-export/src/server/xlsx-exporter.ts
T
ChengLei Shao 786e1e4f25 fix: export with decimal field (#5316)
* fix: export with decimal field

* fix: test
2024-09-26 07:12:03 +08:00

222 lines
6.2 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import {
FindOptions,
ICollection,
ICollectionManager,
IField,
IModel,
IRelationField,
} from '@nocobase/data-source-manager';
import XLSX from 'xlsx';
import { deepGet } from './utils/deep-get';
import { NumberField } from '@nocobase/database';
type ExportColumn = {
dataIndex: Array<string>;
title?: string;
defaultTitle: string;
};
type ExportOptions = {
collectionManager: ICollectionManager;
collection: ICollection;
repository?: any;
columns: Array<ExportColumn>;
findOptions?: FindOptions;
chunkSize?: number;
};
class XlsxExporter {
/**
* You can adjust the maximum number of exported rows based on business needs and system
* available resources. However, please note that you need to fully understand the risks
* after the modification. Increasing the maximum number of rows that can be exported may
* increase system resource usage, leading to increased processing delays for other
* requests, or even server processes being recycled by the operating system.
*
* 您可以根据业务需求和系统可用资源等参数,调整最大导出数量的限制。但请注意,您需要充分了解修改之后的风险,
* 增加最大可导出的行数可能会导致系统资源占用率升高,导致其他请求处理延迟增加、无法处理、甚至
* 服务端进程被操作系统回收等问题。
*/
limit = process.env['EXPORT_LIMIT'] ? parseInt(process.env['EXPORT_LIMIT']) : 2000;
constructor(private options: ExportOptions) {}
async run(ctx?): Promise<XLSX.WorkBook> {
const { collection, columns, chunkSize, repository } = this.options;
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.sheet_new();
// write headers
XLSX.utils.sheet_add_aoa(worksheet, [this.renderHeaders()], {
origin: 'A1',
});
let startRowNumber = 2;
await (repository || collection.repository).chunk({
...this.getFindOptions(),
chunkSize: chunkSize || 200,
callback: async (rows, options) => {
const chunkData = rows.map((r) => {
return columns.map((col) => {
return this.renderCellValue(r, col, ctx);
});
});
XLSX.utils.sheet_add_aoa(worksheet, chunkData, {
origin: `A${startRowNumber}`,
});
startRowNumber += rows.length;
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
},
});
for (const col of columns) {
const field = this.findFieldByDataIndex(col.dataIndex);
if (field instanceof NumberField) {
// set column cell type to number
const colIndex = columns.indexOf(col);
const cellRange = XLSX.utils.decode_range(worksheet['!ref']);
for (let r = 1; r <= cellRange.e.r; r++) {
const cell = worksheet[XLSX.utils.encode_cell({ c: colIndex, r })];
// if cell and cell.v is a number, set cell.t to 'n'
if (cell && isNumeric(cell.v)) {
cell.t = 'n';
}
}
}
}
XLSX.utils.book_append_sheet(workbook, worksheet, 'Data');
return workbook;
}
private getAppendOptionsFromColumns() {
return this.options.columns
.map((col) => {
if (col.dataIndex.length > 1) {
return col.dataIndex.join('.');
}
const field = this.options.collection.getField(col.dataIndex[0]);
if (!field) {
throw new Error(`Field "${col.dataIndex[0]}" not found: , please check the columns configuration.`);
}
if (field.isRelationField()) {
return col.dataIndex[0];
}
return null;
})
.filter(Boolean);
}
private getFindOptions() {
const { findOptions = {} } = this.options;
findOptions.limit = this.limit;
const appendOptions = this.getAppendOptionsFromColumns();
if (appendOptions.length) {
return {
...findOptions,
appends: appendOptions,
};
}
return findOptions;
}
private findFieldByDataIndex(dataIndex: Array<string>): IField {
const { collection } = this.options;
const currentField = collection.getField(dataIndex[0]);
if (dataIndex.length > 1) {
let targetCollection: ICollection;
for (let i = 0; i < dataIndex.length; i++) {
const isLast = i === dataIndex.length - 1;
if (isLast) {
return targetCollection.getField(dataIndex[i]);
}
targetCollection = (currentField as IRelationField).targetCollection();
}
}
return currentField;
}
private renderHeaders() {
return this.options.columns.map((col) => {
const field = this.findFieldByDataIndex(col.dataIndex);
if (col.title) {
return col.title;
}
return field?.options.title || col.defaultTitle;
});
}
private renderRawValue(value) {
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value);
}
return value;
}
private getFieldRenderer(field?: IField, ctx?): (value) => any {
const InterfaceClass = this.options.collectionManager.getFieldInterface(field?.options?.interface);
if (!InterfaceClass) {
return this.renderRawValue;
}
const fieldInternface = new InterfaceClass(field?.options);
return (value) => fieldInternface.toString(value, ctx);
}
private renderCellValue(rowData: IModel, column: ExportColumn, ctx?) {
const { dataIndex } = column;
rowData = rowData.toJSON();
const value = rowData[dataIndex[0]];
const field = this.findFieldByDataIndex(dataIndex);
const render = this.getFieldRenderer(field, ctx);
if (dataIndex.length > 1) {
const deepValue = deepGet(rowData, dataIndex);
if (Array.isArray(deepValue)) {
return deepValue.map(render).join(',');
}
return render(deepValue);
}
return render(value);
}
}
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
export default XlsxExporter;