一、什么是表格冻结 当数据较多时将某些行列进行冻结,即固定在某一位置展示,比如当数据较多时,表头可以固定在首行展示,不受滚动影响。由于项目中表格及交叉透视表的功能已经实现,因此只能在原来基础上进行功能的改造,因此需要在原来表格基础上添加表头及多列冻结。
二、实现方案
方案一 两个表格拼凑 第一个表格展示头部和固定行 第二个表格展示其它数据 示意图如下
优点:固定行数没有限制
缺点:适用场景,仅横向 纵向均无滚动条时可用
若有横向滚动条,两个表格的滚动条是分开的,会出现滚动 A B 不动,或滚动 B A 不动的情况,Windows 下会展示两个横向滚动条,不美观
若有纵向滚动条,在 Windows 下滚动条是默认有宽度的,滚动条的宽占用了表格 B 的部分宽度,导致表头和表主体列错位,示意图如下
方案二 修改表格样式 将合并行上移 ElementUI 的 Table 有合并行这个功能,合并行位于 footer 中,固定在表格底部,即使 body 有横向或纵向滚动,样式和功能都已经优化好了。示意图如下
要做的就是把样式调整成我们想要的,使用定位把 footer 放到 header 下,body 向下移一行的距离即可。
三、初步实现表格冻结 1、表头和首列冻结功能设置入口 在表格编辑的右边控制面板 → 高级中增加表格表头和首列冻结设置项
2、表头和首列冻结实现 sugar 中表格分为简单表格和交叉透视表,因此实现冻结需要满足两种表格要求,而且需要不影响之前表格布局和样式情况下,进行冻结功能添加。
(一)监听表格滚动 通过监听表格滚动,当滚动条滚动时并且表头固定设置打开时,此时应该固定 thead 位置,但是由于 thead 会随滚动条滚动,此时可以用 div 模拟 thead 进行定位在顶部。
图(一)
图(二)
图一通过 div 模拟 thead 固定在表格顶部,但是 thead 每个单元格宽度如何完美呈现在模拟 thead 的 div 中呢?
(二)表格信息记录
通过查找 thead 元素,并将表格每行及每列的宽高、边框大小、背景颜色等信息进行记录,然后在用 div 进行模拟时,根据记录的数据设置每行每列样式,做到与原表格样式一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 if (this .refs.theVeryTable && this .dom) { let fixedThs = (this .refs.theVeryTable as HTMLElement).getElementsByTagName('th' ); let superHeaderThSizeArr = []; let tableContainerWidth = (this .refs.tableContainer as HTMLElement).clientWidth; for (let i = 0 ; i < fixedThs.length; i++) { superHeaderThSizeArr.push([ fixedThs[i].clientWidth + fixedThs[i].clientLeft, fixedThs[i].clientHeight + fixedThs[i].clientTop, fixedThs[i].clientLeft, fixedThs[i].clientTop ]); } if ( JSON .stringify(superHeaderThSizeArr) !== JSON .stringify(this .state.superHeaderThSizeArr) || tableContainerWidth !== this .state.tableContainerWidth ) { this .setState({ superHeaderThSizeArr }); } } {superHeaders.map((headerItem: PlainObject[], headerIndex: number ) => { return ( <div key={headerIndex}> {headerItem && headerItem.map ? headerItem.map((headerTr: PlainObject, headerTrIndex: number ) => { const thStyle: PlainObject = { width: this .state.superHeaderThSizeArr[headerTrIndex][0 ], height: this .state.superHeaderThSizeArr[headerTrIndex][1 ], borderLeft: this .state.superHeaderThSizeArr[headerTrIndex][2 ] ? this .state.superHeaderThSizeArr[headerTrIndex][2 ] + `px solid ${_lineColor} ` : 0 , borderBottom: '1px solid #e8e8e8' , display: 'inline-block' , fontSize: 1 + parseInt (config.fontSize || 13 , 10 ), padding: `${config.paddingTB || 8 } px ${config.paddingLR || 12 } px` , float: 'left' , fontWeight: 'bold' }; if (headerTr['name' ] === undefined ) { thStyle.borderBottom = `solid 1px ${_titleBg} ` ; } return headerTr['colspan' ] > 0 ? ( <div key={headerTrIndex} style={thStyle} > {headerTr['name' ]} </div> ) : null; }) : null} </ div> ); })}
虽然样式一致,但是当表格内容过多时,页面需要滚动,如何做到模拟的 thead 与 thead 一致随着页面滚动呢?
(三)监听表格页面滚动 当固定首列时,页面内容滚动时,模拟的首列信息需要跟着滚动,因此需要监听页面的滚动,然后根据表格滚动的 scrollTop 信息,将模拟的表格列进行定位,已到达模拟滚动的效果,为了防止页面卡顿可以加入防抖操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 tableScroll() { const { chart: {config} } = this .props; if (this .refs.normalHeader) { let scorllTop; if (this .props.mode === 'editor' ) { let editor = document .getElementById('editor-body' ); scorllTop = editor ? editor.scrollTop + this .headerHeight : 0 ; } else { scorllTop = (document .documentElement.scrollTop || document .body.scrollTop || window .pageYOffset) + this .headerHeight; } let tableHeaderOffset = scorllTop - this .offset(this .refs.normalHeader); let height = Math .min(this .dom.clientHeight, (this .refs.theVeryTable as HTMLElement).offsetHeight); const tableContainerScrollTop = (this .refs.tableContainer as HTMLElement).scrollTop; const normalHeaderHeight = document .getElementsByClassName('normalHeader' ) && document .getElementsByClassName('normalHeader' )[0 ].clientHeight; if (tableHeaderOffset < 0 || (height - 100 - tableHeaderOffset < 0 && this .state.tableShow)) { this .setState({ tableShow: false }); } else if (tableHeaderOffset >= 0 && height - 100 - tableHeaderOffset >= 0 && !this .state.tableShow) { if (config.fix && config.fix.includes('fixedHeader' ) && tableContainerScrollTop > normalHeaderHeight) { this .setState({ tableShow: true }); } } if (this .dom.scrollTop > (this .refs.normalHeader as HTMLElement).offsetTop && !this .state.showFixedDiv) { if (config.fix && config.fix.includes('fixedHeader' )) { this .setState({ showFixedDiv: true }); } else { this .setState({ showFixedDiv: false }); } } else if (this .dom.scrollTop <= (this .refs.normalHeader as HTMLElement).offsetTop && this .state.showFixedDiv) { this .setState({ showFixedDiv: false }); } if (this .dom.scrollLeft !== this .state.scrollLeft) { this .setState({ scrollLeft: this .dom.scrollLeft }); } if (this .dom.scrollTop !== this .state.scrollTop) { this .setState({ scrollTop: this .dom.scrollTop }); } } }
四、最后实现方案 (1)固定行列实现
由于 sugar 中的 table 可能需要固定表头同时并固定列,这样需要三个相同表格来覆盖展示,不是太有好,因此使用 div 模拟的形式去处理,减少了不必要的数据渲染。
div 模拟普通表格时比较简单,当模拟交叉透视表,出现行列合并时,很容易出现排列错误问题。
因此通过模拟 table 中行列布局形式去实现:
1、让每一行有固定高度,防止没内容时导致错位
2、通过 flex 形式对行内元素进行布局,因为固定的列最后一列肯定有元素,因此采用
justify-content:’flex-end’形式去布局页面,每行元素从后往前一次排列,通过固定宽高自动将元素挤到固定位置 3、通过遍历表头元素已经记录表格的宽高信息,通过遍历 tbody 的 tr 记录行的高度(交叉透视表时主要记录普通表头宽高,超级透视表中固定列宽度主要是: 1、colspan < 固定列宽度时,当前所在列宽度 + 向后固定列数个普通表头宽度)
固定列的元素宽高如何计算
<1>通过列的 rowspan 计算固定列元素的宽高计算1>
当前列元素高度 = 当前行高度 + 从当前行高度向后rowspan个行的高度
当前列元素宽度 = 当前元素所在列宽度 + 从当前列宽度向后colspan个列宽度,但是当固定的列小于colspan时,当前所在列宽度 + 向后固定列数个普通表头宽度
风险:由于不同浏览器下可能会有 1px 偏差,导致 div 模拟的行列可能会发生错乱。
<2> 为了防止出现不同浏览器下可能出现错位风险,因此改为用 table 去模拟固定行列,这样可以通过 colspan 和 rowspan 去计算宽高,减少了页面渲染时的计算问题,也优化了一些性能。2>
4、页面滚动
<1>通过设置 scrollTop 进行上下移动,scrollLeft 进行左右移动1>
问题:由于内容滚动时左右固定列也需要滚动,左右列滚动时内容也需要滚动,因为会造成 scroll 事件的互相触发,造成滚动时页面跳动,并造成页面卡顿。
解决办法:使用模拟滚动条来解决 scroll 事件互相触发问题(如:better-scroll、perfect-scrollbar)
5、优化滚动
<1>由于 react 自身原因,在页面滚动时最好不要进行 setState 操作,防止页面的 diff 过程,并且防止当存在 componentDidUpdate 时,频繁触发 componentDidUpdate 内操作1>
<2>不要进行大量的计算,防止在滚动时,出现大量计算,阻塞页面渲染造成页面卡顿2>
<3>滚动时加入防抖3>