mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
表格支持导出为 Excel (#1077)
This commit is contained in:
parent
550bebef52
commit
12be84bd61
@ -1232,39 +1232,17 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在
|
||||
|
||||
在`headerToolbar`或者`footerToolbar`数组中添加`export-csv`字符串,可以实现点击下载 CSV 的功能,注意这里只包括当前分页的数据,要下载全部数据需要通过后端 API 实现。
|
||||
|
||||
```schema:height="600" scope="body"
|
||||
{
|
||||
"type": "crud",
|
||||
"api": "https://houtai.baidu.com/api/sample",
|
||||
"headerToolbar": ["export-csv"],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
可以在[示例](../../examples/crud/export-excel-csv)中进行测试。
|
||||
|
||||
### 导出 Excel
|
||||
|
||||
在`headerToolbar`或者`footerToolbar`数组中添加`export-excel`字符串,可以实现点击下载 Excel 的功能,和导出 CSV 一样只包括当前分页的数据,但它们有明显区别:
|
||||
|
||||
1. 导出 CSV 是将 api 返回数据导出,表头是数据里的 key,而 Excel 的表头使用的是 label。
|
||||
2. 导出 Excel 更重视展现一致,支持合并单元格、链接、mapping 映射、图片(需要加[跨域 Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS))。
|
||||
3. 导出 Excel 只在 `mode` 为 `table` 时能用。
|
||||
|
||||
可以在[示例](../../examples/crud/export-excel-csv)中进行测试。
|
||||
|
||||
### 显隐显示查询条件表单
|
||||
|
||||
|
142
examples/components/CRUD/ExportCSVExcel.jsx
Normal file
142
examples/components/CRUD/ExportCSVExcel.jsx
Normal file
@ -0,0 +1,142 @@
|
||||
export default {
|
||||
$schema: 'https://houtai.baidu.com/v2/schemas/page.json#',
|
||||
title: 'CSV 导出的是原始数据,而 Excel 是尽可能还原展现效果',
|
||||
body: {
|
||||
type: 'crud',
|
||||
headerToolbar: ['export-excel', 'export-csv'],
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
icon: __uri('../../static/ie.png'),
|
||||
link: 'https://www.microsoft.com/',
|
||||
engine: 'Trident',
|
||||
browser: 'Internet Explorer 4.2',
|
||||
platform: 'Win 95+',
|
||||
version: '4',
|
||||
grade: 'A'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/ie.png'),
|
||||
link: 'https://www.microsoft.com/',
|
||||
engine: 'Trident',
|
||||
browser: 'Internet Explorer 4.2',
|
||||
platform: 'Win 95+',
|
||||
version: '4',
|
||||
grade: 'B'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/ie.png'),
|
||||
link: 'https://www.microsoft.com/',
|
||||
engine: 'Trident',
|
||||
browser: 'AOL browser (AOL desktop)',
|
||||
platform: 'Win 95+',
|
||||
version: '4',
|
||||
grade: 'C'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/ie.png'),
|
||||
link: 'https://www.microsoft.com/',
|
||||
engine: 'Trident',
|
||||
browser: 'AOL browser (AOL desktop)',
|
||||
platform: 'Win 98',
|
||||
version: '3',
|
||||
grade: 'A'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/ie.png'),
|
||||
link: 'https://www.microsoft.com/',
|
||||
engine: 'Trident',
|
||||
browser: 'AOL browser (AOL desktop)',
|
||||
platform: 'Win 98',
|
||||
version: '4',
|
||||
grade: 'A'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/firefox.png'),
|
||||
link: 'https://www.mozilla.org/',
|
||||
engine: 'Gecko',
|
||||
browser: 'Firefox 1.0',
|
||||
platform: 'Win 98+ / OSX.2+',
|
||||
version: '4',
|
||||
grade: 'A'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/firefox.png'),
|
||||
link: 'https://www.mozilla.org/',
|
||||
engine: 'Gecko',
|
||||
browser: 'Firefox 1.0',
|
||||
platform: 'Win 98+ / OSX.2+',
|
||||
version: '5',
|
||||
grade: 'A'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/firefox.png'),
|
||||
link: 'https://www.mozilla.org/',
|
||||
engine: 'Gecko',
|
||||
browser: 'Firefox 2.0',
|
||||
platform: 'Win 98+ / OSX.2+',
|
||||
version: '5',
|
||||
grade: 'B'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/firefox.png'),
|
||||
link: 'https://www.mozilla.org/',
|
||||
engine: 'Gecko',
|
||||
browser: 'Firefox 2.0',
|
||||
platform: 'Win 98+ / OSX.2+',
|
||||
version: '5',
|
||||
grade: 'C'
|
||||
},
|
||||
{
|
||||
icon: __uri('../../static/firefox.png'),
|
||||
link: 'https://www.mozilla.org/',
|
||||
engine: 'Gecko',
|
||||
browser: 'Firefox 2.0',
|
||||
platform: 'Win 98+ / OSX.2+',
|
||||
version: '5',
|
||||
grade: 'D'
|
||||
}
|
||||
]
|
||||
},
|
||||
combineNum: 3,
|
||||
columns: [
|
||||
{
|
||||
name: 'icon',
|
||||
label: '图标',
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
label: '官网',
|
||||
type: 'link'
|
||||
},
|
||||
{
|
||||
name: 'engine',
|
||||
label: '引擎'
|
||||
},
|
||||
{
|
||||
name: 'browser',
|
||||
label: '浏览器'
|
||||
},
|
||||
{
|
||||
name: 'platform',
|
||||
label: '操作系统'
|
||||
},
|
||||
{
|
||||
name: 'version',
|
||||
label: '引擎版本'
|
||||
},
|
||||
{
|
||||
name: 'grade',
|
||||
label: 'CSS等级',
|
||||
type: 'mapping',
|
||||
map: {
|
||||
A: '优',
|
||||
B: '中',
|
||||
C: '差',
|
||||
D: '极差'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
@ -46,6 +46,7 @@ import MergeCellSchema from './CRUD/MergeCell';
|
||||
import HeaderGroupSchema from './CRUD/HeaderGroup';
|
||||
import HeaderHideSchema from './CRUD/HeaderHide';
|
||||
import LoadOnceTableCrudSchema from './CRUD/LoadOnce';
|
||||
import ExportCSVExcelSchema from './CRUD/ExportCSVExcel';
|
||||
import SdkTest from './Sdk/Test';
|
||||
import JSONSchemaForm from './Form/Schem';
|
||||
import SimpleDialogSchema from './Dialog/Simple';
|
||||
@ -344,6 +345,11 @@ export const examples = [
|
||||
label: '一次性加载',
|
||||
path: '/examples/crud/load-once',
|
||||
component: makeSchemaRenderer(LoadOnceTableCrudSchema)
|
||||
},
|
||||
{
|
||||
label: '导出 Excel/CSV',
|
||||
path: '/examples/crud/export-excel-csv',
|
||||
component: makeSchemaRenderer(ExportCSVExcelSchema)
|
||||
}
|
||||
// {
|
||||
// label: '测试',
|
||||
|
BIN
examples/static/firefox.png
Normal file
BIN
examples/static/firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
examples/static/ie.png
Normal file
BIN
examples/static/ie.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
14
fis-conf.js
14
fis-conf.js
@ -34,6 +34,7 @@ fis.set('project.files', [
|
||||
'scss/**.scss',
|
||||
'/examples/*.html',
|
||||
'/examples/*.tpl',
|
||||
'/examples/static/*.png',
|
||||
'/src/**.html',
|
||||
'mock/**'
|
||||
]);
|
||||
@ -388,6 +389,7 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
'!zrender/**',
|
||||
'!echarts/**',
|
||||
'!papaparse/**',
|
||||
'!exceljs/**',
|
||||
'!docsearch.js/**',
|
||||
'!monaco-editor/**.css',
|
||||
'!src/components/RichText.tsx',
|
||||
@ -405,6 +407,8 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
|
||||
'papaparse.js': ['papaparse/**'],
|
||||
|
||||
'exceljs.js': ['exceljs/**'],
|
||||
|
||||
'charts.js': ['zrender/**', 'echarts/**'],
|
||||
|
||||
'rest.js': [
|
||||
@ -417,7 +421,8 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
'!jquery/**',
|
||||
'!zrender/**',
|
||||
'!echarts/**',
|
||||
'!papaparse/**'
|
||||
'!papaparse/**',
|
||||
'!exceljs/**'
|
||||
]
|
||||
}),
|
||||
postpackager: [
|
||||
@ -578,7 +583,8 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
'!jquery/**',
|
||||
'!zrender/**',
|
||||
'!echarts/**',
|
||||
'!papaparse/**'
|
||||
'!papaparse/**',
|
||||
'!exceljs/**'
|
||||
],
|
||||
'pkg/rich-text.js': [
|
||||
'src/components/RichText.js',
|
||||
@ -588,6 +594,7 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
'pkg/tinymce.js': ['src/components/Tinymce.tsx', 'tinymce/**'],
|
||||
'pkg/charts.js': ['zrender/**', 'echarts/**'],
|
||||
'pkg/papaparse.js': ['papaparse/**'],
|
||||
'pkg/exceljs.js': ['exceljs/**'],
|
||||
'pkg/api-mock.js': ['mock/*.ts'],
|
||||
'pkg/app.js': [
|
||||
'/examples/components/App.tsx',
|
||||
@ -605,7 +612,8 @@ if (fis.project.currentMedia() === 'publish') {
|
||||
'!src/components/RichText.js',
|
||||
'!zrender/**',
|
||||
'!echarts/**',
|
||||
'!papaparse/**'
|
||||
'!papaparse/**',
|
||||
'!exceljs/**'
|
||||
],
|
||||
|
||||
'pkg/npm.css': ['node_modules/*/**.css', '!monaco-editor/**'],
|
||||
|
@ -46,6 +46,7 @@
|
||||
"dom-helpers": "^3.3.1",
|
||||
"downshift": "3.1.4",
|
||||
"echarts": "^4.1.0",
|
||||
"exceljs": "^4.2.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"flv.js": "1.5.0",
|
||||
"froala-editor": "2.9.6",
|
||||
|
@ -65,7 +65,8 @@ export type CRUDBultinToolbarType =
|
||||
| 'switch-per-page'
|
||||
| 'load-more'
|
||||
| 'filter-toggler'
|
||||
| 'export-csv';
|
||||
| 'export-csv'
|
||||
| 'export-excel';
|
||||
|
||||
export interface CRUDBultinToolbar extends Omit<BaseSchema, 'type'> {
|
||||
type: CRUDBultinToolbarType;
|
||||
|
@ -39,6 +39,7 @@ import {SchemaPopOver} from '../PopOver';
|
||||
import {SchemaQuickEdit} from '../QuickEdit';
|
||||
import {SchemaCopyable} from '../Copyable';
|
||||
import {SchemaRemark} from '../Remark';
|
||||
import {toDataURL, getImageDimensions} from '../../utils/image';
|
||||
|
||||
/**
|
||||
* 表格列,不指定类型时默认为文本类型。
|
||||
@ -271,6 +272,17 @@ export interface TableProps extends RendererProps {
|
||||
popOverContainer?: any;
|
||||
canAccessSuperData?: boolean;
|
||||
}
|
||||
/**
|
||||
* 将 url 转成绝对地址
|
||||
*/
|
||||
const getAbsoluteUrl = (function () {
|
||||
let link: HTMLAnchorElement;
|
||||
return function (url: string) {
|
||||
if (!link) link = document.createElement('a');
|
||||
link.href = url;
|
||||
return link.href;
|
||||
};
|
||||
})();
|
||||
|
||||
export default class Table extends React.Component<TableProps, object> {
|
||||
static propsList: Array<string> = [
|
||||
@ -1642,6 +1654,8 @@ export default class Table extends React.Component<TableProps, object> {
|
||||
} else if (type === 'drag-toggler') {
|
||||
this.renderedToolbars.push(type);
|
||||
return this.renderDragToggler();
|
||||
} else if (type === 'export-excel') {
|
||||
return this.renderExportExcel();
|
||||
}
|
||||
|
||||
return void 0;
|
||||
@ -1723,6 +1737,169 @@ export default class Table extends React.Component<TableProps, object> {
|
||||
);
|
||||
}
|
||||
|
||||
renderExportExcel() {
|
||||
const {
|
||||
store,
|
||||
classPrefix: ns,
|
||||
classnames: cx,
|
||||
translate: __,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
if (!columns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 按名字快速查找列信息
|
||||
const columnNameMap: {[key: string]: any} = {};
|
||||
for (const column of columns) {
|
||||
if (column.name) {
|
||||
columnNameMap[column.name] = column;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
classPrefix={ns}
|
||||
onClick={() => {
|
||||
(require as any)(['exceljs'], async (ExcelJS: any) => {
|
||||
if (!store.data.items || store.data.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('sheet');
|
||||
worksheet.views = [{state: 'frozen', xSplit: 0, ySplit: 1}];
|
||||
|
||||
const items = store.data.items;
|
||||
// 基于第一行数据来生成列名,所以必须保证第一行数据是完整的
|
||||
const firstRowKeys = Object.keys(items[0]);
|
||||
const firstRowLabels = firstRowKeys.map(key => {
|
||||
if (key in columnNameMap) {
|
||||
return columnNameMap[key].label || key;
|
||||
}
|
||||
return key;
|
||||
});
|
||||
const firstRow = worksheet.getRow(1);
|
||||
firstRow.values = firstRowLabels;
|
||||
worksheet.autoFilter = {
|
||||
from: {
|
||||
row: 1,
|
||||
column: 1
|
||||
},
|
||||
to: {
|
||||
row: 1,
|
||||
column: firstRowKeys.length
|
||||
}
|
||||
};
|
||||
// 数据从第二行开始
|
||||
let rowIndex = 1;
|
||||
for (const row of store.rows) {
|
||||
rowIndex += 1;
|
||||
const sheetRow = worksheet.getRow(rowIndex);
|
||||
let columIndex = 0;
|
||||
for (const key of firstRowKeys) {
|
||||
columIndex += 1;
|
||||
if (!(key in row.data)) {
|
||||
continue;
|
||||
}
|
||||
// 处理合并单元格
|
||||
if (key in row.rowSpans) {
|
||||
if (row.rowSpans[key] === 0) {
|
||||
continue;
|
||||
} else {
|
||||
// start row, start column, end row, end column
|
||||
worksheet.mergeCells(
|
||||
rowIndex,
|
||||
columIndex,
|
||||
rowIndex + row.rowSpans[key] - 1,
|
||||
columIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
const value = row.data[key];
|
||||
const type = columnNameMap[key]?.type || 'plain';
|
||||
if (type === 'image') {
|
||||
const imageData = await toDataURL(value);
|
||||
const imageDimensions = await getImageDimensions(imageData);
|
||||
const imageMatch = imageData.match(/data:image\/(.*);/);
|
||||
let imageExt = 'png';
|
||||
if (imageMatch) {
|
||||
imageExt = imageMatch[1];
|
||||
}
|
||||
// 目前 excel 只支持这些格式,所以其它格式直接输出 url
|
||||
if (
|
||||
imageExt != 'png' &&
|
||||
imageExt != 'jpeg' &&
|
||||
imageExt != 'gif'
|
||||
) {
|
||||
sheetRow.getCell(columIndex).value = value;
|
||||
continue;
|
||||
}
|
||||
const imageId = workbook.addImage({
|
||||
base64: imageData,
|
||||
extension: imageExt
|
||||
});
|
||||
const linkURL = getAbsoluteUrl(value);
|
||||
worksheet.addImage(imageId, {
|
||||
// 这里坐标位置是从 0 开始的,所以要减一
|
||||
tl: {col: columIndex - 1, row: rowIndex - 1},
|
||||
ext: {
|
||||
width: imageDimensions.width,
|
||||
height: imageDimensions.height
|
||||
},
|
||||
hyperlinks: {
|
||||
tooltip: linkURL
|
||||
}
|
||||
});
|
||||
} else if (type == 'link') {
|
||||
const linkURL = getAbsoluteUrl(value);
|
||||
sheetRow.getCell(columIndex).value = {
|
||||
text: value,
|
||||
hyperlink: linkURL
|
||||
};
|
||||
} else if (type === 'mapping') {
|
||||
// 拷贝自 Mapping.tsx
|
||||
const map = columnNameMap[key].map;
|
||||
if (
|
||||
typeof value !== 'undefined' &&
|
||||
map &&
|
||||
(map[value] ?? map['*'])
|
||||
) {
|
||||
const viewValue =
|
||||
map[value] ??
|
||||
(value === true && map['1']
|
||||
? map['1']
|
||||
: value === false && map['0']
|
||||
? map['0']
|
||||
: map['*']); // 兼容平台旧用法:即 value 为 true 时映射 1 ,为 false 时映射 0
|
||||
sheetRow.getCell(columIndex).value = viewValue;
|
||||
} else {
|
||||
sheetRow.getCell(columIndex).value = value;
|
||||
}
|
||||
} else {
|
||||
sheetRow.getCell(columIndex).value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
|
||||
if (buffer) {
|
||||
var blob = new Blob([buffer], {
|
||||
type:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
saveAs(blob, 'data.xlsx');
|
||||
}
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{__('导出 Excel')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderActions(region: string) {
|
||||
let {actions, render, store, classnames: cx, data} = this.props;
|
||||
|
||||
|
42
src/utils/image.ts
Normal file
42
src/utils/image.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file image 相关的工具
|
||||
* @param url
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将 url 转成 dataurl
|
||||
* @param url
|
||||
*/
|
||||
export const toDataURL = (url: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onload = function () {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(xhr.response);
|
||||
};
|
||||
xhr.onerror = reject;
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 url 获取图片尺寸
|
||||
* @param url
|
||||
*/
|
||||
export const getImageDimensions = (url: string) => {
|
||||
return new Promise<{width: number; height: number}>(function (
|
||||
resolved,
|
||||
rejected
|
||||
) {
|
||||
const i = new Image();
|
||||
i.onload = function () {
|
||||
resolved({width: i.width, height: i.height});
|
||||
};
|
||||
i.src = url;
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user