import React, { useState, useRef } from "react";
import { Button, Transfer, Row, Col, Label, Radio, Select, Checkbox } from "tinper-bee";
import PopDialog from "components/Pop";
import FormItemPro from 'components/FormItemPro';
import { Info } from "utils";
import moment from "moment";
const Option = Select.Option;
import "./index.less";
//#region 列可配置式文件导出组件
const FileExport = ({
grid,
onBeforeExportFile,
fileName = `fileName(${moment().format("YYYY-MM-DD HH:mm:ss")}).xlsx`
}) => {
const [isShowDialog, setIsShowDialog] = useState(false);
const [transferData, setTransferData] = useState([]); //穿越框数据
const [selectedKeys, setSelectedKeys] = useState([]);
const [targetKeys, setTargetKeys] = useState([]);
// 项移动操作
const handleChange = (nextTargetKeys, direction, moveKeys) => {
setTargetKeys(nextTargetKeys);
};
// 项选择事件
const handleSelectedKeyChange = (sourceSelectedKeys, targetSelectedKeys) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
// 滚动事件
const handleScroll = (direction, e) => {
console.log("direction:", direction);
console.log("target:", e.target);
};
// 准备导出动作
const handleReadyExportFile = () => {
if (!grid || !(grid instanceof React.Component || grid.current instanceof React.Component)) {
Info("未检测到传入Grid组件实例,请检查!");
return;
}
const { columns, data } = grid.props || grid.current.props;
if (!columns || !(columns instanceof Array) || !columns.filter(item =>
Boolean(item.title) && (Boolean(item.key) || item.children.length)).length) {
Info("未检测到 [列] 相关数据,请检查!");
return;
}
if (!data || !Array.isArray(data) || !data.length) {
Info("未检测到 [表体] 相关数据,请检查!");
return;
}
//开始导出文件之前的回调, 方便调用者处理数据。
onBeforeExportFile && onBeforeExportFile();
// 将【操作】列设置为 禁导 状态
const [operationCol] = columns.filter(item => item.title === "操作");
operationCol && Object.assign(operationCol, { disabled: true });
//匿名函数递归计算表头的层级
const level = +function func(data) {
let level = 0;
data.forEach(item => {
if (item.children) {
level++;
func(item.children)
}
})
return level;
}(columns)
if (level > 2) {
Info('暂不支持3级及以上表头的自定义列导出!');
return;
}
// 计算穿越框需要的key=>title数据
const arrTransfer = [];
+function func(data) {
data.forEach(item => {
if (item.children && item.children.length) {
arrTransfer.push(...item.children);
} else {
arrTransfer.push(item);
}
})
}(columns);
if (arrTransfer.length) {
setTransferData(arrTransfer);
setIsShowDialog(true);
}
};
// 开始正式导出文件
const handleExportFIle = () => {
if (!targetKeys.length) {
Info("请选择至少导出1列数据!");
return;
}
const arrTable = []; //整个表体需要的数据
const { data, columns } = grid.props || grid.current.props;
const arrHeaderOne = [];
columns.forEach(item => {
if (!item.children) {
targetKeys.includes(item.key) && arrHeaderOne.push(item.title);
} else {
item.children.forEach(el => {
targetKeys.includes(el.key) && arrHeaderOne.push(item.title);
})
}
})
arrTable.push(arrHeaderOne);
let keyMapTitle = transferData.map(item => ({ [item.key]: item.title }));
keyMapTitle = Object.assign({}, ...keyMapTitle);
data.forEach(item => {
const arr = [];
targetKeys.forEach(key => {
const [column] = transferData.filter(item => item.key === key);
const { exportKey } = column;
if (exportKey) arr.push(item[exportKey]);
else arr.push(item[key]);
});
arr.filter(item => Boolean(item)).length && arrTable.push(arr);
});
const ws = XLSX.utils.aoa_to_sheet(arrTable);
// 将定义的列宽width写入表格;
if (!ws["!cols"]) ws["!cols"] = [];
targetKeys.forEach(key => {
const [column] = transferData.filter(item => item.key === key);
if (column) {
ws["!cols"].push({
wpx: column.width || 50
});
}
});
// 处理单元格合并事件
if (!ws["!merges"]) ws["!merges"] = [];
// 行合并
transferData
.filter(item => Boolean(item.render))
.filter(item => item.render.toString().includes("rowSpan"))
.forEach(item => {
const sc = targetKeys.findIndex(el => el === item.key); //起始列索引
data.forEach((record, index) => {
let sr = 2; //基础的起始行索引
let er = sr; //预定义的结束行索引
if (!!item.render(record[item.key], record)) {
if (item.render(record[item.key], record).props.rowSpan > 0) {
sr += index;
er = item.render(record[item.key], record).props.rowSpan - 1;
var merge = {
s: {
r: sr,
c: sc
},
e: {
r: er + sr,
c: sc
}
};
ws["!merges"].push(merge);
}
}
});
});
//表头中的列合并
columns.forEach(item => {
if (item.children && item.children.length) {
const arrSame = [];
item.children.forEach(el => {
targetKeys.includes(el.key) && arrSame.push(el.key);
})
const [key] = arrSame;
const sc = targetKeys.findIndex(item => item === key);
const ec = sc + arrSame.length - 1;
var merge = {
s: {
r: 0,
c: sc
},
e: {
r: 0,
c: ec
}
};
ws["!merges"].push(merge);
}
})
// 表体中的列合并
transferData
.filter(item => Boolean(item.render))
.filter(item => item.render.toString().includes("colSpan"))
.forEach(item => {
const sc = targetKeys.findIndex(el => el === item.key);
data.forEach((record, index) => {
let sr = 2;
let er = 0;
if (!!item.render(record[item.key], record)) {
if (item.render(record[item.key], record).props.colSpan > 0) {
sr += index;
er = item.render(record[item.key], record).props.colSpan - 1;
var merge = {
s: {
r: sr,
c: sc
},
e: {
r: sr,
c: er + sc
}
};
ws["!merges"].push(merge);
}
}
});
});
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "SheetJS");
XLSX.writeFile(wb, fileName);
setIsShowDialog(false);
};
return (
<span classNames="file-export">
<Button colors="primary" onClick={handleReadyExportFile}>
导出
</Button>
{isShowDialog && (
<PopDialog
show={true}
title="列导出配置"
width="500"
className="export-file-dialog"
autoFocus={false}
enforceFocus={false}
close={() => setIsShowDialog(false)}
btns={[
{
label: "导出",
fun: handleExportFIle,
icon: "uf-correct"
},
{
label: "取消",
fun: () => setIsShowDialog(false),
icon: "uf-back"
}
]}
>
<Transfer
dataSource={transferData}
titles={["待配置列", "已配置列"]}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={handleChange}
onSelectChange={handleSelectedKeyChange}
onScroll={handleScroll}
render={item => item.title}
lazy={{
container: "modal"
}}
/>
</PopDialog>
)}
</span>
);
};
//#endregion
//#region 文件导入组件
const FileImport = ({ grid, onAfterImportFile }) => {
const inputRef = useRef(null); //获取input DOM元素
const [isShowDialog, setIsShowDialog] = useState(false); //模态框
const [importMode, setImportMode] = useState('edit'); //导入模式
const [arrKeyMapTitle, setArrKeyMapTitle] = useState([{ key: 'rowNo', title: '序号', isPrimaryKey: true, isSelected: false }]) //数据集合
// 文件导入回调
const handleFileChange = e => {
e.persist()
// 获取上传的文件对象
const { files } = e.target;
// 通过FileReader对象读取文件
const fileReader = new FileReader();
const rABS = !!fileReader.readAsBinaryString;
fileReader.onload = event => {
const { result } = event.target;
//读取得到整份excel表格对象
const workbook = XLSX.read(result, { type: rABS ? 'binary' : 'array' });
let dataImport = []; // 存储获取到的数据
// 遍历每张工作表进行读取(这里默认只读取第一张表)
for (const sheet in workbook.Sheets) {
if (workbook.Sheets.hasOwnProperty(sheet)) {
// 利用 sheet_to_json 方法将 excel 转成 json 数据
dataImport = dataImport.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet]));
break; // 如果只取第一张表,就取消注释这行
}
}
// // 解决 input type=file不能重复上传同一个文件
inputRef.current.setAttribute('type', 'text');
inputRef.current.setAttribute('type', 'file');
const { columns, data } = grid.props || grid.current.props; //columns=Grid实例的列配置;Data=Grid实例的原有行数据
//匿名函数递归获取扁平化的列数据结构
const arrFlatColumns = [];
+function func(data, level) {
data.forEach(item => {
item.level = level;
arrFlatColumns.push(item)
if (item.children) {
func(item.children, level + 1)
}
})
}(columns, 0)
const titleMapKey = {}; //根据columns,获取title=>key之间的映射
const keyMapTitle = {}; //根据columns,获取Key=>title之间的映射
const arrKeyMapTitle = []; //根据columns,获取key, title之间的映射
+function func(data) {
data.forEach(item => {
if (item.children && item.children.length) {
func(item.children)
} else {
arrKeyMapTitle.push({ key: item.key, title: item.title })
titleMapKey[item.title] = item.key;
keyMapTitle[item.key] = item.title;
}
})
}(columns)
const [row] = arrKeyMapTitle.filter(item => item.key === 'rowNo');
if( row ) Object.assign(row, { isPrimaryKey: true });
else
Object.assign(arrKeyMapTitle[0], { isPrimaryKey: true });
}
setArrKeyMapTitle(arrKeyMapTitle)
const dataFromExcel = []; //来自于Excel的数据
// 简单的判断一下:是 新增模式,还是编辑模式
if (dataImport.length - 2 > data.length) { //新增格式
setImportMode('add')
} else setImportMode('edit');
// 三层及以上表头
if (Math.max(...arrFlatColumns.map(item => item.level)) > 1) {
Info('暂不支持3层及以上表头的导入');
return;
}
// 一层表头
if (Math.max(...arrFlatColumns.map(item => item.level)) === 0) {
dataImport.forEach(item => dataFromExcel.push(item))
}
// 二层表头
if (Math.max(...arrFlatColumns.map(item => item.level)) === 1) {
const [objHeaderOne] = dataImport.slice(0, 1);
dataImport.slice(1).forEach(item => {
const obj = {};
Object.entries(item).forEach(el => {
const [key, value] = el;
obj[objHeaderOne[key]] = value;
})
dataFromExcel.push(obj);
})
}
// 准备导出数据
dataFromExcel.forEach(item => Object.entries(item).forEach(el => {
const [key, value] = el;
Object.assign(item, { [titleMapKey[key]]: value })
delete item[key];
}))
FileExport.dataFromExcel = dataFromExcel;
setIsShowDialog(true)
};
// 打开文件
if (rABS) fileReader.readAsBinaryString(files[0]); else fileReader.readAsArrayBuffer(files[0]);
}
// 正式导入文件
const handleFileImport = () => {
const { data } = grid.props || grid.current.props;
switch (importMode) {
case 'add':
data.length = 0;
FileExport.dataFromExcel.forEach(item=>data.push(item))
break;
case 'edit':
const primaryKey = arrKeyMapTitle.filter(item => item.isPrimaryKey === true)?.[0]?.key;
data.length && data.filter(item=> !!item[primaryKey]).forEach(item=>{
const { rowNo } = item;
const [ dataRow ] = FileExport.dataFromExcel.filter(i=> i.rowNo === rowNo);
const arrEditableColumns = arrKeyMapTitle.filter(i => i.isSelected === true);
arrEditableColumns.length && arrEditableColumns.forEach(i=>{
const { key } = i;
Object.assign(item, { [key] : dataRow[key] });
})
})
break;
default:
break;
}
// 完成导入后的回调函数
onAfterImportFile && onAfterImportFile();
setIsShowDialog(false);
}
// 主键列改变事件
const handlePrimaryKeyChange = value => {
const newArrKeyMapTitle = JSON.parse(JSON.stringify(arrKeyMapTitle))
const [row] = newArrKeyMapTitle.filter(item => item.isPrimaryKey === true);
Object.assign(row, { isPrimaryKey: false });
const [newRow] = newArrKeyMapTitle.filter(item => item.key === value);
Object.assign(newRow, { isPrimaryKey: true, isSelected: false });
setArrKeyMapTitle(newArrKeyMapTitle)
}
// 可编辑列选择事件
const handleEditableColumnsChange = (key, checked) => {
const newArrKeyMapTitle = JSON.parse(JSON.stringify(arrKeyMapTitle))
const [row] = newArrKeyMapTitle.filter(item => item.key === key);
Object.assign(row, { isSelected: checked });
setArrKeyMapTitle(newArrKeyMapTitle)
}
return (
<span class="file-import">
<label class='file-import-btn'><input ref={inputRef} type='file' accept='.xlsx, .xls'
onChange={handleFileChange} style={{ display: 'none' }} />导入</label>
{isShowDialog && (
<PopDialog
show={true}
title="导入配置"
width='500'
className="file-import-dialog"
autoFocus={false}
enforceFocus={false}
close={() => setIsShowDialog(false)}
btns={[
{
label: "导入",
fun: handleFileImport,
icon: "uf-correct"
},
{
label: "取消",
fun: () => setIsShowDialog(false),
icon: "uf-back"
}
]}
>
<Row className="form-panel">
<Col lg={12} md={12} xs={12} sm={12}>
<FormItemPro>
<Label>导入模式</Label>
<Radio.RadioGroup
name="import-mode"
selectedValue={importMode}
onChange={value => setImportMode(value)}
>
<Radio colors="primary" value="add" >新增</Radio>
<Radio colors="success" value="edit" >编辑</Radio>
</Radio.RadioGroup>
</FormItemPro>
</Col>
{importMode === 'edit' && <>
<Col lg={12} md={12} xs={12} sm={12}>
<FormItemPro>
<Label>主键列</Label>
<Select
placeholder='请选择主键列'
onChange={value => handlePrimaryKeyChange(value)}
optionFilterProp="children"
value={arrKeyMapTitle.filter(item => item.isPrimaryKey === true)[0] ? arrKeyMapTitle.filter(item => item.isPrimaryKey === true)[0].title : '序号'}
>
{!!arrKeyMapTitle.length &&
arrKeyMapTitle.map(item => {
const { key, title } = item;
return <Option key={key} value={key} >
{title}
</Option>
})
}
</Select>
</FormItemPro>
</Col>
<Col lg={12} md={12} xs={12} sm={12}>
<FormItemPro>
<Label>可编辑列</Label>
<div class='editable-columns' >
{!!arrKeyMapTitle.length && arrKeyMapTitle.map(item => {
const { key, title, isPrimaryKey, isSelected } = item;
return <Checkbox
key={key}
disabled={isPrimaryKey}
checked={isSelected}
onChange={checked => handleEditableColumnsChange(key, checked)}>
{title}
</Checkbox>
})}
</div>
</FormItemPro>
</Col>
</>
}
</Row>
</PopDialog>
)}
</span>
);
}
//#endregion
export { FileExport, FileImport };
基于用友开源前端库、XLSX插件、React Hooks编写的文件导出组件
最新推荐文章于 2023-03-31 10:26:26 发布