基于用友开源前端库、XLSX插件、React Hooks编写的文件导出组件

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 };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值