@gmfe/table-x 2.14.30-alpha.0 → 2.14.30-beta.1
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 +4 -5
- package/src/base/index.js +1 -2
- package/src/base/td.js +1 -1
- package/src/base/thead.js +1 -1
- package/src/base/tr.js +2 -2
- package/src/hoc/two_level_header_table_x/flatten_helper.js +29 -0
- package/src/hoc/two_level_header_table_x/rebuild_helper.js +92 -0
- package/src/hoc/two_level_header_table_x/table_x.js +169 -0
- package/src/hoc/two_level_header_table_x/td.js +60 -0
- package/src/hoc/two_level_header_table_x/thead.js +278 -0
- package/src/hoc/two_level_header_table_x/tr.js +53 -0
- package/src/hoc/two_level_header_table_x/transform_helper.js +149 -0
- package/src/hoc/two_level_header_table_x.js +36 -0
- package/src/index.js +4 -0
- package/src/index.less +155 -3
- package/src/util/index.js +8 -6
- package/LICENSE +0 -16
- package/src/hoc/sticky_layout.js +0 -149
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmfe/table-x",
|
|
3
|
-
"version": "2.14.30-
|
|
3
|
+
"version": "2.14.30-beta.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "liyatang <liyatang@qq.com>",
|
|
6
6
|
"homepage": "https://github.com/gmfe/gmfe#readme",
|
|
@@ -27,9 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@gm-common/tool": "^1.0.0",
|
|
30
|
-
"@gmfe/
|
|
31
|
-
"@gmfe/
|
|
32
|
-
"@gmfe/react": "^2.14.30-alpha.0",
|
|
30
|
+
"@gmfe/locales": "^2.14.29",
|
|
31
|
+
"@gmfe/react": "^2.14.29",
|
|
33
32
|
"classnames": "^2.2.5",
|
|
34
33
|
"lodash": "^4.17.14",
|
|
35
34
|
"prop-types": "^15.7.2",
|
|
@@ -37,5 +36,5 @@
|
|
|
37
36
|
"react-window": "^1.8.5",
|
|
38
37
|
"sortablejs": "^1.10.1"
|
|
39
38
|
},
|
|
40
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "8d291ad2d8b321f27918ac65cf201bd1eb8872a8"
|
|
41
40
|
}
|
package/src/base/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import classNames from 'classnames'
|
|
|
6
6
|
import _ from 'lodash'
|
|
7
7
|
import THead from './thead'
|
|
8
8
|
import Tr from './tr'
|
|
9
|
-
import { StickyLayout } from '@gmfe/react'
|
|
10
9
|
|
|
11
10
|
// 给定初始值,交由getColumnStyle控制。width逻辑保持跟react-table(v6)的用法一致。
|
|
12
11
|
const defaultColumn = __DEFAULT_COLUMN
|
|
@@ -140,4 +139,4 @@ TableX.defaultProps = {
|
|
|
140
139
|
isTrHighlight: () => false
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
export default
|
|
142
|
+
export default TableX
|
package/src/base/td.js
CHANGED
package/src/base/thead.js
CHANGED
|
@@ -4,7 +4,7 @@ import Th from './th'
|
|
|
4
4
|
|
|
5
5
|
const THead = ({ headerGroups, totalWidth }) => {
|
|
6
6
|
return (
|
|
7
|
-
<thead className='gm-table-x-thead
|
|
7
|
+
<thead className='gm-table-x-thead'>
|
|
8
8
|
{headerGroups.map((headerGroup, i) => (
|
|
9
9
|
<tr key={i} className='gm-table-x-tr'>
|
|
10
10
|
{headerGroup.headers.map((column, i) => (
|
package/src/base/tr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
import React from 'react'
|
|
4
|
-
import Td from '
|
|
4
|
+
import Td from '@gmfe/table-x/src/base/td'
|
|
5
5
|
|
|
6
6
|
const Tr = ({
|
|
7
7
|
row,
|
|
@@ -10,7 +10,7 @@ const Tr = ({
|
|
|
10
10
|
style,
|
|
11
11
|
totalWidth,
|
|
12
12
|
isTrDisable,
|
|
13
|
-
isTrHighlight
|
|
13
|
+
isTrHighlight
|
|
14
14
|
}) => {
|
|
15
15
|
const props = {
|
|
16
16
|
...row.getRowProps(),
|
|
@@ -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
|
+
column.subColumns.forEach(subCol => {
|
|
15
|
+
const { fixed: _subColFixed, ...subColWithoutFixed } = subCol
|
|
16
|
+
flattened.push({
|
|
17
|
+
...subColWithoutFixed,
|
|
18
|
+
parentKey: column.id || column.Header, // 父级标识
|
|
19
|
+
parentHeader: column.Header,
|
|
20
|
+
parentFixed: column.fixed
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
// 没有 subColumns,直接添加
|
|
25
|
+
flattened.push(column)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
return flattened
|
|
29
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
// 普通列(排除特殊列)
|
|
22
|
+
const normalCols = flatColumns.filter(
|
|
23
|
+
col => !specialColumnIds.includes(col.id)
|
|
24
|
+
)
|
|
25
|
+
// 收集分组数据:parentKey -> { parentHeader, parentFixed, subCols: [] }
|
|
26
|
+
const groupMap = new Map()
|
|
27
|
+
|
|
28
|
+
// 遍历一次:收集所有分组的数据
|
|
29
|
+
normalCols.forEach(col => {
|
|
30
|
+
if (col.parentKey) {
|
|
31
|
+
// 有 parentKey:二级分类的子列
|
|
32
|
+
if (!groupMap.has(col.parentKey)) {
|
|
33
|
+
groupMap.set(col.parentKey, {
|
|
34
|
+
parentHeader: col.parentHeader,
|
|
35
|
+
parentFixed: col.parentFixed,
|
|
36
|
+
subCols: []
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
// 移除临时属性并添加到分组
|
|
40
|
+
const {
|
|
41
|
+
parentKey: _p,
|
|
42
|
+
parentHeader: _h,
|
|
43
|
+
parentFixed: parentFixedValue,
|
|
44
|
+
...colWithoutParent
|
|
45
|
+
} = col
|
|
46
|
+
const { fixed: _subColFixed, ...colWithoutFixed } = colWithoutParent
|
|
47
|
+
groupMap.get(col.parentKey).subCols.push({
|
|
48
|
+
...colWithoutFixed,
|
|
49
|
+
fixed: parentFixedValue
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// 再次遍历:按 flatColumns 的顺序输出普通列
|
|
55
|
+
const processedGroups = new Set()
|
|
56
|
+
|
|
57
|
+
normalCols.forEach(col => {
|
|
58
|
+
if (col.parentKey) {
|
|
59
|
+
// 有 parentKey:在第一次遇到的位置输出分组
|
|
60
|
+
if (!processedGroups.has(col.parentKey)) {
|
|
61
|
+
processedGroups.add(col.parentKey)
|
|
62
|
+
const group = groupMap.get(col.parentKey)
|
|
63
|
+
const visibleSubCols = group.subCols.filter(
|
|
64
|
+
subCol => subCol.show !== false
|
|
65
|
+
)
|
|
66
|
+
if (visibleSubCols.length > 0) {
|
|
67
|
+
nested.push({
|
|
68
|
+
Header: group.parentHeader,
|
|
69
|
+
id: col.parentKey,
|
|
70
|
+
fixed: group.parentFixed,
|
|
71
|
+
subColumns: visibleSubCols
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// 无 parentKey:单独列,直接输出
|
|
77
|
+
if (col.show !== false) {
|
|
78
|
+
nested.push(col)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// 特殊列(select、diy、expand)必须放在最后
|
|
84
|
+
// 从 flatColumns 中提取特殊列,保持它们在 flatColumns 中的相对顺序
|
|
85
|
+
const specialCols = flatColumns.filter(col =>
|
|
86
|
+
specialColumnIds.includes(col.id)
|
|
87
|
+
)
|
|
88
|
+
// 确保特殊列在最后
|
|
89
|
+
nested.push(...specialCols)
|
|
90
|
+
|
|
91
|
+
return nested
|
|
92
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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 './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,
|
|
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
|
+
const rebuiltColumns = useMemo(() => {
|
|
39
|
+
return rebuildNestedColumnsFromFlat(columns)
|
|
40
|
+
}, [columns])
|
|
41
|
+
|
|
42
|
+
const { transformedColumns, firstLevelHeaders } = useMemo(() => {
|
|
43
|
+
return transformColumnsForTwoLevel(rebuiltColumns)
|
|
44
|
+
}, [rebuiltColumns])
|
|
45
|
+
|
|
46
|
+
const visibleColumns = useMemo(() => {
|
|
47
|
+
return transformedColumns.filter(c => c.show !== false)
|
|
48
|
+
}, [transformedColumns])
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
getTableProps,
|
|
52
|
+
headerGroups,
|
|
53
|
+
getTableBodyProps,
|
|
54
|
+
rows,
|
|
55
|
+
prepareRow
|
|
56
|
+
} = useTable({
|
|
57
|
+
columns: visibleColumns,
|
|
58
|
+
data,
|
|
59
|
+
defaultColumn
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
let totalWidth = 0
|
|
63
|
+
if (rows[0] && rows[0].cells.length > 0) {
|
|
64
|
+
prepareRow(rows[0])
|
|
65
|
+
const last = rows[0].cells[rows[0].cells.length - 1].column
|
|
66
|
+
totalWidth = last.totalLeft + last.totalWidth
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const gtp = getTableProps()
|
|
70
|
+
const tableStyle =
|
|
71
|
+
totalWidth > 0
|
|
72
|
+
? { width: '100%', minWidth: totalWidth + 'px' }
|
|
73
|
+
: { width: '100%' }
|
|
74
|
+
|
|
75
|
+
const tableProps = {
|
|
76
|
+
...gtp,
|
|
77
|
+
style: tableStyle,
|
|
78
|
+
className: classNames(
|
|
79
|
+
'gm-table-x-table',
|
|
80
|
+
'gm-table-x-table-two-level',
|
|
81
|
+
gtp.className
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const gtbp = getTableBodyProps()
|
|
86
|
+
const tableBodyProps = {
|
|
87
|
+
...gtbp,
|
|
88
|
+
className: 'gm-table-x-tbody'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const handleScroll = e => {
|
|
92
|
+
onScroll && onScroll(e)
|
|
93
|
+
afterScroll()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const RenderRow = ({ index, style }) => {
|
|
97
|
+
const row = rows[index]
|
|
98
|
+
prepareRow(row)
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Tr
|
|
102
|
+
key={row.index}
|
|
103
|
+
row={row}
|
|
104
|
+
SubComponent={SubComponent}
|
|
105
|
+
keyField={keyField}
|
|
106
|
+
style={style}
|
|
107
|
+
totalWidth={totalWidth}
|
|
108
|
+
isTrDisable={isTrDisable}
|
|
109
|
+
isTrHighlight={isTrHighlight}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
{...rest}
|
|
117
|
+
className={classNames(
|
|
118
|
+
'gm-table-x',
|
|
119
|
+
{
|
|
120
|
+
'gm-table-x-empty': data.length === 0,
|
|
121
|
+
'gm-table-x-tiled': tiled
|
|
122
|
+
},
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
onScroll={handleScroll}
|
|
126
|
+
>
|
|
127
|
+
<table {...tableProps}>
|
|
128
|
+
<TwoLevelTHead
|
|
129
|
+
headerGroups={headerGroups}
|
|
130
|
+
firstLevelHeaders={firstLevelHeaders}
|
|
131
|
+
totalWidth={totalWidth}
|
|
132
|
+
/>
|
|
133
|
+
<tbody {...tableBodyProps}>
|
|
134
|
+
{_.map(rows, row =>
|
|
135
|
+
RenderRow({
|
|
136
|
+
index: row.index,
|
|
137
|
+
style: {}
|
|
138
|
+
})
|
|
139
|
+
)}
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
{loading && <Loading style={{ marginTop: '92px' }} />}
|
|
143
|
+
{!loading && data.length === 0 && <Empty />}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
TwoLevelTableX.propTypes = {
|
|
149
|
+
columns: PropTypes.array.isRequired,
|
|
150
|
+
data: PropTypes.array.isRequired,
|
|
151
|
+
loading: PropTypes.bool,
|
|
152
|
+
SubComponent: PropTypes.func,
|
|
153
|
+
keyField: PropTypes.string,
|
|
154
|
+
tiled: PropTypes.bool,
|
|
155
|
+
isTrDisable: PropTypes.func,
|
|
156
|
+
isTrHighlight: PropTypes.func,
|
|
157
|
+
onScroll: PropTypes.func,
|
|
158
|
+
className: PropTypes.string,
|
|
159
|
+
style: PropTypes.object
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
TwoLevelTableX.defaultProps = {
|
|
163
|
+
keyField: 'value',
|
|
164
|
+
tiled: false,
|
|
165
|
+
isTrDisable: () => false,
|
|
166
|
+
isTrHighlight: () => false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default TwoLevelTableX
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import { getColumnStyle } from '../../util'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
// cell.render('Cell') 是一个react组件,如果这个组件return undefined,那就就会报错
|
|
7
|
+
// 这里是为了兼容 cell.render('Cell') 返回undefined的情况
|
|
8
|
+
class TdCatchErr extends React.Component {
|
|
9
|
+
componentDidCatch(error, errorInfo) {
|
|
10
|
+
console.warn(error)
|
|
11
|
+
console.warn(errorInfo.componentStack)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
return this.props.children
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Td = ({ cell, totalWidth }) => {
|
|
20
|
+
const cp = cell.getCellProps()
|
|
21
|
+
const { tdClassName } = cell.column
|
|
22
|
+
const tdProps = {
|
|
23
|
+
...cp,
|
|
24
|
+
className: classNames('gm-table-x-td', tdClassName, {
|
|
25
|
+
'gm-table-x-fixed-left': cell.column.fixed === 'left',
|
|
26
|
+
'gm-table-x-fixed-right': cell.column.fixed === 'right'
|
|
27
|
+
}),
|
|
28
|
+
style: {
|
|
29
|
+
...cp.style,
|
|
30
|
+
...getColumnStyle(cell.column)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cell.column.fixed === 'left') {
|
|
35
|
+
// 用到 fixed,可以利用 totalLeft
|
|
36
|
+
tdProps.style.left = cell.column.totalLeft
|
|
37
|
+
} else if (cell.column.fixed === 'right') {
|
|
38
|
+
tdProps.style.right =
|
|
39
|
+
totalWidth - cell.column.totalLeft - cell.column.totalWidth
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 如果 Cell 返回 undefined,转换为 null(null 不会报错,undefined 会)
|
|
43
|
+
const cellContent = cell.render('Cell')
|
|
44
|
+
const safeCellContent = cellContent === undefined ? null : cellContent
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<td {...tdProps}>
|
|
48
|
+
<TdCatchErr>{safeCellContent}</TdCatchErr>
|
|
49
|
+
</td>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Td.whyDidYouRender = true
|
|
54
|
+
|
|
55
|
+
Td.propTypes = {
|
|
56
|
+
cell: PropTypes.object.isRequired,
|
|
57
|
+
totalWidth: PropTypes.number.isRequired
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default React.memo(Td)
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
const hasNestedHeaders = headerGroups.length > 1
|
|
15
|
+
const firstLevelGroup = hasNestedHeaders ? headerGroups[0] : null
|
|
16
|
+
const secondLevelGroup = hasNestedHeaders ? headerGroups[1] : headerGroups[0]
|
|
17
|
+
|
|
18
|
+
// 构建第一级表头的渲染信息
|
|
19
|
+
const firstLevelCells = []
|
|
20
|
+
let secondLevelIndex = 0 // 在第二级表头中的索引
|
|
21
|
+
|
|
22
|
+
firstLevelHeaders.forEach((firstLevelHeader, idx) => {
|
|
23
|
+
if (firstLevelHeader.hasSubColumns) {
|
|
24
|
+
// 有子列:从第二级表头中获取对应的列
|
|
25
|
+
const subColumnCount = firstLevelHeader.subColumnCount
|
|
26
|
+
const subColumns = secondLevelGroup.headers.slice(
|
|
27
|
+
secondLevelIndex,
|
|
28
|
+
secondLevelIndex + subColumnCount
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// 获取对应的第一级分组列对象
|
|
32
|
+
const groupColumn = firstLevelGroup
|
|
33
|
+
? firstLevelGroup.headers.find(col => {
|
|
34
|
+
// 通过 id 或 Header 匹配
|
|
35
|
+
return (
|
|
36
|
+
col.id === firstLevelHeader.id ||
|
|
37
|
+
(col.columns && col.columns.length === subColumnCount)
|
|
38
|
+
)
|
|
39
|
+
}) || firstLevelGroup.headers[idx]
|
|
40
|
+
: null
|
|
41
|
+
|
|
42
|
+
// 计算分组列的总宽度(所有子列的宽度之和)
|
|
43
|
+
let totalSubWidth = 0
|
|
44
|
+
subColumns.forEach(subCol => {
|
|
45
|
+
const style = getColumnStyle(subCol)
|
|
46
|
+
const width = parseFloat(style.width) || subCol.totalWidth || 0
|
|
47
|
+
totalSubWidth += width
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
firstLevelCells.push({
|
|
51
|
+
type: 'group',
|
|
52
|
+
header: firstLevelHeader,
|
|
53
|
+
colSpan: subColumnCount,
|
|
54
|
+
subColumns: subColumns,
|
|
55
|
+
totalWidth: totalSubWidth, // 添加总宽度
|
|
56
|
+
column: groupColumn,
|
|
57
|
+
secondLevelStartIndex: secondLevelIndex
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
secondLevelIndex += subColumnCount
|
|
61
|
+
} else {
|
|
62
|
+
let singleColumn = null
|
|
63
|
+
|
|
64
|
+
// 优先从 firstLevelGroup 中查找单列(用于 getHeaderProps 等)
|
|
65
|
+
if (firstLevelGroup) {
|
|
66
|
+
singleColumn = firstLevelGroup.headers.find(col => {
|
|
67
|
+
// 通过 id 或 accessor 匹配
|
|
68
|
+
const matchId = col.id === firstLevelHeader.id
|
|
69
|
+
const matchAccessor =
|
|
70
|
+
col.accessor === firstLevelHeader.accessor ||
|
|
71
|
+
(typeof col.accessor === 'function' &&
|
|
72
|
+
typeof firstLevelHeader.accessor === 'function' &&
|
|
73
|
+
col.accessor.toString() === firstLevelHeader.accessor.toString())
|
|
74
|
+
return matchId || matchAccessor
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 如果 firstLevelGroup 中没找到,从 secondLevelGroup 获取
|
|
79
|
+
if (!singleColumn) {
|
|
80
|
+
singleColumn = secondLevelGroup.headers[secondLevelIndex]
|
|
81
|
+
}
|
|
82
|
+
firstLevelCells.push({
|
|
83
|
+
type: 'single',
|
|
84
|
+
header: firstLevelHeader,
|
|
85
|
+
colSpan: 1,
|
|
86
|
+
rowSpan: 2, // 明确设置为 2,占两行
|
|
87
|
+
column: singleColumn,
|
|
88
|
+
secondLevelIndex: secondLevelIndex
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
secondLevelIndex += 1
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<thead className='gm-table-x-thead gm-table-x-thead-two-level'>
|
|
97
|
+
{/* 第一级表头行 */}
|
|
98
|
+
<tr className='gm-table-x-tr gm-table-x-tr-first-level'>
|
|
99
|
+
{firstLevelCells.map((cell, idx) => {
|
|
100
|
+
const { column, header, colSpan, rowSpan, type } = cell
|
|
101
|
+
|
|
102
|
+
// 对于分组列,使用分组列对象;对于单列,使用单列对象
|
|
103
|
+
const headerColumn = column
|
|
104
|
+
const hp = headerColumn ? headerColumn.getHeaderProps() : {}
|
|
105
|
+
const { thClassName, style } = headerColumn || {}
|
|
106
|
+
|
|
107
|
+
// 计算样式
|
|
108
|
+
let cellStyle = {
|
|
109
|
+
...hp.style,
|
|
110
|
+
...style
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fixed =
|
|
114
|
+
type === 'group'
|
|
115
|
+
? header.fixed // 分组列直接使用 header.fixed,不 fallback 到 headerColumn
|
|
116
|
+
: headerColumn?.fixed !== undefined
|
|
117
|
+
? headerColumn.fixed
|
|
118
|
+
: header.fixed
|
|
119
|
+
|
|
120
|
+
// 判断是否是最后一列(通过检查是否是 firstLevelCells 的最后一个元素)
|
|
121
|
+
const isLastCell = idx === firstLevelCells.length - 1
|
|
122
|
+
|
|
123
|
+
if (type === 'group') {
|
|
124
|
+
const { flex, width, minWidth, maxWidth, ...restStyle } = cellStyle
|
|
125
|
+
cellStyle = restStyle
|
|
126
|
+
} else if (headerColumn) {
|
|
127
|
+
const columnStyle = getColumnStyle(headerColumn)
|
|
128
|
+
const { flex, ...restColumnStyle } = columnStyle
|
|
129
|
+
cellStyle = {
|
|
130
|
+
...cellStyle,
|
|
131
|
+
...restColumnStyle // 只使用 width 和 maxWidth,不使用 flex
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 如果有固定列,确保宽度不被压缩
|
|
135
|
+
if (fixed === 'left' || fixed === 'right') {
|
|
136
|
+
const width = restColumnStyle.width
|
|
137
|
+
if (width) {
|
|
138
|
+
cellStyle.minWidth = width
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (fixed === 'left') {
|
|
143
|
+
if (
|
|
144
|
+
type === 'group' &&
|
|
145
|
+
cell.subColumns &&
|
|
146
|
+
cell.subColumns.length > 0
|
|
147
|
+
) {
|
|
148
|
+
cellStyle.left = cell.subColumns[0].totalLeft
|
|
149
|
+
} else if (headerColumn) {
|
|
150
|
+
cellStyle.left = headerColumn.totalLeft
|
|
151
|
+
}
|
|
152
|
+
} else if (fixed === 'right') {
|
|
153
|
+
// 对于分组列,使用最后一个子列的位置
|
|
154
|
+
if (
|
|
155
|
+
type === 'group' &&
|
|
156
|
+
cell.subColumns &&
|
|
157
|
+
cell.subColumns.length > 0
|
|
158
|
+
) {
|
|
159
|
+
const lastSubCol = cell.subColumns[cell.subColumns.length - 1]
|
|
160
|
+
// 如果是最后一列,直接设置 right: 0
|
|
161
|
+
if (isLastCell) {
|
|
162
|
+
cellStyle.right = 0
|
|
163
|
+
} else {
|
|
164
|
+
cellStyle.right =
|
|
165
|
+
totalWidth - lastSubCol.totalLeft - lastSubCol.totalWidth
|
|
166
|
+
}
|
|
167
|
+
} else if (headerColumn) {
|
|
168
|
+
// 如果是最后一列,直接设置 right: 0
|
|
169
|
+
if (isLastCell) {
|
|
170
|
+
cellStyle.right = 0
|
|
171
|
+
} else {
|
|
172
|
+
cellStyle.right =
|
|
173
|
+
totalWidth - headerColumn.totalLeft - headerColumn.totalWidth
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const { colSpan: hpColSpan, rowSpan: hpRowSpan, ...restHp } = hp || {}
|
|
178
|
+
const finalRowSpan =
|
|
179
|
+
rowSpan !== undefined ? Number(rowSpan) : undefined
|
|
180
|
+
const finalColSpan =
|
|
181
|
+
colSpan !== undefined
|
|
182
|
+
? Number(colSpan)
|
|
183
|
+
: hpColSpan !== undefined
|
|
184
|
+
? Number(hpColSpan)
|
|
185
|
+
: undefined
|
|
186
|
+
|
|
187
|
+
const thProps = {
|
|
188
|
+
...restHp,
|
|
189
|
+
...(finalColSpan !== undefined && { colSpan: finalColSpan }),
|
|
190
|
+
...(finalRowSpan !== undefined && { rowSpan: finalRowSpan }), // 只有定义了才添加
|
|
191
|
+
className: classNames(
|
|
192
|
+
'gm-table-x-th',
|
|
193
|
+
'gm-table-x-th-first-level',
|
|
194
|
+
hp?.className,
|
|
195
|
+
thClassName,
|
|
196
|
+
{
|
|
197
|
+
'gm-table-x-fixed-left': fixed === 'left',
|
|
198
|
+
'gm-table-x-fixed-right': fixed === 'right'
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
style: cellStyle
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<th key={idx} {...thProps}>
|
|
206
|
+
{typeof header.Header === 'function' ? (
|
|
207
|
+
<header.Header />
|
|
208
|
+
) : (
|
|
209
|
+
header.Header
|
|
210
|
+
)}
|
|
211
|
+
{headerColumn?.canSort && (
|
|
212
|
+
<SortHeader
|
|
213
|
+
{...headerColumn.getSortByToggleProps()}
|
|
214
|
+
type={
|
|
215
|
+
headerColumn.isSorted
|
|
216
|
+
? headerColumn.isSortedDesc
|
|
217
|
+
? 'desc'
|
|
218
|
+
: 'asc'
|
|
219
|
+
: null
|
|
220
|
+
}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
</th>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</tr>
|
|
227
|
+
|
|
228
|
+
{/* 第二级表头行(只有有子列的情况才显示) */}
|
|
229
|
+
{hasNestedHeaders && secondLevelGroup && (
|
|
230
|
+
<tr className='gm-table-x-tr gm-table-x-tr-second-level'>
|
|
231
|
+
{secondLevelGroup.headers.map((column, idx) => {
|
|
232
|
+
// 找到这个列对应的第一级表头
|
|
233
|
+
const correspondingFirstLevel = firstLevelCells.find(cell => {
|
|
234
|
+
if (cell.type === 'single') {
|
|
235
|
+
return cell.secondLevelIndex === idx
|
|
236
|
+
} else if (cell.type === 'group') {
|
|
237
|
+
return (
|
|
238
|
+
idx >= cell.secondLevelStartIndex &&
|
|
239
|
+
idx < cell.secondLevelStartIndex + cell.colSpan
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
return false
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
correspondingFirstLevel &&
|
|
247
|
+
correspondingFirstLevel.type === 'single'
|
|
248
|
+
) {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
return (
|
|
252
|
+
<Th
|
|
253
|
+
key={idx}
|
|
254
|
+
column={{
|
|
255
|
+
...column,
|
|
256
|
+
// column.fixed 已经在 transformColumnsForTwoLevel 中正确设置
|
|
257
|
+
thClassName: classNames(
|
|
258
|
+
column.thClassName,
|
|
259
|
+
'gm-table-x-th-second-level'
|
|
260
|
+
)
|
|
261
|
+
}}
|
|
262
|
+
totalWidth={totalWidth}
|
|
263
|
+
/>
|
|
264
|
+
)
|
|
265
|
+
})}
|
|
266
|
+
</tr>
|
|
267
|
+
)}
|
|
268
|
+
</thead>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
TwoLevelTHead.propTypes = {
|
|
273
|
+
headerGroups: PropTypes.array.isRequired,
|
|
274
|
+
firstLevelHeaders: PropTypes.array.isRequired,
|
|
275
|
+
totalWidth: PropTypes.number.isRequired
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export default React.memo(TwoLevelTHead)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import Td from './td'
|
|
5
|
+
|
|
6
|
+
const Tr = ({
|
|
7
|
+
row,
|
|
8
|
+
SubComponent,
|
|
9
|
+
keyField,
|
|
10
|
+
style,
|
|
11
|
+
totalWidth,
|
|
12
|
+
isTrDisable,
|
|
13
|
+
isTrHighlight
|
|
14
|
+
}) => {
|
|
15
|
+
const props = {
|
|
16
|
+
...row.getRowProps(),
|
|
17
|
+
style,
|
|
18
|
+
className: classNames('gm-table-x-tr', {
|
|
19
|
+
'gm-table-x-tr-disable': isTrDisable(row.original, row.index),
|
|
20
|
+
'gm-table-x-tr-highlight': isTrHighlight(row.original, row.index),
|
|
21
|
+
'gm-table-x-tr-odd': row.index % 2 === 0,
|
|
22
|
+
'gm-table-x-tr-even': row.index % 2 !== 0
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 目前视为了 sortable 用。值可能是 undefined,keyField 没作用的情况
|
|
27
|
+
const dataId = row.original[keyField]
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<tr data-id={dataId} {...props}>
|
|
32
|
+
{row.cells.map((cell, cellIndex) => (
|
|
33
|
+
<Td key={cellIndex} cell={cell} totalWidth={totalWidth} />
|
|
34
|
+
))}
|
|
35
|
+
</tr>
|
|
36
|
+
{SubComponent && SubComponent(row)}
|
|
37
|
+
</>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Tr.whyDidYouRender = true
|
|
42
|
+
|
|
43
|
+
Tr.propTypes = {
|
|
44
|
+
row: PropTypes.object.isRequired,
|
|
45
|
+
SubComponent: PropTypes.func,
|
|
46
|
+
keyField: PropTypes.string.isRequired,
|
|
47
|
+
style: PropTypes.object.isRequired,
|
|
48
|
+
totalWidth: PropTypes.number.isRequired,
|
|
49
|
+
isTrDisable: PropTypes.func,
|
|
50
|
+
isTrHighlight: PropTypes.func
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default React.memo(Tr)
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
// 重要:二级分类的 fixed 应该与一级分类保持一致
|
|
17
|
+
// 所以验证时,所有子列应该使用一级表头的 fixed(column.fixed)
|
|
18
|
+
const expectedFixed = column.fixed !== undefined ? column.fixed : undefined
|
|
19
|
+
const fixedStates = subColumns.map(subCol => {
|
|
20
|
+
// 子列的 fixed 应该与一级表头一致,如果有不一致的,记录实际的 fixed 用于检查
|
|
21
|
+
return subCol.fixed !== undefined ? subCol.fixed : expectedFixed
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const allSame = fixedStates.every(fixed => fixed === expectedFixed)
|
|
25
|
+
|
|
26
|
+
if (!allSame) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
error:
|
|
30
|
+
`一级表头 "${column.Header || column.id}" 下的子列 fixed 状态不一致。` +
|
|
31
|
+
`为了避免滚动时表头宽度错位,请确保同一一级表头下的所有子列要么全部固定,要么全部不固定。`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { valid: true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 将带有 subColumns 的 columns 转换为 react-table 可以理解的嵌套结构
|
|
40
|
+
* 输入:[{ Header: '菜品信息', subColumns: [...] }]
|
|
41
|
+
* 输出:[{ Header: '菜品信息', columns: [...] }] (react-table 格式)
|
|
42
|
+
*/
|
|
43
|
+
export function transformColumnsForTwoLevel(columns) {
|
|
44
|
+
const transformedColumns = []
|
|
45
|
+
const firstLevelHeaders = []
|
|
46
|
+
const specialColumnIds = [
|
|
47
|
+
TABLE_X_SELECT_ID,
|
|
48
|
+
TABLE_X_DIY_ID,
|
|
49
|
+
TABLE_X_EXPAND_ID
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// 分离普通列和特殊列
|
|
53
|
+
const normalColumns = []
|
|
54
|
+
const specialColumns = []
|
|
55
|
+
|
|
56
|
+
columns.forEach(column => {
|
|
57
|
+
if (specialColumnIds.includes(column.id)) {
|
|
58
|
+
specialColumns.push(column)
|
|
59
|
+
} else {
|
|
60
|
+
normalColumns.push(column)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// 先处理普通列
|
|
65
|
+
normalColumns.forEach((column, index) => {
|
|
66
|
+
if (column.subColumns && column.subColumns.length > 0) {
|
|
67
|
+
// 过滤可见的子列
|
|
68
|
+
const visibleSubCols = column.subColumns.filter(
|
|
69
|
+
subCol => subCol.show !== false
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// 如果所有子列都被隐藏,跳过这个一级表头
|
|
73
|
+
if (visibleSubCols.length === 0) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 验证 fixed 状态(使用可见的子列)
|
|
78
|
+
const validation = validateSubColumnsFixed({
|
|
79
|
+
...column,
|
|
80
|
+
subColumns: visibleSubCols
|
|
81
|
+
})
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
console.error(
|
|
84
|
+
`[TwoLevelTableX] 配置错误 (第 ${index + 1} 个列):`,
|
|
85
|
+
validation.error
|
|
86
|
+
)
|
|
87
|
+
if (process.env.NODE_ENV === 'development') {
|
|
88
|
+
throw new Error(validation.error)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
firstLevelHeaders.push({
|
|
93
|
+
...column,
|
|
94
|
+
hasSubColumns: true,
|
|
95
|
+
subColumnCount: visibleSubCols.length // 使用可见子列的数量
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// 确定统一的 fixed 状态
|
|
99
|
+
// 重要:只使用一级表头的 fixed 属性,二级分类的 fixed 应该与一级分类保持一致
|
|
100
|
+
const unifiedFixed = column.fixed !== undefined ? column.fixed : 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
|
+
// 移除子列自己的 fixed 属性,强制使用一级表头的 fixed
|
|
109
|
+
const { fixed: _subColFixed, ...subColWithoutFixed } = subCol
|
|
110
|
+
return {
|
|
111
|
+
...subColWithoutFixed,
|
|
112
|
+
fixed: unifiedFixed // 二级分类的 fixed 与一级分类保持一致
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
} else {
|
|
117
|
+
// 没有 subColumns,直接添加(会占两行)
|
|
118
|
+
firstLevelHeaders.push({
|
|
119
|
+
...column,
|
|
120
|
+
hasSubColumns: false,
|
|
121
|
+
subColumnCount: 1
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
transformedColumns.push(column)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 最后处理特殊列(select、diy、expand)放在最后
|
|
129
|
+
// 注意:移除特殊列的 fixed 属性,让它们按数组顺序显示在最后(不固定)
|
|
130
|
+
// 如果用户需要特殊列固定在右边,可以手动设置 fixed: 'right'
|
|
131
|
+
specialColumns.forEach(column => {
|
|
132
|
+
const { fixed, ...columnWithoutFixed } = column
|
|
133
|
+
const adjustedColumn = {
|
|
134
|
+
...columnWithoutFixed,
|
|
135
|
+
// 特殊列应该在最后,移除 fixed: 'left',让它们按数组顺序显示
|
|
136
|
+
// 如果需要固定在右边,可以改为 fixed: 'right'
|
|
137
|
+
fixed: fixed === 'left' ? undefined : fixed
|
|
138
|
+
}
|
|
139
|
+
transformedColumns.push(adjustedColumn)
|
|
140
|
+
firstLevelHeaders.push({
|
|
141
|
+
...adjustedColumn,
|
|
142
|
+
hasSubColumns: false,
|
|
143
|
+
subColumnCount: 1,
|
|
144
|
+
isSpecialColumn: true
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return { transformedColumns, firstLevelHeaders }
|
|
149
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
* 注意:当存在一级表头相同的列时,在column配置中加id标识区分,避免混乱
|
|
17
|
+
*/
|
|
18
|
+
function twoLevelHeaderTableXHOC(Component) {
|
|
19
|
+
const TwoLevelHeaderTableXWrapper = props => {
|
|
20
|
+
const { columns: originalColumns, ...rest } = props
|
|
21
|
+
|
|
22
|
+
const flattenedColumns = useMemo(() => {
|
|
23
|
+
return flattenColumnsForSelectAndDiy(originalColumns)
|
|
24
|
+
}, [originalColumns])
|
|
25
|
+
|
|
26
|
+
return <Component {...rest} columns={flattenedColumns} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
TwoLevelHeaderTableXWrapper.propTypes = {
|
|
30
|
+
columns: PropTypes.array.isRequired
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return TwoLevelHeaderTableXWrapper
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
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,70 @@
|
|
|
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;
|
|
25
|
+
.gm-table-x-tbody {
|
|
26
|
+
display: table-row-group !important;
|
|
27
|
+
.gm-table-x-tr {
|
|
28
|
+
display: table-row !important;
|
|
29
|
+
height: 60px;
|
|
30
|
+
.gm-table-x-td {
|
|
31
|
+
display: table-cell !important;
|
|
32
|
+
flex: none !important;
|
|
33
|
+
flex-grow: unset !important;
|
|
34
|
+
flex-shrink: unset !important;
|
|
35
|
+
flex-basis: unset !important;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
padding: 8px;
|
|
38
|
+
white-space: normal;
|
|
39
|
+
word-wrap: break-word;
|
|
40
|
+
word-break: break-all;
|
|
41
|
+
vertical-align: middle;
|
|
42
|
+
text-align: center;
|
|
43
|
+
&:first-child {
|
|
44
|
+
padding-left: 20px;
|
|
45
|
+
}
|
|
46
|
+
&:last-child {
|
|
47
|
+
padding-right: 20px;
|
|
48
|
+
}
|
|
49
|
+
&.gm-table-x-icon-column {
|
|
50
|
+
padding-right: 5px;
|
|
51
|
+
padding-left: 5px;
|
|
52
|
+
.gm-table-x-icon.gm-popover-active {
|
|
53
|
+
color: @brand-primary;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
img {
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
max-height: 60px;
|
|
59
|
+
object-fit: contain;
|
|
60
|
+
vertical-align: middle;
|
|
61
|
+
display: block;
|
|
62
|
+
margin: 0 auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 保持与原版 TableX 一致的 expand 样式
|
|
66
|
+
.gm-table-x-expand {
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
padding-top: 2px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 二级表头的固定列边框处理(使用 inset box-shadow 创建边框,确保滚动时边框始终可见)
|
|
72
|
+
&.gm-table-x-fixed-left {
|
|
73
|
+
border-right: none !important;
|
|
74
|
+
box-shadow: inset -1px 0 0 0 @gm-table-x-border-color;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&.gm-table-x-fixed-right {
|
|
78
|
+
border-left: none !important;
|
|
79
|
+
box-shadow: inset 1px 0 0 0 @gm-table-x-border-color;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
22
86
|
.gm-table-x-tr {
|
|
23
87
|
display: flex;
|
|
24
88
|
min-height: 60px;
|
|
@@ -80,6 +144,77 @@
|
|
|
80
144
|
}
|
|
81
145
|
}
|
|
82
146
|
|
|
147
|
+
// 二级表头样式
|
|
148
|
+
// 注意:为了支持 rowspan,二级表头需要使用原生的表格布局模型
|
|
149
|
+
.gm-table-x-thead-two-level {
|
|
150
|
+
display: table-header-group !important;
|
|
151
|
+
|
|
152
|
+
.gm-table-x-tr-first-level {
|
|
153
|
+
display: table-row !important;
|
|
154
|
+
|
|
155
|
+
.gm-table-x-th-first-level {
|
|
156
|
+
// 第一级表头样式
|
|
157
|
+
display: table-cell !important;
|
|
158
|
+
border: none !important;
|
|
159
|
+
vertical-align: middle;
|
|
160
|
+
text-align: center;
|
|
161
|
+
padding: 8px;
|
|
162
|
+
flex: none !important;
|
|
163
|
+
// 确保固定列有背景色,这样边框才能正确显示
|
|
164
|
+
background: @gm-table-x-header-bg !important;
|
|
165
|
+
position: relative;
|
|
166
|
+
|
|
167
|
+
// 使用伪元素创建所有边框
|
|
168
|
+
&::before {
|
|
169
|
+
content: '';
|
|
170
|
+
position: absolute;
|
|
171
|
+
top: 0;
|
|
172
|
+
left: 0;
|
|
173
|
+
right: 0;
|
|
174
|
+
bottom: 0;
|
|
175
|
+
border: 1px solid #d8dee7;
|
|
176
|
+
border-bottom-width: 2px;
|
|
177
|
+
pointer-events: none;
|
|
178
|
+
z-index: 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.gm-table-x-tr-second-level {
|
|
184
|
+
display: table-row !important;
|
|
185
|
+
.gm-table-x-th-second-level {
|
|
186
|
+
// 第二级表头样式
|
|
187
|
+
display: table-cell !important;
|
|
188
|
+
border: none !important;
|
|
189
|
+
vertical-align: middle;
|
|
190
|
+
text-align: center;
|
|
191
|
+
padding: 8px;
|
|
192
|
+
flex: none !important;
|
|
193
|
+
flex-grow: unset !important;
|
|
194
|
+
flex-shrink: unset !important;
|
|
195
|
+
flex-basis: unset !important;
|
|
196
|
+
box-sizing: border-box;
|
|
197
|
+
// 确保固定列有背景色,这样边框才能正确显示
|
|
198
|
+
background: @gm-table-x-header-bg !important;
|
|
199
|
+
position: relative;
|
|
200
|
+
|
|
201
|
+
// 使用伪元素创建所有边框(除了顶部,因为顶部由一级表头的底部边框覆盖)
|
|
202
|
+
&::before {
|
|
203
|
+
content: '';
|
|
204
|
+
position: absolute;
|
|
205
|
+
top: 0;
|
|
206
|
+
left: 0;
|
|
207
|
+
right: 0;
|
|
208
|
+
bottom: 0;
|
|
209
|
+
border: 1px solid #d8dee7;
|
|
210
|
+
border-top: none;
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
z-index: 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
83
218
|
.gm-table-x-tbody {
|
|
84
219
|
display: block; // 覆盖tbody 默认display
|
|
85
220
|
|
|
@@ -211,13 +346,30 @@
|
|
|
211
346
|
position: sticky !important;
|
|
212
347
|
z-index: 1;
|
|
213
348
|
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 表头固定列需要更高的 z-index
|
|
352
|
+
.gm-table-x-th {
|
|
353
|
+
&.gm-table-x-fixed-left,
|
|
354
|
+
&.gm-table-x-fixed-right {
|
|
355
|
+
z-index: 11 !important;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
214
358
|
|
|
359
|
+
// tbody 单元格固定列使用 inset box-shadow
|
|
360
|
+
.gm-table-x-td {
|
|
215
361
|
&.gm-table-x-fixed-left {
|
|
216
|
-
border-right:
|
|
362
|
+
border-right: none !important;
|
|
363
|
+
// 使用 box-shadow 创建边框,确保在滚动时边框始终可见
|
|
364
|
+
// inset box-shadow 会创建内部阴影,不会随内容滚动
|
|
365
|
+
box-shadow: inset -1px 0 0 0 @gm-table-x-border-color !important;
|
|
217
366
|
}
|
|
218
367
|
|
|
219
368
|
&.gm-table-x-fixed-right {
|
|
220
|
-
border-left:
|
|
369
|
+
border-left: none !important;
|
|
370
|
+
// 使用 box-shadow 创建边框,确保在滚动时边框始终可见
|
|
371
|
+
// inset box-shadow 会创建内部阴影,不会随内容滚动
|
|
372
|
+
box-shadow: inset 1px 0 0 0 @gm-table-x-border-color !important;
|
|
221
373
|
}
|
|
222
374
|
}
|
|
223
375
|
}
|
|
@@ -432,7 +584,7 @@
|
|
|
432
584
|
}
|
|
433
585
|
}
|
|
434
586
|
|
|
435
|
-
.gm-react-table-x-diy-modal-content{
|
|
587
|
+
.gm-react-table-x-diy-modal-content {
|
|
436
588
|
border-top: 1px solid @gm-table-x-border-color;
|
|
437
589
|
border-bottom: 1px solid @gm-table-x-border-color;
|
|
438
590
|
}
|
package/src/util/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import PropTypes from 'prop-types'
|
|
|
5
5
|
import _ from 'lodash'
|
|
6
6
|
import SVGEmpty from '../../svg/empty.svg'
|
|
7
7
|
import { Flex, EVENT_TYPE } from '@gmfe/react'
|
|
8
|
-
import BatchActionBar from '
|
|
9
|
-
import SortHeader from '
|
|
8
|
+
import BatchActionBar from '@gmfe/table-x/src/util/batch_action_bar'
|
|
9
|
+
import SortHeader from '@gmfe/table-x/src/util/sort_header'
|
|
10
10
|
import {
|
|
11
11
|
OperationHeader,
|
|
12
12
|
OperationDelete,
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
OperationCell,
|
|
16
16
|
OperationRowEdit,
|
|
17
17
|
OperationIconTip
|
|
18
|
-
} from '
|
|
19
|
-
import { EditButton, EditOperation } from '
|
|
18
|
+
} from '@gmfe/table-x/src/util/operation'
|
|
19
|
+
import { EditButton, EditOperation } from '@gmfe/table-x/src/util/edit'
|
|
20
20
|
|
|
21
21
|
const TABLE_X_SELECT_ID = 'table_x_select_id'
|
|
22
22
|
const TABLE_X_EXPAND_ID = 'table_x_expand_id'
|
|
@@ -83,12 +83,14 @@ const Empty = () => {
|
|
|
83
83
|
)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const Loading = () => {
|
|
86
|
+
const Loading = ({ style, ...rest }) => {
|
|
87
87
|
return (
|
|
88
88
|
<Mask
|
|
89
89
|
style={{
|
|
90
|
-
backgroundColor: 'rgba(255,255,255,0.8)'
|
|
90
|
+
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
91
|
+
...style
|
|
91
92
|
}}
|
|
93
|
+
{...rest}
|
|
92
94
|
>
|
|
93
95
|
{getLocale('加载数据中...')}
|
|
94
96
|
</Mask>
|
package/LICENSE
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
ISC License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020-present, gmfe contributors
|
|
4
|
-
|
|
5
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
-
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
-
copyright notice and this permission notice appear in all copies.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
-
|
package/src/hoc/sticky_layout.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @author: stanfer
|
|
3
|
-
* @description:
|
|
4
|
-
* @createDate: 2025/11/24 11:05
|
|
5
|
-
* @Version: 1.0
|
|
6
|
-
* @last modify time:
|
|
7
|
-
**/
|
|
8
|
-
import React, { useRef, useEffect } from 'react'
|
|
9
|
-
|
|
10
|
-
function stickyLayout(Component) {
|
|
11
|
-
const StickyLayout = ({ ...rest }) => {
|
|
12
|
-
const currentStickyRef = useRef(null);
|
|
13
|
-
const topSentinelRef = useRef(null);
|
|
14
|
-
const bottomSentinelRef = useRef(null);
|
|
15
|
-
// 统一管理
|
|
16
|
-
const observerRef = useRef(null);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!currentStickyRef.current) return;
|
|
20
|
-
|
|
21
|
-
const options = {
|
|
22
|
-
root: null, // 相对于浏览器视口
|
|
23
|
-
rootMargin: '0px',
|
|
24
|
-
threshold: 0, // 只要有一个像素进入/离开视口就触发
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
observerRef.current = new IntersectionObserver((entries) => {
|
|
28
|
-
entries.forEach((entry) => {
|
|
29
|
-
const target = entry.target;
|
|
30
|
-
|
|
31
|
-
if (target === topSentinelRef.current) {
|
|
32
|
-
if (entry.isIntersecting) {
|
|
33
|
-
console.log('=======> 顶部进入');
|
|
34
|
-
let heightSum = 0, tableRoot;
|
|
35
|
-
const fullTab = document.querySelectorAll('.gm-framework-full-tabs')
|
|
36
|
-
const commonStickyHeader = document.querySelectorAll('.common-sticky-header')
|
|
37
|
-
const rtTable = document.querySelectorAll('.rt-table')
|
|
38
|
-
const rtTheadHeader = document.querySelectorAll('.rt-thead')
|
|
39
|
-
rtTheadHeader.forEach((el) => {
|
|
40
|
-
el.style.position = 'sticky';
|
|
41
|
-
el.style.top = 0;
|
|
42
|
-
el.style.zIndex = 10;
|
|
43
|
-
heightSum += el.offsetHeight + 10; // 元素本身高 + 上下 10 padding
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
commonStickyHeader.forEach((item, idx) => {
|
|
47
|
-
if (fullTab.length) {
|
|
48
|
-
currentStickyRef.current.style.zIndex = 101;
|
|
49
|
-
}
|
|
50
|
-
// console.log(item.className);
|
|
51
|
-
if (item.className.includes('gm-box-table-header')) {
|
|
52
|
-
heightSum += item.offsetHeight + 10; // 元素本身高 + 上下 10 padding
|
|
53
|
-
// console.log(idx, item.getBoundingClientRect(), 'gm-box-table-header: ', item.getBoundingClientRect().height + 20);
|
|
54
|
-
}
|
|
55
|
-
if (item.className.includes('gm-table-x-thead')) {
|
|
56
|
-
heightSum += item.offsetHeight + 8; // 元素本身高 + 上下 8 padding
|
|
57
|
-
tableRoot = item.parentElement.parentElement;
|
|
58
|
-
// console.log(idx, item.getBoundingClientRect(), 'gm-table-x-thead: ', item.getBoundingClientRect().height + 16);
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
if (rtTable.length) {
|
|
63
|
-
rtTable.forEach((el) => {
|
|
64
|
-
if (fullTab.length) {
|
|
65
|
-
el.style.maxHeight = `calc(100vh - ${heightSum + (fullTab.length * 40) + 50}px)`; // +50 顶部导航栏
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
el.style.maxHeight = `calc(100vh - ${heightSum + 50}px)`; // +50 顶部导航栏
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
if (tableRoot) {
|
|
72
|
-
if (fullTab.length) {
|
|
73
|
-
tableRoot.style.maxHeight = `calc(100vh - ${heightSum + (fullTab.length * 40) + 50}px)`; // +50 顶部导航栏
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
tableRoot.style.maxHeight = `calc(100vh - ${heightSum + 50}px)`; // +50 顶部导航栏
|
|
77
|
-
}
|
|
78
|
-
// currentStickyRef.current.style.maxHeight = 'unset';
|
|
79
|
-
console.log('currentStickyRef 实例:', currentStickyRef.current.style);
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
currentStickyRef.current.style.maxHeight = '50%'; // TODO: 这块需要之后要走外部参数
|
|
83
|
-
console.log('顶部离开 <========');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (target === bottomSentinelRef.current) {
|
|
87
|
-
if (entry.isIntersecting) {
|
|
88
|
-
console.log('=======> 底部进入');
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.log('底部离开 <========');
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}, options);
|
|
95
|
-
|
|
96
|
-
if (topSentinelRef.current) {
|
|
97
|
-
observerRef.current.observe(topSentinelRef.current);
|
|
98
|
-
}
|
|
99
|
-
if (bottomSentinelRef.current) {
|
|
100
|
-
observerRef.current.observe(bottomSentinelRef.current);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return () => {
|
|
104
|
-
if (observerRef.current) {
|
|
105
|
-
observerRef.current.disconnect();
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}, [])
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<div ref={currentStickyRef} className='common-sticky-layout'>
|
|
112
|
-
<div
|
|
113
|
-
ref={topSentinelRef}
|
|
114
|
-
sentinel="topSentinel"
|
|
115
|
-
style={{
|
|
116
|
-
position: 'absolute',
|
|
117
|
-
top: '0',
|
|
118
|
-
left: '0',
|
|
119
|
-
width: '100%',
|
|
120
|
-
height: '1px', // 尽量小,不占空间
|
|
121
|
-
pointerEvents: 'none', // 防止它拦截任何鼠标事件
|
|
122
|
-
backgroundColor: 'transparent', // 完全透明
|
|
123
|
-
}}
|
|
124
|
-
/>
|
|
125
|
-
<Component {...rest} />
|
|
126
|
-
<div
|
|
127
|
-
ref={bottomSentinelRef}
|
|
128
|
-
sentinel="bottomSentinel"
|
|
129
|
-
style={{
|
|
130
|
-
position: 'absolute',
|
|
131
|
-
bottom: '0',
|
|
132
|
-
left: '0',
|
|
133
|
-
width: '100%',
|
|
134
|
-
height: '1px',
|
|
135
|
-
pointerEvents: 'none',
|
|
136
|
-
backgroundColor: 'transparent',
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
StickyLayout.propTypes = StickyLayout.propTypes;
|
|
144
|
-
StickyLayout.defaultProps = StickyLayout.defaultProps;
|
|
145
|
-
|
|
146
|
-
return StickyLayout
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default stickyLayout
|