feat: 解析 Excel (#2546)

* feat-input-excel

* 增加翻译
This commit is contained in:
吴多益 2021-09-13 10:22:50 +08:00 committed by GitHub
parent 1885b3390b
commit 54c13b9809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 3 deletions

View File

@ -0,0 +1,126 @@
---
title: InputExcel 解析 Excel
description:
type: 0
group: null
menuName: InputExcel
icon:
order: 14
---
这个组件是通过前端对 Excel 进行解析,将结果作为表单项,使用它有两个好处:
1. 节省后端开发成本,无需再次解析 Excel
2. 可以前端实时预览效果,比如配合 input-table 组件进行二次修改
## 基本使用
默认情况下只解析第一个 sheet 的内容,下面的例子中,选择上传文件后,就能知道最终会解析成什么数据
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "input-excel",
"name": "excel",
"label": "上传 Excel"
}
]
}
```
默认模式是解析成对象数组,将第一行作为对象里的键,可以上传一个类似这样的 Excel 内容试试
```
|名称|网址|
|amis|https://baidu.gitee.io/amis|
|百度|https://www.baidu.com|
```
解析后的的数据格式将会是
```json
[
{
"名称": "amis",
"网址": "https://baidu.gitee.io/amis"
},
{
"名称": "百度",
"网址": "https://www.baidu.com"
}
]
```
## 二维数组模式
除了默认配置的对象数组格式,还可以使用二维数组方式,方法是设置 `"parseMode": "array"`
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "input-excel",
"name": "excel",
"parseMode": "array",
"label": "上传 Excel"
}
]
}
```
如果是前面的例子,解析结果将会是
```json
[
["名称", "网址"],
["amis", "https://baidu.gitee.io/amis"],
["百度", "https://www.baidu.com"]
]
```
## 解析多个 sheet
默认配置只解析第一个 sheet如果要解析多个 sheet可以通过 `"allSheets": true` 开启多个 sheet 的读取,这时的数据会增加一个层级。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"body": [
{
"type": "input-excel",
"name": "excel",
"allSheets": true,
"label": "上传 Excel"
}
]
}
```
如果按之前的例子,结果将会是
```json
[
{
"sheetName": "Sheet1",
"data": [
{
"名称": "amis",
"网址": "https://baidu.gitee.io/amis"
},
{
"名称": "百度",
"网址": "https://www.baidu.com"
}
]
}
]
```

View File

@ -383,9 +383,9 @@ export const components = [
path: '/zh-CN/components/form/input-datetime-range',
getComponent: () =>
// @ts-ignore
import('../../docs/zh-CN/components/form/input-datetime-range.md').then(
makeMarkdownRenderer
)
import(
'../../docs/zh-CN/components/form/input-datetime-range.md'
).then(makeMarkdownRenderer)
},
{
label: 'InputMonthRange 月份范围',
@ -423,6 +423,15 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'InputExcel Excel 解析',
path: '/zh-CN/components/form/input-excel',
getComponent: () =>
// @ts-ignore
import('../../docs/zh-CN/components/form/input-excel.md').then(
makeMarkdownRenderer
)
},
{
label: 'InputFile 文件上传',
path: '/zh-CN/components/form/input-file',

View File

@ -0,0 +1,42 @@
// Excel 上传的配置基于 https://react-dropzone.js.org/
.#{$ns}ExcelControl {
&-container {
display: flex;
flex-direction: column;
font-family: sans-serif;
cursor: pointer;
}
&-container > p {
font-size: 1rem;
}
&-container > em {
font-size: 0.8rem;
}
&-dropzone {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--gap-md);
border-width: 2px;
border-radius: 2px;
border-color: #eeeeee;
border-style: dashed;
background-color: #fafafa;
color: #bdbdbd;
outline: none;
transition: border 0.24s ease-in-out;
}
&-dropzone:focus {
border-color: var(--primary);
}
&-dropzone.disabled {
opacity: 0.6;
}
}

View File

@ -86,6 +86,7 @@
@import '../components/form/image';
@import '../components/form/file';
@import '../components/form/excel';
@import '../components/form/editor';
@import '../components/form/rich-text';
@import '../components/form/tinymce';

View File

@ -221,6 +221,7 @@ export type SchemaType =
| 'input-date-range'
| 'input-time-range'
| 'input-datetime-range'
| 'input-excel'
| 'diff-editor'
// editor 系列

View File

@ -103,6 +103,7 @@ import './renderers/Form/InputArray';
import './renderers/Form/Combo';
import './renderers/Form/ConditionBuilder';
import './renderers/Form/InputSubForm';
import './renderers/Form/InputExcel';
import './renderers/Form/InputRichText';
import './renderers/Form/Editor';
import './renderers/Form/DiffEditor';

View File

@ -80,6 +80,7 @@ register('en-US', {
'Dialog.close': 'Close',
'Embed.invalidRoot': 'Invalid root selector',
'Embed.downloading': 'Start downloading',
'Excel.placeholder': `Drag 'n' drop excel here, or click to select`,
'fetchFailed': 'Fetch api failed',
'File.continueAdd': 'Continue add',
'File.dragDrop': `Drag 'n' drop some files here`,

View File

@ -82,6 +82,7 @@ register('zh-CN', {
'Dialog.close': '关闭',
'Embed.invalidRoot': '选择器不对,页面上没有此元素',
'Embed.downloading': '文件即将开始下载。。',
'Excel.placeholder': '拖拽 Excel 到这,或点击上传',
'fetchFailed': '初始化失败',
'File.continueAdd': '继续添加',
'File.dragDrop': '将文件拖拽到此处',

View File

@ -0,0 +1,156 @@
import React, {Suspense} from 'react';
import Dropzone from 'react-dropzone';
import {FileRejection} from 'react-dropzone';
import {autobind} from '../../utils/helper';
import {FormItem, FormControlProps, FormBaseControl} from './Item';
/**
* Excel
* https://baidu.gitee.io/amis/docs/components/form/input-excel
*/
export interface InputExcelControlSchema extends FormBaseControl {
/**
* Excel
*/
type: 'input-excel';
/**
* sheet
*/
allSheets: boolean;
/**
* array object
*/
parseMode: 'array' | 'object';
/**
*
*/
includeEmpty: boolean;
}
export interface ExcelProps
extends FormControlProps,
Omit<
InputExcelControlSchema,
'type' | 'className' | 'descriptionClassName' | 'inputClassName'
> {}
export interface ExcelControlState {
open: boolean;
}
export default class ExcelControl extends React.PureComponent<
ExcelProps,
ExcelControlState
> {
static defaultProps: Partial<ExcelProps> = {
allSheets: false,
parseMode: 'object',
includeEmpty: true
};
state: ExcelControlState = {
open: false
};
@autobind
handleDrop(files: File[]) {
const {allSheets, onChange} = this.props;
const excel = files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(excel);
reader.onload = async () => {
if (reader.result) {
import('exceljs').then(async (ExcelJS: any) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(reader.result);
if (allSheets) {
const sheetsResult: any[] = [];
workbook.eachSheet((worksheet: any) => {
sheetsResult.push({
sheetName: worksheet.name,
data: this.readWorksheet(worksheet)
});
onChange(sheetsResult);
});
} else {
const worksheet = workbook.worksheets[0];
onChange(this.readWorksheet(worksheet));
}
});
}
};
}
/**
* sheet
*/
readWorksheet(worksheet: any) {
const result: any[] = [];
const {parseMode} = this.props;
if (parseMode === 'array') {
worksheet.eachRow((row: any, rowNumber: number) => {
const values = row.values;
values.shift(); // excel 返回的值是从 1 开始的0 节点永远是 null
result.push(values);
});
return result;
} else {
let firstRowValues: any[] = [];
worksheet.eachRow((row: any, rowNumber: number) => {
// 将第一列作为字段名
if (rowNumber == 1) {
firstRowValues = row.values;
} else {
const data: any = {};
row.eachCell((cell: any, colNumber: any) => {
if (firstRowValues[colNumber]) {
data[firstRowValues[colNumber]] = cell.value;
}
});
result.push(data);
}
});
return result;
}
}
render() {
const {
className,
classnames: cx,
classPrefix: ns,
value,
disabled,
translate: __
} = this.props;
return (
<div className={cx('ExcelControl', className)}>
<Dropzone
key="drop-zone"
onDrop={this.handleDrop}
accept=".xlsx"
multiple={false}
disabled={disabled}
>
{({getRootProps, getInputProps}) => (
<section className={cx('ExcelControl-container', className)}>
<div {...getRootProps({className: cx('ExcelControl-dropzone')})}>
<input {...getInputProps()} />
<p>{__('Excel.placeholder')}</p>
</div>
</section>
)}
</Dropzone>
</div>
);
}
}
@FormItem({
type: 'input-excel'
})
export class ExcelControlRenderer extends ExcelControl {}