代码文件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 => (<thkey={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 => (<tdkey={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 }}><divclassName="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;
}