背景
当页面滚动的时候,如果超过表格的部分,表格的头部会固定在某个位置,方便用户看到数据栏的标。项目采用的是vue2+antdv
,但是这个版本的table
没有sticky
属性,所以需要自行解决。
滚动前:
滚动后:
原理
知道大概是用了sticky
属性,但是奈何我在怎么修改元素的style
还是没有起到想要的作用,经过多番寻找,找到了这样一个问题:Table header sticky feature,在这最后有一个老哥提到了自己封装的一个指令:antd-table-sticky.js。
所以研究一下它的基本实现原理,在antdv table
中,它的页面结构是这样的:
div.ant-tablediv.ant-table-contentdiv.ant-table-scrolldiv.ant-table-bodytable.ant-table-fixedcolgroupthead.ant-table-theadtbody.ant-table-tbody
之前我以为是在ant-table-content
上或者是内部的html
中加入关于sticky
的属性,然而通过测试,大佬的方法主要是复制了一个.ant-table-content
元素,放在同级,将其设置fixed
div.ant-table-contentdiv.ant-table-scrolldiv.ant-table-bodytable.ant-table-fixedcolgroupthead.ant-table-theadtbody.ant-table-tbody
然后修改这个ant-table-content
的样式,并当作兄弟组件插入,最后的结果是这样的:
div.ant-tablediv.ant-table-contentdiv.ant-table-content(position: fixed; top: 64px; z-index: 1000; background-color: rgb(255, 255, 255); width: 1114px;)
如图所示:
实现的过程大概是这样的:
- 获取滚动容器,支持
id
传入获取指定的容器,不然就是默认window
; - 深度拷贝
ant-table-content
节点,并将这个节点设置样式:position: fixed;top: ${fixedTop}px; z-index: ${zIndex}; ${stickyStyle.cssText};background-color: ${bgColor}
- 监听:
- 监听容器滚动,如果滚动到指定位置,就插入拷贝的节点,否则就移除;
- 监听容器的大小,要重置拷贝节点的宽度;
- 监听
.ant-table-body
的滚动,设置拷贝节点的横向位置
- 当表格发生更新的时候,移除
.ant-table-tbody,.ant-table-placeholder
,这样就只有表头了
源码
/*** 使Ant Design Vue的Table组件支持表头sticky* 单元格宽度必须固定*/
import { throttle } from 'lodash'let listenAction;
let container;
let stickyHeader = null;
let originEl = null;
let bindingConfig = {};const originSelector = '.ant-table-content';
const scrollSelector = '.ant-table-scroll .ant-table-body';
const toRemoveSelector = '.ant-table-tbody,.ant-table-placeholder';// 获取指令参数
const getBindingConfig = (binding) => {const params = binding.value || {};const {fixedTop = 64,zIndex = 1000,bgColor = '#fff',disabled,scrollContainerId} = params;return { fixedTop, zIndex, disabled, scrollContainerId, bgColor };
};const unwatch = () => {container && container.removeEventListener('scroll', listenAction);container && container.removeEventListener('resize', resizeStickyHeader);originEl && originEl.querySelector(scrollSelector).removeEventListener('scroll', setScrollX);
};const watch = () => {container && container.addEventListener('scroll', listenAction);container && container.addEventListener('resize', resizeStickyHeader);originEl && originEl.querySelector(scrollSelector).addEventListener('scroll', setScrollX);
};// 根据表格实际内容修改表头内容
const adaptStickyHeader = () => {stickyHeader.innerHTML = originEl.innerHTML;stickyHeader.querySelector(scrollSelector).style.overflowX = 'hidden';const tbodyList = Array.from(stickyHeader.querySelectorAll(toRemoveSelector));tbodyList.forEach((tbody) => {tbody.parentNode.removeChild(tbody);});resizeStickyHeader();setScrollX();
};// 根据实际内容设置宽度
const resizeStickyHeader = throttle(() => {stickyHeader.style.width = `${originEl.getBoundingClientRect().width}px`;
});// 根据表格横向滚动,设置表头的横向位置
const setScrollX = throttle(() => {const stickyHeaderScroller = stickyHeader.querySelector(scrollSelector);const originScroller = originEl.querySelector(scrollSelector);stickyHeaderScroller.scrollLeft = originScroller.scrollLeft;
});export default {bind(el, binding) {// 获取 .ant-table-contentoriginEl = el.querySelector(originSelector);// 读取指令参数bindingConfig = getBindingConfig(binding);const { disabled, fixedTop, zIndex, scrollContainerId, bgColor } = bindingConfig;if (disabled) return;// 如果没有指定父级容器的id,则默认为浏览器container = document.getElementById(scrollContainerId) || window;let active = false;stickyHeader = originEl.cloneNode(true);const stickyStyle = stickyHeader.style;stickyStyle.cssText = `position: fixed;top: ${fixedTop}px; z-index: ${zIndex}; ${stickyStyle.cssText};background-color: ${bgColor}`;const sticky = () => {if (active) return;setScrollX();originEl.insertAdjacentElement('afterend', stickyHeader);active = true;};const reset = () => {if (!active) return;stickyHeader.parentNode?.removeChild(stickyHeader);active = false;};listenAction = throttle(() => {const rectEl = originEl?.parentNode;const rect = rectEl.getBoundingClientRect();const offsetTop = rect.top;if (offsetTop <= fixedTop) {return sticky();}reset();});watch();},unbind: unwatch,update(el, binding) {bindingConfig = getBindingConfig(binding);originEl = el.querySelector(originSelector);if (bindingConfig.disabled) {stickyHeader.parentNode?.removeChild(stickyHeader);unwatch();return;}adaptStickyHeader();},
};
参考
- Table header sticky feature
- antd-table-sticky.js