政务数据资源中心(Government data Resource center) 03专项3期主要建设内容
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

683 lines
27 KiB

/**
* Created by Xumeng 2020/04/22.
*/
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Row, Col, Space, Button, message, notification, Form, Input, Tooltip,
} from 'antd';
import moment from 'moment';
import XLSX from 'xlsx';
import { fromJS } from 'immutable';
import { Request } from '@peace/utils';
import FileSaver from 'file-saver';
import './index.less';
// 通用前端导入导出组件 使用方法查看底部propTypes
function ExportAndImport (props) {
const [form] = Form.useForm();
const [exportLoading, setExportLoading] = useState(false);
const [importLoading, setImportLoading] = useState(false);
const {
importDataCallback, onImportSucess, handelData, importMethod = 'post',
} = props;
useEffect(() => () => {
// 只有unmount 时调用
notification.close('import-notification');
});
const importExcel = (file, type) => {
setImportLoading(true);
// 获取上传的文件对象
const { files } = file.target;
// 判断xls、xlsx格式
if (files[0].type.indexOf('sheet') > -1 || files[0].type.indexOf('ms-excel') > -1) {
// 通过FileReader对象读取文件
const fileReader = new FileReader();
fileReader.onload = (event) => {
try {
const { importRequest = true, importUrl, importQuery } = props;
if (importRequest && !importUrl) {
message.error('获取导入接口失败!');
form.resetFields();
return;
}
const { result } = event.target;
// 以二进制流方式读取得到整份excel表格对象
const workbook = XLSX.read(result, { type: 'binary', cellDates: true });
let data = []; // 存储获取到的数据
// 遍历每张工作表进行读取(这里默认只读取第一张表)
for (const sheet in workbook.Sheets) {
if (workbook.Sheets.hasOwnProperty(sheet)) {
// 利用 sheet_to_json 方法将 excel 转成 json 数据
data = data.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet]));
break; // 如果只取第一张表,就取消注释这行
}
}
if (data.length > 10000) {
message.error('一次最多导入10000条数据,请分批导入!');
form.resetFields();
setImportLoading(false);
return;
}
if (importRequest) {
message.success('获取文件数据成功,开始处理导入...');
const importData = handelData ? handelData(data) : data;
Request[importMethod](importUrl, { data: importData }, importQuery || {}).then((res) => {
message.success('导入数据成功');
form.resetFields();
notification.close('import-notification');
setImportLoading(false);
onImportSucess && onImportSucess();
}, (err) => {
if (err.status === 500) {
message.error('数据导入出错,导入失败');
} else if (err.status === 400) {
message.error(err.body.message || '数据验证出错,请检查数据格式是否正确');
} else {
message.error('导入失败');
}
form.resetFields();
setImportLoading(false);
});
} else {
form.resetFields();
setImportLoading(false);
importDataCallback && importDataCallback(data, type);
notification.close('import-notification');
}
} catch (e) {
console.log(e);
// 这里可以抛出文件类型错误不正确的相关提示
message.error('文件格式不正确!');
setImportLoading(false);
form.resetFields();
}
};
// fileReader.onloadend = (event) => {
// console.log(event)
// }
// 以二进制方式打开文件
fileReader.readAsBinaryString(files[0]);
} else {
message.error('文件格式不正确!');
form.resetFields();
setImportLoading(false);
}
};
const loop = (data, keypath, values) => { // deal with array
const dkey = keypath.slice(0, 1)[0];
console.log(dkey);
if (dkey) {
const dvalue = data[dkey];
const otherKeypath = keypath.slice(1);
if (Array.isArray(dvalue)) {
if (otherKeypath.length) {
const immutableData = fromJS(data);
for (let index = 0; index < dvalue.length; index++) {
const tmp = immutableData.getIn([dkey, index]).toJS();
loop(tmp, otherKeypath, values);
}
}
} else {
values.push(dvalue);
}
}
return values;
};
const getColumnData = (opts) => {
const {
data, keypath, render, spliter, rawdata,
} = opts;
let v = null;
const outer = data[keypath[0]];
if (Array.isArray(outer)) {
const values = loop(data, keypath, []);
v = rawdata ? values : values.join(spliter || ',');
} else {
v = fromJS(data).getIn(keypath);
}
// 处理render
if (render && typeof render === 'function') {
v = render(outer, data);
}
return v;
};
const getDataSource = (attrs, filterData) => {
// let token = JSON.parse(sessionStorage.getItem('user')).token;
const dataSource = filterData.map((item) => {
const record = {};
attrs.forEach((attr) => {
const {
key, dataIndex, render, child,
} = attr;
if (child) {
record[key] = getDataSource(child, item[key]);
} else {
const v = getColumnData({
data: item,
keypath: dataIndex || [key],
render: render || null,
});
record[key] = v;
}
});
return record;
});
return dataSource;
};
// 暂时只处理两层
const getFlatData = (attrs, filterData, dataToAoa, deep = 0) => {
filterData.map((item) => {
let cur = dataToAoa[deep];
if (!cur) {
cur = dataToAoa[deep] = [];
}
attrs.map((attr, index) => {
const { key, child } = attr;
if (child) {
if (Array.isArray(item[key])) {
// getFlatData(child,item[key],dataToAoa,deep)
item[key].map((s, i) => {
if (i == 0) {
child.map((c) => {
cur.push(s[c.key]);
});
} else {
deep++;
const childCur = dataToAoa[deep] = [];
pushNull(childCur, index);
child.map((c) => {
childCur.push(s[c.key]);
});
}
});
}
} else {
cur.push(item[key]);
}
});
deep++;
});
};
const getHeader = (headers, excelHeader, deep, perOffset) => {
let offset = 0;
let cur = excelHeader[deep];
if (!cur) {
cur = excelHeader[deep] = [];
}
pushNull(cur, perOffset - cur.length);
for (let i = 0; i < headers.length; i++) {
const head = headers[i];
cur.push(head.name);
if (head.hasOwnProperty('child') && Array.isArray(head.child) && head.child.length > 0) {
const childOffset = getHeader(head.child, excelHeader, deep + 1, cur.length - 1);
pushNull(cur, childOffset - 1);
offset += childOffset;
} else {
offset++;
}
}
return offset;
};
const pushNull = (arr, count) => {
for (let i = 0; i < count; i++) {
arr.push(null);
}
};
const fillNull = (arr) => {
const max = Math.max(...(arr.map((a) => a.length)));
arr.filter((e) => e.length < max).forEach((e) => pushNull(e, max - e.length));
};
const doMerges = (arr) => {
// 要么横向合并 要么纵向合并
const deep = arr.length;
const merges = [];
for (let y = 0; y < deep; y++) {
// 先处理横向合并
const row = arr[y];
let colSpan = 0;
for (let x = 0; x < row.length; x++) {
if (row[x] === null) {
colSpan++;
if (((x + 1) === row.length) && (colSpan > 0 && x > colSpan)) {
merges.push({ s: { r: y, c: x - colSpan }, e: { r: y, c: x } });
}
} else if (colSpan > 0 && x > colSpan) {
merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x - 1 } });
colSpan = 0;
} else {
colSpan = 0;
}
}
}
// 再处理纵向合并
const colLength = arr[0].length;
for (let x = 0; x < colLength; x++) {
let rowSpan = 0;
for (let y = 0; y < deep; y++) {
if (arr[y][x] != null) {
rowSpan = 0;
} else {
rowSpan++;
}
}
if (rowSpan > 0) {
merges.push({ s: { r: deep - rowSpan - 1, c: x }, e: { r: deep - 1, c: x } });
}
}
return merges;
};
// 内容暂只出了纵向合并
const doContetMerges = (arr, headerLength) => {
const deep = arr.length;
const merges = [];
// 处理纵向合并
const colLength = arr[0].length;
for (let x = 0; x < colLength; x++) {
let rowSpan = 0;
const mergY = 0;
for (let y = 0; y < deep; y++) {
if (rowSpan > 0) {
// 如果还有null 继续加
if (arr[y][x] === null) {
rowSpan += 1;
} else {
// 不为null 增加merge
merges.push({ s: { r: headerLength + (y - rowSpan - 1), c: x }, e: { r: headerLength + y - 1, c: x } });
rowSpan = 0;
}
} else if (arr[y][x] === null) {
rowSpan += 1;
}
}
if (rowSpan > 0) {
merges.push({ s: { r: headerLength + (deep - rowSpan - 1), c: x }, e: { r: headerLength + deep - 1, c: x } });
rowSpan = 0;
}
}
return merges;
};
const exportMergeExcel = async () => {
setExportLoading(true);
const {
column, data, fileName, exportUrl, exportQuery, exportBody, requestType, header, showYearMouth,
} = props || {};
let resultData = [];
if (exportUrl) {
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => {
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
if (typeof data === 'object' && data.rows) {
return data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => {
if (typeof data === 'object' && data.rows) {
return data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
});
if (!resultData) {
return;
}
} else {
resultData = data;
}
const excelHeader = [];
getHeader(column, excelHeader, 0, 0);
fillNull(excelHeader);
// console.log(excelHeader);
const loopData = getDataSource(column, resultData);
// console.log(loopData)
const dataToAoa = [];
getFlatData(column, loopData, dataToAoa, 0);
fillNull(dataToAoa);
// console.log(dataToAoa);
const aoa = [].concat(excelHeader, dataToAoa);
// console.log(aoa)
const headerMerges = doMerges(excelHeader);
const contentMerages = doContetMerges(dataToAoa, excelHeader.length);
const merges = [].concat(headerMerges, contentMerages);
// console.log(contentMerages)
// let opts = {
// defaultCellStyle: {
// font: { name: "宋体", sz: 11, color: { auto: 1 } },
// border: {
// color: { auto: 1 }
// },
// alignment: {
// /// 自动换行
// wrapText: 1,
// // 居中
// horizontal: "center",
// vertical: "center",
// indent: 0
// }
// }
// }
const sheet = XLSX.utils.aoa_to_sheet(aoa);
// let newSheet = {};
// for (let [key, value] of Object.entries(sheet)) {
// if(key == '!ref'){
// newSheet[key] = value
// }else if(typeof value === 'object'){
// newSheet[key] = {
// ...value,
// s: opts.defaultCellStyle
// }
// }
// }
const wpx = column.map((c) => ({
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100,
}));
sheet['!cols'] = wpx;
sheet['!merges'] = merges;
// 构建 workbook 对象
const workbook = XLSX.utils.book_new();
const time = moment().format('YYYY-MM-DD');
XLSX.utils.book_append_sheet(workbook, sheet, 'mySheet');
// 导出 Excel
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : '导出数据.xlsx');
setExportLoading(false);
// message.success(`成功导出了 ${loopData.length || 0} 条数据`);
};
const exportProExcel = async () => {
setExportLoading(true);
const {
column, data, fileName, exportUrl, exportQuery, exportBody, requestType, showYearMouth,
} = props || {};
let resultData = [];
if (exportUrl) {
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => {
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
if (typeof data === 'object') {
return data.data ? data.data : data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => {
if (showYearMouth) {
}
if (typeof data === 'object' && data.rows) {
return data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
});
if (!resultData) {
return;
}
} else {
resultData = data;
}
const loopData = getDataSource(column, resultData);
let content = '';
let header = '<tr>';
// header += `<th><div>序号</div></th>`;
column.map((colum) => {
header += `<th><div>${colum.name}</div></th>`;
});
header += '</tr>';
loopData.map((data) => {
content += '<tr>';
column.map((c) => {
if (c.style) {
content += `<th style="${c.style}"><div>${data[c.dataIndex || c.key]}</div></th>`;
} else {
content += `<th><div>${data[c.dataIndex || c.key]}</div></th>`;
}
});
content += '</tr>';
});
const exportTable = `\uFEFF
<table style='text-alagin:center' border="1">
${header}
${content}
</table>
`;
const time = moment().format('YYYY-MM-DD');
const tempStrs = new Blob([exportTable], { type: 'text/xls' });
FileSaver.saveAs(tempStrs, fileName ? `${fileName}-${time}.xls` : '导出数据.xls');
setExportLoading(false);
// message.success(`成功导出了 ${loopData.length || 0} 条数据`);
};
const exportExcel = async () => {
setExportLoading(true);
const {
column, data, fileName, exportUrl, exportQuery, exportBody, requestType,
} = props || {};
const _headers = column
.map((item, i) => ({ key: item.key, title: item.name, position: String.fromCharCode(65 + i) + 1 }))
.reduce((prev, next) => ({ ...prev, [next.position]: { key: next.key, v: next.title } }), {});
let resultData = [];
if (exportUrl) {
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then((data) => {
// 数据接口返回的结果 如果是对象 必须把返回数组放入rows
if (typeof data === 'object' && (data.rows || data.data)) {
return data.data ? data.data : data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
}) : await Request.get(exportUrl, exportQuery || {}).then((data) => {
if (typeof data === 'object' && data.rows) {
return data.rows;
}
return data;
}, (err) => {
message.error('获取数据失败,导出失败!');
});
if (!resultData) {
return;
}
} else {
resultData = data;
}
const loopDate = getDataSource(column, resultData);
const wpx = column.map((c) => ({
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100,
}));
if (!(loopDate.length > 0)) {
setExportLoading(false);
return;
}
const _data = loopDate
.map((item, i) => column.map((key, j) => ({ content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) })))
// 对刚才的结果进行降维处理(二维数组变成一维数组)
.reduce((prev, next) => prev.concat(next))
// 转换成 worksheet 需要的结构
.reduce((prev, next) => ({ ...prev, [next.position]: { v: next.content } }), {});
// 合并 column 和 data
const output = { ..._headers, ..._data };
// 获取所有单元格的位置
const outputPos = Object.keys(output);
// 计算出范围 ,["A1",..., "H2"]
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`;
// 构建 workbook 对象
const workbook = {
SheetNames: ['mySheet'],
Sheets: {
mySheet: {
...output,
'!ref': ref,
'!cols': wpx,
},
},
};
const time = moment().format('YYYY-MM-DD');
// 导出 Excel
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : '导出数据.xlsx');
setExportLoading(false);
// message.success(`成功导出了 ${loopDate.length || 0} 条数据`);
};
const exportTemplete = async () => {
const { importTemColumn, importTemData, fileName } = props || {};
const _headers = importTemColumn
.map((item, i) => {
let group = 0; // 用于处理Z1的时候,重计算AA1
if (parseInt(i / 26) > group) {
group = parseInt(i / 26);
}
if (group > 0) { // AA1 BA1 CA1
const position = String.fromCharCode(65 + (group - 1));
return { key: item.key, title: item.name, position: position + String.fromCharCode(65 + (i % 26)) + 1 };
} return { key: item.key, title: item.name, position: String.fromCharCode(65 + i) + 1 };
})
.reduce((prev, next) => ({ ...prev, [next.position]: { key: next.key, v: next.title } }), {});
const loopDate = getDataSource(importTemColumn, importTemData);
const wpx = importTemColumn.map((c) => ({
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100,
}));
const _data = loopDate.length ? loopDate
.map((item, i) => importTemColumn.map((key, j) => ({ content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) })))
// 对刚才的结果进行降维处理(二维数组变成一维数组)
.reduce((prev, next) => prev.concat(next))
// 转换成 worksheet 需要的结构
.reduce((prev, next) => ({ ...prev, [next.position]: { v: next.content } }), {}) : [];
// 合并 column 和 data
const output = { ..._headers, ..._data };
// 获取所有单元格的位置
const outputPos = Object.keys(output);
// 计算出范围 ,["A1",..., "H2"]
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`;
// 构建 workbook 对象
const workbook = {
SheetNames: ['mySheet'],
Sheets: {
mySheet: {
...output,
'!ref': ref,
'!cols': wpx,
},
},
};
// 导出 Excel
XLSX.writeFile(workbook, fileName ? `${fileName}-导入模板.xlsx` : '导入模板.xlsx');
};
const tips = (type) => {
const { tips, templeteBth = true } = props;
const description = (
<div className="export-import">
{tips && tips}
<Row gutter={16}>
<Col span={12}>
<Form form={form} initialValues={{}}>
<Form.Item name="import-file">
<Input className="file-uploader" type="file" accept=".xlsx, .xls" onChange={(e) => importExcel(e, type)} />
<Button style={props.btnStyle} className={props.btnClass} loading={importLoading}>
选择文件
</Button>
</Form.Item>
</Form>
</Col>
{templeteBth && (
<Col span={12}>
<Button style={props.btnStyle} className={props.btnClass} onClick={exportTemplete}>
模板下载
</Button>
</Col>
)}
</Row>
</div>
);
notification.info({
message: '支持 .xlsx、.xls 格式的文件',
description,
key: 'import-notification',
duration: null,
});
};
return (
<Space>
{
props.import && (
<Button style={props.btnStyle} className={props.btnClass} loading={importLoading} onClick={tips}>
{props.importBtnName || '导入'}
</Button>
)
}
{
props.export && (
<Tooltip placement="top" title={props.exportBtnTips || '默认导出所有数据'}>
<Button style={props.btnStyle} className={props.btnClass} loading={exportLoading} onClick={props.exportType === 'pro' ? exportProExcel : exportExcel}>
{props.exportBtnName || '导出'}
</Button>
</Tooltip>
)
}
</Space>
);
}
ExportAndImport.propTypes = {
export: PropTypes.bool, // 是否显示导出按钮
exportBtnName: PropTypes.string, // 导出按钮文字
importBtnName: PropTypes.string, // 导入按钮文字
import: PropTypes.bool, // 是否显示导入按钮
variedImport: PropTypes.bool, // 是否显示多样导入
variedImportDisable: PropTypes.bool, // 多样导入禁用
variedImportBtnName: PropTypes.string, // 多样导入文字
column: PropTypes.array, // 导出显示的header数组 兼容antd column 可直接拿table的column使用 注:column每列的属性wpx设置导出的execl每列的宽度值 默认 100
data: PropTypes.array, // 导出的数据 兼容antd table 数组嵌套处理
exportUrl: PropTypes.string, // 导出数据从接口获取的url地址 返回的数据1、数组必须支持column的设置 ,2、如果是对象,数组需放在rows属性上
exportBody: PropTypes.object, // 导出数据接口body参数
exportQuery: PropTypes.object, // 导出数据从接口获取的url地址上的参数
exportBtnTips: PropTypes.string, // 导出按钮tips文字提示
importUrl: PropTypes.string, // 导入接口url
importQuery: PropTypes.object, // 导入接口url地址上的参数
btnClass: PropTypes.string, // 按钮className
btnStyle: PropTypes.object, // 按钮style
tips: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), // 上传文件提示的信息
onImportSucess: PropTypes.func, // 上传成功后 返回处理函数
importTemColumn: PropTypes.array, // 导入模板设置 头部字段数组
importTemData: PropTypes.array, // 导入模板默认数据
requestType: PropTypes.string, // 请求类型
importDataCallback: PropTypes.func, // 上传后数据返回
templeteBth: PropTypes.bool, // 模板按钮
importRequest: PropTypes.bool, // 请求导入接口,false时搭配importDataCallback,
exportType: PropTypes.string, // 导出执行的函数名
};
export default ExportAndImport;