@gmfe/table-x 2.14.30-beta.4 → 2.14.30

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.
@@ -1,169 +0,0 @@
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
@@ -1,126 +0,0 @@
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
- state = {
10
- hasError: false
11
- }
12
-
13
- static getDerivedStateFromError(error) {
14
- // 捕获子组件树中的错误,返回新的 state
15
- return { hasError: true }
16
- }
17
-
18
- componentDidCatch(error, errorInfo) {
19
- // 静默处理错误,不打印日志
20
- }
21
-
22
- render() {
23
- if (this.state.hasError) {
24
- return null
25
- }
26
- const children = this.props.children
27
- // 如果 children 是 undefined 或 null,返回 null(React 允许返回 null)
28
- if (children === undefined || children === null) {
29
- return null
30
- }
31
- return children
32
- }
33
- }
34
-
35
- // 创建一个包装组件,用于包装 Cell 组件的 type,确保它总是返回有效值
36
- // 这个组件会包装原始的 Cell 组件,如果原始组件返回 undefined,就返回 null
37
- const SafeCellWrapper = OriginalComponent => {
38
- return function WrappedCell(props) {
39
- try {
40
- const result = OriginalComponent(props)
41
- // 如果原始组件返回 undefined,返回 null
42
- return result === undefined ? null : result
43
- } catch (error) {
44
- return null
45
- }
46
- }
47
- }
48
-
49
- // 错误边界组件,专门用于捕获 Cell 组件返回 undefined 的错误
50
- class SafeCellErrorBoundary extends React.Component {
51
- state = {
52
- hasError: false
53
- }
54
-
55
- static getDerivedStateFromError(error) {
56
- return { hasError: true }
57
- }
58
-
59
- componentDidCatch(error, errorInfo) {
60
- // 静默处理错误
61
- }
62
-
63
- render() {
64
- if (this.state.hasError) {
65
- return null
66
- }
67
- return this.props.children
68
- }
69
- }
70
-
71
- const Td = ({ cell, totalWidth }) => {
72
- const cp = cell.getCellProps()
73
- const { tdClassName } = cell.column
74
- const tdProps = {
75
- ...cp,
76
- className: classNames('gm-table-x-td', tdClassName, {
77
- 'gm-table-x-fixed-left': cell.column.fixed === 'left',
78
- 'gm-table-x-fixed-right': cell.column.fixed === 'right'
79
- }),
80
- style: {
81
- ...cp.style,
82
- ...getColumnStyle(cell.column)
83
- }
84
- }
85
-
86
- if (cell.column.fixed === 'left') {
87
- // 用到 fixed,可以利用 totalLeft
88
- tdProps.style.left = cell.column.totalLeft
89
- } else if (cell.column.fixed === 'right') {
90
- tdProps.style.right =
91
- totalWidth - cell.column.totalLeft - cell.column.totalWidth
92
- }
93
-
94
- // 如果 Cell 返回 undefined,转换为 null(null 不会报错,undefined 会)
95
- let cellContent = cell.render('Cell')
96
-
97
- // cellContent 是一个 React 元素,但它的 type(组件)在渲染时可能返回 undefined
98
- // 我们需要用一个包装组件来替换原始的 type,确保总是返回有效值
99
- if (cellContent && React.isValidElement(cellContent)) {
100
- // 创建一个新的 React 元素,用包装组件替换原始的 type
101
- const OriginalComponent = cellContent.type
102
- const WrappedComponent = SafeCellWrapper(OriginalComponent)
103
- cellContent = React.createElement(
104
- SafeCellErrorBoundary,
105
- null,
106
- React.createElement(WrappedComponent, cellContent.props)
107
- )
108
- } else if (cellContent === undefined || cellContent === null) {
109
- cellContent = null
110
- }
111
-
112
- return (
113
- <td {...tdProps}>
114
- <TdCatchErr>{cellContent}</TdCatchErr>
115
- </td>
116
- )
117
- }
118
-
119
- Td.whyDidYouRender = true
120
-
121
- Td.propTypes = {
122
- cell: PropTypes.object.isRequired,
123
- totalWidth: PropTypes.number.isRequired
124
- }
125
-
126
- export default React.memo(Td)
@@ -1,293 +0,0 @@
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 {
178
- colSpan: hpColSpan,
179
- rowSpan: hpRowSpan,
180
- key: hpKey,
181
- ...restHp
182
- } = hp || {}
183
- const finalRowSpan =
184
- rowSpan !== undefined ? Number(rowSpan) : undefined
185
- const finalColSpan =
186
- colSpan !== undefined
187
- ? Number(colSpan)
188
- : hpColSpan !== undefined
189
- ? Number(hpColSpan)
190
- : undefined
191
-
192
- const thProps = {
193
- ...restHp,
194
- ...(finalColSpan !== undefined && { colSpan: finalColSpan }),
195
- ...(finalRowSpan !== undefined && { rowSpan: finalRowSpan }), // 只有定义了才添加
196
- className: classNames(
197
- 'gm-table-x-th',
198
- 'gm-table-x-th-first-level',
199
- hp?.className,
200
- thClassName,
201
- {
202
- 'gm-table-x-fixed-left': fixed === 'left',
203
- 'gm-table-x-fixed-right': fixed === 'right'
204
- }
205
- ),
206
- style: cellStyle
207
- }
208
-
209
- // 生成唯一的 key:结合 id/accessor 和索引,确保即使 id/accessor 相同,key 也是唯一的
210
- // 如果有 hpKey,也加上索引以确保唯一性(因为 hpKey 可能也会重复)
211
- const baseKey = hpKey || header.id || header.accessor
212
- const headerKey = baseKey ? `${baseKey}-${idx}` : `header-${idx}`
213
- return (
214
- <th key={headerKey} {...thProps}>
215
- {typeof header.Header === 'function' ? (
216
- <header.Header />
217
- ) : (
218
- header.Header
219
- )}
220
- {headerColumn?.canSort && (
221
- <SortHeader
222
- {...headerColumn.getSortByToggleProps()}
223
- type={
224
- headerColumn.isSorted
225
- ? headerColumn.isSortedDesc
226
- ? 'desc'
227
- : 'asc'
228
- : null
229
- }
230
- />
231
- )}
232
- </th>
233
- )
234
- })}
235
- </tr>
236
-
237
- {/* 第二级表头行(只有有子列的情况才显示) */}
238
- {hasNestedHeaders && secondLevelGroup && (
239
- <tr className='gm-table-x-tr gm-table-x-tr-second-level'>
240
- {secondLevelGroup.headers.map((column, idx) => {
241
- // 找到这个列对应的第一级表头
242
- const correspondingFirstLevel = firstLevelCells.find(cell => {
243
- if (cell.type === 'single') {
244
- return cell.secondLevelIndex === idx
245
- } else if (cell.type === 'group') {
246
- return (
247
- idx >= cell.secondLevelStartIndex &&
248
- idx < cell.secondLevelStartIndex + cell.colSpan
249
- )
250
- }
251
- return false
252
- })
253
-
254
- if (
255
- correspondingFirstLevel &&
256
- correspondingFirstLevel.type === 'single'
257
- ) {
258
- return null
259
- }
260
- // 生成唯一的 key:结合 id/accessor 和索引,确保即使 id/accessor 相同,key 也是唯一的
261
- const columnKey = column.id
262
- ? `${column.id}-${idx}`
263
- : column.accessor
264
- ? `${column.accessor}-${idx}`
265
- : `col-${idx}`
266
- return (
267
- <Th
268
- key={columnKey}
269
- column={{
270
- ...column,
271
- // column.fixed 已经在 transformColumnsForTwoLevel 中正确设置
272
- thClassName: classNames(
273
- column.thClassName,
274
- 'gm-table-x-th-second-level'
275
- )
276
- }}
277
- totalWidth={totalWidth}
278
- />
279
- )
280
- })}
281
- </tr>
282
- )}
283
- </thead>
284
- )
285
- }
286
-
287
- TwoLevelTHead.propTypes = {
288
- headerGroups: PropTypes.array.isRequired,
289
- firstLevelHeaders: PropTypes.array.isRequired,
290
- totalWidth: PropTypes.number.isRequired
291
- }
292
-
293
- export default React.memo(TwoLevelTHead)
@@ -1,53 +0,0 @@
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)