@gmfe/table-x 2.14.29 → 2.14.30-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmfe/table-x",
3
- "version": "2.14.29",
3
+ "version": "2.14.30-beta.0",
4
4
  "description": "",
5
5
  "author": "liyatang <liyatang@qq.com>",
6
6
  "homepage": "https://github.com/gmfe/gmfe#readme",
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 扁平化 columns(用于 select 和 diy 处理)
3
+ * 将带有 subColumns 的 columns 展开为扁平结构
4
+ *
5
+ * 对于有 subColumns 的列,会给每个 subColumn 添加:
6
+ * - parentKey: 父级列的 id(用于重建时识别分组)
7
+ * - parentHeader: 父级列的 Header(用于重建时显示一级表头)
8
+ * - parentFixed: 父级列的 fixed 属性(用于重建时设置 fixed)
9
+ */
10
+ export function flattenColumnsForSelectAndDiy(columns) {
11
+ const flattened = []
12
+ columns.forEach(column => {
13
+ if (column.subColumns && column.subColumns.length > 0) {
14
+ // 如果有 subColumns,将子列展开,并添加父级信息
15
+ column.subColumns.forEach(subCol => {
16
+ flattened.push({
17
+ ...subCol,
18
+ parentKey: column.id || column.Header, // 父级标识
19
+ parentHeader: column.Header, // 父级表头文本
20
+ parentFixed: column.fixed // 父级 fixed 属性
21
+ })
22
+ })
23
+ } else {
24
+ // 没有 subColumns,直接添加
25
+ flattened.push(column)
26
+ }
27
+ })
28
+ return flattened
29
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ TABLE_X_SELECT_ID,
3
+ TABLE_X_DIY_ID,
4
+ TABLE_X_EXPAND_ID
5
+ } from '../../util'
6
+
7
+ /**
8
+ * 根据扁平化的 columns 重建嵌套结构
9
+ * 输入:
10
+ * - flatColumns: 处理后的扁平 columns(每个 subColumn 都带有 parentKey、parentHeader、parentFixed 属性)
11
+ * 输出:重建的嵌套结构(严格按照 flatColumns 的顺序输出)
12
+ */
13
+ export function rebuildNestedColumnsFromFlat(flatColumns) {
14
+ const nested = []
15
+ const specialColumnIds = [
16
+ TABLE_X_SELECT_ID,
17
+ TABLE_X_DIY_ID,
18
+ TABLE_X_EXPAND_ID
19
+ ]
20
+
21
+ // 特殊列(select、diy)放在最前面
22
+ const specialCols = flatColumns.filter(col =>
23
+ specialColumnIds.includes(col.id)
24
+ )
25
+ nested.push(...specialCols)
26
+ // 普通列(排除特殊列)
27
+ const normalCols = flatColumns.filter(
28
+ col => !specialColumnIds.includes(col.id)
29
+ )
30
+ // 收集分组数据:parentKey -> { parentHeader, parentFixed, subCols: [] }
31
+ const groupMap = new Map()
32
+
33
+ // 遍历一次:收集所有分组的数据
34
+ normalCols.forEach(col => {
35
+ if (col.parentKey) {
36
+ // 有 parentKey:二级分类的子列
37
+ if (!groupMap.has(col.parentKey)) {
38
+ groupMap.set(col.parentKey, {
39
+ parentHeader: col.parentHeader,
40
+ parentFixed: col.parentFixed,
41
+ subCols: []
42
+ })
43
+ }
44
+ // 移除临时属性并添加到分组
45
+ const {
46
+ parentKey: _p,
47
+ parentHeader: _h,
48
+ parentFixed: _f,
49
+ ...colWithoutParent
50
+ } = col
51
+ groupMap.get(col.parentKey).subCols.push(colWithoutParent)
52
+ }
53
+ })
54
+
55
+ // 再次遍历:按 flatColumns 的顺序输出
56
+ const processedGroups = new Set()
57
+
58
+ normalCols.forEach(col => {
59
+ if (col.parentKey) {
60
+ // 有 parentKey:在第一次遇到的位置输出分组
61
+ if (!processedGroups.has(col.parentKey)) {
62
+ processedGroups.add(col.parentKey)
63
+ const group = groupMap.get(col.parentKey)
64
+ const visibleSubCols = group.subCols.filter(
65
+ subCol => subCol.show !== false
66
+ )
67
+ if (visibleSubCols.length > 0) {
68
+ nested.push({
69
+ Header: group.parentHeader,
70
+ id: col.parentKey,
71
+ fixed: group.parentFixed,
72
+ subColumns: visibleSubCols
73
+ })
74
+ }
75
+ }
76
+ } else {
77
+ // 无 parentKey:单独列,直接输出
78
+ if (col.show !== false) {
79
+ nested.push(col)
80
+ }
81
+ }
82
+ })
83
+
84
+ return nested
85
+ }
@@ -0,0 +1,176 @@
1
+ import React, { useMemo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { useTable } from 'react-table'
4
+ import { Empty, Loading, afterScroll, __DEFAULT_COLUMN } from '../../util'
5
+ import classNames from 'classnames'
6
+ import _ from 'lodash'
7
+ import TwoLevelTHead from './thead'
8
+ import Tr from '../../base/tr'
9
+ import { rebuildNestedColumnsFromFlat } from './rebuild_helper'
10
+ import { transformColumnsForTwoLevel } from './transform_helper'
11
+
12
+ const defaultColumn = __DEFAULT_COLUMN
13
+
14
+ /**
15
+ * 支持两级表头的 TableX 组件
16
+ *
17
+ * 接收:
18
+ * - columns: 处理后的扁平 columns(经过 select、diy HOC 处理,每个 subColumn 都带有 parentKey、parentHeader、parentFixed 属性)
19
+ *
20
+ * 功能:
21
+ * 1. 根据 parentKey 重建嵌套结构
22
+ * 2. 转换为 react-table 格式
23
+ * 3. 使用 TwoLevelTHead 渲染两级表头
24
+ */
25
+ const TwoLevelTableX = ({
26
+ columns, // 处理后的扁平 columns
27
+ data,
28
+ loading,
29
+ SubComponent,
30
+ keyField = 'value',
31
+ className,
32
+ tiled = false,
33
+ onScroll,
34
+ isTrDisable = () => false,
35
+ isTrHighlight = () => false,
36
+ ...rest
37
+ }) => {
38
+ // 步骤1: 根据扁平化的 columns 重建嵌套结构
39
+ const rebuiltColumns = useMemo(() => {
40
+ return rebuildNestedColumnsFromFlat(columns)
41
+ }, [columns])
42
+
43
+ // 步骤2: 转换为 react-table 格式
44
+ const { transformedColumns, firstLevelHeaders } = useMemo(() => {
45
+ return transformColumnsForTwoLevel(rebuiltColumns)
46
+ }, [rebuiltColumns])
47
+
48
+ // 步骤3: 过滤隐藏的列(与原 TableX 逻辑一致)
49
+ const visibleColumns = useMemo(() => {
50
+ return transformedColumns.filter(c => c.show !== false)
51
+ }, [transformedColumns])
52
+
53
+ // 步骤4: 使用 react-table 处理列和数据(与原 TableX 逻辑一致)
54
+ const {
55
+ getTableProps,
56
+ headerGroups,
57
+ getTableBodyProps,
58
+ rows,
59
+ prepareRow
60
+ } = useTable({
61
+ columns: visibleColumns,
62
+ data,
63
+ defaultColumn
64
+ })
65
+
66
+ // 步骤5: 计算总宽度(与原 TableX 逻辑完全一致)
67
+ let totalWidth = 0
68
+ if (rows[0] && rows[0].cells.length > 0) {
69
+ prepareRow(rows[0])
70
+ const last = rows[0].cells[rows[0].cells.length - 1].column
71
+ totalWidth = last.totalLeft + last.totalWidth
72
+ }
73
+
74
+ // 步骤6: 准备 table props
75
+ // 注意:对于 display: table,minWidth 可能不会自动扩展,需要同时设置 width: 100% 来撑满容器
76
+ const gtp = getTableProps()
77
+ const tableStyle =
78
+ totalWidth > 0
79
+ ? { width: '100%', minWidth: totalWidth + 'px' }
80
+ : { width: '100%' }
81
+
82
+ const tableProps = {
83
+ ...gtp,
84
+ style: tableStyle,
85
+ className: classNames(
86
+ 'gm-table-x-table',
87
+ 'gm-table-x-table-two-level',
88
+ gtp.className
89
+ )
90
+ }
91
+
92
+ const gtbp = getTableBodyProps()
93
+ const tableBodyProps = {
94
+ ...gtbp,
95
+ className: 'gm-table-x-tbody'
96
+ }
97
+
98
+ const handleScroll = e => {
99
+ onScroll && onScroll(e)
100
+ afterScroll()
101
+ }
102
+
103
+ const RenderRow = ({ index, style }) => {
104
+ const row = rows[index]
105
+ prepareRow(row)
106
+
107
+ return (
108
+ <Tr
109
+ key={row.index}
110
+ row={row}
111
+ SubComponent={SubComponent}
112
+ keyField={keyField}
113
+ style={style}
114
+ totalWidth={totalWidth}
115
+ isTrDisable={isTrDisable}
116
+ isTrHighlight={isTrHighlight}
117
+ />
118
+ )
119
+ }
120
+
121
+ return (
122
+ <div
123
+ {...rest}
124
+ className={classNames(
125
+ 'gm-table-x',
126
+ {
127
+ 'gm-table-x-empty': data.length === 0,
128
+ 'gm-table-x-tiled': tiled
129
+ },
130
+ className
131
+ )}
132
+ onScroll={handleScroll}
133
+ >
134
+ <table {...tableProps}>
135
+ <TwoLevelTHead
136
+ headerGroups={headerGroups}
137
+ firstLevelHeaders={firstLevelHeaders}
138
+ totalWidth={totalWidth}
139
+ />
140
+ <tbody {...tableBodyProps}>
141
+ {_.map(rows, row =>
142
+ RenderRow({
143
+ index: row.index,
144
+ style: {}
145
+ })
146
+ )}
147
+ </tbody>
148
+ </table>
149
+ {loading && <Loading />}
150
+ {!loading && data.length === 0 && <Empty />}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ TwoLevelTableX.propTypes = {
156
+ columns: PropTypes.array.isRequired,
157
+ data: PropTypes.array.isRequired,
158
+ loading: PropTypes.bool,
159
+ SubComponent: PropTypes.func,
160
+ keyField: PropTypes.string,
161
+ tiled: PropTypes.bool,
162
+ isTrDisable: PropTypes.func,
163
+ isTrHighlight: PropTypes.func,
164
+ onScroll: PropTypes.func,
165
+ className: PropTypes.string,
166
+ style: PropTypes.object
167
+ }
168
+
169
+ TwoLevelTableX.defaultProps = {
170
+ keyField: 'value',
171
+ tiled: false,
172
+ isTrDisable: () => false,
173
+ isTrHighlight: () => false
174
+ }
175
+
176
+ export default TwoLevelTableX
@@ -0,0 +1,317 @@
1
+ import PropTypes from 'prop-types'
2
+ import React from 'react'
3
+ import classNames from 'classnames'
4
+ import Th from '../../base/th'
5
+ import { getColumnStyle, SortHeader } from '../../util'
6
+
7
+ /**
8
+ * 二级表头组件
9
+ * 渲染两级表头:
10
+ * - 第一行:一级表头(有子列的显示一级表头,没有子列的占两行)
11
+ * - 第二行:二级表头(只有有子列的一级表头才显示)
12
+ */
13
+ const TwoLevelTHead = ({ headerGroups, firstLevelHeaders, totalWidth }) => {
14
+ // react-table 的 headerGroups 结构:
15
+ // - 当 columns 有嵌套结构时:headerGroups[0] 是第一级(分组),headerGroups[1] 是第二级(实际列)
16
+ // - 当 columns 没有嵌套时:headerGroups 只有一行,就是实际列
17
+ // 重要:当混合嵌套列和单列时,headerGroups[0] 只包含有 columns 的分组列,不包含单列
18
+ // 单列只会在 headerGroups[1] 中出现
19
+
20
+ const hasNestedHeaders = headerGroups.length > 1
21
+ const firstLevelGroup = hasNestedHeaders ? headerGroups[0] : null
22
+ const secondLevelGroup = hasNestedHeaders ? headerGroups[1] : headerGroups[0]
23
+
24
+ // 构建第一级表头的渲染信息
25
+ const firstLevelCells = []
26
+ let secondLevelIndex = 0 // 在第二级表头中的索引
27
+
28
+ firstLevelHeaders.forEach((firstLevelHeader, idx) => {
29
+ if (firstLevelHeader.hasSubColumns) {
30
+ // 有子列:从第二级表头中获取对应的列
31
+ const subColumnCount = firstLevelHeader.subColumnCount
32
+ const subColumns = secondLevelGroup.headers.slice(
33
+ secondLevelIndex,
34
+ secondLevelIndex + subColumnCount
35
+ )
36
+
37
+ // 计算总宽度(所有子列的宽度之和)
38
+ let totalSubWidth = 0
39
+ subColumns.forEach(subCol => {
40
+ const style = getColumnStyle(subCol)
41
+ const width = parseFloat(style.width) || subCol.totalWidth || 0
42
+ totalSubWidth += width
43
+ })
44
+
45
+ // 获取对应的第一级分组列对象
46
+ const groupColumn = firstLevelGroup
47
+ ? firstLevelGroup.headers.find(col => {
48
+ // 通过 id 或 Header 匹配
49
+ return (
50
+ col.id === firstLevelHeader.id ||
51
+ (col.columns && col.columns.length === subColumnCount)
52
+ )
53
+ }) || firstLevelGroup.headers[idx]
54
+ : null
55
+
56
+ firstLevelCells.push({
57
+ type: 'group',
58
+ header: firstLevelHeader,
59
+ colSpan: subColumnCount,
60
+ subColumns: subColumns,
61
+ totalWidth: totalSubWidth,
62
+ column: groupColumn,
63
+ secondLevelStartIndex: secondLevelIndex
64
+ })
65
+
66
+ secondLevelIndex += subColumnCount
67
+ } else {
68
+ // 没有子列:占两行(rowspan=2)
69
+ // 重要:当混合嵌套列和单列时,react-table 会将单列同时放在 firstLevelGroup 和 secondLevelGroup 中
70
+ // 我们需要从 firstLevelGroup 中获取单列的列对象(如果存在),否则从 secondLevelGroup 获取
71
+ let singleColumn = null
72
+
73
+ // 优先从 firstLevelGroup 中查找单列(用于 getHeaderProps 等)
74
+ if (firstLevelGroup) {
75
+ singleColumn = firstLevelGroup.headers.find(col => {
76
+ // 通过 id 或 accessor 匹配
77
+ const matchId = col.id === firstLevelHeader.id
78
+ const matchAccessor =
79
+ col.accessor === firstLevelHeader.accessor ||
80
+ (typeof col.accessor === 'function' &&
81
+ typeof firstLevelHeader.accessor === 'function' &&
82
+ col.accessor.toString() === firstLevelHeader.accessor.toString())
83
+ return matchId || matchAccessor
84
+ })
85
+ }
86
+
87
+ // 如果 firstLevelGroup 中没找到,从 secondLevelGroup 获取
88
+ if (!singleColumn) {
89
+ singleColumn = secondLevelGroup.headers[secondLevelIndex]
90
+ }
91
+ firstLevelCells.push({
92
+ type: 'single',
93
+ header: firstLevelHeader,
94
+ colSpan: 1,
95
+ rowSpan: 2, // 明确设置为 2,占两行
96
+ column: singleColumn,
97
+ secondLevelIndex: secondLevelIndex
98
+ })
99
+
100
+ secondLevelIndex += 1
101
+ }
102
+ })
103
+
104
+ return (
105
+ <thead className='gm-table-x-thead gm-table-x-thead-two-level'>
106
+ {/* 第一级表头行 */}
107
+ <tr className='gm-table-x-tr gm-table-x-tr-first-level'>
108
+ {firstLevelCells.map((cell, idx) => {
109
+ const {
110
+ column,
111
+ header,
112
+ colSpan,
113
+ rowSpan,
114
+ type,
115
+ totalWidth: cellTotalWidth
116
+ } = cell
117
+
118
+ // 对于分组列,使用分组列对象;对于单列,使用单列对象
119
+ const headerColumn = column || cell.column
120
+ const hp = headerColumn ? headerColumn.getHeaderProps() : {}
121
+ const { thClassName, style } = headerColumn || {}
122
+
123
+ // 计算样式
124
+ let cellStyle = {
125
+ ...hp.style,
126
+ ...style
127
+ }
128
+
129
+ // 处理固定列(需要先定义,因为在设置宽度时需要用到)
130
+ // 重要:只有明确设置了 fixed 的列才会被固定
131
+ // 对于分组列,直接使用 header.fixed(一级表头的原始 fixed 状态)
132
+ // 对于单列,使用 headerColumn.fixed(react-table 处理后的状态)或 header.fixed
133
+ // 关键:不要从 headerColumn 推断 fixed 状态,因为 headerColumn 可能是 react-table 处理后的对象
134
+ const fixed =
135
+ type === 'group'
136
+ ? header.fixed // 分组列直接使用 header.fixed,不 fallback 到 headerColumn
137
+ : headerColumn?.fixed !== undefined
138
+ ? headerColumn.fixed
139
+ : header.fixed
140
+
141
+ // 如果是分组列,使用 colSpan 时不需要手动设置宽度(由浏览器自动计算)
142
+ // 但我们需要移除 flex 相关的样式,因为使用 table-cell 时 flex 不起作用
143
+ if (type === 'group') {
144
+ // 移除 flex 相关样式,只保留 width 和 maxWidth(如果需要)
145
+ // 对于 table-layout: fixed,我们需要确保分组列的宽度正确设置
146
+ // colSpan 会自动处理宽度,但为了确保固定列的宽度不被压缩,我们显式设置宽度
147
+ const { flex, ...restStyle } = cellStyle
148
+ cellStyle = restStyle
149
+
150
+ // 如果有固定列,显式设置宽度以确保不被压缩
151
+ if (fixed === 'left' || fixed === 'right') {
152
+ cellStyle.width = `${cellTotalWidth}px`
153
+ cellStyle.minWidth = `${cellTotalWidth}px`
154
+ }
155
+ } else if (headerColumn) {
156
+ // 对于单列,也需要移除 flex,使用 width
157
+ const columnStyle = getColumnStyle(headerColumn)
158
+ const { flex, ...restColumnStyle } = columnStyle
159
+ cellStyle = {
160
+ ...cellStyle,
161
+ ...restColumnStyle // 只使用 width 和 maxWidth,不使用 flex
162
+ }
163
+
164
+ // 如果有固定列,确保宽度不被压缩
165
+ if (fixed === 'left' || fixed === 'right') {
166
+ const width = restColumnStyle.width
167
+ if (width) {
168
+ cellStyle.minWidth = width
169
+ }
170
+ }
171
+ }
172
+ // 只有当 fixed 明确为 'left' 或 'right' 时才设置 left/right 和添加 fixed 类
173
+ // 这样确保只有明确设置了 fixed 的列才会被固定
174
+ if (fixed === 'left') {
175
+ // 对于分组列,使用第一个子列的 totalLeft
176
+ if (
177
+ type === 'group' &&
178
+ cell.subColumns &&
179
+ cell.subColumns.length > 0
180
+ ) {
181
+ cellStyle.left = cell.subColumns[0].totalLeft
182
+ } else if (headerColumn) {
183
+ cellStyle.left = headerColumn.totalLeft
184
+ }
185
+ } else if (fixed === 'right') {
186
+ // 对于分组列,使用最后一个子列的位置
187
+ if (
188
+ type === 'group' &&
189
+ cell.subColumns &&
190
+ cell.subColumns.length > 0
191
+ ) {
192
+ const lastSubCol = cell.subColumns[cell.subColumns.length - 1]
193
+ cellStyle.right =
194
+ totalWidth - lastSubCol.totalLeft - lastSubCol.totalWidth
195
+ } else if (headerColumn) {
196
+ cellStyle.right =
197
+ totalWidth - headerColumn.totalLeft - headerColumn.totalWidth
198
+ }
199
+ }
200
+ // 注意:如果 fixed 是 undefined 或其他值,不应该设置 left/right,也不应该添加 fixed 类
201
+
202
+ // 确保我们的 colSpan 和 rowSpan 优先级最高,不会被 hp 中的值覆盖
203
+ const { colSpan: hpColSpan, rowSpan: hpRowSpan, ...restHp } = hp || {}
204
+
205
+ // 确保 rowSpan 和 colSpan 是数字类型(不是字符串)
206
+ const finalRowSpan =
207
+ rowSpan !== undefined
208
+ ? Number(rowSpan)
209
+ : hpRowSpan !== undefined
210
+ ? Number(hpRowSpan)
211
+ : undefined
212
+ const finalColSpan =
213
+ colSpan !== undefined
214
+ ? Number(colSpan)
215
+ : hpColSpan !== undefined
216
+ ? Number(hpColSpan)
217
+ : undefined
218
+
219
+ const thProps = {
220
+ ...restHp,
221
+ ...(finalColSpan !== undefined && { colSpan: finalColSpan }),
222
+ ...(finalRowSpan !== undefined && { rowSpan: finalRowSpan }), // 只有定义了才添加
223
+ className: classNames(
224
+ 'gm-table-x-th',
225
+ 'gm-table-x-th-first-level',
226
+ hp?.className,
227
+ thClassName,
228
+ {
229
+ 'gm-table-x-fixed-left': fixed === 'left',
230
+ 'gm-table-x-fixed-right': fixed === 'right'
231
+ }
232
+ ),
233
+ style: cellStyle
234
+ }
235
+
236
+ return (
237
+ <th key={idx} {...thProps}>
238
+ {typeof header.Header === 'function' ? (
239
+ <header.Header />
240
+ ) : (
241
+ header.Header
242
+ )}
243
+ {headerColumn?.canSort && (
244
+ <SortHeader
245
+ {...headerColumn.getSortByToggleProps()}
246
+ type={
247
+ headerColumn.isSorted
248
+ ? headerColumn.isSortedDesc
249
+ ? 'desc'
250
+ : 'asc'
251
+ : null
252
+ }
253
+ />
254
+ )}
255
+ </th>
256
+ )
257
+ })}
258
+ </tr>
259
+
260
+ {/* 第二级表头行(只有有子列的情况才显示) */}
261
+ {hasNestedHeaders && secondLevelGroup && (
262
+ <tr className='gm-table-x-tr gm-table-x-tr-second-level'>
263
+ {secondLevelGroup.headers.map((column, idx) => {
264
+ // 找到这个列对应的第一级表头
265
+ const correspondingFirstLevel = firstLevelCells.find(cell => {
266
+ if (cell.type === 'single') {
267
+ return cell.secondLevelIndex === idx
268
+ } else if (cell.type === 'group') {
269
+ return (
270
+ idx >= cell.secondLevelStartIndex &&
271
+ idx < cell.secondLevelStartIndex + cell.colSpan
272
+ )
273
+ }
274
+ return false
275
+ })
276
+
277
+ // 如果是单列(rowspan=2),这一行不渲染(已经被第一行占用)
278
+ if (
279
+ correspondingFirstLevel &&
280
+ correspondingFirstLevel.type === 'single'
281
+ ) {
282
+ return null
283
+ }
284
+
285
+ // 为第二级表头添加特殊类名,以便在 CSS 中覆盖样式
286
+ // 重要:二级表头的 fixed 状态应该从对应的一级表头继承
287
+ // 只有当对应的一级表头设置了 fixed 时,二级表头才应该固定
288
+ // 注意:column.fixed 已经在 transformColumnsForTwoLevel 中根据一级表头的 fixed 状态设置了
289
+ // 所以我们直接使用 column.fixed 即可,不需要再次从 firstLevelHeader 继承
290
+ return (
291
+ <Th
292
+ key={idx}
293
+ column={{
294
+ ...column,
295
+ // column.fixed 已经在 transformColumnsForTwoLevel 中正确设置
296
+ thClassName: classNames(
297
+ column.thClassName,
298
+ 'gm-table-x-th-second-level'
299
+ )
300
+ }}
301
+ totalWidth={totalWidth}
302
+ />
303
+ )
304
+ })}
305
+ </tr>
306
+ )}
307
+ </thead>
308
+ )
309
+ }
310
+
311
+ TwoLevelTHead.propTypes = {
312
+ headerGroups: PropTypes.array.isRequired,
313
+ firstLevelHeaders: PropTypes.array.isRequired,
314
+ totalWidth: PropTypes.number.isRequired
315
+ }
316
+
317
+ export default React.memo(TwoLevelTHead)
@@ -0,0 +1,125 @@
1
+ import {
2
+ TABLE_X_SELECT_ID,
3
+ TABLE_X_DIY_ID,
4
+ TABLE_X_EXPAND_ID
5
+ } from '../../util'
6
+
7
+ /**
8
+ * 检查一级表头下的子列 fixed 状态是否一致
9
+ */
10
+ function validateSubColumnsFixed(column) {
11
+ if (!column.subColumns || column.subColumns.length === 0) {
12
+ return { valid: true }
13
+ }
14
+
15
+ const subColumns = column.subColumns
16
+ const fixedStates = subColumns.map(subCol => {
17
+ return subCol.fixed !== undefined ? subCol.fixed : column.fixed
18
+ })
19
+
20
+ const firstFixed = fixedStates[0]
21
+ const allSame = fixedStates.every(fixed => fixed === firstFixed)
22
+
23
+ if (!allSame) {
24
+ return {
25
+ valid: false,
26
+ error:
27
+ `一级表头 "${column.Header || column.id}" 下的子列 fixed 状态不一致。` +
28
+ `为了避免滚动时表头宽度错位,请确保同一一级表头下的所有子列要么全部固定,要么全部不固定。`
29
+ }
30
+ }
31
+
32
+ return { valid: true }
33
+ }
34
+
35
+ /**
36
+ * 将带有 subColumns 的 columns 转换为 react-table 可以理解的嵌套结构
37
+ * 输入:[{ Header: '菜品信息', subColumns: [...] }]
38
+ * 输出:[{ Header: '菜品信息', columns: [...] }] (react-table 格式)
39
+ */
40
+ export function transformColumnsForTwoLevel(columns) {
41
+ const transformedColumns = []
42
+ const firstLevelHeaders = []
43
+ const specialColumnIds = [
44
+ TABLE_X_SELECT_ID,
45
+ TABLE_X_DIY_ID,
46
+ TABLE_X_EXPAND_ID
47
+ ]
48
+
49
+ columns.forEach((column, index) => {
50
+ // 特殊列(由其他 HOC 添加)直接添加,不进行转换
51
+ if (specialColumnIds.includes(column.id)) {
52
+ transformedColumns.push(column)
53
+ firstLevelHeaders.push({
54
+ ...column,
55
+ hasSubColumns: false,
56
+ subColumnCount: 1,
57
+ isSpecialColumn: true
58
+ })
59
+ return
60
+ }
61
+
62
+ if (column.subColumns && column.subColumns.length > 0) {
63
+ // 过滤可见的子列
64
+ const visibleSubCols = column.subColumns.filter(
65
+ subCol => subCol.show !== false
66
+ )
67
+
68
+ // 如果所有子列都被隐藏,跳过这个一级表头
69
+ if (visibleSubCols.length === 0) {
70
+ return
71
+ }
72
+
73
+ // 验证 fixed 状态(使用可见的子列)
74
+ const validation = validateSubColumnsFixed({
75
+ ...column,
76
+ subColumns: visibleSubCols
77
+ })
78
+ if (!validation.valid) {
79
+ console.error(
80
+ `[TwoLevelTableX] 配置错误 (第 ${index + 1} 个列):`,
81
+ validation.error
82
+ )
83
+ if (process.env.NODE_ENV === 'development') {
84
+ throw new Error(validation.error)
85
+ }
86
+ }
87
+
88
+ firstLevelHeaders.push({
89
+ ...column,
90
+ hasSubColumns: true,
91
+ subColumnCount: visibleSubCols.length // 使用可见子列的数量
92
+ })
93
+
94
+ // 确定统一的 fixed 状态(使用可见的子列)
95
+ const unifiedFixed =
96
+ visibleSubCols[0]?.fixed !== undefined
97
+ ? visibleSubCols[0].fixed
98
+ : column.fixed !== undefined
99
+ ? column.fixed
100
+ : undefined
101
+
102
+ // 转换为 react-table 的嵌套结构(只使用可见的子列)
103
+ transformedColumns.push({
104
+ Header: column.Header,
105
+ id: column.id || `group_${transformedColumns.length}`,
106
+ fixed: unifiedFixed,
107
+ columns: visibleSubCols.map(subCol => ({
108
+ ...subCol,
109
+ fixed: subCol.fixed !== undefined ? subCol.fixed : unifiedFixed
110
+ }))
111
+ })
112
+ } else {
113
+ // 没有 subColumns,直接添加(会占两行)
114
+ firstLevelHeaders.push({
115
+ ...column,
116
+ hasSubColumns: false,
117
+ subColumnCount: 1
118
+ })
119
+
120
+ transformedColumns.push(column)
121
+ }
122
+ })
123
+
124
+ return { transformedColumns, firstLevelHeaders }
125
+ }
@@ -0,0 +1,38 @@
1
+ import React, { useMemo } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { flattenColumnsForSelectAndDiy } from './two_level_header_table_x/flatten_helper'
4
+
5
+ /**
6
+ * 两级表头 HOC
7
+ * 功能:将带 subColumns 的 columns 扁平化,传递给内部 Component 处理
8
+ *
9
+ * 使用方式:
10
+ * const TwoLevelTable = twoLevelHeaderTableXHOC(
11
+ * selectTableXHOC(
12
+ * diyTableXHOC(TwoLevelTableX)
13
+ * )
14
+ * )
15
+ *
16
+ * 注意:内部 Component(TwoLevelTableX)只需要接收 columns prop(已扁平化,包含 parentKey、parentHeader、parentFixed)
17
+ */
18
+ function twoLevelHeaderTableXHOC(Component) {
19
+ const TwoLevelHeaderTableXWrapper = props => {
20
+ const { columns: originalColumns, ...rest } = props
21
+
22
+ // 扁平化 columns(用于 select 和 diy 处理,每个 subColumn 会添加 parentKey、parentHeader、parentFixed)
23
+ const flattenedColumns = useMemo(() => {
24
+ return flattenColumnsForSelectAndDiy(originalColumns)
25
+ }, [originalColumns])
26
+
27
+ // 传递给内部 Component
28
+ return <Component {...rest} columns={flattenedColumns} />
29
+ }
30
+
31
+ TwoLevelHeaderTableXWrapper.propTypes = {
32
+ columns: PropTypes.array.isRequired
33
+ }
34
+
35
+ return TwoLevelHeaderTableXWrapper
36
+ }
37
+
38
+ export default twoLevelHeaderTableXHOC
package/src/index.js CHANGED
@@ -7,6 +7,8 @@ import sortableTableXHOC from './hoc/sortable_table_x'
7
7
  import subTableXHOC from './hoc/sub_table_x'
8
8
  import editTableXHOC from './hoc/edit_table_x'
9
9
  import diyTableXHOC from './hoc/diy_table_x'
10
+ import twoLevelHeaderTableXHOC from './hoc/two_level_header_table_x'
11
+ import TwoLevelTableX from './hoc/two_level_header_table_x/table_x'
10
12
 
11
13
  import {
12
14
  TABLE_X,
@@ -42,6 +44,8 @@ const TableXUtil = {
42
44
  export {
43
45
  TableXUtil,
44
46
  TableX,
47
+ TwoLevelTableX,
48
+ twoLevelHeaderTableXHOC,
45
49
  TableXVirtualized,
46
50
  selectTableXHOC,
47
51
  expandTableXHOC,
package/src/index.less CHANGED
@@ -19,6 +19,35 @@
19
19
  transform: translateZ(0);
20
20
  display: block; // 覆盖table 默认display
21
21
 
22
+ // 当有两级表头时,表格需要使用表格布局来支持 rowspan
23
+ &.gm-table-x-table-two-level {
24
+ display: table !important; // 使用表格布局以支持 rowspan
25
+ // 固定列的宽度保护
26
+ .gm-table-x-th,
27
+ .gm-table-x-td {
28
+ &.gm-table-x-fixed-left,
29
+ &.gm-table-x-fixed-right {
30
+ box-sizing: border-box;
31
+ }
32
+ }
33
+
34
+ .gm-table-x-tbody {
35
+ display: table-row-group !important;
36
+ .gm-table-x-tr {
37
+ display: table-row !important;
38
+ .gm-table-x-td {
39
+ display: table-cell !important;
40
+ // 移除 flex 相关样式
41
+ flex: none !important;
42
+ flex-grow: unset !important;
43
+ flex-shrink: unset !important;
44
+ flex-basis: unset !important;
45
+ box-sizing: border-box;
46
+ }
47
+ }
48
+ }
49
+ }
50
+
22
51
  .gm-table-x-tr {
23
52
  display: flex;
24
53
  min-height: 60px;
@@ -80,6 +109,68 @@
80
109
  }
81
110
  }
82
111
 
112
+ // 二级表头样式
113
+ // 注意:为了支持 rowspan,二级表头需要使用原生的表格布局模型
114
+ .gm-table-x-thead-two-level {
115
+ display: table-header-group !important;
116
+
117
+ .gm-table-x-tr-first-level {
118
+ display: table-row !important;
119
+
120
+ .gm-table-x-th-first-level {
121
+ // 第一级表头样式
122
+ display: table-cell !important;
123
+ border: 1px solid #d8dee7 !important;
124
+ border-bottom: 2px solid #d8dee7 !important;
125
+ vertical-align: middle;
126
+ text-align: center;
127
+ padding: 8px;
128
+ flex: none !important;
129
+ // 确保固定列有背景色,这样边框才能正确显示
130
+ background: @gm-table-x-header-bg !important;
131
+ // 固定列样式:和普通表格保持一致
132
+ &.gm-table-x-fixed-left {
133
+ background: @gm-table-x-header-bg !important;
134
+ border-right: 1px solid @gm-table-x-border-color !important;
135
+ }
136
+
137
+ &.gm-table-x-fixed-right {
138
+ background: @gm-table-x-header-bg !important;
139
+ border-left: 1px solid @gm-table-x-border-color !important;
140
+ }
141
+ }
142
+ }
143
+
144
+ .gm-table-x-tr-second-level {
145
+ display: table-row !important;
146
+ .gm-table-x-th-second-level {
147
+ // 第二级表头样式
148
+ display: table-cell !important;
149
+ border: 1px solid #d8dee7 !important;
150
+ border-top: none !important;
151
+ vertical-align: middle;
152
+ text-align: center;
153
+ padding: 8px;
154
+ flex: none !important;
155
+ flex-grow: unset !important;
156
+ flex-shrink: unset !important;
157
+ flex-basis: unset !important;
158
+ box-sizing: border-box;
159
+ // 确保固定列有背景色,这样边框才能正确显示
160
+ background: @gm-table-x-header-bg !important;
161
+ // 固定列样式:和普通表格保持一致
162
+ &.gm-table-x-fixed-left {
163
+ background: @gm-table-x-header-bg !important;
164
+ border-right: 1px solid @gm-table-x-border-color !important;
165
+ }
166
+ &.gm-table-x-fixed-right {
167
+ background: @gm-table-x-header-bg !important;
168
+ border-left: 1px solid @gm-table-x-border-color !important;
169
+ }
170
+ }
171
+ }
172
+ }
173
+
83
174
  .gm-table-x-tbody {
84
175
  display: block; // 覆盖tbody 默认display
85
176
 
@@ -432,7 +523,7 @@
432
523
  }
433
524
  }
434
525
 
435
- .gm-react-table-x-diy-modal-content{
526
+ .gm-react-table-x-diy-modal-content {
436
527
  border-top: 1px solid @gm-table-x-border-color;
437
528
  border-bottom: 1px solid @gm-table-x-border-color;
438
529
  }