@fjyueke/arco-mcp 1.0.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.
Files changed (191) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +244 -0
  4. package/metaData/components/action-sheet/doc.md +102 -0
  5. package/metaData/components/action-sheet/example/index.tsx +199 -0
  6. package/metaData/components/action-sheet/example/methods.tsx +6 -0
  7. package/metaData/components/avatar/doc.md +57 -0
  8. package/metaData/components/avatar/example/group.tsx +69 -0
  9. package/metaData/components/avatar/example/index.tsx +196 -0
  10. package/metaData/components/badge/doc.md +28 -0
  11. package/metaData/components/badge/example/index.tsx +128 -0
  12. package/metaData/components/button/doc.md +45 -0
  13. package/metaData/components/button/example/index.tsx +264 -0
  14. package/metaData/components/carousel/doc.md +89 -0
  15. package/metaData/components/carousel/example/index.tsx +1481 -0
  16. package/metaData/components/cell/doc.md +68 -0
  17. package/metaData/components/cell/example/arrow.tsx +13 -0
  18. package/metaData/components/cell/example/cell.tsx +71 -0
  19. package/metaData/components/cell/example/group.tsx +63 -0
  20. package/metaData/components/cell/example/index.tsx +19 -0
  21. package/metaData/components/checkbox/doc.md +91 -0
  22. package/metaData/components/checkbox/example/checkbox.tsx +158 -0
  23. package/metaData/components/checkbox/example/group.tsx +103 -0
  24. package/metaData/components/checkbox/example/index.tsx +25 -0
  25. package/metaData/components/circle-progress/doc.md +40 -0
  26. package/metaData/components/circle-progress/example/index.tsx +323 -0
  27. package/metaData/components/collapse/doc.md +74 -0
  28. package/metaData/components/collapse/example/collapse.tsx +183 -0
  29. package/metaData/components/collapse/example/group.tsx +136 -0
  30. package/metaData/components/collapse/example/index.tsx +16 -0
  31. package/metaData/components/collapse/example/utils.tsx +20 -0
  32. package/metaData/components/context-provider/doc.md +44 -0
  33. package/metaData/components/context-provider/example/index.tsx +208 -0
  34. package/metaData/components/count-down/doc.md +40 -0
  35. package/metaData/components/count-down/example/hooks.tsx +118 -0
  36. package/metaData/components/count-down/example/index.tsx +63 -0
  37. package/metaData/components/date-picker/doc.md +102 -0
  38. package/metaData/components/date-picker/example/index.tsx +545 -0
  39. package/metaData/components/dialog/doc.md +200 -0
  40. package/metaData/components/dialog/example/index.tsx +328 -0
  41. package/metaData/components/dialog/example/methods.tsx +56 -0
  42. package/metaData/components/divider/doc.md +25 -0
  43. package/metaData/components/divider/example/index.tsx +87 -0
  44. package/metaData/components/dropdown/doc.md +80 -0
  45. package/metaData/components/dropdown/example/dropdown.tsx +308 -0
  46. package/metaData/components/dropdown/example/index.tsx +19 -0
  47. package/metaData/components/dropdown/example/options.tsx +103 -0
  48. package/metaData/components/dropdown-menu/doc.md +91 -0
  49. package/metaData/components/dropdown-menu/example/dropdown-menu.tsx +270 -0
  50. package/metaData/components/dropdown-menu/example/index.tsx +16 -0
  51. package/metaData/components/ellipsis/doc.md +33 -0
  52. package/metaData/components/ellipsis/example/index.tsx +93 -0
  53. package/metaData/components/ellipsis/example/js-ellipsis.tsx +229 -0
  54. package/metaData/components/ellipsis/example/native-ellipsis.tsx +48 -0
  55. package/metaData/components/form/doc.md +90 -0
  56. package/metaData/components/form/example/form-item-context.tsx +8 -0
  57. package/metaData/components/form/example/form-item.tsx +378 -0
  58. package/metaData/components/form/example/index.tsx +87 -0
  59. package/metaData/components/form/example/linked-container.tsx +66 -0
  60. package/metaData/components/grid/doc.md +39 -0
  61. package/metaData/components/grid/example/index.tsx +252 -0
  62. package/metaData/components/icon/doc.md +15 -0
  63. package/metaData/components/icon/example/index.tsx +32 -0
  64. package/metaData/components/image/doc.md +62 -0
  65. package/metaData/components/image/example/index.tsx +528 -0
  66. package/metaData/components/image-picker/doc.md +116 -0
  67. package/metaData/components/image-picker/example/add-icon.tsx +14 -0
  68. package/metaData/components/image-picker/example/index.tsx +175 -0
  69. package/metaData/components/image-preview/doc.md +119 -0
  70. package/metaData/components/image-preview/example/index.tsx +1290 -0
  71. package/metaData/components/image-preview/example/methods.tsx +72 -0
  72. package/metaData/components/index-bar/doc.md +83 -0
  73. package/metaData/components/index-bar/example/group.tsx +93 -0
  74. package/metaData/components/index-bar/example/index.tsx +271 -0
  75. package/metaData/components/index-bar/example/side-bar.tsx +82 -0
  76. package/metaData/components/input/doc.md +60 -0
  77. package/metaData/components/input/example/hooks.tsx +259 -0
  78. package/metaData/components/input/example/index.tsx +134 -0
  79. package/metaData/components/input/example/props.tsx +183 -0
  80. package/metaData/components/keyboard/doc.md +73 -0
  81. package/metaData/components/keyboard/example/index.tsx +238 -0
  82. package/metaData/components/load-more/doc.md +48 -0
  83. package/metaData/components/load-more/example/index.tsx +395 -0
  84. package/metaData/components/loading/doc.md +34 -0
  85. package/metaData/components/loading/example/index.tsx +312 -0
  86. package/metaData/components/masking/doc.md +93 -0
  87. package/metaData/components/masking/example/index.tsx +410 -0
  88. package/metaData/components/masking/example/methods.tsx +88 -0
  89. package/metaData/components/nav-bar/doc.md +40 -0
  90. package/metaData/components/nav-bar/example/back-icon.tsx +28 -0
  91. package/metaData/components/nav-bar/example/index.tsx +305 -0
  92. package/metaData/components/notice-bar/doc.md +34 -0
  93. package/metaData/components/notice-bar/example/index.tsx +294 -0
  94. package/metaData/components/notify/doc.md +81 -0
  95. package/metaData/components/notify/example/index.tsx +171 -0
  96. package/metaData/components/notify/example/methods.tsx +83 -0
  97. package/metaData/components/pagination/doc.md +41 -0
  98. package/metaData/components/pagination/example/arrow.tsx +14 -0
  99. package/metaData/components/pagination/example/index.tsx +292 -0
  100. package/metaData/components/picker/doc.md +89 -0
  101. package/metaData/components/picker/example/index.tsx +217 -0
  102. package/metaData/components/picker-view/doc.md +57 -0
  103. package/metaData/components/picker-view/example/cascader.tsx +141 -0
  104. package/metaData/components/picker-view/example/index.tsx +297 -0
  105. package/metaData/components/picker-view/example/multi-picker.tsx +66 -0
  106. package/metaData/components/picker-view/example/picker-cell.tsx +416 -0
  107. package/metaData/components/popover/doc.md +144 -0
  108. package/metaData/components/popover/example/index.tsx +23 -0
  109. package/metaData/components/popover/example/menu.tsx +145 -0
  110. package/metaData/components/popover/example/popover-inner.tsx +147 -0
  111. package/metaData/components/popover/example/popover.tsx +268 -0
  112. package/metaData/components/popup/doc.md +101 -0
  113. package/metaData/components/popup/example/index.tsx +125 -0
  114. package/metaData/components/popup/example/methods.tsx +6 -0
  115. package/metaData/components/popup-swiper/doc.md +108 -0
  116. package/metaData/components/popup-swiper/example/index.tsx +332 -0
  117. package/metaData/components/popup-swiper/example/methods.tsx +6 -0
  118. package/metaData/components/portal/doc.md +14 -0
  119. package/metaData/components/portal/example/index.tsx +35 -0
  120. package/metaData/components/progress/doc.md +42 -0
  121. package/metaData/components/progress/example/index.tsx +256 -0
  122. package/metaData/components/pull-refresh/doc.md +37 -0
  123. package/metaData/components/pull-refresh/example/android-pull-refresh.tsx +260 -0
  124. package/metaData/components/pull-refresh/example/index.tsx +44 -0
  125. package/metaData/components/pull-refresh/example/ios-pull-refresh.tsx +216 -0
  126. package/metaData/components/radio/doc.md +89 -0
  127. package/metaData/components/radio/example/group.tsx +92 -0
  128. package/metaData/components/radio/example/index.tsx +25 -0
  129. package/metaData/components/radio/example/radio.tsx +22 -0
  130. package/metaData/components/rate/doc.md +33 -0
  131. package/metaData/components/rate/example/index.tsx +281 -0
  132. package/metaData/components/search-bar/doc.md +97 -0
  133. package/metaData/components/search-bar/example/association.tsx +89 -0
  134. package/metaData/components/search-bar/example/cancel-button.tsx +34 -0
  135. package/metaData/components/search-bar/example/highlight.tsx +66 -0
  136. package/metaData/components/search-bar/example/index.tsx +251 -0
  137. package/metaData/components/show-monitor/doc.md +34 -0
  138. package/metaData/components/show-monitor/example/index.tsx +646 -0
  139. package/metaData/components/skeleton/doc.md +101 -0
  140. package/metaData/components/skeleton/example/elements.tsx +290 -0
  141. package/metaData/components/skeleton/example/index.tsx +96 -0
  142. package/metaData/components/skeleton/example/skeleton-context.tsx +7 -0
  143. package/metaData/components/slider/doc.md +42 -0
  144. package/metaData/components/slider/example/index.tsx +326 -0
  145. package/metaData/components/slider/example/marks.tsx +50 -0
  146. package/metaData/components/slider/example/popover.tsx +38 -0
  147. package/metaData/components/slider/example/thumb.tsx +48 -0
  148. package/metaData/components/slider/example/useSliderIcon.tsx +38 -0
  149. package/metaData/components/slider/example/useSliderInit.tsx +170 -0
  150. package/metaData/components/stepper/doc.md +46 -0
  151. package/metaData/components/stepper/example/index.tsx +166 -0
  152. package/metaData/components/stepper/example/useButtonClick.tsx +74 -0
  153. package/metaData/components/stepper/example/useInputEvent.tsx +65 -0
  154. package/metaData/components/stepper/example/useValue.tsx +43 -0
  155. package/metaData/components/steps/doc.md +57 -0
  156. package/metaData/components/steps/example/index.tsx +123 -0
  157. package/metaData/components/steps/example/step.tsx +126 -0
  158. package/metaData/components/sticky/doc.md +45 -0
  159. package/metaData/components/sticky/example/index.tsx +430 -0
  160. package/metaData/components/swipe-action/doc.md +44 -0
  161. package/metaData/components/swipe-action/example/index.tsx +338 -0
  162. package/metaData/components/swipe-action/example/item.tsx +40 -0
  163. package/metaData/components/swipe-load/doc.md +43 -0
  164. package/metaData/components/swipe-load/example/index.tsx +339 -0
  165. package/metaData/components/swipe-load/example/type.tsx +153 -0
  166. package/metaData/components/switch/doc.md +38 -0
  167. package/metaData/components/switch/example/index.tsx +237 -0
  168. package/metaData/components/tab-bar/doc.md +57 -0
  169. package/metaData/components/tab-bar/example/index.tsx +18 -0
  170. package/metaData/components/tab-bar/example/item.tsx +74 -0
  171. package/metaData/components/tab-bar/example/tab-bar.tsx +105 -0
  172. package/metaData/components/tabs/doc.md +187 -0
  173. package/metaData/components/tabs/example/index.tsx +514 -0
  174. package/metaData/components/tabs/example/tab-cell-underline.tsx +320 -0
  175. package/metaData/components/tabs/example/tab-cell.tsx +433 -0
  176. package/metaData/components/tabs/example/tab-pane.tsx +349 -0
  177. package/metaData/components/tag/doc.md +80 -0
  178. package/metaData/components/tag/example/index.tsx +23 -0
  179. package/metaData/components/tag/example/list.tsx +97 -0
  180. package/metaData/components/tag/example/tag.tsx +85 -0
  181. package/metaData/components/textarea/doc.md +67 -0
  182. package/metaData/components/textarea/example/index.tsx +258 -0
  183. package/metaData/components/toast/doc.md +90 -0
  184. package/metaData/components/toast/example/index.tsx +335 -0
  185. package/metaData/components/toast/example/methods.tsx +87 -0
  186. package/metaData/components/transition/doc.md +19 -0
  187. package/metaData/components/transition/example/index.tsx +113 -0
  188. package/metaData/components/uploader/doc.md +82 -0
  189. package/metaData/components/uploader/example/index.tsx +208 -0
  190. package/metaData/components-index.json +242 -0
  191. package/package.json +56 -0
@@ -0,0 +1,238 @@
1
+ import { cls, defaultLocale } from '@arco-design/mobile-utils';
2
+ import React, {
3
+ useRef,
4
+ forwardRef,
5
+ Ref,
6
+ useImperativeHandle,
7
+ useMemo,
8
+ useCallback,
9
+ useEffect,
10
+ useContext,
11
+ } from 'react';
12
+ import { GlobalContext } from '../context-provider';
13
+ import { IconKeyboard, IconKeyboardDelete } from '../icon';
14
+ import Popup, { PopupRef } from '../popup';
15
+ import { ColumnData, ColumnObjData, KeyboardProps, KeyboardRef } from './type';
16
+
17
+ export * from './type';
18
+
19
+ // 键盘乱序
20
+ // @en let keyboard random
21
+ const makeArrayRandom = (targetArray: Array<number>) => {
22
+ const randomFn = () => {
23
+ return Math.random() - 0.5;
24
+ };
25
+ return targetArray.sort(randomFn);
26
+ };
27
+
28
+ const splitArray = (target: Array<number>) => {
29
+ return target.reduce<number[][]>((acc, cur) => {
30
+ if (!acc[acc.length - 1] || acc[acc.length - 1].length >= 3) {
31
+ acc.push([]);
32
+ }
33
+ acc[acc.length - 1].push(cur);
34
+ return acc;
35
+ }, []);
36
+ };
37
+
38
+ // 纯数字类字符
39
+ // @en Pure numeric characters
40
+ const contentArray: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
41
+
42
+ /**
43
+ * 数字键盘组件
44
+ * @en Keyboard component
45
+ * @type 数据录入
46
+ * @type_en Data Entry
47
+ * @name 数字键盘
48
+ * @name_en Keyboard
49
+ */
50
+ const Keyboard = forwardRef((props: KeyboardProps, ref: Ref<KeyboardRef>) => {
51
+ const {
52
+ normalKeyClass,
53
+ normalKeyStyle,
54
+ className = '',
55
+ style,
56
+ type = 'number',
57
+ randomOrder = false,
58
+ title,
59
+ rightColumns,
60
+ confirmClosable,
61
+ confirmButton,
62
+ deleteButton,
63
+ keyboardButton,
64
+ close,
65
+ onConfirm,
66
+ onDelete,
67
+ onChange,
68
+ ...resetProps
69
+ } = props;
70
+ const popupRef = useRef<PopupRef>(null);
71
+ const keyboardRef = useRef<HTMLDivElement | null>(null);
72
+ const { prefixCls, locale = defaultLocale } = useContext(GlobalContext);
73
+ const prefix = `${prefixCls}-keyboard`;
74
+ // 3x4键盘的按键内容
75
+ // @en 3x4 Keyboard button content
76
+ const displayData = useMemo(() => {
77
+ const numberArr = randomOrder ? makeArrayRandom(contentArray) : contentArray;
78
+ const lastNum = numberArr.slice(-1)[0];
79
+ const finalData: ColumnData[][] = splitArray(numberArr.slice(0, -1));
80
+ switch (type) {
81
+ case 'confirm':
82
+ finalData.push([lastNum, '.']);
83
+ break;
84
+ case 'tool':
85
+ finalData.push(['.', lastNum, { type: 'delete' }]);
86
+ break;
87
+ case 'number':
88
+ default:
89
+ finalData.push([{ type: 'keyboard' }, lastNum, { type: 'delete' }]);
90
+ break;
91
+ }
92
+ return finalData;
93
+ }, [type]);
94
+
95
+ // 右边一列
96
+ // @en Right column
97
+ const rightColumn = useMemo<ColumnData[]>(() => {
98
+ switch (type) {
99
+ case 'number':
100
+ return [];
101
+ case 'confirm': {
102
+ return [{ type: 'delete' }];
103
+ }
104
+ case 'tool':
105
+ return ['+', '-', '×', '÷'];
106
+ default:
107
+ return [];
108
+ }
109
+ }, [type]);
110
+
111
+ // 确认按钮需要在type为confirm时展示
112
+ // @en Display then confirm button when type is confirm
113
+ const getConfirmButton = useCallback(() => {
114
+ if (type !== 'confirm') {
115
+ return null;
116
+ }
117
+ return (
118
+ <div
119
+ onClick={() => {
120
+ confirmClosable && close();
121
+ onConfirm?.();
122
+ }}
123
+ className={cls(`${prefix}-key`, `${prefix}-key-confirm`)}
124
+ >
125
+ {confirmButton || locale.Keyboard.confirm}
126
+ </div>
127
+ );
128
+ }, [type]);
129
+
130
+ // 点击空白处关闭键盘
131
+ // @en Close keyboard when clicked the blank space
132
+ useEffect(() => {
133
+ const handlerDocumentClick = (e: MouseEvent) => {
134
+ if (!keyboardRef.current) {
135
+ return;
136
+ }
137
+ if (
138
+ !keyboardRef.current.contains(e.target as Node) &&
139
+ e.target !== keyboardRef.current
140
+ ) {
141
+ close();
142
+ }
143
+ };
144
+ document.addEventListener('click', handlerDocumentClick);
145
+ return () => {
146
+ document.removeEventListener('click', handlerDocumentClick);
147
+ };
148
+ }, []);
149
+
150
+ useImperativeHandle(ref, () => ({
151
+ keyboard: keyboardRef.current,
152
+ ...popupRef.current!,
153
+ }));
154
+
155
+ const handleButtonClick = (ele: ColumnObjData) => {
156
+ switch (ele.type) {
157
+ case 'delete':
158
+ onDelete?.();
159
+ break;
160
+ case 'keyboard':
161
+ close();
162
+ break;
163
+ default:
164
+ onChange?.(ele.value);
165
+ }
166
+ };
167
+
168
+ const renderButtonContent = (ele: ColumnObjData) => {
169
+ switch (ele.type) {
170
+ case 'delete':
171
+ return deleteButton || <IconKeyboardDelete />;
172
+ case 'keyboard':
173
+ return keyboardButton || <IconKeyboard />;
174
+ default:
175
+ return ele.content;
176
+ }
177
+ };
178
+
179
+ const renderButton = (data: ColumnData, key: string, extraClass?: Record<string, any>) => {
180
+ const ele: ColumnObjData =
181
+ typeof data === 'string' || typeof data === 'number'
182
+ ? {
183
+ type: 'content',
184
+ content: data,
185
+ value: data,
186
+ }
187
+ : data;
188
+ return (
189
+ <button
190
+ onClick={() => handleButtonClick(ele)}
191
+ className={cls(`${prefix}-key`, normalKeyClass, extraClass)}
192
+ key={key}
193
+ style={normalKeyStyle}
194
+ >
195
+ {renderButtonContent(ele)}
196
+ </button>
197
+ );
198
+ };
199
+
200
+ const renderKeyboardRightColumns = () => {
201
+ if (rightColumns) {
202
+ return rightColumns;
203
+ }
204
+ return rightColumn.length > 0 ? (
205
+ <div className={`${prefix}-col`}>
206
+ {rightColumn.map((item, index) => renderButton(item, String(index)))}
207
+ {getConfirmButton()}
208
+ </div>
209
+ ) : null;
210
+ };
211
+
212
+ return (
213
+ <Popup ref={popupRef} maskClass={`${prefix}-popup`} close={close} {...resetProps}>
214
+ <div className={cls(prefix, `${className}`)} style={style} ref={keyboardRef}>
215
+ {title}
216
+ <div className={`${prefix}-wrapper`}>
217
+ <div className={`${prefix}-key-wrapper`}>
218
+ {displayData.map((item, index) => (
219
+ <div className={`${prefix}-row`} key={index}>
220
+ {item.map((e, idx) =>
221
+ renderButton(e, String(idx), {
222
+ [`${prefix}-key-special`]:
223
+ type === 'confirm' &&
224
+ index === displayData.length - 1 &&
225
+ idx === 0,
226
+ }),
227
+ )}
228
+ </div>
229
+ ))}
230
+ </div>
231
+ {renderKeyboardRightColumns()}
232
+ </div>
233
+ </div>
234
+ </Popup>
235
+ );
236
+ });
237
+
238
+ export default Keyboard;
@@ -0,0 +1,48 @@
1
+ ### 反馈
2
+
3
+ # 加载更多 LoadMore
4
+
5
+ 上拉加载组件,支持`scroll`和`click`两种触发加载方式,支持滚动监听。支持受控与不受控两种形式。<br>如果引入组件后发现仅触发了初始的`getData`,请确认是否在`getData`方法内没有调用`callback`移除 loading 状态,且未设置`blockWhenLoading`属性为 false。
6
+
7
+ ======
8
+
9
+ > 属性/Props
10
+
11
+ |参数|描述|类型|默认值|
12
+ |----------|-------------|------|------|
13
+ |style|自定义样式|CSSProperties|-|
14
+ |className|自定义类名|string|-|
15
+ |disabled|是否禁用加载能力|boolean|-|
16
+ |beforeReadyArea|组件加载但尚未启用状态下的内容|ReactNode|null|
17
+ |loadingArea|加载中状态下的内容|ReactNode|"正在努力加载中..."|
18
+ |noMoreArea|无更多数据状态下的内容|ReactNode|"没有更多数据了"|
19
+ |prepareArea|准备加载状态下的内容|ReactNode|"上拉/点击加载更多"|
20
+ |retryArea|加载重试状态下的内容|ReactNode|"加载失败,点击重试"|
21
+ |defaultStatus|组件加载初始状态,传入 "before\-ready" 则先加载组件但不请求数据|"before\-ready" \| "prepare"|"prepare"|
22
+ |status|当前状态,传入则受控|"before\-ready" \| "prepare" \| "loading" \| "nomore" \| "retry"|-|
23
+ |getScrollContainer|待计算滚动容器|() =\> HTMLElement \| Window|() => window|
24
+ |getOffsetNode|当多个 loadmore 在同一页面时,通过传入节点的 offsetHeight \+ offsetTop 代替 scrollHeight|() =\> HTMLElement \| Window|-|
25
+ |trigger|触发loading的时机,当为click时,点击后将触发getData|"scroll" \| "click"|"scroll"|
26
+ |threshold|滚动到离列表底部多远的位置触发getData,触发状态时机为'scroll'时有效|number|200|
27
+ |getData|请求数据方法,可在异步任务结束后根据任务结果调用callback修改loadmore内部状态|(callback: (status: LoadMoreStatus) =\> void) =\> void|-|
28
+ |throttle|节流粒度|number|0|
29
+ |blockWhenLoading|是否在loading状态下不触发getData|boolean|true|
30
+ |getDataAtFirst|刚加载好组件时是否自动先请求一次,trigger=scroll时有效|boolean|true|
31
+ |getDataWhenNoScrollAtFirst|当 getDataAtFirst 值为 false 且数据不满一屏时是否触发一次请求,trigger=scroll时有效|boolean|false|
32
+ |onStatusChange|状态改变时回调|(status: LoadMoreStatus, scene?: string) =\> void|-|
33
+ |onClick|组件被点击时回调|(e: MouseEvent\<HTMLDivElement, MouseEvent\>) =\> void|-|
34
+ |onEndReached|滚动到(底部 \- threshold)距离时触发|() =\> void|-|
35
+
36
+ > 引用/Refs
37
+
38
+ |参数|描述|类型|
39
+ |----------|-------------|------|
40
+ |dom|最外层元素 DOM|HTMLDivElement|
41
+ |changeStatus|手动更改组件状态|(status: LoadMoreStatus, scene?: string) =\> void|
42
+ |getDataWithEndReachCheck|判断是否滚动到底部并手动触发数据获取|() =\> void|
43
+
44
+ > LoadMoreStatus
45
+
46
+ ```
47
+ "before-ready" | "prepare" | "loading" | "nomore" | "retry"
48
+ ```
@@ -0,0 +1,395 @@
1
+ import React, {
2
+ useRef,
3
+ forwardRef,
4
+ Ref,
5
+ useImperativeHandle,
6
+ ReactNode,
7
+ useEffect,
8
+ useCallback,
9
+ useState,
10
+ } from 'react';
11
+ import lodashThrottle from 'lodash.throttle';
12
+ import {
13
+ getScrollContainerAttribute,
14
+ getValidScrollContainer,
15
+ defaultLocale,
16
+ ILocale,
17
+ cls,
18
+ } from '@arco-design/mobile-utils';
19
+ import { ContextLayout } from '../context-provider';
20
+ import { useLatestRef, useUpdateEffect } from '../_helpers';
21
+
22
+ export type LoadMoreStatus = 'before-ready' | 'prepare' | 'loading' | 'nomore' | 'retry';
23
+
24
+ export interface LoadMoreProps {
25
+ /**
26
+ * 自定义样式
27
+ * @en Custom stylesheet
28
+ */
29
+ style?: React.CSSProperties;
30
+ /**
31
+ * 自定义类名
32
+ * @en Custom classname
33
+ */
34
+ className?: string;
35
+ /**
36
+ * 是否禁用加载能力
37
+ * @en Whether to disable the loading capability
38
+ */
39
+ disabled?: boolean;
40
+ /**
41
+ * 组件加载但尚未启用状态下的内容
42
+ * @en Content when the component is loaded but not yet enabled
43
+ * @default null
44
+ */
45
+ beforeReadyArea?: ReactNode;
46
+ /**
47
+ * 加载中状态下的内容
48
+ * @en Content in loading state
49
+ * @default "正在努力加载中..."
50
+ * @default_en "Trying to load..."
51
+ */
52
+ loadingArea?: ReactNode;
53
+ /**
54
+ * 无更多数据状态下的内容
55
+ * @en Content with no more data
56
+ * @default "没有更多数据了"
57
+ * @default_en "No more data"
58
+ */
59
+ noMoreArea?: ReactNode;
60
+ /**
61
+ * 准备加载状态下的内容
62
+ * @en Content in ready-to-load state
63
+ * @default "上拉/点击加载更多"
64
+ * @default_en "Pull up to load more" or "Click to load more"
65
+ */
66
+ prepareArea?: ReactNode;
67
+ /**
68
+ * 加载重试状态下的内容
69
+ * @en Load content in retry state
70
+ * @default "加载失败,点击重试"
71
+ * @default_en "failed to load, click to retry"
72
+ */
73
+ retryArea?: ReactNode;
74
+ /**
75
+ * 组件加载初始状态,传入 "before-ready" 则先加载组件但不请求数据
76
+ * @en The component is loaded in the initial state. Inputing in "before-ready" will load the component first without requesting data
77
+ * @default "prepare"
78
+ */
79
+ defaultStatus?: 'before-ready' | 'prepare';
80
+ /**
81
+ * 当前状态,传入则受控
82
+ * @en Current state, the component is controlledc when it is input
83
+ */
84
+ status?: LoadMoreStatus;
85
+ /**
86
+ * 待计算滚动容器
87
+ * @en Scrolling container to be calculated
88
+ * @default () => window
89
+ */
90
+ getScrollContainer?: () => HTMLElement | Window | null;
91
+ /**
92
+ * 当多个 loadmore 在同一页面时,通过传入节点的 offsetHeight + offsetTop 代替 scrollHeight
93
+ * @en When multiple loadmores are on the same page, pass in the offsetHeight + offsetTop of the node instead of scrollHeight
94
+ */
95
+ getOffsetNode?: () => HTMLElement | Window | null;
96
+ /**
97
+ * 触发loading的时机,当为click时,点击后将触发getData
98
+ * @en The timing of triggering loading, when it is click, getData will be triggered after clicking
99
+ * @default "scroll"
100
+ */
101
+ trigger?: 'scroll' | 'click';
102
+ /**
103
+ * 滚动到离列表底部多远的位置触发getData,触发状态时机为'scroll'时有效
104
+ * @en Scroll to how far from the bottom of the list to trigger getData, valid when the trigger state timing is 'scroll'
105
+ * @default 200
106
+ */
107
+ threshold?: number;
108
+ /**
109
+ * 请求数据方法,可在异步任务结束后根据任务结果调用callback修改loadmore内部状态
110
+ * @en The request data method, after the asynchronous task ends, the callback can be called according to the task result to modify the internal state of loadmore
111
+ */
112
+ getData?: (callback: (status: LoadMoreStatus) => void) => void;
113
+ /**
114
+ * 节流粒度
115
+ * @en Throttle granularity
116
+ * @default 0
117
+ */
118
+ throttle?: number;
119
+ /**
120
+ * 是否在loading状态下不触发getData
121
+ * @en Whether to not trigger getData in the loading state
122
+ * @default true
123
+ */
124
+ blockWhenLoading?: boolean;
125
+ /**
126
+ * 刚加载好组件时是否自动先请求一次,trigger=scroll时有效
127
+ * @en Whether to automatically request once when the component is just loaded, valid when trigger=scroll
128
+ * @default true
129
+ */
130
+ getDataAtFirst?: boolean;
131
+ /**
132
+ * 当 getDataAtFirst 值为 false 且数据不满一屏时是否触发一次请求,trigger=scroll时有效
133
+ * @en Whether to trigger a request when getDataAtFirst equals false and the data is not full of one screen, valid when trigger=scroll
134
+ * @default false
135
+ */
136
+ getDataWhenNoScrollAtFirst?: boolean;
137
+ /**
138
+ * 状态改变时回调
139
+ * @en Callback when state changes
140
+ */
141
+ onStatusChange?: (status: LoadMoreStatus, scene?: string) => void;
142
+ /**
143
+ * 组件被点击时回调
144
+ * @en Callback when the component is clicked
145
+ */
146
+ onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
147
+ /**
148
+ * 滚动到(底部 - threshold)距离时触发
149
+ * @en Callback when scrolling to (bottom - threshold) distance
150
+ */
151
+ onEndReached?: () => void;
152
+ }
153
+
154
+ export interface LoadMoreRef {
155
+ /**
156
+ * 最外层元素 DOM
157
+ * @en The outermost element DOM
158
+ */
159
+ dom: HTMLDivElement | null;
160
+ /**
161
+ * 手动更改组件状态
162
+ * @en Change component state manually
163
+ */
164
+ changeStatus: (status: LoadMoreStatus, scene?: string) => void;
165
+ /**
166
+ * 判断是否滚动到底部并手动触发数据获取
167
+ * @en Determine whether to scroll to the bottom and manually trigger data acquisition
168
+ */
169
+ getDataWithEndReachCheck: () => void;
170
+ }
171
+
172
+ /**
173
+ * 上拉加载组件,支持`scroll`和`click`两种触发加载方式,支持滚动监听。支持受控与不受控两种形式。<br>如果引入组件后发现仅触发了初始的`getData`,请确认是否在`getData`方法内没有调用`callback`移除 loading 状态,且未设置`blockWhenLoading`属性为 false。
174
+ * @en Pull-up loading component, supports `scroll` and `click` two trigger loading methods, supports scroll monitoring. Both controlled and uncontrolled forms are supported. <br>If only the initial `getData` is triggered after the component is introduced, please make sure that the `callback` is not called in the `getData` method to remove the loading state, and the `blockWhenLoading` property is not set to false.
175
+ * @type 反馈
176
+ * @type_en Feedback
177
+ * @name 加载更多
178
+ * @name_en LoadMore
179
+ */
180
+ const LoadMore = forwardRef((props: LoadMoreProps, ref: Ref<LoadMoreRef>) => {
181
+ const {
182
+ className = '',
183
+ style,
184
+ disabled,
185
+ beforeReadyArea,
186
+ loadingArea,
187
+ noMoreArea,
188
+ prepareArea,
189
+ retryArea,
190
+ defaultStatus = 'prepare',
191
+ status,
192
+ getScrollContainer,
193
+ getOffsetNode,
194
+ trigger = 'scroll',
195
+ threshold = 200,
196
+ throttle = 0,
197
+ getDataAtFirst = true,
198
+ getData,
199
+ blockWhenLoading = true,
200
+ onStatusChange,
201
+ onClick,
202
+ onEndReached,
203
+ getDataWhenNoScrollAtFirst = false,
204
+ } = props;
205
+ const domRef = useRef<HTMLDivElement | null>(null);
206
+ const requestAtFirst = trigger === 'scroll' ? getDataAtFirst : false;
207
+ const [innerStatus, setInnerStatus] = useState<LoadMoreStatus>(defaultStatus);
208
+ const lastScrollEndRef = useRef(false);
209
+ const nowStatus = status || innerStatus;
210
+ const statusRef = useRef<LoadMoreStatus>(nowStatus);
211
+ const disabledRef = useLatestRef(disabled);
212
+
213
+ const changeStatus = useCallback(
214
+ (st: LoadMoreStatus, scene?: string) => {
215
+ setInnerStatus(st);
216
+ onStatusChange && onStatusChange(st, scene);
217
+ },
218
+ [onStatusChange],
219
+ );
220
+
221
+ const triggerGetData = useCallback(
222
+ (scene: string) => {
223
+ if (disabledRef.current) {
224
+ return;
225
+ }
226
+ if (blockWhenLoading && statusRef.current === 'loading') {
227
+ return;
228
+ }
229
+ changeStatus('loading', scene);
230
+ getData?.(st => {
231
+ changeStatus(st, 'manual');
232
+ });
233
+ },
234
+ [blockWhenLoading, changeStatus, getData],
235
+ );
236
+
237
+ useEffect(() => {
238
+ if (!blockWhenLoading || nowStatus !== 'loading') {
239
+ lastScrollEndRef.current = false;
240
+ }
241
+ statusRef.current = nowStatus;
242
+ }, [nowStatus]);
243
+
244
+ useUpdateEffect(() => {
245
+ // 当状态被设置为 prepare ,且页面总长度不足一屏时,再触发一次 getData
246
+ // @en When the state is set to prepare and the total page length is less than one screen, trigger getData again
247
+ if (trigger === 'scroll' && nowStatus === 'prepare' && checkNeedTrigger(0, threshold)) {
248
+ triggerGetData('pageEnd');
249
+ }
250
+ }, [nowStatus]);
251
+
252
+ useEffect(() => {
253
+ if (requestAtFirst) {
254
+ if (statusRef.current === 'prepare' && !disabled) {
255
+ triggerGetData('requestAtFirst');
256
+ }
257
+ } else {
258
+ if (
259
+ trigger === 'scroll' &&
260
+ nowStatus === 'prepare' &&
261
+ checkNeedTrigger(0, threshold) &&
262
+ !disabled &&
263
+ getDataWhenNoScrollAtFirst
264
+ ) {
265
+ triggerGetData('pageEnd');
266
+ }
267
+ }
268
+ }, [trigger, disabled]);
269
+
270
+ const handleContainerScroll = useCallback(() => {
271
+ const scrollTop = getScrollContainerAttribute('scrollTop', getScrollContainer);
272
+ if (checkNeedTrigger(scrollTop, threshold + 1)) {
273
+ if (!lastScrollEndRef.current) {
274
+ lastScrollEndRef.current = true;
275
+ onEndReached && onEndReached();
276
+ if (!['nomore', 'retry', 'before-ready'].includes(statusRef.current)) {
277
+ triggerGetData('scrollEnd');
278
+ }
279
+ }
280
+ } else {
281
+ lastScrollEndRef.current = false;
282
+ }
283
+ }, [getScrollContainer, threshold, triggerGetData, onEndReached]);
284
+
285
+ useEffect(() => {
286
+ let binded: HTMLElement | Window | null = null;
287
+ const scrollFunc = throttle
288
+ ? lodashThrottle(handleContainerScroll, throttle)
289
+ : handleContainerScroll;
290
+ if (trigger === 'scroll' && !disabled) {
291
+ const container = getValidScrollContainer(getScrollContainer);
292
+ if (container) {
293
+ container.addEventListener('scroll', scrollFunc);
294
+ binded = container;
295
+ }
296
+ }
297
+ return () => {
298
+ if (binded) {
299
+ binded.removeEventListener('scroll', scrollFunc);
300
+ }
301
+ };
302
+ }, [trigger, disabled, getScrollContainer, handleContainerScroll, throttle]);
303
+
304
+ const getDataWithEndReachCheck = () => {
305
+ if (checkNeedTrigger(0, threshold)) {
306
+ triggerGetData('pageEnd');
307
+ }
308
+ };
309
+
310
+ useImperativeHandle(
311
+ ref,
312
+ () => ({
313
+ dom: domRef.current,
314
+ changeStatus,
315
+ getDataWithEndReachCheck,
316
+ }),
317
+ [changeStatus],
318
+ );
319
+
320
+ function checkNeedTrigger(top: number, ths: number) {
321
+ const scrollHeight = getOffsetNode
322
+ ? getScrollContainerAttribute('offsetHeight', getOffsetNode) +
323
+ getScrollContainerAttribute('offsetTop', getOffsetNode)
324
+ : getScrollContainerAttribute('scrollHeight', getScrollContainer);
325
+ const clientHeight = getScrollContainerAttribute('clientHeight', getScrollContainer);
326
+ return scrollHeight - top - clientHeight <= ths;
327
+ }
328
+
329
+ function handleClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
330
+ if (
331
+ (trigger === 'click' && statusRef.current === 'prepare') ||
332
+ statusRef.current === 'retry'
333
+ ) {
334
+ triggerGetData('click');
335
+ }
336
+ onClick && onClick(e);
337
+ }
338
+
339
+ function renderArea(locale: ILocale) {
340
+ switch (nowStatus) {
341
+ case 'before-ready':
342
+ return beforeReadyArea;
343
+ case 'prepare':
344
+ return prepareArea === void 0 ? (
345
+ <div className="load-more-text prepare">
346
+ {trigger === 'scroll'
347
+ ? locale.LoadMore.prepareScrollText
348
+ : locale.LoadMore.prepareClickText}
349
+ {locale.LoadMore.loadMoreText}
350
+ </div>
351
+ ) : (
352
+ prepareArea
353
+ );
354
+ case 'loading':
355
+ return loadingArea === void 0 ? (
356
+ <div className="load-more-text loading">{locale.LoadMore.loadingText}</div>
357
+ ) : (
358
+ loadingArea
359
+ );
360
+ case 'nomore':
361
+ return noMoreArea === void 0 ? (
362
+ <div className="load-more-text nomore">{locale.LoadMore.noDataText}</div>
363
+ ) : (
364
+ noMoreArea
365
+ );
366
+ case 'retry':
367
+ return retryArea === void 0 ? (
368
+ <div className="load-more-text retry">{locale.LoadMore.failLoadText}</div>
369
+ ) : (
370
+ retryArea
371
+ );
372
+ default:
373
+ return null;
374
+ }
375
+ }
376
+
377
+ return (
378
+ <ContextLayout>
379
+ {({ prefixCls, locale = defaultLocale }) => (
380
+ <div
381
+ className={cls(`${prefixCls}-load-more status-${nowStatus}`, className, {
382
+ [`${prefixCls}-load-more-disabled`]: disabled,
383
+ })}
384
+ ref={domRef}
385
+ style={style}
386
+ onClick={handleClick}
387
+ >
388
+ {renderArea(locale)}
389
+ </div>
390
+ )}
391
+ </ContextLayout>
392
+ );
393
+ });
394
+
395
+ export default LoadMore;