大致思路是:建立两个table,一个table里只有thead,一个table里只有tbody,分别把两个table用div包裹起来,设置有tbody的div固定高度,超出overflow:scroll;至于横向滚动的问题,可以设置position: sticky,然后根据左右浮动的个数及对应列所在的index,计算left或者right的长度。
一.为什么要把table拆成两部分?
因为table里thead和tbody本身无法设置高度,超出用stroll这种方式,无效。所以考虑用div来包裹,然后设置高度超出stroll,因为要实现表头固定,body滚动,所以把thead单独提出来作为一个表格,然后用定位的方式并成一个完整的表格。
<div css={select().background("#fff").height("calc(100% - 80px)").position("relative").paddingTop(38)}> // 外部div设置padding高度为表头div的高度,表头div绝对定位,固定在顶部
// 表头table
<div
css={
{
height: 38,
width: "100%",
position: "absolute",
overflowX: "scroll",
overflowY: "hidden",
top: 0,
"&::-webkit-scrollbar": { display: "none" },
} as any
}>
<table
css={select()
.width("100%")
.position("absolute")
.top(0)
.overflowX("scroll")
.borderCollapse("collapse")
.tableLayout(tableLayout || "auto")}
{...otherProps}>
<thead>
<tr css={select().backgroundColor(colors.gray1).borderSpacing(0)}>
{map(endColumns, (column, index) => (
<th
key={column.key}
css={select()
.paddingY(roundedEm(0.7))
.paddingX(roundedEm(0.9))
.width(column.width || "auto")
.textAlign(column.align || "left")
.fontWeight(500)
.fontSize(theme.fontSizes.s)}>
{ column.title }
</th>
))}
</tr>
</thead>
</table>
</div>
// 表体table
<div
css={
{
maxHeight: "100%", // 可以自己指定表体高度
clear: "both",
overflow: "scroll"
} as any
}>
<table
css={select()
.width("100%")
.borderCollapse("collapse")
.tableLayout('fixed')}
{...otherProps}>
<tbody
css={select()
.color(colors.gray6)
.with(
select("tr").borderBottom(`1px solid`).borderColor(theme.state.borderColor).borderSpacing(0)
)
.with(select("tr:last-child").borderBottom(`none`))
.with(select("tr td").padding(roundedEm(0.9)))}>
// 这里是tbody里数据的处理,类似antd的
{map(dataSource, (row, rowIdx) => (
<TableRow
key={get(row, rowKey)}
columns={endColumns}
expandable={expandable}
row={row}
rowIndex={rowIdx}
onRowClick={onRowClick}
rowStyle={rowStyle}
/>
))}
</tbody>
</table>
</div>
{loading ? <Loading /> : size(dataSource) ? null : <Empty />} // 这里做的是数据为空和请求数据是的样式处理
</div>
二.position:sticky是什么?
sticky是position的新属性值,叫黏性定位。它是一个在static和fixed变化的属性,当你的内容位置没有超过容器范围时,它是正常布局,你设置的定位属性(left,right等)是无效的;当你的内容位置超出了容器的范围时,它会变成fixed定位,定位位置根据你设置的left,right的值来定位。
了解了sticky的用法,自然就知道怎么来实现固定列横向滚动表格了。这里贴一个thead表格的横向滚动写法,tbody的表格是一样的。
<thead>
<tr css={select().backgroundColor(colors.gray1).borderSpacing(0)}>
{map(endColumns, (column, index) => (
<th
key={column.key}
style={{
[column.sticky as string]: getStickyOffset(index, columns, column.sticky), // 根据column传过来的左固定还是右固定来计算对应的left或right的值
}}
css={select()
.paddingY(roundedEm(0.7))
.paddingX(roundedEm(0.9))
.width(column.width || "auto")
.textAlign(column.align || "left")
.fontWeight(500)
.fontSize(theme.fontSizes.s)
// 根据column传过来的是否sticky来设置position属性
.with(column.sticky ? stickyColumnStyle(column.sticky) : null)}>
{column.onSort ? (
<div css={{ display: "flex", justifyContent: "flex-start", alignItems: "center" }}>
{column.title}
{column.onSort && <SortTrigger onSort={column.onSort} />}
</div>
) : (
column.title
)}
</th>
))}
</tr>
</thead>
// column数据格式
const columns = [
{
title: "产品名称",
key: "productName",
width: 200,
ellipsis: true,
sticky: "left", // 左侧固定
formatter: (_: string) => (_ ? _ : "-"),
},
{
title: "产品厂商",
key: "manufacturerName",
width: 200,
ellipsis: true,
formatter: (_: any) => (_ ? _ : "-"),
},
{
title: "型号",
key: "productModel",
width: 150,
ellipsis: true,
formatter: (_: any) => (_ ? _ : "-"),
},
{
title: "价格",
key: "productPrice",
width: 100,
ellipsis: true,
formatter: (_: number) => (_ || _ === 0 ? `¥${_}` : "-"),
},
{
title: "操作",
key: "action",
width: 100,
sticky: "right", // 右侧固定
align: "right",
formatter: (_: any, record: any) => (
<span
css={{ color: "#4F78E0", cursor: "pointer" }}>
编辑
</span>
),
},
];
// getStickyOffset
const getStickyOffset = (currentIndex: number, endColumns: ITableColumn<any>[], type: string | undefined) => {
if (currentIndex === 0) {
return 0;
}
let max = 0;
let value = 0;
forEach(endColumns, (item, index) => {
if (currentIndex === index) {
return;
}
max += Number(item.width);
if (currentIndex > index) {
value += Number(item.width);
}
});
if (type === "right") {
return max - value;
}
return value;
};
// stickyColumnStyle
const stickyColumnStyle = (type: "left" | "right") =>
select()
.position("sticky")
// .zIndex(2)
.background("inherit")
.with(
select("&:after")
.content(`""`)
.position("absolute")
.height("100%")
.width("30px")
.top(0)
.transform("translate(100%)")
.transition("box-shadow 0.3s")
.pointerEvents("none")
// 可以设置固定后的样式,添加阴影之类的
.with(
type === "left"
? // ? select().right(0).transform(`translateX(100%)`).boxShadow(`inset 10px 0 8px -8px rgb(0 0 0 / 15%)`)
select().right(0).transform(`translateX(100%)`)
: // : select().left(0).transform(`translateX(-100%)`).boxShadow(`inset -10px 0 8px -8px rgb(0 0 0 / 15%)`),
select().left(0).transform(`translateX(-100%)`),
),
);
三.怎么实现表头和表体同步滚动
实现了竖直滚动表头固定,表头和表体可以横向滚动,但是遇到一个问题:表头和表体的滚动是分开的,各滚各的,显然不是我们要的效果,所以,需要实现表头和表体的同步滚动,这里需要用到scroll事件。
在react函数组件里,要操作组件内的dom元素,需要用到useRef来获取dom的实例。
// useRef分别获取thead和tbody两个div的实例
const headerRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement>(null);
return (
<div css={select().background("#fff").height("calc(100% - 80px)").position("relative").paddingTop(38)}>
<div
ref={headerRef}
// 通过赋值scrollLeft,表头移动时带动表体移动
onScroll={() => {
// console.log("headerRef", headerRef.current.scrollLeft);
bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
// console.log("bodyRef", bodyRef);
}}
css={
{...} as any
}>
<table>
<thead>
...
</thead>
</table>
</div>
<div
css={
{
maxHeight: "100%",
clear: "both",
overflow: "scroll"
} as any
}
ref={bodyRef}
onScroll={() => {
// console.log("headerRef", headerRef.current.scrollLeft);
headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
// console.log("bodyRef", bodyRef);
}}>
<table>
<tbody>
...
</tbody>
</table>
</div>
...
</div>
);
以上,就实现了一个简易的固定表头固定列横向滚动纵向滚动的表格。