@bm-fe/react-native-ui-components 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/package.json +66 -0
- package/src/components/NativeDesign/BMTextButton.tsx +154 -0
- package/src/components/NativeDesign/Button.tsx +233 -0
- package/src/components/NativeDesign/ButtonStyles.ts +61 -0
- package/src/components/NativeDesign/TextButton.tsx +111 -0
- package/src/components/NativeDesign/index.ts +14 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/screens/NativeButtonsScreen.tsx +335 -0
- package/src/types/global.d.ts +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 @bm-fe/react-native-multi-bundle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# @bm-fe/react-native-multi-bundle
|
|
2
|
+
|
|
3
|
+
React Native 多 Bundle 系统 - 支持模块按需加载和独立更新
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- ✅ **模块按需加载**:支持懒加载,减少初始包体积
|
|
8
|
+
- ✅ **模块依赖管理**:自动处理模块间的依赖关系
|
|
9
|
+
- ✅ **模块状态管理**:完整的生命周期管理(idle/loading/loaded/failed)
|
|
10
|
+
- ✅ **路由加载器**:`createModuleRouteLoader`,完美支持 React Navigation `getComponent` API
|
|
11
|
+
- ✅ **错误处理与重试**:自动错误处理,支持重试机制和自定义错误组件
|
|
12
|
+
- ✅ **预加载支持**:`preloadModule` 支持关键模块预加载
|
|
13
|
+
- ✅ **开发环境 Mock**:开发环境无需 Native 模块即可运行
|
|
14
|
+
- ✅ **TypeScript 支持**:完整的类型定义
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @bm-fe/react-native-multi-bundle
|
|
20
|
+
# 或
|
|
21
|
+
yarn add @bm-fe/react-native-multi-bundle
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
### 1. 初始化多 Bundle 系统
|
|
27
|
+
|
|
28
|
+
在应用入口(通常是 `App.tsx`)中调用 `initMultiBundle`:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { initMultiBundle, LocalBundleManager } from '@bm-fe/react-native-multi-bundle';
|
|
32
|
+
import { Platform } from 'react-native';
|
|
33
|
+
|
|
34
|
+
function App() {
|
|
35
|
+
const [ready, setReady] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
async function bootstrap() {
|
|
39
|
+
const result = await initMultiBundle({
|
|
40
|
+
modulePaths: ['src/modules/**'],
|
|
41
|
+
sharedDependencies: [
|
|
42
|
+
'node_modules/@bm-fe/react-native-multi-bundle/**',
|
|
43
|
+
'src/navigation/**'
|
|
44
|
+
],
|
|
45
|
+
manifestProvider: async () => {
|
|
46
|
+
return await LocalBundleManager.getCurrentBundleManifest();
|
|
47
|
+
},
|
|
48
|
+
preloadModules: ['settings'],
|
|
49
|
+
devServer: {
|
|
50
|
+
host: Platform.OS === 'android' ? '10.0.2.2' : 'localhost',
|
|
51
|
+
port: 8081,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (result.success) {
|
|
56
|
+
setReady(true);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
bootstrap();
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
if (!ready) {
|
|
64
|
+
return <LoadingScreen />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ... 其余代码
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. 使用模块路由
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createModuleRouteLoader } from '@bm-fe/react-native-multi-bundle';
|
|
75
|
+
|
|
76
|
+
// 创建路由加载器
|
|
77
|
+
export const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
78
|
+
|
|
79
|
+
// 在 React Navigation 中使用
|
|
80
|
+
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. 自定义错误处理(可选)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { createModuleLoader, type ErrorFallbackProps } from '@bm-fe/react-native-multi-bundle';
|
|
87
|
+
|
|
88
|
+
// 自定义错误组件
|
|
89
|
+
function MyErrorFallback({ moduleId, error, onRetry }: ErrorFallbackProps) {
|
|
90
|
+
return (
|
|
91
|
+
<View>
|
|
92
|
+
<Text>模块 {moduleId} 加载失败</Text>
|
|
93
|
+
<Button title="重试" onPress={onRetry} />
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 使用自定义错误组件
|
|
99
|
+
const createHomeScreen = createModuleLoader(
|
|
100
|
+
'home',
|
|
101
|
+
(exports) => exports.routes.Home,
|
|
102
|
+
{
|
|
103
|
+
ErrorFallback: MyErrorFallback,
|
|
104
|
+
onError: (error) => console.error('加载失败:', error),
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 完整集成指南
|
|
110
|
+
|
|
111
|
+
详细的集成步骤请参考:[INTEGRATION.md](./INTEGRATION.md)
|
|
112
|
+
|
|
113
|
+
包括:
|
|
114
|
+
- Metro 配置
|
|
115
|
+
- Native 模块集成(Android/iOS)
|
|
116
|
+
- 模块配置
|
|
117
|
+
- 构建多 Bundle
|
|
118
|
+
|
|
119
|
+
## API 文档
|
|
120
|
+
|
|
121
|
+
### initMultiBundle
|
|
122
|
+
|
|
123
|
+
初始化多 Bundle 系统。
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
function initMultiBundle(config?: MultiBundleConfig): Promise<InitResult>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### createModuleRouteLoader
|
|
130
|
+
|
|
131
|
+
创建模块路由加载器函数(推荐,用于 React Navigation `getComponent` API)。
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
function createModuleRouteLoader(
|
|
135
|
+
moduleId: string,
|
|
136
|
+
routeKey: string
|
|
137
|
+
): () => React.ComponentType<any>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### preloadModule
|
|
141
|
+
|
|
142
|
+
预加载模块。
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
function preloadModule(moduleId: string): Promise<void>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
更多 API 文档请参考:[src/multi-bundle/README.md](./src/multi-bundle/README.md)
|
|
149
|
+
|
|
150
|
+
## 示例应用
|
|
151
|
+
|
|
152
|
+
查看完整示例:[examples/demo-app](./examples/demo-app/README.md)
|
|
153
|
+
|
|
154
|
+
## 版本管理
|
|
155
|
+
|
|
156
|
+
本项目使用 [standard-version](https://github.com/conventional-changelog/standard-version) 进行版本管理,遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
|
157
|
+
|
|
158
|
+
### 版本命令
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# 自动判断版本类型
|
|
162
|
+
npm run version
|
|
163
|
+
|
|
164
|
+
# 指定版本类型
|
|
165
|
+
npm run version:patch # 1.0.0 -> 1.0.1
|
|
166
|
+
npm run version:minor # 1.0.0 -> 1.1.0
|
|
167
|
+
npm run version:major # 1.0.0 -> 2.0.0
|
|
168
|
+
npm run version:beta # 1.0.0 -> 1.0.1-beta.0
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 发布流程
|
|
172
|
+
|
|
173
|
+
### 发布前检查
|
|
174
|
+
|
|
175
|
+
1. **确保代码已提交**
|
|
176
|
+
```bash
|
|
177
|
+
git status
|
|
178
|
+
git add .
|
|
179
|
+
git commit -m "chore: prepare release"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
2. **运行测试**
|
|
183
|
+
```bash
|
|
184
|
+
npm test
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
3. **检查打包内容**
|
|
188
|
+
```bash
|
|
189
|
+
npm pack --dry-run
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 发布 Beta 版本
|
|
193
|
+
|
|
194
|
+
Beta 版本用于测试新功能,不会影响 `latest` 标签。
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# 方式 1:使用便捷脚本(推荐)
|
|
198
|
+
npm run release:beta
|
|
199
|
+
|
|
200
|
+
# 方式 2:分步执行
|
|
201
|
+
npm run version:beta # 更新版本号为 beta(如:1.0.0 -> 1.0.1-beta.0)
|
|
202
|
+
npm run publish:beta # 发布到 beta 标签
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**安装 Beta 版本:**
|
|
206
|
+
```bash
|
|
207
|
+
npm install @bm-fe/react-native-multi-bundle@beta
|
|
208
|
+
# 或安装特定 beta 版本
|
|
209
|
+
npm install @bm-fe/react-native-multi-bundle@1.0.1-beta.0
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 发布正式版本
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# 方式 1:使用便捷脚本(推荐)
|
|
216
|
+
npm run release
|
|
217
|
+
|
|
218
|
+
# 方式 2:分步执行
|
|
219
|
+
npm run version # 自动判断版本类型并更新 CHANGELOG
|
|
220
|
+
npm publish # 发布到 latest 标签
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**发布流程说明:**
|
|
224
|
+
1. `npm run version` 会根据提交信息自动判断版本类型(patch/minor/major)
|
|
225
|
+
2. 自动更新 `CHANGELOG.md`
|
|
226
|
+
3. 自动创建 git tag
|
|
227
|
+
4. `npm publish` 发布到 npm(默认 `latest` 标签)
|
|
228
|
+
|
|
229
|
+
### 版本标签说明
|
|
230
|
+
|
|
231
|
+
- **latest**:稳定版本,用户通过 `npm install @bm-fe/react-native-multi-bundle` 安装
|
|
232
|
+
- **beta**:测试版本,需要显式指定 `@beta` 标签安装
|
|
233
|
+
|
|
234
|
+
### 从 Beta 升级到正式版
|
|
235
|
+
|
|
236
|
+
当 beta 版本稳定后,发布正式版本:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# 1. 更新版本号为正式版本(移除 beta 后缀)
|
|
240
|
+
npm version 1.0.1 --no-git-tag-version
|
|
241
|
+
|
|
242
|
+
# 2. 发布到 latest
|
|
243
|
+
npm publish
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 开发
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# 安装依赖
|
|
250
|
+
npm install
|
|
251
|
+
|
|
252
|
+
# 运行测试
|
|
253
|
+
npm test
|
|
254
|
+
|
|
255
|
+
# 运行示例应用
|
|
256
|
+
cd examples/demo-app
|
|
257
|
+
npm install
|
|
258
|
+
npm start
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 许可证
|
|
262
|
+
|
|
263
|
+
MIT
|
|
264
|
+
|
|
265
|
+
## 贡献
|
|
266
|
+
|
|
267
|
+
欢迎提交 Issue 和 Pull Request!
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bm-fe/react-native-ui-components",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "React Native UI Components Library",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/components",
|
|
10
|
+
"src/screens",
|
|
11
|
+
"src/types",
|
|
12
|
+
"src/index.ts",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"android": "react-native run-android",
|
|
18
|
+
"ios": "react-native run-ios",
|
|
19
|
+
"start": "react-native start",
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"version": "standard-version",
|
|
23
|
+
"version:patch": "standard-version --release-as patch",
|
|
24
|
+
"version:minor": "standard-version --release-as minor",
|
|
25
|
+
"version:major": "standard-version --release-as major",
|
|
26
|
+
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version --no-scripts",
|
|
27
|
+
"publish:beta": "npm publish --tag beta",
|
|
28
|
+
"release:beta": "npm run version:beta && npm run publish:beta",
|
|
29
|
+
"release": "npm run version && npm publish"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18.0.0",
|
|
33
|
+
"react-native": ">=0.70.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@babel/core": "7.25.2",
|
|
37
|
+
"@babel/preset-env": "7.25.2",
|
|
38
|
+
"@babel/runtime": "7.25.0",
|
|
39
|
+
"@react-native-community/cli": "18.0.0",
|
|
40
|
+
"@react-native-community/cli-platform-android": "18.0.0",
|
|
41
|
+
"@react-native-community/cli-platform-ios": "18.0.0",
|
|
42
|
+
"@react-native/babel-preset": "0.79.0",
|
|
43
|
+
"@react-native/eslint-config": "0.79.0",
|
|
44
|
+
"@react-native/gradle-plugin": "0.79.0",
|
|
45
|
+
"@react-native/metro-config": "0.79.0",
|
|
46
|
+
"@react-native/typescript-config": "0.79.0",
|
|
47
|
+
"@testing-library/react-native": "^13.3.3",
|
|
48
|
+
"@types/jest": "29.5.13",
|
|
49
|
+
"@types/react": "19.0.0",
|
|
50
|
+
"@types/react-test-renderer": "19.0.0",
|
|
51
|
+
"eslint": "8.22.0",
|
|
52
|
+
"jest": "29.7.0",
|
|
53
|
+
"prettier": "3.2.5",
|
|
54
|
+
"react": "19.0.0",
|
|
55
|
+
"react-native": "0.79.5",
|
|
56
|
+
"react-test-renderer": "19.0.0",
|
|
57
|
+
"standard-version": "^9.5.0",
|
|
58
|
+
"typescript": "5.5.4"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0 <22.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleProp,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
requireNativeComponent,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BMTextButton 样式类型
|
|
15
|
+
* - black: Widget.BitMart4.Button.TextButton.Black
|
|
16
|
+
* - gray: Widget.BitMart4.Button.TextButton.Gray
|
|
17
|
+
* - blue: Widget.BitMart4.Button.TextButton.Blue
|
|
18
|
+
*/
|
|
19
|
+
export type BMTextButtonStyleType = 'black' | 'gray' | 'blue';
|
|
20
|
+
|
|
21
|
+
interface BMTextButtonProps {
|
|
22
|
+
/** 按钮文本 */
|
|
23
|
+
text: string;
|
|
24
|
+
/** 样式类型 */
|
|
25
|
+
styleType?: BMTextButtonStyleType;
|
|
26
|
+
/** 是否启用 */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** 点击回调 */
|
|
29
|
+
onPress?: () => void;
|
|
30
|
+
/** 样式 */
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NativeBMTextButtonProps {
|
|
35
|
+
text: string;
|
|
36
|
+
styleType: string;
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
onPress?: () => void;
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Android Native 组件
|
|
43
|
+
const NativeBMTextButton =
|
|
44
|
+
Platform.OS === 'android'
|
|
45
|
+
? requireNativeComponent<NativeBMTextButtonProps>('BMTextButton')
|
|
46
|
+
: null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* BMTextButton - 使用 BitMart Design 组件库样式的文本按钮
|
|
50
|
+
*
|
|
51
|
+
* 在 Android 上使用 Native 组件渲染,在 iOS 上使用 RN 组件模拟
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <BMTextButton
|
|
56
|
+
* text="Button Text Link"
|
|
57
|
+
* styleType="black"
|
|
58
|
+
* onPress={() => console.log('pressed')}
|
|
59
|
+
* />
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const BMTextButton: React.FC<BMTextButtonProps> = ({
|
|
63
|
+
text,
|
|
64
|
+
styleType = 'black',
|
|
65
|
+
enabled = true,
|
|
66
|
+
onPress,
|
|
67
|
+
style,
|
|
68
|
+
}) => {
|
|
69
|
+
const handlePress = useCallback(() => {
|
|
70
|
+
if (enabled && onPress) {
|
|
71
|
+
onPress();
|
|
72
|
+
}
|
|
73
|
+
}, [enabled, onPress]);
|
|
74
|
+
|
|
75
|
+
// DEBUG: 打印调试信息
|
|
76
|
+
console.log(
|
|
77
|
+
`🔴 BMTextButton render - Platform: ${Platform.OS}, NativeBMTextButton exists: ${!!NativeBMTextButton}, text: ${text}`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Android: 使用 Native 组件
|
|
81
|
+
if (Platform.OS === 'android' && NativeBMTextButton) {
|
|
82
|
+
console.log('🔴 Rendering Native BMTextButton with red container');
|
|
83
|
+
return (
|
|
84
|
+
<View style={[styles.container, style]}>
|
|
85
|
+
<NativeBMTextButton
|
|
86
|
+
text={text}
|
|
87
|
+
styleType={styleType}
|
|
88
|
+
enabled={enabled}
|
|
89
|
+
onPress={handlePress}
|
|
90
|
+
style={styles.nativeButton}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// iOS: 使用 RN 组件模拟(后续可以替换为 iOS Native 组件)
|
|
97
|
+
const textColor = getTextColorForStyleType(styleType, enabled);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<TouchableOpacity
|
|
101
|
+
onPress={handlePress}
|
|
102
|
+
disabled={!enabled}
|
|
103
|
+
style={[styles.container, style]}
|
|
104
|
+
activeOpacity={0.7}
|
|
105
|
+
>
|
|
106
|
+
<Text style={[styles.text, { color: textColor }]}>{text}</Text>
|
|
107
|
+
</TouchableOpacity>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 根据样式类型获取文本颜色
|
|
113
|
+
*/
|
|
114
|
+
function getTextColorForStyleType(
|
|
115
|
+
styleType: BMTextButtonStyleType,
|
|
116
|
+
enabled: boolean,
|
|
117
|
+
): string {
|
|
118
|
+
if (!enabled) {
|
|
119
|
+
return '#CCCCCC';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch (styleType) {
|
|
123
|
+
case 'black':
|
|
124
|
+
return '#151515';
|
|
125
|
+
case 'gray':
|
|
126
|
+
return '#666666';
|
|
127
|
+
case 'blue':
|
|
128
|
+
return '#2196F3';
|
|
129
|
+
default:
|
|
130
|
+
return '#151515';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const styles = StyleSheet.create({
|
|
135
|
+
container: {
|
|
136
|
+
alignSelf: 'flex-start',
|
|
137
|
+
},
|
|
138
|
+
// DEBUG: 红色背景 - RN 容器层
|
|
139
|
+
debugContainerRed: {
|
|
140
|
+
backgroundColor: 'red',
|
|
141
|
+
padding: 8,
|
|
142
|
+
},
|
|
143
|
+
nativeButton: {
|
|
144
|
+
minHeight: 48,
|
|
145
|
+
minWidth: 150,
|
|
146
|
+
},
|
|
147
|
+
text: {
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
fontWeight: '500',
|
|
150
|
+
color: '#151515',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export default BMTextButton;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleProp,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
View,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
requireNativeComponent,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 支持的按钮样式类型
|
|
13
|
+
*/
|
|
14
|
+
export type ButtonType = 'Primary' | 'Secondary' | 'Green' | 'Red' | 'White';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 支持的按钮尺寸
|
|
18
|
+
*/
|
|
19
|
+
export type ButtonSize = 'XLarge' | 'Large' | 'Medium' | 'Small' | 'XSmall' | 'XXSmall';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 按钮样式名称(格式:Type.Size)
|
|
23
|
+
*/
|
|
24
|
+
export type ButtonStyleName = `${ButtonType}.${ButtonSize}`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 图标位置(内部使用)
|
|
28
|
+
*/
|
|
29
|
+
export type IconPosition = 'left' | 'right';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 按钮尺寸映射表(对应 Android 样式中的 android:layout_height)
|
|
33
|
+
* 用于 iOS 端的尺寸计算
|
|
34
|
+
*/
|
|
35
|
+
const BUTTON_SIZE_MAP: Record<ButtonSize, { height: number; minWidth: number }> = {
|
|
36
|
+
XLarge: { height: 56, minWidth: 120 }, // Widget.BitMart4.Button.*.XLarge
|
|
37
|
+
Large: { height: 48, minWidth: 100 }, // Widget.BitMart4.Button.*.Large
|
|
38
|
+
Medium: { height: 40, minWidth: 80 }, // Widget.BitMart4.Button.*.Medium
|
|
39
|
+
Small: { height: 36, minWidth: 70 }, // Widget.BitMart4.Button.*.Small
|
|
40
|
+
XSmall: { height: 32, minWidth: 60 }, // Widget.BitMart4.Button.*.XSmall
|
|
41
|
+
XXSmall: { height: 26, minWidth: 50 }, // Widget.BitMart4.Button.*.XXSmall
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 从 styleName 中提取尺寸
|
|
46
|
+
*/
|
|
47
|
+
const getSizeFromStyleName = (styleName: ButtonStyleName): ButtonSize => {
|
|
48
|
+
const parts = styleName.split('.');
|
|
49
|
+
return parts[1] as ButtonSize;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 根据按钮样式自动推断图标颜色
|
|
54
|
+
*/
|
|
55
|
+
const getIconColorForStyle = (styleName: ButtonStyleName): string => {
|
|
56
|
+
const type = styleName.split('.')[0] as ButtonType;
|
|
57
|
+
|
|
58
|
+
// Secondary 和 White 使用蓝色图标,其他使用白色图标
|
|
59
|
+
switch (type) {
|
|
60
|
+
case 'Secondary':
|
|
61
|
+
case 'White':
|
|
62
|
+
return '#0066FF';
|
|
63
|
+
case 'Primary':
|
|
64
|
+
case 'Green':
|
|
65
|
+
case 'Red':
|
|
66
|
+
default:
|
|
67
|
+
return '#ffffff';
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
interface ButtonProps {
|
|
72
|
+
/** 按钮文本 */
|
|
73
|
+
text: string;
|
|
74
|
+
/** 按钮样式,格式:Type.Size(如 "Primary.XLarge")*/
|
|
75
|
+
styleName?: ButtonStyleName;
|
|
76
|
+
/** 是否启用 */
|
|
77
|
+
enabled?: boolean;
|
|
78
|
+
/** 是否显示加载状态(原生进度条/ActivityIndicator)*/
|
|
79
|
+
loading?: boolean;
|
|
80
|
+
/** 点击回调 */
|
|
81
|
+
onPress?: () => void;
|
|
82
|
+
/** 样式 */
|
|
83
|
+
style?: StyleProp<ViewStyle>;
|
|
84
|
+
/** 左侧图标(SVG 字符串,颜色自动推断) */
|
|
85
|
+
iconLeft?: string;
|
|
86
|
+
/** 右侧图标(SVG 字符串,颜色自动推断) */
|
|
87
|
+
iconRight?: string;
|
|
88
|
+
/** 是否全宽布局(自动填充父容器宽度,常用于 Footer 按钮) */
|
|
89
|
+
fullWidth?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface NativeSizeChangeEvent {
|
|
93
|
+
nativeEvent: {
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface NativeButtonProps {
|
|
100
|
+
text: string;
|
|
101
|
+
styleName?: string;
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
loading?: boolean;
|
|
104
|
+
onPress?: () => void;
|
|
105
|
+
onSizeChange?: (event: NativeSizeChangeEvent) => void;
|
|
106
|
+
style?: StyleProp<ViewStyle>;
|
|
107
|
+
iconSvgString?: string;
|
|
108
|
+
iconColor?: string;
|
|
109
|
+
iconPosition?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Native Components for different platforms
|
|
113
|
+
const NativeButton =
|
|
114
|
+
Platform.OS === 'android'
|
|
115
|
+
? requireNativeComponent<NativeButtonProps>('PrimaryXLargeButton')
|
|
116
|
+
: requireNativeComponent<NativeButtonProps>('PrimaryButtonView');
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Button - 跨平台原生按钮组件
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* // 普通按钮
|
|
124
|
+
* <Button text="Submit" styleName={ButtonStyles.Primary.Large} onPress={handleSubmit} />
|
|
125
|
+
*
|
|
126
|
+
* // 左侧图标(图标颜色自动推断)
|
|
127
|
+
* <Button text="Back" styleName={ButtonStyles.Primary.Large} iconLeft={IC_BUTTON_SVG} />
|
|
128
|
+
*
|
|
129
|
+
* // 右侧图标
|
|
130
|
+
* <Button text="Next" styleName={ButtonStyles.Primary.Large} iconRight={IC_BUTTON_SVG} />
|
|
131
|
+
*
|
|
132
|
+
* // 全宽按钮
|
|
133
|
+
* <Button text="Confirm" styleName={ButtonStyles.Primary.Large} fullWidth />
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export const Button: React.FC<ButtonProps> = ({
|
|
137
|
+
text,
|
|
138
|
+
styleName = 'Primary.XLarge',
|
|
139
|
+
enabled = true,
|
|
140
|
+
loading = false,
|
|
141
|
+
onPress,
|
|
142
|
+
style,
|
|
143
|
+
iconLeft,
|
|
144
|
+
iconRight,
|
|
145
|
+
fullWidth = false,
|
|
146
|
+
}) => {
|
|
147
|
+
const [nativeSize, setNativeSize] = useState<{ width: number; height: number } | null>(null);
|
|
148
|
+
|
|
149
|
+
const handlePress = useCallback(() => {
|
|
150
|
+
if (enabled && onPress) {
|
|
151
|
+
onPress();
|
|
152
|
+
}
|
|
153
|
+
}, [enabled, onPress]);
|
|
154
|
+
|
|
155
|
+
const handleSizeChange = useCallback((event: NativeSizeChangeEvent) => {
|
|
156
|
+
// fullWidth 模式下忽略原生测量
|
|
157
|
+
if (fullWidth) return;
|
|
158
|
+
|
|
159
|
+
const { width, height } = event.nativeEvent;
|
|
160
|
+
setNativeSize({ width, height });
|
|
161
|
+
}, [fullWidth]);
|
|
162
|
+
|
|
163
|
+
const size = getSizeFromStyleName(styleName);
|
|
164
|
+
const dimensions = BUTTON_SIZE_MAP[size];
|
|
165
|
+
|
|
166
|
+
// 确定使用哪个图标(优先使用 iconLeft)
|
|
167
|
+
const iconSvg = iconLeft || iconRight;
|
|
168
|
+
const iconPosition = iconRight ? 'trailing' : 'leading';
|
|
169
|
+
|
|
170
|
+
// 自动推断图标颜色
|
|
171
|
+
const iconColor = iconSvg ? getIconColorForStyle(styleName) : undefined;
|
|
172
|
+
|
|
173
|
+
const baseStyle: ViewStyle = useMemo(() => {
|
|
174
|
+
// fullWidth 模式:使用 100% 宽度,由 wrapper 的 flex 布局控制实际宽度
|
|
175
|
+
if (fullWidth) {
|
|
176
|
+
return {
|
|
177
|
+
width: '100%' as any,
|
|
178
|
+
height: dimensions.height,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 非 fullWidth 模式:检查是否由父级决定宽度
|
|
183
|
+
const flat = style ? StyleSheet.flatten(style) as ViewStyle : null;
|
|
184
|
+
const userWantsOwnWidth = flat && (
|
|
185
|
+
flat.width !== undefined ||
|
|
186
|
+
flat.flex !== undefined ||
|
|
187
|
+
flat.position === 'absolute'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (userWantsOwnWidth) {
|
|
191
|
+
// 由父级决定宽度,只设高度
|
|
192
|
+
return {
|
|
193
|
+
height: nativeSize ? nativeSize.height : dimensions.height,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 自动宽度模式:使用原生测量的尺寸或默认尺寸
|
|
198
|
+
if (nativeSize) {
|
|
199
|
+
return { width: nativeSize.width, height: nativeSize.height };
|
|
200
|
+
}
|
|
201
|
+
return { height: dimensions.height, minWidth: dimensions.minWidth };
|
|
202
|
+
}, [fullWidth, style, nativeSize, dimensions.height, dimensions.minWidth]);
|
|
203
|
+
|
|
204
|
+
const mergedStyle = [baseStyle, style];
|
|
205
|
+
|
|
206
|
+
const buttonElement = (
|
|
207
|
+
<NativeButton
|
|
208
|
+
text={text}
|
|
209
|
+
styleName={styleName}
|
|
210
|
+
enabled={enabled}
|
|
211
|
+
loading={loading}
|
|
212
|
+
onPress={handlePress}
|
|
213
|
+
onSizeChange={handleSizeChange}
|
|
214
|
+
iconSvgString={iconSvg}
|
|
215
|
+
iconColor={iconColor}
|
|
216
|
+
iconPosition={iconPosition}
|
|
217
|
+
style={mergedStyle}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// fullWidth 模式:wrapper 使用 flex 布局,按钮使用 100% 填充
|
|
222
|
+
if (fullWidth) {
|
|
223
|
+
return (
|
|
224
|
+
<View style={{ flex: 1, alignSelf: 'stretch' }}>
|
|
225
|
+
{buttonElement}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return buttonElement;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export default Button;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button 样式常量定义
|
|
3
|
+
*
|
|
4
|
+
* 使用方式:
|
|
5
|
+
* import { ButtonStyles } from '@/components/NativeDesign/ButtonStyles';
|
|
6
|
+
*
|
|
7
|
+
* <Button styleName={ButtonStyles.Primary.Large} />
|
|
8
|
+
* <TextButton styleName={ButtonStyles.Text.Blue} />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ButtonStyles = {
|
|
12
|
+
// Button 组件样式
|
|
13
|
+
Primary: {
|
|
14
|
+
XLarge: 'Primary.XLarge' as const,
|
|
15
|
+
Large: 'Primary.Large' as const,
|
|
16
|
+
Medium: 'Primary.Medium' as const,
|
|
17
|
+
Small: 'Primary.Small' as const,
|
|
18
|
+
XSmall: 'Primary.XSmall' as const,
|
|
19
|
+
XXSmall: 'Primary.XXSmall' as const,
|
|
20
|
+
},
|
|
21
|
+
Secondary: {
|
|
22
|
+
XLarge: 'Secondary.XLarge' as const,
|
|
23
|
+
Large: 'Secondary.Large' as const,
|
|
24
|
+
Medium: 'Secondary.Medium' as const,
|
|
25
|
+
Small: 'Secondary.Small' as const,
|
|
26
|
+
XSmall: 'Secondary.XSmall' as const,
|
|
27
|
+
XXSmall: 'Secondary.XXSmall' as const,
|
|
28
|
+
},
|
|
29
|
+
Green: {
|
|
30
|
+
XLarge: 'Green.XLarge' as const,
|
|
31
|
+
Large: 'Green.Large' as const,
|
|
32
|
+
Medium: 'Green.Medium' as const,
|
|
33
|
+
Small: 'Green.Small' as const,
|
|
34
|
+
XSmall: 'Green.XSmall' as const,
|
|
35
|
+
XXSmall: 'Green.XXSmall' as const,
|
|
36
|
+
},
|
|
37
|
+
Red: {
|
|
38
|
+
XLarge: 'Red.XLarge' as const,
|
|
39
|
+
Large: 'Red.Large' as const,
|
|
40
|
+
Medium: 'Red.Medium' as const,
|
|
41
|
+
Small: 'Red.Small' as const,
|
|
42
|
+
XSmall: 'Red.XSmall' as const,
|
|
43
|
+
XXSmall: 'Red.XXSmall' as const,
|
|
44
|
+
},
|
|
45
|
+
White: {
|
|
46
|
+
XLarge: 'White.XLarge' as const,
|
|
47
|
+
Large: 'White.Large' as const,
|
|
48
|
+
Medium: 'White.Medium' as const,
|
|
49
|
+
Small: 'White.Small' as const,
|
|
50
|
+
XSmall: 'White.XSmall' as const,
|
|
51
|
+
XXSmall: 'White.XXSmall' as const,
|
|
52
|
+
},
|
|
53
|
+
// TextButton 组件样式
|
|
54
|
+
Text: {
|
|
55
|
+
Black: 'Black' as const,
|
|
56
|
+
Gray: 'Gray' as const,
|
|
57
|
+
Blue: 'Blue' as const,
|
|
58
|
+
},
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
export default ButtonStyles;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Platform, StyleProp, ViewStyle, requireNativeComponent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 支持的文本按钮样式类型
|
|
6
|
+
*/
|
|
7
|
+
type TextButtonStyleName = 'Black' | 'Gray' | 'Blue';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 尺寸映射(文本按钮通常没有固定高度,但提供最小高度参考)
|
|
11
|
+
*/
|
|
12
|
+
const TEXT_BUTTON_SIZE_MAP: Record<TextButtonStyleName, { minHeight: number }> = {
|
|
13
|
+
Black: { minHeight: 28 }, // Widget.BitMart4.Button.TextButton.Black
|
|
14
|
+
Gray: { minHeight: 28 }, // Widget.BitMart4.Button.TextButton.Gray
|
|
15
|
+
Blue: { minHeight: 28 }, // Widget.BitMart4.Button.TextButton.Blue
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface TextButtonProps {
|
|
19
|
+
/** 按钮文本 */
|
|
20
|
+
text: string;
|
|
21
|
+
/** 按钮样式,支持 "Black", "Gray", "Blue" */
|
|
22
|
+
styleName?: TextButtonStyleName;
|
|
23
|
+
/** 是否启用 */
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
/** 点击回调 */
|
|
26
|
+
onPress?: () => void;
|
|
27
|
+
/** 样式 */
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface NativeSizeChangeEvent {
|
|
32
|
+
nativeEvent: {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface NativeTextButtonProps {
|
|
39
|
+
text: string;
|
|
40
|
+
styleName?: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
onPress?: () => void;
|
|
43
|
+
onSizeChange?: (event: NativeSizeChangeEvent) => void;
|
|
44
|
+
style?: StyleProp<ViewStyle>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Native Components for different platforms
|
|
48
|
+
const NativeTextButton =
|
|
49
|
+
Platform.OS === 'android'
|
|
50
|
+
? requireNativeComponent<NativeTextButtonProps>('TextButton')
|
|
51
|
+
: requireNativeComponent<NativeTextButtonProps>('TextButtonView');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* TextButton - 跨平台文本按钮组件
|
|
55
|
+
*
|
|
56
|
+
* Android: 基于 AppCompatButton + ContextThemeWrapper,支持 BitMart4 TextButton 样式
|
|
57
|
+
* iOS: 基于 UILabel + 手势识别,实现纯文本按钮效果,支持 Black/Gray/Blue 样式
|
|
58
|
+
*
|
|
59
|
+
* 按钮宽度会根据文字内容自适应,文字保持单行显示
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <TextButton
|
|
64
|
+
* text="确认"
|
|
65
|
+
* styleName="Blue"
|
|
66
|
+
* onPress={() => console.log('pressed')}
|
|
67
|
+
* />
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export const TextButton: React.FC<TextButtonProps> = ({
|
|
71
|
+
text,
|
|
72
|
+
styleName = 'Black', // 默认黑色文本样式
|
|
73
|
+
enabled = true,
|
|
74
|
+
onPress,
|
|
75
|
+
style,
|
|
76
|
+
}) => {
|
|
77
|
+
// 用于存储原生测量的尺寸
|
|
78
|
+
const [nativeSize, setNativeSize] = useState<{ width: number; height: number } | null>(null);
|
|
79
|
+
|
|
80
|
+
const handlePress = useCallback(() => {
|
|
81
|
+
if (enabled && onPress) {
|
|
82
|
+
onPress();
|
|
83
|
+
}
|
|
84
|
+
}, [enabled, onPress]);
|
|
85
|
+
|
|
86
|
+
// 处理原生尺寸变化事件
|
|
87
|
+
const handleSizeChange = useCallback((event: NativeSizeChangeEvent) => {
|
|
88
|
+
const { width, height } = event.nativeEvent;
|
|
89
|
+
setNativeSize({ width, height });
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// 获取样式对应的尺寸信息
|
|
93
|
+
const dimensions = TEXT_BUTTON_SIZE_MAP[styleName];
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<NativeTextButton
|
|
97
|
+
text={text}
|
|
98
|
+
styleName={styleName}
|
|
99
|
+
enabled={enabled}
|
|
100
|
+
onPress={handlePress}
|
|
101
|
+
onSizeChange={handleSizeChange}
|
|
102
|
+
style={[
|
|
103
|
+
// 使用原生测量的尺寸(如果有),否则使用默认尺寸
|
|
104
|
+
nativeSize
|
|
105
|
+
? { width: nativeSize.width, height: nativeSize.height }
|
|
106
|
+
: { minHeight: dimensions.minHeight },
|
|
107
|
+
style,
|
|
108
|
+
]}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeDesign - BitMart Design 组件库的 React Native 桥接组件
|
|
3
|
+
*
|
|
4
|
+
* 这些组件使用 Android 的 com.bitmart.android:design 组件库的样式,
|
|
5
|
+
* 通过 Native Module 桥接到 React Native 中使用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { BMTextButton } from './BMTextButton';
|
|
9
|
+
export type { BMTextButtonStyleType } from './BMTextButton';
|
|
10
|
+
|
|
11
|
+
export { Button } from './Button';
|
|
12
|
+
export { TextButton } from './TextButton';
|
|
13
|
+
export type { ButtonType, ButtonSize, ButtonStyleName } from './Button';
|
|
14
|
+
export { ButtonStyles } from './ButtonStyles';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NativeDesign';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
SafeAreaView,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Switch,
|
|
8
|
+
Text,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { Button, ButtonStyleName, ButtonStyles, TextButton } from '../components/NativeDesign';
|
|
13
|
+
import { IC_BUTTON_SVG } from '../assets/svgs/svgStrings';
|
|
14
|
+
|
|
15
|
+
const NativeButtonsScreen: React.FC = () => {
|
|
16
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
17
|
+
|
|
18
|
+
const handleButtonPress = (styleName: string) => {
|
|
19
|
+
Alert.alert('按钮点击', `点击了样式: ${styleName}`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const tabs = [
|
|
23
|
+
{ id: 0, title: '文本按钮' },
|
|
24
|
+
{ id: 1, title: 'Primary' },
|
|
25
|
+
{ id: 2, title: 'Secondary' },
|
|
26
|
+
{ id: 3, title: 'Green' },
|
|
27
|
+
{ id: 4, title: 'Red' },
|
|
28
|
+
{ id: 5, title: 'White' },
|
|
29
|
+
{ id: 6, title: 'Footer' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const renderTabContent = () => {
|
|
33
|
+
switch (activeTab) {
|
|
34
|
+
case 0: return <TextButtonsTab onButtonPress={handleButtonPress} />;
|
|
35
|
+
case 1: return <PrimaryButtonsTab onButtonPress={handleButtonPress} />;
|
|
36
|
+
case 2: return <SecondaryButtonsTab onButtonPress={handleButtonPress} />;
|
|
37
|
+
case 3: return <GreenButtonsTab onButtonPress={handleButtonPress} />;
|
|
38
|
+
case 4: return <RedButtonsTab onButtonPress={handleButtonPress} />;
|
|
39
|
+
case 5: return <WhiteButtonsTab onButtonPress={handleButtonPress} />;
|
|
40
|
+
case 6: return <FooterButtonsTab onButtonPress={handleButtonPress} />;
|
|
41
|
+
default: return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<SafeAreaView style={styles.safeArea}>
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<View style={styles.header}>
|
|
49
|
+
<Text style={styles.headerTitle}>Native Buttons</Text>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
{/* Tab 导航 */}
|
|
53
|
+
<View style={styles.tabContainer}>
|
|
54
|
+
<ScrollView
|
|
55
|
+
horizontal
|
|
56
|
+
showsHorizontalScrollIndicator={false}
|
|
57
|
+
contentContainerStyle={styles.tabScrollContent}
|
|
58
|
+
>
|
|
59
|
+
{tabs.map((tab) => (
|
|
60
|
+
<TouchableOpacity
|
|
61
|
+
key={tab.id}
|
|
62
|
+
style={[styles.tab, activeTab === tab.id && styles.tabActive]}
|
|
63
|
+
onPress={() => setActiveTab(tab.id)}
|
|
64
|
+
>
|
|
65
|
+
<Text style={[styles.tabText, activeTab === tab.id && styles.tabTextActive]}>
|
|
66
|
+
{tab.title}
|
|
67
|
+
</Text>
|
|
68
|
+
</TouchableOpacity>
|
|
69
|
+
))}
|
|
70
|
+
</ScrollView>
|
|
71
|
+
</View>
|
|
72
|
+
|
|
73
|
+
{/* Tab 内容 */}
|
|
74
|
+
<ScrollView
|
|
75
|
+
style={styles.container}
|
|
76
|
+
contentContainerStyle={styles.contentContainer}
|
|
77
|
+
showsVerticalScrollIndicator={false}
|
|
78
|
+
>
|
|
79
|
+
{renderTabContent()}
|
|
80
|
+
</ScrollView>
|
|
81
|
+
</SafeAreaView>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ─── Text Buttons Tab ────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const TextButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
88
|
+
<View style={styles.tabContent}>
|
|
89
|
+
<SectionTitle title="常规文本按钮" />
|
|
90
|
+
<View style={styles.buttonGroup}>
|
|
91
|
+
<TextButton text="Black Text Button" styleName={ButtonStyles.Text.Black} onPress={() => onButtonPress('TextButton.Black')} />
|
|
92
|
+
<TextButton text="Gray Text Button" styleName={ButtonStyles.Text.Gray} onPress={() => onButtonPress('TextButton.Gray')} style={styles.buttonMargin} />
|
|
93
|
+
<TextButton text="Blue Text Button" styleName={ButtonStyles.Text.Blue} onPress={() => onButtonPress('TextButton.Blue')} style={styles.buttonMargin} />
|
|
94
|
+
</View>
|
|
95
|
+
|
|
96
|
+
<SectionTitle title="禁用状态" />
|
|
97
|
+
<View style={styles.buttonGroup}>
|
|
98
|
+
<TextButton text="Black Text Button (Disabled)" styleName={ButtonStyles.Text.Black} enabled={false} onPress={() => onButtonPress('TextButton.Black.Disabled')} />
|
|
99
|
+
<TextButton text="Gray Text Button (Disabled)" styleName={ButtonStyles.Text.Gray} enabled={false} onPress={() => onButtonPress('TextButton.Gray.Disabled')} style={styles.buttonMargin} />
|
|
100
|
+
<TextButton text="Blue Text Button (Disabled)" styleName={ButtonStyles.Text.Blue} enabled={false} onPress={() => onButtonPress('TextButton.Blue.Disabled')} style={styles.buttonMargin} />
|
|
101
|
+
</View>
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// ─── Primary Buttons Tab ─────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
const PrimaryButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => {
|
|
108
|
+
const [loading, setLoading] = useState(false);
|
|
109
|
+
return (
|
|
110
|
+
<View style={styles.tabContent}>
|
|
111
|
+
<SectionTitle title="常规" />
|
|
112
|
+
<View style={styles.buttonGroup}>
|
|
113
|
+
{(['XLarge', 'Large', 'Medium', 'Small', 'XSmall', 'XXSmall'] as const).map((size, i) => (
|
|
114
|
+
<Button key={size} text={`Primary ${size}`} styleName={ButtonStyles.Primary[size]} onPress={() => onButtonPress(`Primary.${size}`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
115
|
+
))}
|
|
116
|
+
</View>
|
|
117
|
+
|
|
118
|
+
<SectionTitle title="左图标(SVG原生)" />
|
|
119
|
+
<View style={styles.buttonGroup}>
|
|
120
|
+
{(['XLarge', 'Large', 'Medium', 'Small', 'XSmall', 'XXSmall'] as const).map((size, i) => (
|
|
121
|
+
<Button key={size} text="Button" styleName={ButtonStyles.Primary[size]} iconLeft={IC_BUTTON_SVG} onPress={() => onButtonPress(`Primary.${size}.Icon.Leading`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
122
|
+
))}
|
|
123
|
+
</View>
|
|
124
|
+
|
|
125
|
+
<SectionTitle title="右图标(SVG原生)" />
|
|
126
|
+
<View style={styles.buttonGroup}>
|
|
127
|
+
{(['XLarge', 'Large', 'Medium', 'Small', 'XSmall', 'XXSmall'] as const).map((size, i) => (
|
|
128
|
+
<Button key={size} text="Button" styleName={ButtonStyles.Primary[size]} iconRight={IC_BUTTON_SVG} onPress={() => onButtonPress(`Primary.${size}.Icon.Trailing`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
129
|
+
))}
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
<SectionTitle title="加载状态" rightElement={<LoadingSwitch value={loading} onChange={setLoading} />} />
|
|
133
|
+
<View style={styles.buttonGroup}>
|
|
134
|
+
<Button text="Primary XLarge" styleName={ButtonStyles.Primary.XLarge} loading={loading} onPress={() => onButtonPress('Primary.XLarge')} />
|
|
135
|
+
<Button text="Primary Large" styleName={ButtonStyles.Primary.Large} loading={loading} onPress={() => onButtonPress('Primary.Large')} style={styles.buttonMargin} />
|
|
136
|
+
<Button text="Primary Medium" styleName={ButtonStyles.Primary.Medium} loading={loading} onPress={() => onButtonPress('Primary.Medium')} style={styles.buttonMargin} />
|
|
137
|
+
<Button text="Button" styleName={ButtonStyles.Primary.Large} loading={loading} iconLeft={IC_BUTTON_SVG} onPress={() => onButtonPress('Primary.Large.Icon.Leading')} style={styles.buttonMargin} />
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
<SectionTitle title="禁用状态" />
|
|
141
|
+
<View style={styles.buttonGroup}>
|
|
142
|
+
<Button text="Primary Large (Disabled)" styleName={ButtonStyles.Primary.Large} enabled={false} onPress={() => onButtonPress('Primary.Large.Disabled')} />
|
|
143
|
+
<Button text="Primary Medium (Disabled)" styleName={ButtonStyles.Primary.Medium} enabled={false} onPress={() => onButtonPress('Primary.Medium.Disabled')} style={styles.buttonMargin} />
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ─── Generic Color Tabs (Secondary / Green / Red / White) ────────────────────
|
|
150
|
+
|
|
151
|
+
type ColorTabProps = { colorKey: 'Secondary' | 'Green' | 'Red' | 'White'; onButtonPress: (style: string) => void };
|
|
152
|
+
|
|
153
|
+
const ColorButtonsTab: React.FC<ColorTabProps> = ({ colorKey, onButtonPress }) => {
|
|
154
|
+
const [loading, setLoading] = useState(false);
|
|
155
|
+
const sizes = ['XLarge', 'Large', 'Medium', 'Small', 'XSmall', 'XXSmall'] as const;
|
|
156
|
+
const styleMap = ButtonStyles[colorKey] as Record<string, string>;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<View style={styles.tabContent}>
|
|
160
|
+
<SectionTitle title="常规" />
|
|
161
|
+
<View style={styles.buttonGroup}>
|
|
162
|
+
{sizes.map((size, i) => (
|
|
163
|
+
<Button key={size} text="Button" styleName={styleMap[size] as ButtonStyleName} onPress={() => onButtonPress(`${colorKey}.${size}`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
164
|
+
))}
|
|
165
|
+
</View>
|
|
166
|
+
|
|
167
|
+
<SectionTitle title="左图标(SVG原生)" />
|
|
168
|
+
<View style={styles.buttonGroup}>
|
|
169
|
+
{sizes.map((size, i) => (
|
|
170
|
+
<Button key={size} text="Button" styleName={styleMap[size] as ButtonStyleName} iconLeft={IC_BUTTON_SVG} onPress={() => onButtonPress(`${colorKey}.${size}.Icon.Leading`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
171
|
+
))}
|
|
172
|
+
</View>
|
|
173
|
+
|
|
174
|
+
<SectionTitle title="右图标(SVG原生)" />
|
|
175
|
+
<View style={styles.buttonGroup}>
|
|
176
|
+
{sizes.map((size, i) => (
|
|
177
|
+
<Button key={size} text="Button" styleName={styleMap[size] as ButtonStyleName} iconRight={IC_BUTTON_SVG} onPress={() => onButtonPress(`${colorKey}.${size}.Icon.Trailing`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
178
|
+
))}
|
|
179
|
+
</View>
|
|
180
|
+
|
|
181
|
+
<SectionTitle title="加载状态" rightElement={<LoadingSwitch value={loading} onChange={setLoading} />} />
|
|
182
|
+
<View style={styles.buttonGroup}>
|
|
183
|
+
{sizes.slice(0, 4).map((size, i) => (
|
|
184
|
+
<Button key={size} text="Button" styleName={styleMap[size] as ButtonStyleName} loading={loading} onPress={() => onButtonPress(`${colorKey}.${size}`)} style={i > 0 ? styles.buttonMargin : undefined} />
|
|
185
|
+
))}
|
|
186
|
+
</View>
|
|
187
|
+
</View>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const SecondaryButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
192
|
+
<ColorButtonsTab colorKey="Secondary" onButtonPress={onButtonPress} />
|
|
193
|
+
);
|
|
194
|
+
const GreenButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
195
|
+
<ColorButtonsTab colorKey="Green" onButtonPress={onButtonPress} />
|
|
196
|
+
);
|
|
197
|
+
const RedButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
198
|
+
<ColorButtonsTab colorKey="Red" onButtonPress={onButtonPress} />
|
|
199
|
+
);
|
|
200
|
+
const WhiteButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
201
|
+
<ColorButtonsTab colorKey="White" onButtonPress={onButtonPress} />
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// ─── Footer Buttons Tab ──────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const FooterButtonsTab: React.FC<{ onButtonPress: (style: string) => void }> = ({ onButtonPress }) => (
|
|
207
|
+
<View style={styles.tabContent}>
|
|
208
|
+
<SectionTitle title="底部全宽按钮" />
|
|
209
|
+
<View style={styles.footerButtonGroup}>
|
|
210
|
+
<Button text="Button" styleName={ButtonStyles.Primary.Large} fullWidth onPress={() => onButtonPress('Primary.Large')} />
|
|
211
|
+
<Button text="Button" styleName={ButtonStyles.Secondary.Large} fullWidth onPress={() => onButtonPress('Secondary.Large')} style={styles.footerButtonSpacing} />
|
|
212
|
+
<View style={styles.footerTwoButtonRow}>
|
|
213
|
+
<Button text="Button" styleName={ButtonStyles.Secondary.Large} fullWidth onPress={() => onButtonPress('Secondary.Large')} />
|
|
214
|
+
<Button text="Button" styleName={ButtonStyles.Primary.Large} fullWidth onPress={() => onButtonPress('Primary.Large')} />
|
|
215
|
+
</View>
|
|
216
|
+
</View>
|
|
217
|
+
</View>
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// ─── Shared helpers ──────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
const SectionTitle: React.FC<{ title: string; rightElement?: React.ReactNode }> = ({ title, rightElement }) => (
|
|
223
|
+
<View style={styles.titleRow}>
|
|
224
|
+
<Text style={styles.title}>{title}</Text>
|
|
225
|
+
{rightElement}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const LoadingSwitch: React.FC<{ value: boolean; onChange: (v: boolean) => void }> = ({ value, onChange }) => (
|
|
230
|
+
<View style={styles.switchRow}>
|
|
231
|
+
<Text style={styles.switchLabel}>进度条</Text>
|
|
232
|
+
<Switch value={value} onValueChange={onChange} />
|
|
233
|
+
</View>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
const styles = StyleSheet.create({
|
|
239
|
+
safeArea: {
|
|
240
|
+
flex: 1,
|
|
241
|
+
backgroundColor: '#FFFFFF',
|
|
242
|
+
},
|
|
243
|
+
header: {
|
|
244
|
+
height: 56,
|
|
245
|
+
paddingHorizontal: 16,
|
|
246
|
+
justifyContent: 'center',
|
|
247
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
248
|
+
borderBottomColor: '#E0E0E0',
|
|
249
|
+
backgroundColor: '#FFFFFF',
|
|
250
|
+
},
|
|
251
|
+
headerTitle: {
|
|
252
|
+
fontSize: 18,
|
|
253
|
+
fontWeight: '600',
|
|
254
|
+
color: '#151515',
|
|
255
|
+
},
|
|
256
|
+
tabContainer: {
|
|
257
|
+
backgroundColor: '#FFFFFF',
|
|
258
|
+
borderBottomWidth: 1,
|
|
259
|
+
borderBottomColor: '#E0E0E0',
|
|
260
|
+
},
|
|
261
|
+
tabScrollContent: {
|
|
262
|
+
paddingHorizontal: 16,
|
|
263
|
+
},
|
|
264
|
+
tab: {
|
|
265
|
+
paddingVertical: 12,
|
|
266
|
+
paddingHorizontal: 16,
|
|
267
|
+
marginRight: 8,
|
|
268
|
+
borderBottomWidth: 2,
|
|
269
|
+
borderBottomColor: 'transparent',
|
|
270
|
+
},
|
|
271
|
+
tabActive: {
|
|
272
|
+
borderBottomColor: '#2196F3',
|
|
273
|
+
},
|
|
274
|
+
tabText: {
|
|
275
|
+
fontSize: 14,
|
|
276
|
+
fontWeight: '400',
|
|
277
|
+
color: '#666666',
|
|
278
|
+
},
|
|
279
|
+
tabTextActive: {
|
|
280
|
+
fontWeight: '600',
|
|
281
|
+
color: '#2196F3',
|
|
282
|
+
},
|
|
283
|
+
container: {
|
|
284
|
+
flex: 1,
|
|
285
|
+
backgroundColor: '#FFFFFF',
|
|
286
|
+
},
|
|
287
|
+
contentContainer: {
|
|
288
|
+
paddingBottom: 24,
|
|
289
|
+
},
|
|
290
|
+
tabContent: {
|
|
291
|
+
padding: 24,
|
|
292
|
+
},
|
|
293
|
+
titleRow: {
|
|
294
|
+
flexDirection: 'row',
|
|
295
|
+
alignItems: 'center',
|
|
296
|
+
marginBottom: 16,
|
|
297
|
+
},
|
|
298
|
+
title: {
|
|
299
|
+
flex: 1,
|
|
300
|
+
fontSize: 20,
|
|
301
|
+
fontWeight: '600',
|
|
302
|
+
color: '#151515',
|
|
303
|
+
},
|
|
304
|
+
switchRow: {
|
|
305
|
+
flexDirection: 'row',
|
|
306
|
+
alignItems: 'center',
|
|
307
|
+
gap: 8,
|
|
308
|
+
},
|
|
309
|
+
switchLabel: {
|
|
310
|
+
fontSize: 14,
|
|
311
|
+
color: '#666666',
|
|
312
|
+
},
|
|
313
|
+
buttonGroup: {
|
|
314
|
+
alignItems: 'center',
|
|
315
|
+
marginBottom: 32,
|
|
316
|
+
},
|
|
317
|
+
buttonMargin: {
|
|
318
|
+
marginTop: 16,
|
|
319
|
+
},
|
|
320
|
+
footerButtonGroup: {
|
|
321
|
+
width: '100%',
|
|
322
|
+
alignSelf: 'stretch',
|
|
323
|
+
},
|
|
324
|
+
footerButtonSpacing: {
|
|
325
|
+
marginTop: 16,
|
|
326
|
+
},
|
|
327
|
+
footerTwoButtonRow: {
|
|
328
|
+
flexDirection: 'row',
|
|
329
|
+
width: '100%',
|
|
330
|
+
marginTop: 16,
|
|
331
|
+
gap: 16,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
export default NativeButtonsScreen;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ModuleRegistry } from '../multi-bundle/ModuleRegistry';
|
|
2
|
+
export {};
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
var __ModuleRegistry: typeof ModuleRegistry;
|
|
6
|
+
|
|
7
|
+
// React Native 环境中的 process.env 类型定义
|
|
8
|
+
namespace NodeJS {
|
|
9
|
+
interface ProcessEnv {
|
|
10
|
+
MODULE_DEBUG?: string;
|
|
11
|
+
NODE_ENV?: 'development' | 'production' | 'test';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|