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

mac2025-07-24  7

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 };
最新回复(0)