UNPKG

bee-table

Version:
467 lines (443 loc) 20.1 kB
import React, { Component } from "react"; import PropTypes from "prop-types"; import { convertListToTree } from "./utils"; import Table from '../Table'; import {arrayMoveTo} from "./util"; const DEFAULT_ROW_HEIGHT = Table.defaultProps.height||30;//缺省的默认行高 // 确保索引不超出数据范围 function validIndex(totalCount,currentIndex){ let cursorIndex = currentIndex||0; if(currentIndex<0)cursorIndex = 0; //确保不小于0 if(currentIndex>totalCount-1)cursorIndex = totalCount-1; //确保不超出总数据行 return cursorIndex; } /** * 获取数据开始和结束的索引位置 * @param totalCount 数据总行数 * @param showCount 可视区域显示的总行数 * @param hiddenCount 单个缓冲区隐藏的行数 * @param currentIndex 可视区域显示的首行索引(游标) * @return {Array} */ function computeIndex(totalCount,showCount,hiddenCount,currentIndex){ let screenCount = showCount + hiddenCount * 2;//渲染的总行数 let startIndex = 0,endIndex = 0; let cursorIndex = validIndex(totalCount,currentIndex); if(totalCount<=screenCount){ //总数不足或刚好一屏 startIndex = 0; endIndex = totalCount>1?totalCount - 1:1;//注意只有1行的场景 }else{//总数超过一屏 if(cursorIndex==0){//等于总数据顶边界 startIndex = 0; endIndex = screenCount - 1; }else if(cursorIndex==totalCount-1){//等于总数据底边界 endIndex = totalCount - 1; startIndex = endIndex - screenCount; }else{ startIndex = cursorIndex - (hiddenCount - 1); if(startIndex<0)startIndex = 0; //注意上行有计算出为负值的情况 endIndex = startIndex + (screenCount -1); } if(endIndex<0)endIndex = 0;// 确保结束位置区间不越界 //如果结束位置超过总记录数,则把超过的部分在开始位置往前移动 if(endIndex>totalCount-1){ startIndex = startIndex - (endIndex-(totalCount-1)); endIndex = totalCount-1; } if(startIndex<0)startIndex = 0;// 确保开始位置区间不越界 } return {screenCount,startIndex,endIndex}; } //获取每行默认高度 function getDefaultRowHeight(nextProps){ const rowHeight = nextProps.height ? nextProps.height : DEFAULT_ROW_HEIGHT; return rowHeight; } //获取显示在可视区域的行数 function getShowCount(nextProps){ const scrollY = nextProps.scroll&&nextProps.scroll.y ? parseInt(nextProps.scroll.y) : 0; //默认可视区域高度 const rowHeight = getDefaultRowHeight(nextProps); //默认每行高度 const showCount = scrollY && rowHeight ? Math.floor(scrollY / rowHeight) : 20; return showCount; } // 新重构版bigData export default function bigDataX(Table) { return class BigDataX extends Component { static defaultProps = { data: [], loadBuffer: 50, //单个缓冲区大小,缓冲区有前后两个区,所以总缓冲区是x2 rowKey: "key", onExpand() {}, scroll: {}, currentIndex:-1, isTree:null, height:null, childrenColumnName: 'children' }; static propTypes = { loadBuffer: PropTypes.number, height: PropTypes.number, scroll: PropTypes.any, expandedRowKeys: PropTypes.string, rowKey: PropTypes.string, currentIndex: PropTypes.number, isTree: PropTypes.bool, data: PropTypes.any, onExpandedRowsChange: PropTypes.func, childrenColumnName: PropTypes.string, }; constructor(props) { super(props); this.state = { needRender:false, scrollTop: 0, showCount: getShowCount(props), treeType: this.checkIsTreeType(props.isTree,props.data), }; this.currentIndex = 0; this.cachedRowHeight = {}; //缓存每行的高度 let expandedRowKeys = []; let rows = [...props.data]; if (props.defaultExpandAllRows) { for (let i = 0; i < rows.length; i++) { const row = rows[i]; expandedRowKeys.push(this.getRowKey(row, i)); rows = rows.concat(row[props.childrenColumnName] || []); } } else { expandedRowKeys = props.expandedRowKeys || props.defaultExpandedRowKeys; } this.expandedRowKeys = props.expandedRowKeys || expandedRowKeys || [];//缓存展开的行键值 this.flatTreeKeysMap = {}; //树表,扁平结构数据的 Map 映射,方便获取各节点信息 this.flatTreeData = []; //深度遍历处理后的data数组,拉平的数据,包含展开的子集行(未展开的不包含) if(this.state.treeType){ let deep = this.deepTraversal(props.data,null,this.expandedRowKeys); this.flatTreeKeysMap = deep.flatTreeKeysMap; this.flatTreeData = deep.flatTreeData; } } componentWillReceiveProps(nextProps,nextContext) { const props = this.props; const {expandedRowKeys:newExpandedKeys,data:newData} = nextProps; const _this = this,dataLen = newData.length; let { treeType }=this.state; // if ('expandedRowKeys' in nextProps){ // this.expandedRowKeys = newExpandedKeys; // } if ('defaultExpandAllRows' in nextProps) { const { defaultExpandAllRows, data, expandedRowKeys, childrenColumnName = 'children'} = nextProps; if (defaultExpandAllRows && !expandedRowKeys) { let _expandedRowKeys = []; let rows = [...(data || [])]; for (let i = 0; i < rows.length; i++) { const row = rows[i]; _expandedRowKeys.push(this.getRowKey(row, i)); rows = rows.concat(row[childrenColumnName] || []); } this.expandedRowKeys = _expandedRowKeys; } else if (!defaultExpandAllRows && !expandedRowKeys) { this.expandedRowKeys = [] } } if ('expandedRowKeys' in nextProps) { this.expandedRowKeys = newExpandedKeys; } if ('isTree' in nextProps || 'data' in nextProps){ treeType = _this.checkIsTreeType(nextProps.isTree,nextProps.data); this.setState({treeType}) } if ('height' in nextProps && nextProps.height !== props.height){ // _this.cachedRowHeight = {}; //清除缓存每行的高度 _this.cachedRowHeight = _this.setCachedRowHeight(_this.cachedRowHeight); } // 可视滚动区域变化时 if ('scroll' in nextProps && nextProps.scroll.y !== props.scroll.y) { // _this.cachedRowHeight = {}; //清除缓存每行的高度 _this.cachedRowHeight = _this.setCachedRowHeight(_this.cachedRowHeight); if(!this.props.ignoreScrollYChange){//y不变化则无需重置currentIndex _this.currentIndex = 0; } //显示在可视区域的行数 this.setState({showCount:getShowCount(nextProps)}); } if('data' in nextProps){ //fix: 滚动加载场景中,数据动态改变下占位计算错误的问题(26 Jun) if (newData.toString() !== props.data.toString()) { // _this.cachedRowHeight = {}; //清除缓存每行的高度 _this.cachedRowHeight = _this.setCachedRowHeight(_this.cachedRowHeight); if(_this.state.scrollTop <= 0) { // 增加scrollTop 判断,ncc场景下滚动条不在最上层,数据变更时 会出现空白,因为重置了currentIndex没有重置滚动条 _this.currentIndex = 0; } } this.flatTreeKeysMap = {}; //树表,扁平结构数据的 Map 映射,方便获取各节点信息 this.flatTreeData = []; //深度遍历处理后的data数组 if(treeType){ let deep = this.deepTraversal(newData,null,this.expandedRowKeys); this.flatTreeKeysMap = deep.flatTreeKeysMap; this.flatTreeData = deep.flatTreeData; } } //为外部定位currentIndex提供支持,以确保currentIndex行显示在可视区域 if('currentIndex' in nextProps){ //如果传currentIndex,会判断该条数据是否在可视区域,如果没有的话,则重新计算startIndex和endIndex if(nextProps.currentIndex !== -1){ let totalCount = this.state.treeType ? this.flatTreeData.length:dataLen; this.currentIndex = validIndex(totalCount,nextProps.currentIndex); let newScrollTop = _this.getSumHeight(0, this.currentIndex,this.state.treeType); //重新设定scrollTop值 if(newScrollTop!==this.state.scrollTop){ this.setState({scrollTop:newScrollTop}) } } } } setCachedRowHeight = (cachedRowHeight = {}) => { let target = {}; Object.keys(cachedRowHeight).forEach(key => { // 如果是展开的行,也要缓存 if (typeof key === 'string' && (key || "").includes('_expanded')) { target[key] = cachedRowHeight[key]; } }); return target; } /** * 深度遍历树形 data,把数据拍平,变为一维数组 * @param {*} treeData * @param {*} parentKey 标识父节点 * @param {*} arrData 排平后的数组数据 * @param {*} keyMap key与数据的关系 */ deepTraversal = (treeData, parentKey=null,expandedKeys=[],arrData,keyMap) => { let flatTreeData = arrData||[],flatTreeKeysMap = keyMap||{}, dataCopy = treeData; if(Array.isArray(dataCopy)){ for (let i=0, l=dataCopy.length; i<l; i++) { let { children, ...props } = dataCopy[i], key = this.getRowKey(dataCopy[i],i),//bugfix生成key字段,否则树无法展开 _isLeaf = (children && children.length > 0) ? false : true, //如果父节点是收起状态,则子节点的展开状态无意义。(一级节点或根节点直接判断自身状态即可) isExpanded = expandedKeys.includes(key); let dataCopyI = Object.assign({ key, isExpanded, parentKey : parentKey, _isLeaf,//是否叶子节点 index: flatTreeData.length },props); flatTreeData.push(dataCopyI); // 取每项数据放入一个新数组 flatTreeKeysMap[key] = dataCopyI; // 优化递归逻辑,如果当前节点是收起状态,则不遍历其子节点 if (Array.isArray(children) && children.length > 0 && isExpanded){ this.deepTraversal(children, key,expandedKeys,flatTreeData,flatTreeKeysMap); } } } return {flatTreeData,flatTreeKeysMap}; } /** * 将截取后的 List 数组转换为 Tree 结构 */ getTreeDataFromList = (treeList) => { // 属性配置设置 let attr = {id: 'key', parendId: 'parentKey', rootId: null,_isLeaf: '_isLeaf'}; let treeData = convertListToTree(treeList, attr, this.flatTreeKeysMap); return treeData; } //获取每行的唯一键 getRowKey(record, index) { const rowKey = this.props.rowKey; const key = typeof rowKey === "function" ? rowKey(record, index) : record[rowKey]; return key; } /** *判断是否是树形结构 */ checkIsTreeType(isTree,data) { if(typeof isTree == 'boolean')return isTree; let tempData = data||[]; let hasChildren = tempData.some((item)=>item.children!==undefined); return hasChildren; } componentWillUnmount() { this.cachedRowHeight = {}; //缓存每行的高度 this.expandedRowKeys = [];//缓存展开的行键值 this.flatTreeKeysMap = {}; //树表,扁平结构数据的 Map 映射,方便获取各节点信息 this.flatTreeData = []; //拉平的树表数据 } //获取当前索引行的高度 getRowHeightByIndex = (isTreeType,index,defaultRowHeight)=>{ let {height,data} = this.props; let currentRowHeight = height; let records = isTreeType ? this.flatTreeData : data; if (!records.length) return 0; let record = records[index]; let key = this.getRowKey(record,index); if(!height){//如果没有指定固定行高(即动态行高) let cacheRowH = this.cachedRowHeight[key]; if (cacheRowH !== undefined) { currentRowHeight = cacheRowH;//缓存中存在则使用缓存的行高 }else{ currentRowHeight = defaultRowHeight;//如果缓存行高也不存在则使用默认行高 } } currentRowHeight = currentRowHeight + (this.cachedRowHeight[key + `_expanded`] || 0); return currentRowHeight; } //计算总行高 getSumHeight(start, end,isTreeType) { let defaultRowHeight = getDefaultRowHeight(this.props); let sumHeight = 0; for (let index = start; index < end; index++) { let currentRowHeight = this.getRowHeightByIndex(isTreeType,index,defaultRowHeight);//获取行高 sumHeight += currentRowHeight; } return sumHeight; } //表格尺寸变化时,重新计算滚动及显示位置 handleResize = ()=>{ let {scrollTop,treeType} = this.state; this.handleScrollY(scrollTop,treeType); } /** *@description 根据返回的scrollTop计算当前的索引。此处做了两次缓存一个是根据上一次的currentIndex计算当前currentIndex。另一个是根据当前内容区的数据是否在缓存中如果在则不重新render页面 *@param 最新一次滚动的scrollTop *@param treeType是否是树状表 *@param callback表体滚动过程中触发的回调 */ handleScrollY = (nextScrollTop, treeType, callback) => { const defaultRowHeight = getDefaultRowHeight(this.props); let tempScrollTop = nextScrollTop; let {data} = this.props; let records = treeType ? this.flatTreeData : data; let index = 0; //滚动后的位置索引 while (tempScrollTop > 0 && index < records.length) { let currentRowHeight = this.getRowHeightByIndex(treeType,index,defaultRowHeight);//获取行高 tempScrollTop -= currentRowHeight; if (tempScrollTop > -1) {//确保scrollTop存在小数误差1px的情况也能正确取值 index += 1; } } // console.log('AAA-->newIndex****',index); //如果之前的索引和下一次的不一样则更新索引和滚动的位置 if (this.currentIndex !== index) { this.currentIndex = index; } this.setState({scrollTop:nextScrollTop}); callback && callback(nextScrollTop) }; setRowHeight = (height, index,rowKey, isExpandedRow)=> { // this.cachedRowHeight[rowKey] = height; if (isExpandedRow) { // this.cachedRowHeight[rowKey] = this.cachedRowHeight[rowKey] + height; this.cachedRowHeight[rowKey + `_expanded`] = height; } else { this.cachedRowHeight[rowKey] = height; } } onExpand = (expandState, record,index) => { const _this = this; let {treeType,needRender} = this.state; const { data ,onExpand} = this.props; const rowKey = this.getRowKey(record, index); //滚动加载expandedRowKeys自己维护,否则有展开不全的问题 if(!this.props.expandedRowKeys){ if(expandState){ //展开 this.expandedRowKeys.push(rowKey); //追加折叠行key this.setState({ needRender: !needRender }); }else{ //折叠 this.expandedRowKeys = this.expandedRowKeys.filter((val)=>val!=rowKey); //移除折叠行key this.setState({ needRender: !needRender }); } } // expandState为true时,记录下 onExpand&&onExpand(expandState, record,index); if(treeType) { //重新递归数据 let deep = this.deepTraversal(data,null,this.expandedRowKeys); _this.flatTreeData = deep.flatTreeData; _this.flatTreeKeysMap = deep.flatTreeKeysMap; } //展开/折叠由于行数据变化所以得清除缓存的行高 // this.cachedRowHeight = {}; if (!expandState) { this.cachedRowHeight[rowKey + `_expanded`] = 0; } }; //行拖拽-大数据表格下的实现 __onRowDragStart = (options)=>{ let {dragStartKey} = options; let {data} = this.props,currentIndex,record; data.forEach((da,i)=>{ // tr 的唯一标识通过 data.key 或 rowKey 两种方式传进来 let trKey = da.key ? da.key : this.getRowKey(da, i); if(trKey == dragStartKey){ currentIndex = i; record = da; } }); this.props.onDragRowStart && this.props.onDragRowStart(record,currentIndex); } //行拖拽-大数据表格下的实现 __onRowDragDrop = (options)=>{ let {dragTargetKey,dragTargetIndex,dropTargetKey,dropTargetIndex} = options; let {data} = this.props,record; for(let i=0;i<data.length;i++){ let da = data[i]; // tr 的唯一标识通过 data.key 或 rowKey 两种方式传进来 let trKey = da.key ? da.key : this.getRowKey(da, i); if(trKey == dragTargetKey){ record = da;break; //匹配到后则退出减少性能开销 } } if (dragTargetIndex > -1) { data = arrayMoveTo(data,dragTargetIndex,dropTargetIndex); this.props.onDropRow && this.props.onDropRow(data,record,dropTargetIndex); this.setState({needRender:!this.state.needRender}); } else { this.props.onDropRow && this.props.onDropRow(data,record,dropTargetIndex); } } render() { const { data,loadBuffer } = this.props; const { scrollTop,showCount,treeType} = this.state; let expandedRowKeys = this.props.expandedRowKeys?this.props.expandedRowKeys: this.expandedRowKeys; let totalCount = treeType ? this.flatTreeData.length : data.length; let buffer = computeIndex(totalCount,showCount,loadBuffer,this.currentIndex);//计算出缓冲区各索引值 const lazyLoad = { startIndex: buffer.startIndex, endIndex:buffer.endIndex, startParentIndex: buffer.startIndex //为树状节点做准备 }; lazyLoad.preHeight = this.getSumHeight(0, lazyLoad.startIndex,treeType); lazyLoad.sufHeight = this.getSumHeight(lazyLoad.endIndex, totalCount-1,treeType); // console.log("AAA--->总数据:"+totalCount+"渲染总行数:"+buffer.screenCount+"缓冲区行数:"+hiddenCount+"可视区域行数:"+showCount); // console.log("AAA--->startIndex:"+buffer.startIndex+"--->endIndex:"+buffer.endIndex+"--->currentIndex:"+this.currentIndex+"-->scrollTop:"+scrollTop); let dataSource = []; // 存放截取需要显示的数据 if(treeType){ let sliceTreeList = this.flatTreeData.slice(buffer.startIndex, buffer.endIndex+1);//从拉平的数据中截取 dataSource = this.getTreeDataFromList(sliceTreeList);//将截取的list数据重新组织成tree结构 }else{ dataSource = data.slice(buffer.startIndex, buffer.endIndex+1); } return ( <Table {...this.props} data={dataSource} lazyLoad={lazyLoad} ref={el => this.table = el} handleScrollY={this.handleScrollY} scrollTop={scrollTop} setRowHeight={this.setRowHeight} // setRowParentIndex={this.setRowParentIndex} onResize={this.handleResize} onExpand={this.onExpand} onExpandedRowsChange={this.props.onExpandedRowsChange} expandedRowKeys={expandedRowKeys} onRowDragStart={this.__onRowDragStart} onRowDragDrop={this.__onRowDragDrop} // className={'lazy-table'} /> ); } }; }