@seakoi/native-ui 1.2.0 → 1.3.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 (178) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commonjs/components/base/index.js +33 -11
  3. package/dist/commonjs/components/base/input/base-input.js +4 -2
  4. package/dist/commonjs/components/base/overflow/all-mode-overflow.js +49 -0
  5. package/dist/commonjs/components/base/overflow/fixed-count-overflow.js +70 -0
  6. package/dist/commonjs/components/base/overflow/index.js +16 -0
  7. package/dist/commonjs/components/base/overflow/overflow.js +72 -0
  8. package/dist/commonjs/components/base/overflow/responsive-overflow.js +280 -0
  9. package/dist/commonjs/components/base/overflow/style/index.js +39 -0
  10. package/dist/commonjs/components/base/overflow/types.js +5 -0
  11. package/dist/commonjs/components/base/picker/picker-content.js +16 -12
  12. package/dist/commonjs/components/base/picker/picker-field.js +9 -7
  13. package/dist/commonjs/components/base/picker/picker.js +6 -1
  14. package/dist/commonjs/components/base/picker/style/index.js +7 -1
  15. package/dist/commonjs/components/base/portal/portal-host.js +5 -3
  16. package/dist/commonjs/components/base/select/hooks/use-select-actions.js +155 -0
  17. package/dist/commonjs/components/base/select/hooks/use-select-options.js +169 -0
  18. package/dist/commonjs/components/base/select/hooks/use-selector.js +104 -0
  19. package/dist/commonjs/components/base/select/index.js +16 -0
  20. package/dist/commonjs/components/base/select/select-multiple-content.js +182 -0
  21. package/dist/commonjs/components/base/select/select-popup.js +233 -0
  22. package/dist/commonjs/components/base/select/select-single-content.js +100 -0
  23. package/dist/commonjs/components/base/select/select-suffix.js +67 -0
  24. package/dist/commonjs/components/base/select/select.js +285 -0
  25. package/dist/commonjs/components/base/select/style/index.js +40 -0
  26. package/dist/commonjs/components/base/select/style/select-multiple-content-styles.js +46 -0
  27. package/dist/commonjs/components/base/select/style/select-popup-styles.js +67 -0
  28. package/dist/commonjs/components/base/select/style/select-single-content-styles.js +28 -0
  29. package/dist/commonjs/components/base/select/style/select-styles.js +46 -0
  30. package/dist/commonjs/components/base/select/style/select-suffix-styles.js +21 -0
  31. package/dist/commonjs/components/base/select/types.js +5 -0
  32. package/dist/commonjs/components/base/tabs/style/index.js +37 -0
  33. package/dist/commonjs/components/base/tabs/tabs.js +90 -45
  34. package/dist/commonjs/native-provider/native-provider.js +5 -5
  35. package/dist/commonjs/shared/utils/index.js +11 -0
  36. package/dist/commonjs/shared/utils/object.js +39 -0
  37. package/dist/module/components/base/index.js +2 -0
  38. package/dist/module/components/base/input/base-input.js +4 -2
  39. package/dist/module/components/base/overflow/all-mode-overflow.js +43 -0
  40. package/dist/module/components/base/overflow/fixed-count-overflow.js +64 -0
  41. package/dist/module/components/base/overflow/index.js +3 -0
  42. package/dist/module/components/base/overflow/overflow.js +66 -0
  43. package/dist/module/components/base/overflow/responsive-overflow.js +274 -0
  44. package/dist/module/components/base/overflow/style/index.js +36 -0
  45. package/dist/module/components/base/overflow/types.js +3 -0
  46. package/dist/module/components/base/picker/picker-content.js +16 -12
  47. package/dist/module/components/base/picker/picker-field.js +9 -7
  48. package/dist/module/components/base/picker/picker.js +6 -1
  49. package/dist/module/components/base/picker/style/index.js +6 -0
  50. package/dist/module/components/base/portal/portal-host.js +4 -3
  51. package/dist/module/components/base/select/hooks/use-select-actions.js +151 -0
  52. package/dist/module/components/base/select/hooks/use-select-options.js +162 -0
  53. package/dist/module/components/base/select/hooks/use-selector.js +100 -0
  54. package/dist/module/components/base/select/index.js +3 -0
  55. package/dist/module/components/base/select/select-multiple-content.js +176 -0
  56. package/dist/module/components/base/select/select-popup.js +227 -0
  57. package/dist/module/components/base/select/select-single-content.js +94 -0
  58. package/dist/module/components/base/select/select-suffix.js +61 -0
  59. package/dist/module/components/base/select/select.js +279 -0
  60. package/dist/module/components/base/select/style/index.js +7 -0
  61. package/dist/module/components/base/select/style/select-multiple-content-styles.js +43 -0
  62. package/dist/module/components/base/select/style/select-popup-styles.js +64 -0
  63. package/dist/module/components/base/select/style/select-single-content-styles.js +25 -0
  64. package/dist/module/components/base/select/style/select-styles.js +43 -0
  65. package/dist/module/components/base/select/style/select-suffix-styles.js +18 -0
  66. package/dist/module/components/base/select/types.js +3 -0
  67. package/dist/module/components/base/tabs/style/index.js +33 -0
  68. package/dist/module/components/base/tabs/tabs.js +92 -47
  69. package/dist/module/native-provider/native-provider.js +5 -5
  70. package/dist/module/shared/utils/index.js +2 -1
  71. package/dist/module/shared/utils/object.js +35 -0
  72. package/dist/typescript/components/base/index.d.ts +2 -0
  73. package/dist/typescript/components/base/index.d.ts.map +1 -1
  74. package/dist/typescript/components/base/input/base-input.d.ts.map +1 -1
  75. package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts +11 -0
  76. package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts.map +1 -0
  77. package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts +11 -0
  78. package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts.map +1 -0
  79. package/dist/typescript/components/base/overflow/index.d.ts +3 -0
  80. package/dist/typescript/components/base/overflow/index.d.ts.map +1 -0
  81. package/dist/typescript/components/base/overflow/overflow.d.ts +13 -0
  82. package/dist/typescript/components/base/overflow/overflow.d.ts.map +1 -0
  83. package/dist/typescript/components/base/overflow/responsive-overflow.d.ts +12 -0
  84. package/dist/typescript/components/base/overflow/responsive-overflow.d.ts.map +1 -0
  85. package/dist/typescript/components/base/overflow/style/index.d.ts +31 -0
  86. package/dist/typescript/components/base/overflow/style/index.d.ts.map +1 -0
  87. package/dist/typescript/components/base/overflow/types.d.ts +77 -0
  88. package/dist/typescript/components/base/overflow/types.d.ts.map +1 -0
  89. package/dist/typescript/components/base/picker/picker-content.d.ts +12 -0
  90. package/dist/typescript/components/base/picker/picker-content.d.ts.map +1 -1
  91. package/dist/typescript/components/base/picker/picker-field.d.ts.map +1 -1
  92. package/dist/typescript/components/base/picker/picker.d.ts +9 -0
  93. package/dist/typescript/components/base/picker/picker.d.ts.map +1 -1
  94. package/dist/typescript/components/base/picker/style/index.d.ts +6 -0
  95. package/dist/typescript/components/base/picker/style/index.d.ts.map +1 -1
  96. package/dist/typescript/components/base/portal/portal-host.d.ts +1 -0
  97. package/dist/typescript/components/base/portal/portal-host.d.ts.map +1 -1
  98. package/dist/typescript/components/base/portal/types.d.ts +2 -0
  99. package/dist/typescript/components/base/portal/types.d.ts.map +1 -1
  100. package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts +144 -0
  101. package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts.map +1 -0
  102. package/dist/typescript/components/base/select/hooks/use-select-options.d.ts +91 -0
  103. package/dist/typescript/components/base/select/hooks/use-select-options.d.ts.map +1 -0
  104. package/dist/typescript/components/base/select/hooks/use-selector.d.ts +90 -0
  105. package/dist/typescript/components/base/select/hooks/use-selector.d.ts.map +1 -0
  106. package/dist/typescript/components/base/select/index.d.ts +3 -0
  107. package/dist/typescript/components/base/select/index.d.ts.map +1 -0
  108. package/dist/typescript/components/base/select/select-multiple-content.d.ts +51 -0
  109. package/dist/typescript/components/base/select/select-multiple-content.d.ts.map +1 -0
  110. package/dist/typescript/components/base/select/select-popup.d.ts +107 -0
  111. package/dist/typescript/components/base/select/select-popup.d.ts.map +1 -0
  112. package/dist/typescript/components/base/select/select-single-content.d.ts +48 -0
  113. package/dist/typescript/components/base/select/select-single-content.d.ts.map +1 -0
  114. package/dist/typescript/components/base/select/select-suffix.d.ts +43 -0
  115. package/dist/typescript/components/base/select/select-suffix.d.ts.map +1 -0
  116. package/dist/typescript/components/base/select/select.d.ts +40 -0
  117. package/dist/typescript/components/base/select/select.d.ts.map +1 -0
  118. package/dist/typescript/components/base/select/style/index.d.ts +6 -0
  119. package/dist/typescript/components/base/select/style/index.d.ts.map +1 -0
  120. package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts +40 -0
  121. package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts.map +1 -0
  122. package/dist/typescript/components/base/select/style/select-popup-styles.d.ts +61 -0
  123. package/dist/typescript/components/base/select/style/select-popup-styles.d.ts.map +1 -0
  124. package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts +22 -0
  125. package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts.map +1 -0
  126. package/dist/typescript/components/base/select/style/select-styles.d.ts +40 -0
  127. package/dist/typescript/components/base/select/style/select-styles.d.ts.map +1 -0
  128. package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts +15 -0
  129. package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts.map +1 -0
  130. package/dist/typescript/components/base/select/types.d.ts +206 -0
  131. package/dist/typescript/components/base/select/types.d.ts.map +1 -0
  132. package/dist/typescript/components/base/tabs/style/index.d.ts +29 -0
  133. package/dist/typescript/components/base/tabs/style/index.d.ts.map +1 -0
  134. package/dist/typescript/components/base/tabs/tabs.d.ts +26 -5
  135. package/dist/typescript/components/base/tabs/tabs.d.ts.map +1 -1
  136. package/dist/typescript/native-provider/native-provider.d.ts +2 -0
  137. package/dist/typescript/native-provider/native-provider.d.ts.map +1 -1
  138. package/dist/typescript/shared/utils/index.d.ts +1 -0
  139. package/dist/typescript/shared/utils/index.d.ts.map +1 -1
  140. package/dist/typescript/shared/utils/object.d.ts +21 -0
  141. package/dist/typescript/shared/utils/object.d.ts.map +1 -0
  142. package/package.json +1 -1
  143. package/src/components/base/index.ts +2 -0
  144. package/src/components/base/input/base-input.tsx +4 -2
  145. package/src/components/base/overflow/all-mode-overflow.tsx +49 -0
  146. package/src/components/base/overflow/fixed-count-overflow.tsx +71 -0
  147. package/src/components/base/overflow/index.ts +2 -0
  148. package/src/components/base/overflow/overflow.tsx +60 -0
  149. package/src/components/base/overflow/responsive-overflow.tsx +349 -0
  150. package/src/components/base/overflow/style/index.ts +32 -0
  151. package/src/components/base/overflow/types.ts +75 -0
  152. package/src/components/base/picker/picker-content.tsx +24 -9
  153. package/src/components/base/picker/picker-field.tsx +19 -13
  154. package/src/components/base/picker/picker.tsx +10 -1
  155. package/src/components/base/picker/style/index.ts +4 -0
  156. package/src/components/base/portal/portal-host.tsx +13 -3
  157. package/src/components/base/portal/types.ts +2 -0
  158. package/src/components/base/select/hooks/use-select-actions.ts +263 -0
  159. package/src/components/base/select/hooks/use-select-options.ts +250 -0
  160. package/src/components/base/select/hooks/use-selector.ts +155 -0
  161. package/src/components/base/select/index.ts +2 -0
  162. package/src/components/base/select/select-multiple-content.tsx +292 -0
  163. package/src/components/base/select/select-popup.tsx +384 -0
  164. package/src/components/base/select/select-single-content.tsx +127 -0
  165. package/src/components/base/select/select-suffix.tsx +100 -0
  166. package/src/components/base/select/select.tsx +302 -0
  167. package/src/components/base/select/style/index.ts +5 -0
  168. package/src/components/base/select/style/select-multiple-content-styles.ts +41 -0
  169. package/src/components/base/select/style/select-popup-styles.ts +62 -0
  170. package/src/components/base/select/style/select-single-content-styles.ts +23 -0
  171. package/src/components/base/select/style/select-styles.ts +41 -0
  172. package/src/components/base/select/style/select-suffix-styles.ts +16 -0
  173. package/src/components/base/select/types.ts +261 -0
  174. package/src/components/base/tabs/style/index.ts +32 -0
  175. package/src/components/base/tabs/tabs.tsx +146 -55
  176. package/src/native-provider/native-provider.tsx +4 -4
  177. package/src/shared/utils/index.ts +1 -0
  178. package/src/shared/utils/object.ts +37 -0
@@ -0,0 +1,261 @@
1
+ import type React from 'react';
2
+ import type {
3
+ NativeScrollEvent,
4
+ NativeSyntheticEvent,
5
+ StyleProp,
6
+ TextStyle,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+
10
+ import type { SelectSearchConfig } from '#components/base/select/hooks/use-select-actions';
11
+
12
+ // ==================== 基础类型定义 ====================
13
+
14
+ /**
15
+ * 选项数据类型
16
+ *
17
+ * @template ValueType - 选项值的类型
18
+ */
19
+ export interface SelectOption<ValueType = unknown> {
20
+ /** 选项标签 */
21
+ label: React.ReactNode;
22
+ /** 选项值 */
23
+ value: ValueType;
24
+ /** 是否禁用 */
25
+ disabled?: boolean;
26
+ /** 选项自定义样式 */
27
+ style?: StyleProp<ViewStyle>;
28
+ /** 自定义数据(用于扩展) */
29
+ [key: PropertyKey]: unknown;
30
+ }
31
+
32
+ /**
33
+ * 选项分组类型
34
+ *
35
+ * @template ValueType - 选项值的类型
36
+ */
37
+ export interface SelectOptionGroup<OptionType extends SelectOption = SelectOption> {
38
+ /** 分组标识 */
39
+ key: string;
40
+ /** 分组名称 */
41
+ label: React.ReactNode;
42
+ /** 分组选项 */
43
+ options: OptionType[];
44
+ /** 分组自定义样式 */
45
+ style?: StyleProp<ViewStyle>;
46
+ }
47
+
48
+ /** 选择模式 */
49
+ export type SelectMode = 'multiple' | 'tags';
50
+
51
+ // ==================== 搜索配置 ====================
52
+
53
+ // ==================== Select 主组件 Props ====================
54
+
55
+ /**
56
+ * Select 组件属性
57
+ *
58
+ * @template ValueType - 选项值的类型
59
+ * @template M - 选择模式
60
+ */
61
+ export interface SelectProps<
62
+ ValueType = unknown,
63
+ OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
64
+ M extends SelectMode | undefined = undefined,
65
+ RealValueType extends M extends SelectMode
66
+ ? ValueType[]
67
+ : ValueType = M extends SelectMode ? ValueType[] : ValueType,
68
+ > extends SelectSearchConfig<OptionType> {
69
+ // ==================== 基础属性 ====================
70
+
71
+ /**
72
+ * 是否允许清除
73
+ * @default false
74
+ */
75
+ allowClear?: boolean | { clearIcon?: React.ReactNode };
76
+
77
+ /**
78
+ * 默认选中的值
79
+ */
80
+ defaultValue?: RealValueType;
81
+
82
+ /**
83
+ * 是否禁用
84
+ * @default false
85
+ */
86
+ disabled?: boolean;
87
+
88
+ /**
89
+ * 自定义当前选中的标签内容渲染
90
+ */
91
+ labelRender?: (props: OptionType, info: { index: number }) => React.ReactNode;
92
+
93
+ /**
94
+ * 选择模式(多选或标签)
95
+ */
96
+ mode?: M;
97
+
98
+ /**
99
+ * 数据化配置选项内容
100
+ * @default []
101
+ */
102
+ options?: (OptionType | SelectOptionGroup<OptionType>)[];
103
+
104
+ /**
105
+ * 占位符文本
106
+ * @default '请选择'
107
+ */
108
+ placeholder?: string;
109
+
110
+ /**
111
+ * 自定义前缀
112
+ */
113
+ prefix?: React.ReactNode;
114
+
115
+ /**
116
+ * 自定义多选框清除图标
117
+ */
118
+ removeIcon?: React.ReactNode;
119
+
120
+ /**
121
+ * 配置搜索功能
122
+ * @default false
123
+ */
124
+ showSearch?: boolean;
125
+
126
+ /**
127
+ * 自定义后缀图标
128
+ */
129
+ suffixIcon?: React.ReactNode | ((open: boolean) => React.ReactNode);
130
+
131
+ /**
132
+ * 当前选中的值
133
+ */
134
+ value?: RealValueType;
135
+
136
+ // ==================== 下拉框配置 ====================
137
+
138
+ /**
139
+ * 是否展开下拉菜单
140
+ */
141
+ open?: boolean;
142
+ /**
143
+ * 是否默认展开下拉菜单
144
+ * @default false
145
+ */
146
+ defaultOpen?: boolean;
147
+ /** 自定义下拉框内容 */
148
+ popupRender?: (menu: React.ReactElement) => React.ReactElement;
149
+ /** 下拉列表滚动时回调 */
150
+ onPopupScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
151
+ /**
152
+ * 下拉菜单和选择器同宽
153
+ */
154
+ popupMatchWidth?: boolean | number;
155
+ /**
156
+ * 下拉菜单位置,不指定时自动计算
157
+ */
158
+ popupPlacement?: 'top' | 'bottom';
159
+ /**
160
+ * 加载中状态
161
+ */
162
+ popupLoading?: boolean;
163
+ /**
164
+ * 下拉列表为空时显示的内容
165
+ */
166
+ popupNotFoundContent?: React.ReactNode;
167
+ /**
168
+ * 自定义选中项图标
169
+ */
170
+ popupSelectedIcon?: React.ReactNode;
171
+ /**
172
+ * 自定义渲染下拉选项
173
+ */
174
+ popupOptionRender?: (
175
+ option: OptionType,
176
+ info: { index: number; selected: boolean },
177
+ ) => React.ReactNode;
178
+ /**
179
+ * 下拉菜单最大高度
180
+ * @default {@link DEFAULT_POPUP_MAX_HEIGHT}
181
+ */
182
+ popupMaxHeight?: number;
183
+
184
+ // ==================== 标签配置 ====================
185
+
186
+ /** 最多可选中的数量 */
187
+ tagMaxCount?: number;
188
+ /** 最大标签数量 */
189
+ maxTagCount?: number | 'responsive' | 'all';
190
+ /** 超出最大数量时的占位符 */
191
+ maxTagPlaceholder?:
192
+ | React.ReactNode
193
+ | ((omittedItems: OptionType[]) => React.ReactNode);
194
+ /** 最大标签文本长度 */
195
+ maxTagTextLength?: number;
196
+ /** 自定义标签渲染 */
197
+ tagRender?: (
198
+ option: OptionType,
199
+ props: {
200
+ disabled?: boolean;
201
+ onClose: () => void;
202
+ closable: boolean;
203
+ },
204
+ ) => React.ReactNode;
205
+
206
+ // ==================== 样式属性 ====================
207
+
208
+ /** 根元素自定义样式 */
209
+ style?: StyleProp<ViewStyle>;
210
+ /** 前缀元素样式 */
211
+ prefixStyle?: StyleProp<ViewStyle>;
212
+ /** 内容容器样式 */
213
+ contentStyle?: StyleProp<ViewStyle>;
214
+ /** 占位符样式 */
215
+ placeholderStyle?: StyleProp<TextStyle>;
216
+ /** 输入框样式 */
217
+ inputStyle?: StyleProp<TextStyle>;
218
+ /** 多选标签项容器样式 */
219
+ tagItemStyle?: StyleProp<ViewStyle>;
220
+ /** 多选标签文本内容样式 */
221
+ tagContentStyle?: StyleProp<TextStyle>;
222
+ /** 多选标签删除按钮样式 */
223
+ tagRemoveStyle?: StyleProp<ViewStyle>;
224
+ /** 多选标签整体容器样式 */
225
+ tagContainerStyle?: StyleProp<ViewStyle>;
226
+ /** 后缀元素样式 */
227
+ suffixStyle?: StyleProp<ViewStyle>;
228
+ /** 清除按钮样式 */
229
+ clearStyle?: StyleProp<ViewStyle>;
230
+ /** 弹出菜单容器样式 */
231
+ popupRootStyle?: StyleProp<ViewStyle>;
232
+ /** 弹出菜单列表样式 */
233
+ popupListStyle?: StyleProp<ViewStyle>;
234
+ /** 弹出菜单条目样式 */
235
+ popupListItemStyle?: StyleProp<ViewStyle>;
236
+ /** 分组标签样式 */
237
+ popupGroupLabelStyle?: StyleProp<TextStyle>;
238
+
239
+ // ==================== 事件回调 ====================
240
+
241
+ /** 键盘和鼠标交互时触发 */
242
+ onActive?: (value: ValueType) => void;
243
+
244
+ /** 选中值变化时回调 */
245
+ onChange?: (value: RealValueType) => void;
246
+
247
+ /** 清除内容时回调 */
248
+ onClear?: () => void;
249
+
250
+ /** 取消选中时回调(仅 multiple/tags 模式) */
251
+ onDeselect?: (value: ValueType) => void;
252
+
253
+ /** 展开下拉菜单的回调 */
254
+ onOpenChange?: (open: boolean) => void;
255
+
256
+ /** 被选中时回调 */
257
+ onSelect?: (value: ValueType, option: OptionType) => void;
258
+
259
+ /** 创建新选项时回调(仅 tags 模式) */
260
+ onCreateOption?: (inputValue: string) => OptionType | void;
261
+ }
@@ -0,0 +1,32 @@
1
+ import { createThemedStyles } from '#native-provider';
2
+
3
+ export const useTabsStyles = createThemedStyles(theme => {
4
+ return {
5
+ container: {
6
+ width: '100%',
7
+ },
8
+ tabItem: {
9
+ paddingVertical: 16,
10
+ paddingHorizontal: 10,
11
+ },
12
+ labelText: {
13
+ color: theme.palette.fontGray2,
14
+ },
15
+ labelTab: {
16
+ paddingHorizontal: 16,
17
+ paddingVertical: 4,
18
+ borderWidth: 1,
19
+ borderColor: theme.palette.blueGray1,
20
+ backgroundColor: theme.palette.blueGray1,
21
+ borderRadius: theme.roundness.md,
22
+ gap: 4,
23
+ },
24
+ labelTabActive: {
25
+ backgroundColor: theme.palette.brand1,
26
+ borderColor: theme.palette.brand7,
27
+ },
28
+ labelTextActive: {
29
+ color: theme.palette.brand7,
30
+ },
31
+ };
32
+ });
@@ -1,23 +1,63 @@
1
+ import { useControllableValue, useMemoizedFn } from 'ahooks';
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
3
  import {
3
4
  Animated,
4
5
  type LayoutChangeEvent,
5
6
  type LayoutRectangle,
6
7
  ScrollView,
8
+ type StyleProp,
7
9
  TouchableOpacity,
8
10
  View,
11
+ type ViewStyle,
9
12
  } from 'react-native';
10
13
 
11
- import { Text } from '#components/base';
12
- import { createThemedStyles, useTheme } from '#native-provider';
13
- interface TabsProps {
14
- tabs: string[];
15
- activeTab?: string | number;
16
- onChange?: (tab: string | number) => void;
14
+ import { Badge, Text } from '#components/base';
15
+ import { useTheme } from '#native-provider';
16
+
17
+ import { useTabsStyles } from './style';
18
+ export interface TabItem {
19
+ /** 标签文本 */
20
+ label: React.ReactNode;
21
+ /** 标签值,唯一标识 */
22
+ value: string | number;
23
+ /** 徽标内容(数字或红点) */
24
+ badge?: number | boolean;
25
+ }
26
+
27
+ export interface TabsProps {
28
+ /** 标签数据 */
29
+ tabs: TabItem[];
30
+ /** 当前激活的标签值(受控) */
31
+ value?: string | number;
32
+ /** 默认激活的标签值(非受控) */
33
+ defaultValue?: string | number;
34
+ /** 标签切换回调 */
35
+ onChange?: (value: string | number) => void;
36
+ /** 样式类型 */
37
+ variant?: 'underline' | 'label';
38
+ /** 容器样式 */
39
+ style?: StyleProp<ViewStyle>;
40
+ /** 右侧操作区域 */
41
+ extra?: React.ReactNode;
42
+ /** 是否均分宽度(仅 underline 和 isometric 支持) */
43
+ equalWidth?: boolean;
17
44
  }
18
45
 
19
- export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
20
- const styles = useStyles();
46
+ export const Tabs: React.FC<TabsProps> = ({
47
+ tabs,
48
+ equalWidth = false,
49
+ variant = 'underline',
50
+ ...restProps
51
+ }) => {
52
+ const [activeValue, setActiveValue] = useControllableValue<TabItem['value']>(
53
+ restProps,
54
+ {
55
+ trigger: 'onChange',
56
+ defaultValue: restProps.defaultValue ?? tabs[0]?.value,
57
+ },
58
+ );
59
+
60
+ const styles = useTabsStyles();
21
61
  const theme = useTheme();
22
62
 
23
63
  const tabNum = tabs.length;
@@ -57,34 +97,27 @@ export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
57
97
  initIndicator();
58
98
  };
59
99
 
60
- const normalizedTabs = tabs.map(tab => {
61
- if (typeof tab === 'string') {
62
- return {
63
- label: tab,
64
- value: tab,
65
- };
66
- }
67
- });
68
-
69
100
  const navigateTo = index => {
70
101
  const targetLayout = layouts.current[index];
71
102
 
72
103
  const left =
73
104
  targetLayout.tab.x + (targetLayout.tab.width - targetLayout.text.width) / 2;
74
- const width = targetLayout.text.width;
75
-
76
- Animated.parallel([
77
- Animated.timing(animatedIndicatorLeft.current, {
78
- toValue: left,
79
- useNativeDriver: false,
80
- duration: 300,
81
- }),
82
- Animated.timing(animatedIndicatorWidth.current, {
83
- toValue: width,
84
- useNativeDriver: false,
85
- duration: 300,
86
- }),
87
- ]).start();
105
+
106
+ if (variant === 'underline') {
107
+ const width = targetLayout.text.width;
108
+ Animated.parallel([
109
+ Animated.timing(animatedIndicatorLeft.current, {
110
+ toValue: left,
111
+ useNativeDriver: false,
112
+ duration: 300,
113
+ }),
114
+ Animated.timing(animatedIndicatorWidth.current, {
115
+ toValue: width,
116
+ useNativeDriver: false,
117
+ duration: 300,
118
+ }),
119
+ ]).start();
120
+ }
88
121
 
89
122
  const hh = scrollViewWidthRef.current / 2;
90
123
  scrollViewRef.current?.scrollTo({
@@ -93,12 +126,12 @@ export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
93
126
  });
94
127
  };
95
128
 
96
- const genOnPress = (v, index) => {
97
- onChange?.(v);
129
+ const genOnPress = (v: TabItem, index: number) => {
130
+ setActiveValue?.(v.value);
98
131
  navigateTo(index);
99
132
  };
100
133
 
101
- const initIndicator = useCallback(() => {
134
+ const initIndicator = useMemoizedFn(() => {
102
135
  const layoutItems = layouts.current.filter(item => item.tab && item.text);
103
136
  if (layoutItems.length === layouts.current.length) {
104
137
  setState(s => ({
@@ -106,7 +139,7 @@ export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
106
139
  layoutFinish: true,
107
140
  }));
108
141
  }
109
- }, []);
142
+ });
110
143
 
111
144
  const indicatorJSX = useMemo(() => {
112
145
  return (
@@ -123,24 +156,90 @@ export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
123
156
  );
124
157
  }, [theme]);
125
158
 
126
- const tabsJSX = normalizedTabs.map((item, index) => {
159
+ const renderTab = useMemoizedFn((tab: TabItem, index: number) => {
160
+ const isActive = activeValue === tab.value;
161
+ const badgeCount =
162
+ tab.badge !== undefined
163
+ ? typeof tab.badge === 'number'
164
+ ? tab.badge
165
+ : undefined
166
+ : undefined;
167
+
168
+ if (variant === 'label') {
169
+ return (
170
+ <View
171
+ key={tab.value}
172
+ onLayout={genOnLayoutTab(index)}
173
+ style={[
174
+ { paddingVertical: 10, zIndex: tabNum - index },
175
+ equalWidth && { flex: 1 },
176
+ ]}
177
+ >
178
+ <Badge count={badgeCount} visible={tab.badge !== undefined} size="small">
179
+ <TouchableOpacity
180
+ activeOpacity={0.7}
181
+ onPress={() => genOnPress(tab, index)}
182
+ style={[
183
+ styles.labelTab,
184
+ isActive && styles.labelTabActive,
185
+ equalWidth && {
186
+ alignSelf: 'stretch',
187
+ justifyContent: 'center',
188
+ alignItems: 'center',
189
+ },
190
+ ]}
191
+ >
192
+ <Text
193
+ onLayout={genOnLayoutText(index)}
194
+ style={[styles.labelText, isActive && styles.labelTextActive]}
195
+ size={14}
196
+ >
197
+ {tab.label}
198
+ </Text>
199
+ </TouchableOpacity>
200
+ </Badge>
201
+ </View>
202
+ );
203
+ }
204
+
127
205
  return (
128
206
  <TouchableOpacity
129
- key={item?.value}
130
- style={styles.tabItem}
131
- onPress={() => genOnPress(item, index)}
207
+ key={tab?.value}
208
+ style={[
209
+ styles.tabItem,
210
+ { zIndex: tabNum - index },
211
+ equalWidth && { flex: 1, justifyContent: 'center', alignItems: 'center' },
212
+ ]}
213
+ onPress={() => genOnPress(tab, index)}
132
214
  onLayout={genOnLayoutTab(index)}
133
215
  >
134
- <Text onLayout={genOnLayoutText(index)}>{item?.label}</Text>
216
+ {/* underline 类型:Badge 作为 Text 的包裹层,自动定位在文字右上角 */}
217
+ <Badge
218
+ count={badgeCount}
219
+ visible={tab.badge !== undefined}
220
+ size="small"
221
+ offset={[-4, -4]}
222
+ >
223
+ <Text
224
+ color={isActive ? theme.palette.fontGray1 : theme.palette.fontGray2}
225
+ font={isActive ? 'semiBold' : 'regular'}
226
+ onLayout={genOnLayoutText(index)}
227
+ size={14}
228
+ >
229
+ {tab?.label}
230
+ </Text>
231
+ </Badge>
135
232
  </TouchableOpacity>
136
233
  );
137
234
  });
138
235
 
139
236
  useEffect(() => {
140
237
  if (state.layoutFinish) {
141
- navigateTo(0);
238
+ const index = tabs.findIndex(item => item.value === activeValue);
239
+ navigateTo(index === -1 ? 0 : index);
142
240
  }
143
- }, [state.layoutFinish]);
241
+ // eslint-disable-next-line react-hooks/exhaustive-deps
242
+ }, [state.layoutFinish, activeValue]);
144
243
 
145
244
  return (
146
245
  <View style={styles.container}>
@@ -149,25 +248,17 @@ export const Tabs: React.FC<TabsProps> = ({ tabs, onChange }) => {
149
248
  horizontal
150
249
  showsHorizontalScrollIndicator={false}
151
250
  onLayout={onLayoutScrollView}
251
+ scrollEnabled={!equalWidth}
152
252
  contentContainerStyle={{
153
253
  alignContent: 'center',
154
- columnGap: 16,
254
+ columnGap: 8,
255
+ flexGrow: equalWidth ? 1 : 0,
256
+ paddingHorizontal: 16,
155
257
  }}
156
258
  >
157
- {tabsJSX}
259
+ {tabs.map(renderTab)}
158
260
  {indicatorJSX}
159
261
  </ScrollView>
160
262
  </View>
161
263
  );
162
264
  };
163
-
164
- const useStyles = createThemedStyles(() => {
165
- return {
166
- container: {
167
- paddingHorizontal: 16,
168
- },
169
- tabItem: {
170
- paddingVertical: 16,
171
- },
172
- };
173
- });
@@ -1,7 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
- import { Platform } from 'react-native';
3
2
 
4
- import { Portal } from '#components/base';
3
+ import { Portal, type PortalHostProps } from '#components/base';
5
4
  import { createTheme, type ThemeConfig } from '#shared/theme';
6
5
 
7
6
  import {
@@ -25,6 +24,7 @@ export interface NativeProviderProps {
25
24
  errorBoundary?: Omit<ErrorBoundaryProps, 'children'>;
26
25
  /** 导航配置 */
27
26
  navigators?: ConfigContextValue['navigators'];
27
+ portalHostProps: PortalHostProps;
28
28
  }
29
29
 
30
30
  export const NativeProvider: React.FC<NativeProviderProps> = ({
@@ -33,8 +33,8 @@ export const NativeProvider: React.FC<NativeProviderProps> = ({
33
33
  navigators,
34
34
  errorBoundary,
35
35
  request = null,
36
+ portalHostProps,
36
37
  }) => {
37
- const isNative = Platform.OS !== 'web';
38
38
  const themeContextValue = useMemo(() => createTheme(theme), [theme]);
39
39
 
40
40
  return (
@@ -43,7 +43,7 @@ export const NativeProvider: React.FC<NativeProviderProps> = ({
43
43
  <RequestContext.Provider value={request}>
44
44
  <ThemeContext.Provider value={themeContextValue}>
45
45
  <ErrorBoundary {...errorBoundary}>
46
- {isNative ? <Portal.Host>{children}</Portal.Host> : children}
46
+ <Portal.Host {...portalHostProps}>{children}</Portal.Host>
47
47
  </ErrorBoundary>
48
48
  </ThemeContext.Provider>
49
49
  </RequestContext.Provider>
@@ -1,3 +1,4 @@
1
1
  export * from './element';
2
2
  export * from './error';
3
+ export * from './object';
3
4
  export type * from './types';
@@ -0,0 +1,37 @@
1
+ import { isNil } from 'lodash-es';
2
+
3
+ /**
4
+ * 浅比较两个对象是否相等
5
+ *
6
+ * 仅比较对象的第一层属性,使用严格相等(===)进行比较。
7
+ * 不会递归比较嵌套对象或数组的内容。
8
+ *
9
+ * @param a - 第一个对象
10
+ * @param b - 第二个对象
11
+ * @returns 如果两个对象的所有第一层属性都相等则返回 true,否则返回 false。如果 a/b 有一个非对象,则直接比较二者
12
+ *
13
+ * @example
14
+ * shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
15
+ * shallowEqual({ a: 1 }, { a: 1, b: 2 }); // false
16
+ * shallowEqual({ a: { x: 1 } }, { a: { x: 1 } }); // false
17
+ * shallowEqual(null, null); // true
18
+ * shallowEqual(null, undefined); // false
19
+ * shallowEqual(1, 1); // true
20
+ * shallowEqual(1, '1'); // false
21
+ */
22
+ export const shallowEqual = (a?: object, b?: object): boolean => {
23
+ if (isNil(a) || isNil(b)) {
24
+ return a === b;
25
+ }
26
+
27
+ if (typeof a !== 'object' || typeof b !== 'object') {
28
+ return a === b;
29
+ }
30
+
31
+ const aKeys = Object.keys(a);
32
+ const bKeys = Object.keys(b);
33
+
34
+ if (aKeys.length !== bKeys.length) return false;
35
+
36
+ return aKeys.every(key => a[key] === b[key]);
37
+ };