@gmfe/react 2.14.26 → 2.15.4-alpha.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/LICENSE ADDED
@@ -0,0 +1,16 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmfe/react",
3
- "version": "2.14.26",
3
+ "version": "2.15.4-alpha.0",
4
4
  "description": "",
5
5
  "author": "liyatang <liyatang@qq.com>",
6
6
  "homepage": "https://github.com/gmfe/gmfe#readme",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@gm-common/tool": "^1.0.0",
30
- "@gmfe/locales": "^2.14.26",
30
+ "@gmfe/locales": "^2.15.4-alpha.0",
31
31
  "big.js": "^5.2.2",
32
32
  "classnames": "^2.2.5",
33
33
  "lodash": "^4.17.14",
@@ -35,5 +35,5 @@
35
35
  "prop-types": "^15.7.2",
36
36
  "react-window": "^1.8.5"
37
37
  },
38
- "gitHead": "2fc65546df80d52dcfb37c2c35b615dbade5daa9"
38
+ "gitHead": "2c13925c3699572748de29cf0256ca1431c30f01"
39
39
  }
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import classNames from 'classnames'
4
4
  import Flex from '../flex'
5
+ import { StickyLayout } from '@gmfe/react'
5
6
 
6
7
  // 暂时没什么用
7
8
  const Info = props => {
@@ -18,21 +19,32 @@ Info.propTypes = {
18
19
  style: PropTypes.object
19
20
  }
20
21
 
21
- const BoxTable = props => {
22
- const { info, action, children, className, headerProps = {}, ...rest } = props
22
+ const BoxHeader = StickyLayout(props => {
23
+ const { info, action, headerProps = {} } = props
23
24
  const { className: headerClassName } = headerProps
24
25
 
26
+ return (
27
+ <Flex
28
+ {...headerProps}
29
+ className={classNames(
30
+ 'gm-box-table-header common-sticky-header',
31
+ headerClassName
32
+ )}
33
+ alignCenter
34
+ >
35
+ <Flex>{info}</Flex>
36
+ <Flex flex />
37
+ <Flex>{action}</Flex>
38
+ </Flex>
39
+ )
40
+ })
41
+
42
+ const BoxTable = props => {
43
+ const { children, className, ...rest } = props
44
+
25
45
  return (
26
46
  <div {...rest} className={classNames('gm-box gm-box-table', className)}>
27
- <Flex
28
- {...headerProps}
29
- className={classNames('gm-box-table-header', headerClassName)}
30
- alignCenter
31
- >
32
- <Flex>{info}</Flex>
33
- <Flex flex />
34
- <Flex>{action}</Flex>
35
- </Flex>
47
+ <BoxHeader {...props} />
36
48
  <div>{children}</div>
37
49
  </div>
38
50
  )
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @author: stanfer
3
+ * @description: 粘性布局高阶组件,用于实现表格等组件的粘性头部效果
4
+ * @createDate: 2025/11/24 10:05
5
+ * @Version: 1.0
6
+ **/
7
+ import React, { useRef, useEffect, useMemo } from 'react'
8
+ import PropTypes from 'prop-types'
9
+
10
+ // 常量配置
11
+ const CONSTANTS = {
12
+ PADDING: {
13
+ RT_THEAD: 10,
14
+ BOX_TABLE_HEADER: 10,
15
+ TABLE_X_THEAD: 8
16
+ },
17
+ Z_INDEX: {
18
+ RT_THEAD: 10,
19
+ STICKY_LAYOUT: 101
20
+ },
21
+ HEIGHT: {
22
+ FULL_TAB: 40,
23
+ TOP_NAV: 50,
24
+ DEFAULT_MAX: '50%'
25
+ }
26
+ }
27
+
28
+ // IntersectionObserver 配置
29
+ const OBSERVER_OPTIONS = {
30
+ root: null, // 相对于浏览器视口
31
+ rootMargin: '0px',
32
+ threshold: 0 // 只要有一个像素进入/离开视口就触发
33
+ }
34
+
35
+ // 哨兵元素样式
36
+ const SENTINEL_STYLE = {
37
+ position: 'absolute',
38
+ left: '0',
39
+ width: '100%',
40
+ height: '1px',
41
+ pointerEvents: 'none',
42
+ backgroundColor: 'transparent'
43
+ }
44
+
45
+ /**
46
+ * 计算最大高度
47
+ * @param {number} heightSum - 高度总和
48
+ * @param {number} fullTabCount - 全屏标签页数量
49
+ * @returns {string} 计算后的最大高度值
50
+ */
51
+ const calculateMaxHeight = (heightSum, fullTabCount = 0) => {
52
+ const extraHeight =
53
+ fullTabCount * CONSTANTS.HEIGHT.FULL_TAB + CONSTANTS.HEIGHT.TOP_NAV
54
+ return `calc(100vh - ${heightSum + extraHeight}px)`
55
+ }
56
+
57
+ /**
58
+ * 设置表格最大高度
59
+ * @param {NodeList|Element[]} elements - 需要设置的元素列表
60
+ * @param {number} heightSum - 高度总和
61
+ * @param {number} fullTabCount - 全屏标签页数量
62
+ */
63
+ const setElementsMaxHeight = (elements, heightSum, fullTabCount) => {
64
+ const maxHeight = calculateMaxHeight(heightSum, fullTabCount)
65
+ elements.forEach(el => {
66
+ el.style.maxHeight = maxHeight
67
+ })
68
+ }
69
+
70
+ /**
71
+ * 计算粘性元素的总高度
72
+ * @returns {{heightSum: number, tableRoot: Element|null, fullTabCount: number}}
73
+ */
74
+ const calculateStickyHeight = () => {
75
+ let heightSum = 0
76
+ let tableRoot = null
77
+
78
+ // 查询 DOM 元素
79
+ const fullTab = document.querySelectorAll('.gm-framework-full-tabs')
80
+ const commonStickyHeader = document.querySelectorAll('.common-sticky-header')
81
+ const rtTheadHeader = document.querySelectorAll('.rt-thead')
82
+
83
+ // 处理 rt-thead 元素
84
+ rtTheadHeader.forEach(el => {
85
+ el.style.position = 'sticky'
86
+ el.style.top = '0'
87
+ el.style.zIndex = CONSTANTS.Z_INDEX.RT_THEAD
88
+ heightSum += el.offsetHeight + CONSTANTS.PADDING.RT_THEAD
89
+ })
90
+
91
+ // 处理 common-sticky-header 元素
92
+ commonStickyHeader.forEach(item => {
93
+ if (fullTab.length) {
94
+ const currentSticky = document.querySelector('.common-sticky-layout')
95
+ if (currentSticky) {
96
+ currentSticky.style.zIndex = CONSTANTS.Z_INDEX.STICKY_LAYOUT
97
+ }
98
+ }
99
+
100
+ if (item.className.includes('gm-box-table-header')) {
101
+ heightSum += item.offsetHeight + CONSTANTS.PADDING.BOX_TABLE_HEADER
102
+ }
103
+
104
+ if (item.className.includes('gm-table-x-thead')) {
105
+ heightSum += item.offsetHeight + CONSTANTS.PADDING.TABLE_X_THEAD
106
+ tableRoot = item.parentElement?.parentElement || null
107
+ }
108
+ })
109
+
110
+ return {
111
+ heightSum,
112
+ tableRoot,
113
+ fullTabCount: fullTab.length
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 处理顶部哨兵进入视口
119
+ */
120
+ const handleTopSentinelEnter = currentStickyRef => {
121
+ const { heightSum, tableRoot, fullTabCount } = calculateStickyHeight()
122
+
123
+ // 设置 rt-table 元素的最大高度
124
+ const rtTable = document.querySelectorAll('.rt-table')
125
+ if (rtTable.length) {
126
+ setElementsMaxHeight(rtTable, heightSum, fullTabCount)
127
+ }
128
+
129
+ // 设置 tableRoot 的最大高度
130
+ if (tableRoot) {
131
+ tableRoot.style.maxHeight = calculateMaxHeight(heightSum, fullTabCount)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 处理顶部哨兵离开视口
137
+ */
138
+ const handleTopSentinelLeave = currentStickyRef => {
139
+ if (currentStickyRef.current) {
140
+ currentStickyRef.current.style.maxHeight = CONSTANTS.HEIGHT.DEFAULT_MAX
141
+ }
142
+ }
143
+
144
+ function StickyLayout(Component) {
145
+ const StickyLayout = ({ sticky, ...rest }) => {
146
+ const currentStickyRef = useRef(null)
147
+ const topSentinelRef = useRef(null)
148
+ const bottomSentinelRef = useRef(null)
149
+ const observerRef = useRef(null)
150
+
151
+ // 哨兵元素样式
152
+ const topSentinelStyle = useMemo(
153
+ () => ({ ...SENTINEL_STYLE, top: '0' }),
154
+ []
155
+ )
156
+ const bottomSentinelStyle = useMemo(
157
+ () => ({ ...SENTINEL_STYLE, bottom: '0' }),
158
+ []
159
+ )
160
+
161
+ useEffect(() => {
162
+ if (!sticky || !currentStickyRef.current) return undefined
163
+
164
+ if (typeof window.IntersectionObserver === 'undefined') {
165
+ return undefined
166
+ }
167
+
168
+ observerRef.current = new window.IntersectionObserver(entries => {
169
+ entries.forEach(entry => {
170
+ const target = entry.target
171
+
172
+ if (target === topSentinelRef.current) {
173
+ if (entry.isIntersecting) {
174
+ handleTopSentinelEnter(currentStickyRef)
175
+ } else {
176
+ handleTopSentinelLeave(currentStickyRef)
177
+ }
178
+ }
179
+ })
180
+ }, OBSERVER_OPTIONS)
181
+
182
+ // 观察哨兵元素
183
+ if (topSentinelRef.current) {
184
+ observerRef.current.observe(topSentinelRef.current)
185
+ }
186
+ if (bottomSentinelRef.current) {
187
+ observerRef.current.observe(bottomSentinelRef.current)
188
+ }
189
+
190
+ return () => {
191
+ if (observerRef.current) {
192
+ observerRef.current.disconnect()
193
+ }
194
+ }
195
+ }, [sticky])
196
+
197
+ if (!sticky) {
198
+ return <Component {...rest} />
199
+ }
200
+
201
+ return (
202
+ <div ref={currentStickyRef} className='common-sticky-layout'>
203
+ <div
204
+ ref={topSentinelRef}
205
+ data-sentinel='topSentinel'
206
+ style={topSentinelStyle}
207
+ />
208
+ <Component {...rest} />
209
+ <div
210
+ ref={bottomSentinelRef}
211
+ data-sentinel='bottomSentinel'
212
+ style={bottomSentinelStyle}
213
+ />
214
+ </div>
215
+ )
216
+ }
217
+
218
+ StickyLayout.defaultProps = {
219
+ sticky: false
220
+ }
221
+
222
+ StickyLayout.propTypes = {
223
+ sticky: PropTypes.oneOfType([PropTypes.bool, PropTypes.object])
224
+ }
225
+
226
+ return StickyLayout
227
+ }
228
+
229
+ export default StickyLayout
package/src/index.js CHANGED
@@ -103,6 +103,7 @@ import Selection from './component/selection'
103
103
  import TreeV2 from './component/tree_v2'
104
104
  import RecommendInput from './component/recommend_input'
105
105
  import PicturePreview from './component/picture_preview'
106
+ import StickyLayout from './hoc/sticky_layout'
106
107
 
107
108
  Object.assign(Sheet, {
108
109
  SheetColumn,
@@ -132,8 +133,8 @@ Object.assign(Checkbox, {
132
133
  Object.assign(Select, {
133
134
  Option
134
135
  })
135
-
136
136
  export {
137
+ StickyLayout,
137
138
  // 库之间用
138
139
  EVENT_TYPE,
139
140
  PaginationBase,