feat: CRUD 支持导出 Excel 模板 Closes #9157 (#9228)

* feat: CRUD 支持导出 Excel 模板 Closes #9157

* 更新版本
This commit is contained in:
吴多益 2024-01-05 15:04:02 +08:00 committed by GitHub
parent eb5299e645
commit eb55199ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 32 deletions

View File

@ -2743,6 +2743,49 @@ interface CRUDMatchFunc {
"filename": "自定义文件名${test}",
"api": "/api/mock2/sample"
}],
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade"
}
]`
}
```
### 导出 Excel 模板
> 6.1 及以上版本
配置是 `export-excel-template` 和前面 `export-excel` 不同,这个功能只导出表头,主要用于线下填数据,可以配合 input-excel 组件来上传填好的内容。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"headerToolbar": [{
"type": "export-excel-template",
"label": "导出 Excel 模板",
}],
"columns": [
{
"name": "id",

View File

@ -2,7 +2,7 @@ export default {
title: 'CSV 导出的是原始数据,而 Excel 是尽可能还原展现效果',
body: {
type: 'crud',
headerToolbar: ['export-excel', 'export-csv'],
headerToolbar: ['export-excel', 'export-excel-template', 'export-csv'],
data: {
mapping_type: {
'*': '其他'

View File

@ -38,6 +38,7 @@ register('de-DE', {
'Copyable.tip': 'Kopieren',
'CRUD.exportCSV': 'In CSV exportieren',
'CRUD.exportExcel': 'In Excel exportieren',
'CRUD.exportExcelTemplate': 'In Excel-Vorlage exportieren',
'CRUD.fetchFailed': 'Fehler beim Abrufen',
'CRUD.filter': 'Filtern',
'CRUD.selected': 'Ausgewählte {{total}} Elemente: ',

View File

@ -33,6 +33,7 @@ register('en-US', {
'Copyable.tip': 'Copy',
'CRUD.exportCSV': 'Export CSV',
'CRUD.exportExcel': 'Export Excel',
'CRUD.exportExcelTemplate': 'Export Excel Template',
'CRUD.fetchFailed': 'Fetch failed',
'CRUD.filter': 'Filter',
'CRUD.selected': 'selected {{total}} items: ',

View File

@ -36,6 +36,7 @@ register('zh-CN', {
'Copyable.tip': '点击复制',
'CRUD.exportCSV': '导出 CSV',
'CRUD.exportExcel': '导出 Excel',
'CRUD.exportExcelTemplate': '导出 Excel 模板',
'CRUD.fetchFailed': '获取失败',
'CRUD.filter': '筛选',
'CRUD.selected': '已选{{total}}条:',

View File

@ -46,7 +46,7 @@
"echarts": "5.4.0",
"echarts-stat": "^1.2.0",
"echarts-wordcloud": "^2.1.0",
"exceljs": "^4.3.0",
"exceljs": "^4.4.0",
"file-saver": "^2.0.2",
"hls.js": "1.1.3",
"hoist-non-react-statics": "^3.3.2",
@ -244,4 +244,4 @@
"react-dom": ">=16.8.6"
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}
}

View File

@ -206,10 +206,56 @@ const renderSummary = (
return rowIndex;
};
/**
* map
* @param remoteMappingCache
* @param env mobx env
* @param column
* @param data
* @param rowData
* @returns
*/
async function getMap(
remoteMappingCache: any,
env: any,
column: any,
data: any,
rowData: any
) {
let map = column.pristine.map as Record<string, any>;
const source = column.pristine.source;
if (source) {
let sourceValue = source;
if (isPureVariable(source)) {
map = resolveVariableAndFilter(source as string, rowData, '| raw');
} else if (isEffectiveApi(source, data)) {
const mapKey = JSON.stringify(source);
if (mapKey in remoteMappingCache) {
map = remoteMappingCache[mapKey];
} else {
const res = await env.fetcher(sourceValue, rowData);
if (res.data) {
remoteMappingCache[mapKey] = res.data;
map = res.data;
}
}
}
}
return map;
}
/**
* Excel
* @param ExcelJS ExcelJS
* @param props Table props
* @param toolbar Excel toolbar
* @param withoutData true
*/
export async function exportExcel(
ExcelJS: any,
props: TableProps,
toolbar: ExportExcelToolbar
toolbar: ExportExcelToolbar,
withoutData: boolean = false
) {
const {
store,
@ -333,6 +379,17 @@ export async function exportExcel(
column: firstRowLabels.length
}
};
if (withoutData) {
return exportExcelWithoutData(
workbook,
worksheet,
filteredColumns,
filename,
env,
data
);
}
// 用于 mapping source 的情况
const remoteMappingCache: any = {};
// 数据从第二行开始
@ -443,27 +500,10 @@ export async function exportExcel(
hyperlink: absoluteURL
};
} else if (type === 'mapping' || (type as any) === 'static-mapping') {
let map = column.pristine.map;
let map = await getMap(remoteMappingCache, env, column, data, rowData);
const valueField = column.pristine.valueField || 'value';
const labelField = column.pristine.labelField || 'label';
const source = column.pristine.source;
if (source) {
let sourceValue = source;
if (isPureVariable(source)) {
map = resolveVariableAndFilter(source as string, rowData, '| raw');
} else if (isEffectiveApi(source, data)) {
const mapKey = JSON.stringify(source);
if (mapKey in remoteMappingCache) {
map = remoteMappingCache[mapKey];
} else {
const res = await env.fetcher(sourceValue, rowData);
if (res.data) {
remoteMappingCache[mapKey] = res.data;
map = res.data;
}
}
}
}
if (Array.isArray(map)) {
map = map.reduce((res, now) => {
@ -575,6 +615,10 @@ export async function exportExcel(
// 后置总结行
renderSummary(worksheet, data, affixRow, rowIndex);
downloadFile(workbook, filename);
}
async function downloadFile(workbook: any, filename: string) {
const buffer = await workbook.xlsx.writeBuffer();
if (buffer) {
@ -584,3 +628,47 @@ export async function exportExcel(
saveAs(blob, filename + '.xlsx');
}
}
function numberToLetters(num: number) {
let letters = '';
while (num >= 0) {
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[num % 26] + letters;
num = Math.floor(num / 26) - 1;
}
return letters;
}
/**
*
*/
async function exportExcelWithoutData(
workbook: any,
worksheet: any,
filteredColumns: any[],
filename: string,
env: any,
data: any
) {
let index = 0;
const rowNumber = 100;
const mapCache: any = {};
for (const column of filteredColumns) {
index += 1;
if (column.pristine?.type === 'mapping') {
const map = await getMap(mapCache, env, column, data, {});
if (map && isObject(map)) {
const keys = Object.keys(map);
for (let rowIndex = 1; rowIndex < rowNumber; rowIndex++) {
worksheet.getCell(numberToLetters(index) + rowIndex).dataValidation =
{
type: 'list',
allowBlank: true,
formulae: [`"${keys.join(',')}"`]
};
}
}
}
}
downloadFile(workbook, filename);
}

View File

@ -2215,6 +2215,9 @@ export default class Table extends React.Component<TableProps, object> {
} else if (type === 'export-excel') {
this.renderedToolbars.push(type);
return this.renderExportExcel(toolbar);
} else if (type === 'export-excel-template') {
this.renderedToolbars.push(type);
return this.renderExportExcelTemplate(toolbar);
}
return void 0;
@ -2377,15 +2380,7 @@ export default class Table extends React.Component<TableProps, object> {
}
renderExportExcel(toolbar: ExportExcelToolbar) {
const {
store,
env,
classPrefix: ns,
classnames: cx,
translate: __,
data,
render
} = this.props;
const {store, translate: __, render} = this.props;
let columns = store.filteredColumns || [];
if (!columns) {
@ -2418,6 +2413,38 @@ export default class Table extends React.Component<TableProps, object> {
);
}
/**
* Excel
*/
renderExportExcelTemplate(toolbar: ExportExcelToolbar) {
const {store, translate: __, render} = this.props;
let columns = store.filteredColumns || [];
if (!columns) {
return null;
}
return render(
'exportExcelTemplate',
{
label: __('CRUD.exportExcelTemplate'),
...(toolbar as any),
type: 'button'
},
{
onAction: () => {
import('exceljs').then(async (ExcelJS: any) => {
try {
await exportExcel(ExcelJS, this.props, toolbar, true);
} catch (error) {
console.error(error);
}
});
}
}
);
}
renderActions(region: string) {
let {actions, render, store, classnames: cx, data} = this.props;