由于项目需求,我使用了ant Degisn 的Table组件,随着需求的变化,在原有的表格上增加了列排序、滚动条、列拉伸以及固定列的功能(一万头XX在心中奔腾。。。)。刚开始听到需求时,我是不以为然的,毕竟ant都提供了对应的属性和方法嘛,不慌!。。。结果。。。脸都肿了。
只实现单一功能时,ant做的很完美,但是将这些功能集合到一起时,就出现了表格错位等界面问题。(其实,我觉得很大可能是我没有正确使用)
行吧,那我自己写。
ps:框架是
react
列展示
需求是表格的列的展示可以由用户操作,实现上使用了,antd的filterDropdown
属性:
实现逻辑很简单,通过filterDropdown
属性写一个列多选框,根据用户选中的值来重新定义columns
,主要涉及四个值defaultTitle:列可选项,defaultColunms:所有用户可操作列,thTitle:展示列表头数据,columns:table需要的columns数据
:
// colunms处理
// thTitle 表头
renderColumns=(thTitle)=>{
const columns = [
{
title: '#',
dataIndex: 'rock',
render: text => <div className={styles.colTitle}>{text}</div>,
align: 'center',
width:40,
},// 序号列
];
thTitle.forEach(item => {
// 由于项目后台需求,传递了一些用户无法看到但是又需要返回给后台的数据,所以增加了一个参数判断该列是否展示,表格所有的信息都是从后台获取
if (item.flag) {
columns.push({
title: item.name,
dataIndex: item.thId,
sorter: item.isSorting,
width:140,
render: text => (
<div className={styles.colSql} title={text}>
{text}
</div>
),
// option:将请求回来的表格的所有列名称存入defaultTitle,作为多选框可选项
// defaultValue:在第一次展示时,有些列可能默认不展示,这里需要根据传回来的数据进行判断啦
// value:这里传入用户操作后的值
filterDropdown: () => (
<div className={styles.customFilterDropdown}>
<CheckboxGroup
options={defaultTitle}
defaultValue={defaultShowTitle}
value={this.state.thTitle.length > 0 ? this.state.thTitle : defaultTitle}
onChange={this.onChange}
/>
</div>)
});
}
});
columns.push({
title: '操作',
dataIndex: 'action',
align: 'center',
fixed: 'right',
render: (text, record) => (
<div className={styles.colSql}>
{this.handleAction(text, record)}
</div>
)// 操作列,不同表格拥有不同操作,也是后台传递
});
const defaultColumns = columns;
this.setState({
columns,
defaultColumns,
});
}
}
// 用户操作后触发onChange方法:
onChange = checkedValues => checkedValues => {
const showTitle = [];
const datas = [
{
title: '#',
dataIndex: 'rock',
render: (text, record) => <span className={styles.colTitle}>{text}</span>,
align: 'center',
},// 在重写columns时,有些一些不可操作列需要重新手动存储,比如序号列,操作列等
];
// defaultColunms是获取到的所有列
// columns是需要展示的列
// checkedValues为用户选中列
// showTitle为新的展示表头
this.state.defaultColumns.forEach(r => {
checkedValues.forEach(rs => {
if (r.title === rs) {
datas.push(r);
showTitle.push(r.title);
}
});
});
datas.push({
title: '操作',
dataIndex: 'action',
width: `${100 / (showTitle.length + 1)}%`,
align: 'center',
render: (text, record) => {
return this.handleAction(text, record);
},
});
this.setState({ columns: datas, thTitle: showTitle });
};
由于后期需求调整,这个功能就被去掉了,没有效果图,但是效果一百分!
列排序
首先利用filter
在表头设置排序图标:
const cols = columns.map((col, index) => ({
...col,
filterDropdown: col.sortered, //排序图标占位,根据传入的值判断该列是否需要排序功能
filterIcon: filtered => <div className={styles.sorter}>
<Icon id={`${col.dataIndex}ascend`} type="caret-up" onClick={()=>this.sorter("ascend","descend",col)}/>
<Icon id={`${col.dataIndex}descend`} type="caret-down" onClick={()=>this.sorter("descend","ascend",col)} />
</div>,// 自定义筛选图标,就是两个箭头图标,绑定排序方法
}));
排序方法:
sorter = (sor,col) => {//sor排序规则,col列属性
//排序图标样式变化
const icon=document.querySelectorAll("i");
if(icon.length>0){
icon.forEach(item=>item.style.color="#848484")
};// 先将所有图标改为为排序样式
const docu=document.getElementById(col.dataIndex+sor);
docu.style.color="#1890ff" ;//再改变被选中的图标样式
//
//这里进行数据请求,将值传给Table的onChange方法等操作
//
};
滚动条
antd的滚动条是我觉得最坑的一个功能,由于项目中需要展示大量表格,且每个表格列数不一,在复用Table组件时动不动就会错位,以及在同时使用fixed
属性固定列时,居然会有空白!!!,两个功能居然不能完美兼容。所以我对表格滚动进行了重写,不需要使用antd的scroll属性或者其他属性,实现表格滚动条的自适应。这里利用了Table的components
属性,对ant的默认样式进行覆盖。
// 分别对table、tbody、tr的样式进行重新设置。
const BaseHeader=(props)=>{
const {...restProps}=props;
const tableStyle={
display:'table',
tableLayout:'fixed',
width:'100%',
};
return(
<thead {...restProps} style={tableStyle}/>
)
};
const BaseTable=(props)=>{
const{...restProps}=props;
const tableStyle={
height:tableHeight,
overflow:'auto',
display:'block',
width:'100%'
};
return(
<tbody {...restProps} style={tableStyle}/>
)
};
const BaseRow=(props)=>{
const {...restProps}=props;
const tableStyle={
display:'table',
// tableLayout:'fixed',
width:'100%',
height:lineHeight
};
return(
<tr {...restProps} style={tableStyle}/>
)
};
在components
中调用:
//.js文件
this.components = {
header: {
wrapper:BaseHeader
},
body:{
wrapper:BaseTable,
row:BaseRow,
},
};
//.less文件中
.ant-table-body{
overflow: auto;
}
// 附带滚动条样式优化
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track-piece {
background-color: #e8e8e8;
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
height: 5px;
background-color: rgba(0, 0, 0, 0.5);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
width: 5px;
background-color: rgba(0, 0, 0, 0.5);
-webkit-border-radius: 6px;
}
到这,表格就可以抛弃scroll
属性,实现自适应滚动条了。但是,如果你同时要求固定表格里的某些列,使用了fixed
属性,你会发现,antd为你生成了两个table
(固定列与非固定列),滚动时不能两个表格同时滚动,这明显不符合我们的需求:
// 为了解决这个问题,需求在表格渲染后(componentDidUpdate)再对表格进行一些处理
let timer=null;
componentDidUpdate(){
// 由于可能在同一个页面中存在多个表格,所以需要先获取表格组件最外层div,以避免表格互相影响
const standardTable=document.querySelectorAll("div[class='antd-pro-components-task-manager-log-info-standard-table-index-standardTable']");
for(let tableNum=0;tableNum<standardTable.length;tableNum+=1){// 遍历表格组件
const tbody=standardTable[tableNum].querySelectorAll("tbody");// 获取tbody
if(tbody.length>1){//判断是否需要存在两个表格,不存在则不需要处理滚动
//定时器,不设置定时器的话,两个滚动方法互相影响,导致鼠标滚轮滚动很慢,所以设定一个定时器,在滚动其中一个表格时,取消另一个表格的滚动方法,在用户停止滚动一段时间后再为其加上滚动方法
timer=null;
//为两个表格分别绑定滚动方法
tbody[0].onscroll=()=>this.handleScroll(tbody[1],tbody[0],timer);
tbody[1].onscroll=()=>this.handleScroll(tbody[0],tbody[1],timer)
}
}
}
// table滚动方法
handleScroll=(tbody,tbody2,timer)=>{
tbody.onscroll=null;//取消没有触发滚动操作的表格的滚动方法
clearTimeout(timer);//清空定时器,避免上一次滚动的定时器影响
const self=this;
tbody.scrollTop=tbody2.scrollTop;//表格滚动同步
timer = setTimeout(function() {
tbody.onscroll=()=>self.handleScroll(tbody2,tbody,timer);
}, 300 );//300毫秒后重新为另一个表格绑定滚动方法
};
ps:最后别忘了在组件卸载前清空定时器。
列拉伸
敲重点!!!重写之前所有功能都是为了实现这一个功能!!!万万没想到这一个功能把我坑的那么惨!如果你不需要在滚动固定等功能的基础上实现列拉伸,而只是想单独使用其中一些功能,那么这antd的属性表现得很好,我这些都是废话。
列拉伸的实现逻辑就是根据鼠标按下到放开这一过程中移动的距离先改变表头的宽度,再改变单元格的宽度,所以在拉伸过程中两者会出现错位,但是在松开鼠标后,表格就会对齐。
由于之前尝试使用antd推荐的列拉伸方式,所以我安装了react-resizable
插件,结果最后我居然只使用了它提供的样式。。。所以关于拉伸的样式,你们自己写,可以参考以下下面的样式:
先在表格表头设置一个拉伸功能触发点,我设定的是图中红框位置,相对于表头的绝对定位
position: absolute;
width: 10px;
height: 100%;
bottom: 0;
right: -5px;
cursor: col-resize;
样式设置好了就可以考虑实现功能,实际上就是对渲染后的表格样式进行处理,理清了实现逻辑,其实不看代码,自己就能够实现:
//由于其中涉及了表格样式,所以看个大概意思就好,照搬没有意义,实际实现时需要根据情况进行调整
componentDidUpdate(){
const standardTable=document.querySelectorAll("div[class='antd-pro-components-task-manager-log-info-standard-table-index-standardTable']");
for(let tableNum=0;tableNum<standardTable.length;tableNum+=1){
const table=standardTable[tableNum].querySelectorAll("table");
let actionWidth=0;//该参数是为了设置固定列表格的列宽
for (let m=0;m<table.length;m+=1) {
const th = table[m].querySelectorAll("th");//表头
const docu = table[m].querySelectorAll("tr[class='ant-table-row ant-table-row-level-0']");//表格内容行
if (th.length > 0 && !th[0].onmousedown) {//绑定鼠标方法
this.handleMouse(th, docu);
}
//由于表格其中一列宽度改变,所以需要对其余列列宽进行调整,保证表头与表格内容对齐
for (let n = 0; n < th.length; n += 1) {
const thWidth = th[n].offsetWidth;
if(m===0){
actionWidth=thWidth;
}
const text = th[n].innerText.replace(/[\r\n]/g, "");//表头文本
//避免多选框,序号列等表头宽度过宽,先设定一个定值
if ((text===''&&n===0) || text === "#") {
th[n].style.width = `${numWidth}px`;
}
for (let i = 0; i < docu.length; i += 1) {
const td = docu[i].querySelectorAll("td");
const div = td[n] ? td[n].querySelectorAll("div") : [];
if (div.length > 0) {
div[0].style.width = thWidth < lineWidth ? `${lineWidth - 4}px` : `${thWidth - 4}px`;
td[n].style.width = thWidth < lineWidth ? `${lineWidth}px` : `${thWidth}px`;
}
//由于padding等属性会影响到th宽度的获取,为了保证表格对齐,所以对表头的宽度再一次设置
th[n].style.width = thWidth < lineWidth ? `${lineWidth}px` : `${thWidth}px`;
//根据需求,重新设置多选框,序号列等宽度
if ((text===''&&n===0) || text === "#") {
td[n].style.width = `${numWidth}px`;
th[n].style.width = `${numWidth}px`;
if (div.length > 0) {
div[0].style.width = `${numWidth - 4}px`
}
}
//固定列列宽,防止antd自适应,生成过宽固定列
if(m===1){
if (div.length > 0) {
div[0].style.width = `${actionWidth - 4}px`;
td[n].style.width = `${actionWidth}px`;
}
th[n].style.width = `${actionWidth}px`;
}
}
}
}
}
}
//拉伸方法
handleMouse=(th,docu)=>{
let moveWidth = 0;
for(let n=0;n<th.length;n+=1){
let oldWidth = th[n].clientWidth;
const span=th[n].querySelectorAll("span[class='react-resizable-handle']");
if(span.length>0) {
const self=this;
span[0].onmousedown = function (ev) {
//event的兼容性
ev = ev || window.event;
ev.stopPropagation();
//获取鼠标按下的坐标
const x1 = ev.clientX;
//给可视区域添加鼠标的移动事件
document.onmousemove = function (ev) {
//event的兼容性
ev = ev || window.event;
//获取鼠标移动时的坐标
const x2 = ev.clientX;
//计算出鼠标的移动距离
moveWidth = x2 - x1;
//更改被操作列列宽度
for(let m=0;m<th.length;m+=1){
const thWidth=th[m].clientWidth;
for (let i = 0; i < docu.length; i += 1) {
const td = docu[i].querySelectorAll("td");
const allDiv=td[m]?td[m].querySelectorAll("div"):[];
const div = td[n].querySelectorAll("div");
const text=th[m].innerText.replace(/[\r\n]/g,"");
if((text===""&&m===0)||text==="#"){
td[m].style.width=`${numWidth}px`;
th[m].style.width =`${numWidth}px`;
if(allDiv.length > 0){
allDiv[0].style.width = `${numWidth-4}px`
}
}
//设定一个列宽最小值,如果操作后的列宽小于140,则不调整
if (moveWidth + oldWidth > 140) {
if (div.length > 0) {
div[0].style.width = `${oldWidth + moveWidth - 4}px`
}
td[n].style.width = `${oldWidth + moveWidth}px`;
th[n].style.width = `${oldWidth + moveWidth}px`;
} else {
if (div.length > 0) {
div[0].style.width = `136px`
}
td[n].style.width = `140px`;
th[n].style.width = `140px`;
}
}
}
};
//清除
document.onmouseup = function (ev) {
document.onmousemove = null;
};
};
}
}
};
最后,在实现这些功能中,查看了很多信息,也参考了很多意见,做了很多尝试,在此统一感谢各位大佬。
Bug肯定是会有的,毕竟我是一个沉迷于写Bug的人呐!代码仅作参考~