react0.14+jquery固定表头表格

代码文件index.js如下:

// index.js
import React, { Component, PropTypes } from 'react';
import $ from 'jquery';
import './style.less';

export default class CommonTable extends Component {
    static propTypes = {
        columns: PropTypes.array.isRequired,
        data: PropTypes.array.isRequired,
        height: PropTypes.number,
        rowHeight: PropTypes.number,
        className: PropTypes.string
    };

    static defaultProps = {
        height: 300,
        rowHeight: 36,
        className: '',
    };

    constructor(props) {
        super(props);
        this.state = {
            scrollbarHeight: 0,
        }
    }

    componentDidMount() {
        this._bindEvents();
        this._applyColumnWidths();
        this._syncScrollPositions(); // 初始同步
    }

    componentDidUpdate() {
        this._applyColumnWidths();
        this._syncScrollPositions();
    }

    componentWillUnmount() {
        this._unbindEvents();
    }

    _bindEvents() {
        const $body = $(this.refs.body);
        const $headerInner = $(this.refs.headerInner);
        const $scrollbar = $(this.refs.scrollbar);

        // 表体滚动时,header与底部滚动条同步
        $body.on('scroll.commonTable', () => {
            const left = $body.scrollLeft();
            $headerInner.css('margin-left', -left);
            $scrollbar.scrollLeft(left);
        });

        // 底部滚动条滚动时,body与header同步
        $scrollbar.on('scroll.commonTable', () => {
            const left = $scrollbar.scrollLeft();
            $headerInner.css('margin-left', -left);
            $body.scrollLeft(left);
        });

        $(window).on('resize.commonTable', () => this._applyColumnWidths());
    }

    _unbindEvents() {
        $(this.refs.body).off('.commonTable');
        $(this.refs.scrollbar).off('.commonTable');
        $(window).off('resize.commonTable');
    }

    _syncScrollPositions() {
        // 保证三者初始 scrollLeft 一致
        const $body = $(this.refs.body);
        const $scrollbar = $(this.refs.scrollbar);
        const $headerInner = $(this.refs.headerInner);
        const left = $body.scrollLeft() || $scrollbar.scrollLeft() || 0;
        $body.scrollLeft(left);
        $scrollbar.scrollLeft(left);
        $headerInner.css('margin-left', -left);
    }

    _applyColumnWidths2() {
        const cols = this.props.columns;
        const $headerRow = $(this.refs.headerRow);
        const $bodyTable = $(this.refs.bodyTable);
        const $bodyRows = $bodyTable.find('tr.ct-row');
        const containerWidth = $(this.refs.container).innerWidth();

        // 计算固定列宽和可伸缩列数
        let totalFixed = 0;
        let flexibleCount = 0;
        cols.forEach(col => {
            if (typeof col.width === 'number') totalFixed += col.width;
            else flexibleCount++;
        });

        let finalWidths = [];

        if (totalFixed >= containerWidth) {
            // 总列宽超过容器,固定列宽 + 横向滚动
            finalWidths = cols.map(col =>
                typeof col.width === 'number' ? col.width : 100
            );
            $(this.refs.body).css({ overflowX: 'auto' });
            if (this.state.scrollbarHeight === 0) {
                this.setState({ scrollbarHeight: 16 });
            }
        } else {
            // 列少,均分剩余空间
            const remaining = containerWidth - totalFixed;
            const flexWidth = flexibleCount > 0 ? Math.floor(remaining / flexibleCount) : 0;
            finalWidths = cols.map(col =>
                typeof col.width === 'number' ? col.width : flexWidth
            );
            $(this.refs.body).css({ overflowX: 'hidden' });
            if (this.state.scrollbarHeight === 16) {
                this.setState({ scrollbarHeight: 0 });
            }
        }

        const totalWidth = finalWidths.reduce((a, b) => a + b, 0);

        // 设置表格宽度等于容器宽度
        $bodyTable.css({ tableLayout: 'fixed', width: containerWidth });
        $(this.refs.headerRow).closest('table').css({ tableLayout: 'fixed', width: containerWidth });

        // 设置表头列宽
        $headerRow.find('th').each(function (idx) {
            $(this).css({ width: finalWidths[idx], boxSizing: 'border-box' });
        });

        // 设置表体所有 td 列宽
        $bodyRows.each(function () {
            $(this).find('td').each(function (idx) {
                $(this).css({ width: finalWidths[idx], boxSizing: 'border-box' });
            });
        });

        // 底部滚动条宽度
        $(this.refs.scrollbarInner).css({ width: totalWidth, height: 1 });
    }
    _applyColumnWidths() {
        const cols = this.props.columns;
        const $headerRow = $(this.refs.headerRow);
        const $bodyTable = $(this.refs.bodyTable);
        const $bodyRows = $bodyTable.find('tr.ct-row');
        const containerWidth = $(this.refs.container).innerWidth();

        // 计算固定列宽和可伸缩列数(只算非 fixedWidth 列)
        let totalFixed = 0;
        let flexibleCount = 0;
        cols.forEach(col => {
            if (!!col.fixedWidth || typeof col.width === 'number') totalFixed += col.width || 100;
            else flexibleCount++;
        });

        let finalWidths = [];

        if (totalFixed >= containerWidth) {
            // 总列宽超过容器,固定列宽 + 横向滚动
            finalWidths = cols.map(col => {
                if (!!col.fixedWidth || typeof col.width === 'number') return col.width;
                return 100; // 默认宽度
            });
            $(this.refs.body).css({ overflowX: 'auto' });
            if (this.state.scrollbarHeight === 0) this.setState({ scrollbarHeight: 16 });
        } else {
            // 列少,均分剩余空间
            const remaining = containerWidth - totalFixed;
            const flexWidth = flexibleCount > 0 ? Math.floor(remaining / flexibleCount) : 0;
            finalWidths = cols.map(col => {
                if (!!col.fixedWidth || typeof col.width === 'number') return col.width;
                return flexWidth;
            });
            $(this.refs.body).css({ overflowX: 'hidden' });
            if (this.state.scrollbarHeight === 16) this.setState({ scrollbarHeight: 0 });
        }

        const totalWidth = finalWidths.reduce((a, b) => a + b, 0);

        // 设置表格宽度等于容器宽度
        $bodyTable.css({ tableLayout: 'fixed', width: containerWidth });
        $(this.refs.headerRow).closest('table').css({ tableLayout: 'fixed', width: containerWidth });

        // 应用列宽到表头
        $headerRow.find('th').each(function (idx) {
            $(this).css({ width: finalWidths[idx], boxSizing: 'border-box' });
        });

        // 应用列宽到表体所有 td
        $bodyRows.each(function () {
            $(this).find('td').each(function (idx) {
                $(this).css({ width: finalWidths[idx], boxSizing: 'border-box' });
            });
        });

        // 底部滚动条宽度
        $(this.refs.scrollbarInner).css({ width: totalWidth, height: 1 });
    }

    renderHeader() {
        const { columns } = this.props;
        return (
            <table className="ct-table ct-header-table" ref="headerTable">
                <thead>
                <tr ref="headerRow">
                    {columns.map(col => (
                        <th
                            key={col.key || col.dataIndex}
                            title={col.title || col.key || col.dataIndex}
                            className="ct-cell"
                            style={{ textAlign: col.align ? col.align : 'center' }}
                        >
                            {col.title || col.key || col.dataIndex}
                        </th>
                    ))}
                </tr>
                </thead>
            </table>
        );
    }

    renderBody() {
        const { data, columns, rowHeight } = this.props;
        return (
            <table className="ct-table ct-body-table" ref="bodyTable">
                <tbody>
                {data.map((row, rIdx) => (
                    <tr className="ct-row" key={rIdx} style={{ height: rowHeight }}>
                        {columns.map(col => (
                            <td
                                key={col.key || col.dataIndex}
                                className="ct-cell"
                                title={row[col.key || col.dataIndex] != null ? String(row[col.key || col.dataIndex]) : ''}
                                style={{ textAlign: col.align ? col.align : 'left' }}
                            >
                                {col.render
                                    ? col.render(row[col.key || col.dataIndex], row, rIdx)
                                    : row[col.key || col.dataIndex]}
                            </td>
                        ))}
                    </tr>
                ))}
                </tbody>
            </table>
        );
    }

    render() {
        const { className, height } = this.props;
        const { scrollbarHeight = 16 } = this.state; // 底部横向滚动条高度
        const headerHeight = 36; // 表头固定高度

        // 表体高度 = 总高度 - 表头 - 底部滚动条
        const bodyHeight = height - headerHeight - scrollbarHeight;

        return (
            <div className={`common-table ${className}`} ref="container" style={{ height }}>
                <div className="ct-header-outer" style={{ height: headerHeight }}>
                    <div className="ct-header-inner" ref="headerInner">
                        {this.renderHeader()}
                    </div>
                </div>

                {/* 表体 + 底部滚动条占位布局 */}
                <div className="ct-body-scroll-wrapper" style={{ height: bodyHeight + scrollbarHeight }}>
                    <div
                        className="ct-body-outer"
                        ref="body"
                        style={{ height: bodyHeight, overflowY: 'auto', overflowX: 'auto' }}
                    >
                        {this.renderBody()}
                    </div>

                    {/* 底部滚动条占位 */}
                    <div className="ct-scrollbar" ref="scrollbar" style={{ height: scrollbarHeight }}>
                        <div className="ct-scrollbar-inner" ref="scrollbarInner" />
                    </div>
                </div>
            </div>
        );
    }
}

样式style.less如下:

.common-table {
  position: relative;
  width: 100%;
  font-family: Arial, Helvetica, sans-serif;
  border: 1px solid #DFE1E6;
  background: #fff;
}

.ct-header-outer {
  background: #fff;
  border-bottom: 1px solid #DFE1E6;
  overflow: hidden;
}

.ct-header-inner {
  width: 100%;
  transition: margin-left 0.05s linear;
}

/* 包裹表体和底部滚动条,使用占位,不覆盖表体内容 */
.ct-body-scroll-wrapper {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
}

/* 表体 */
.ct-body-outer {
  width: 100%;
  overflow-y: auto;
  overflow-x: auto;
  flex-shrink: 0;
}

/* 隐藏 body 自带水平滚动条,但显示竖向滚动条 */
.ct-body-outer::-webkit-scrollbar {
  width: 8px;
  height: 0; /* 隐藏水平滚动条 */
}
.ct-body-outer::-webkit-scrollbar-thumb {
  background: #dddddd;
  border-radius: 6px;
  border: 1px solid #dddddd;
}
.ct-body-outer::-webkit-scrollbar-track {
  background: transparent;
}
.ct-body-outer {
  scrollbar-width: auto;
  -ms-overflow-style: auto;
}

.ct-table {
  border-collapse: collapse;
  width: 100%;
  table-layout: fixed;
}

.ct-header-table th {
  padding: 8px 10px;
  border-right: 1px solid #DFE1E6;
  background: #E9EBF0;
  box-sizing: border-box;
  font-size: 14px;
  color: #333333;
  text-align: center;
  line-height: 20px;
  font-weight: 400;
}

.ct-body-table td {
  padding: 8px 10px;
  border-right: 1px solid #DFE1E6;
  border-bottom: 1px solid #E9EBF0;
  box-sizing: border-box;
  font-size: 14px;
  color: #333333;
  line-height: 20px;
  font-weight: 400;
}

.ct-cell {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: default;
}

/* 底部滚动条不再绝对定位,占位在表体下方 */
.ct-scrollbar {
  width: 100%;
  overflow-x: scroll;
  overflow-y: hidden;
  background: #fafafa;
  border-top: 1px solid #ddd;
  flex-shrink: 0;
  height: 16px;
}

.ct-scrollbar-inner {
  height: 1px;
}
posted @ 2025-10-29 10:24  旧色染新烟  阅读(9)  评论(0)    收藏  举报