需求以及效果图
模拟一张表固定行数,列数不固定,且以一列键一列值交替展示;当超出固定宽度后,横向滚动展示
import React from 'react';
import { range } from 'lodash';
import styles from './index.scss';
export interface IkeyValue {
key: string;
value: string | number;
}
interface Props {
data: IkeyValue[];
fixedRows: number;
height: number;
title?: string;
keyWidth?: number;
valueWidth?: number;
maxTableWidth?: number;
}
export const KeyValueTable = ({
data,
title = '标题',
height = 400,
fixedRows = 5,
keyWidth = 180,
valueWidth = 120,
maxTableWidth = 800,
}: Props) => {
// 计算需要的列数
let columnsNeeded = Math.ceil(data.length / fixedRows);
if (columnsNeeded * (keyWidth + valueWidth) < maxTableWidth) {
columnsNeeded = Math.ceil(maxTableWidth / (keyWidth + valueWidth));
}
if (data.length !== columnsNeeded * fixedRows) {
range(columnsNeeded * fixedRows - data.length).forEach(() => {
data.push({ key: '', value: '' });
});
}
// 分组数据 - 按列优先排列
const groupedData = Array.from({ length: columnsNeeded }).map((_, colIndex) =>
Array.from({ length: fixedRows }).map((_, rowIndex) => {
const dataIndex = colIndex * fixedRows + rowIndex;
return dataIndex < data.length ? data[dataIndex] : null;
}),
);
return (
<div
className={styles['kv-table-container']}
style={{
maxWidth: `${maxTableWidth}px`,
width: '100%',
height: `${height}px`,
position: 'relative',
}}>
{/* 表格主体 */}
<div
className={styles['kv-table']}
style={{
display: 'grid',
gridTemplateRows: `auto repeat(${fixedRows}, 1fr)`,
gridAutoColumns: `${keyWidth}px ${valueWidth}px`,
gridAutoFlow: 'column',
overflow: 'auto',
height: '100%',
}}>
{/* 标题 - 跨所有列 */}
<div
className={styles['kv-table__title']}
style={{
gridRow: 1,
gridColumn: `1 / span ${columnsNeeded * 2}`,
position: 'sticky',
top: 0,
zIndex: 2,
}}>
{title}
</div>
{/* 动态渲染键值对 */}
{groupedData.map((column, colIndex) =>
column.map((item, rowIndex) => {
if (!item) return null;
return (
<React.Fragment key={`${colIndex}-${rowIndex}`}>
<div
className={styles['kv-table__column-key']}
title={item.key}
style={{
gridRow: rowIndex + 2,
gridColumn: colIndex * 2 + 1,
width: `${keyWidth}px`,
maxWidth: `${keyWidth}px`,
position: rowIndex === fixedRows - 1 ? 'relative' : 'static',
}}>
{item.key}
</div>
<div
className={styles['kv-table__column-value']}
style={{
gridRow: rowIndex + 2,
gridColumn: colIndex * 2 + 2,
width: `${valueWidth}px`,
}}>
{item.value}
</div>
</React.Fragment>
);
}),
)}
</div>
</div>
);
};
样式
.kv-table-container {
overflow: hidden;
margin-top: 20px;
border: 1px solid #dadce6;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.kv-table {
overflow-x: scroll;
.kv-table__title {
font-family: SourceHanSansCN-Bold;
font-size: 14px;
color: #5c6280;
font-weight: 700;
text-align: center;
border-bottom: 1px solid #dadce6;
background-color: #f5f5f5;
padding: 12px 16px;
position: sticky;
top: 0;
left: 0;
right: 0; /* 确保横向也占满 */
z-index: 2;
}
.kv-table__column-key {
font-size: 14px;
color: #3d3d3d;
font-weight: 400;
padding: 10px 16px;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
background-color: #fafbfd;
white-space: nowrap;
overflow: hidden;
text-align: center;
line-height: 40px;
text-overflow: ellipsis;
box-sizing: border-box;
}
.kv-table__column-value {
padding: 10px 16px;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
font-size: 14px;
color: #3d3d3d;
font-weight: 400;
box-sizing: border-box;
display: flex;
flex-basis: 180px;
width: 180px;
align-items: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:has(> :nth-child(2):last-child) {
width: 100%;
}
&::-webkit-scrollbar {
height: 8px;
width: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
}
}