模拟一张表固定行数,列数不固定,且以一列键一列值交替展示

36 阅读2分钟

需求以及效果图

模拟一张表固定行数,列数不固定,且以一列键一列值交替展示;当超出固定宽度后,横向滚动展示 image.png

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;
    }
  }
}