bee-table
Version:
Table ui component for react
467 lines (443 loc) • 20.1 kB
JavaScript
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'}
/>
);
}
};
}