@coze-arch/cli 0.0.1-alpha.035e0e
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/README.md +142 -0
- package/bin/main +2 -0
- package/lib/__templates__/expo/.coze +12 -0
- package/lib/__templates__/expo/.cozeproj/scripts/dev_build.sh +46 -0
- package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +220 -0
- package/lib/__templates__/expo/.cozeproj/scripts/prod_build.sh +47 -0
- package/lib/__templates__/expo/.cozeproj/scripts/prod_run.sh +34 -0
- package/lib/__templates__/expo/.cozeproj/scripts/server_dev_run.sh +45 -0
- package/lib/__templates__/expo/README.md +72 -0
- package/lib/__templates__/expo/_gitignore +11 -0
- package/lib/__templates__/expo/_npmrc +20 -0
- package/lib/__templates__/expo/client/app/_layout.tsx +33 -0
- package/lib/__templates__/expo/client/app/demo.tsx +1 -0
- package/lib/__templates__/expo/client/app/index.tsx +1 -0
- package/lib/__templates__/expo/client/app.config.ts +75 -0
- package/lib/__templates__/expo/client/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/lib/__templates__/expo/client/assets/images/adaptive-icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/default-avatar.png +0 -0
- package/lib/__templates__/expo/client/assets/images/favicon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/partial-react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@2x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@3x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/splash-icon.png +0 -0
- package/lib/__templates__/expo/client/components/Screen.tsx +330 -0
- package/lib/__templates__/expo/client/components/SmartDateInput.tsx +238 -0
- package/lib/__templates__/expo/client/components/ThemedText.tsx +33 -0
- package/lib/__templates__/expo/client/components/ThemedView.tsx +38 -0
- package/lib/__templates__/expo/client/constants/theme.ts +854 -0
- package/lib/__templates__/expo/client/contexts/AuthContext.tsx +49 -0
- package/lib/__templates__/expo/client/declarations.d.ts +5 -0
- package/lib/__templates__/expo/client/eslint-formatter-simple.mjs +49 -0
- package/lib/__templates__/expo/client/eslint.config.mjs +98 -0
- package/lib/__templates__/expo/client/hooks/useColorScheme.ts +34 -0
- package/lib/__templates__/expo/client/hooks/useTheme.ts +13 -0
- package/lib/__templates__/expo/client/metro.config.js +121 -0
- package/lib/__templates__/expo/client/package.json +93 -0
- package/lib/__templates__/expo/client/screens/demo/index.tsx +25 -0
- package/lib/__templates__/expo/client/screens/demo/styles.ts +28 -0
- package/lib/__templates__/expo/client/scripts/install-missing-deps.js +104 -0
- package/lib/__templates__/expo/client/tsconfig.json +24 -0
- package/lib/__templates__/expo/client/utils/index.ts +54 -0
- package/lib/__templates__/expo/package.json +22 -0
- package/lib/__templates__/expo/pnpm-lock.yaml +13975 -0
- package/lib/__templates__/expo/pnpm-workspace.yaml +3 -0
- package/lib/__templates__/expo/server/package.json +32 -0
- package/lib/__templates__/expo/server/src/index.ts +18 -0
- package/lib/__templates__/expo/server/tsconfig.json +24 -0
- package/lib/__templates__/expo/template.config.js +50 -0
- package/lib/__templates__/expo/tsconfig.json +1 -0
- package/lib/__templates__/nextjs/.coze +12 -0
- package/lib/__templates__/nextjs/README.md +358 -0
- package/lib/__templates__/nextjs/_gitignore +99 -0
- package/lib/__templates__/nextjs/_npmrc +23 -0
- package/lib/__templates__/nextjs/components.json +21 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +18 -0
- package/lib/__templates__/nextjs/next-env.d.ts +6 -0
- package/lib/__templates__/nextjs/next.config.ts +19 -0
- package/lib/__templates__/nextjs/package.json +86 -0
- package/lib/__templates__/nextjs/pnpm-lock.yaml +10493 -0
- package/lib/__templates__/nextjs/postcss.config.mjs +7 -0
- package/lib/__templates__/nextjs/public/file.svg +1 -0
- package/lib/__templates__/nextjs/public/globe.svg +1 -0
- package/lib/__templates__/nextjs/public/next.svg +1 -0
- package/lib/__templates__/nextjs/public/vercel.svg +1 -0
- package/lib/__templates__/nextjs/public/window.svg +1 -0
- package/lib/__templates__/nextjs/scripts/build.sh +14 -0
- package/lib/__templates__/nextjs/scripts/dev.sh +33 -0
- package/lib/__templates__/nextjs/scripts/prepare.sh +9 -0
- package/lib/__templates__/nextjs/scripts/start.sh +15 -0
- package/lib/__templates__/nextjs/src/app/favicon.ico +0 -0
- package/lib/__templates__/nextjs/src/app/globals.css +137 -0
- package/lib/__templates__/nextjs/src/app/layout.tsx +72 -0
- package/lib/__templates__/nextjs/src/app/page.tsx +78 -0
- package/lib/__templates__/nextjs/src/app/robots.ts +11 -0
- package/lib/__templates__/nextjs/src/components/ui/accordion.tsx +66 -0
- package/lib/__templates__/nextjs/src/components/ui/alert-dialog.tsx +157 -0
- package/lib/__templates__/nextjs/src/components/ui/alert.tsx +66 -0
- package/lib/__templates__/nextjs/src/components/ui/aspect-ratio.tsx +11 -0
- package/lib/__templates__/nextjs/src/components/ui/avatar.tsx +53 -0
- package/lib/__templates__/nextjs/src/components/ui/badge.tsx +46 -0
- package/lib/__templates__/nextjs/src/components/ui/breadcrumb.tsx +109 -0
- package/lib/__templates__/nextjs/src/components/ui/button-group.tsx +83 -0
- package/lib/__templates__/nextjs/src/components/ui/button.tsx +62 -0
- package/lib/__templates__/nextjs/src/components/ui/calendar.tsx +220 -0
- package/lib/__templates__/nextjs/src/components/ui/card.tsx +92 -0
- package/lib/__templates__/nextjs/src/components/ui/carousel.tsx +241 -0
- package/lib/__templates__/nextjs/src/components/ui/chart.tsx +357 -0
- package/lib/__templates__/nextjs/src/components/ui/checkbox.tsx +32 -0
- package/lib/__templates__/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/lib/__templates__/nextjs/src/components/ui/command.tsx +184 -0
- package/lib/__templates__/nextjs/src/components/ui/context-menu.tsx +252 -0
- package/lib/__templates__/nextjs/src/components/ui/dialog.tsx +143 -0
- package/lib/__templates__/nextjs/src/components/ui/drawer.tsx +135 -0
- package/lib/__templates__/nextjs/src/components/ui/dropdown-menu.tsx +257 -0
- package/lib/__templates__/nextjs/src/components/ui/empty.tsx +104 -0
- package/lib/__templates__/nextjs/src/components/ui/field.tsx +248 -0
- package/lib/__templates__/nextjs/src/components/ui/form.tsx +167 -0
- package/lib/__templates__/nextjs/src/components/ui/hover-card.tsx +44 -0
- package/lib/__templates__/nextjs/src/components/ui/input-group.tsx +170 -0
- package/lib/__templates__/nextjs/src/components/ui/input-otp.tsx +77 -0
- package/lib/__templates__/nextjs/src/components/ui/input.tsx +21 -0
- package/lib/__templates__/nextjs/src/components/ui/item.tsx +193 -0
- package/lib/__templates__/nextjs/src/components/ui/kbd.tsx +28 -0
- package/lib/__templates__/nextjs/src/components/ui/label.tsx +24 -0
- package/lib/__templates__/nextjs/src/components/ui/menubar.tsx +276 -0
- package/lib/__templates__/nextjs/src/components/ui/navigation-menu.tsx +168 -0
- package/lib/__templates__/nextjs/src/components/ui/pagination.tsx +127 -0
- package/lib/__templates__/nextjs/src/components/ui/popover.tsx +48 -0
- package/lib/__templates__/nextjs/src/components/ui/progress.tsx +31 -0
- package/lib/__templates__/nextjs/src/components/ui/radio-group.tsx +45 -0
- package/lib/__templates__/nextjs/src/components/ui/resizable.tsx +63 -0
- package/lib/__templates__/nextjs/src/components/ui/scroll-area.tsx +58 -0
- package/lib/__templates__/nextjs/src/components/ui/select.tsx +190 -0
- package/lib/__templates__/nextjs/src/components/ui/separator.tsx +28 -0
- package/lib/__templates__/nextjs/src/components/ui/sheet.tsx +139 -0
- package/lib/__templates__/nextjs/src/components/ui/sidebar.tsx +724 -0
- package/lib/__templates__/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/lib/__templates__/nextjs/src/components/ui/slider.tsx +63 -0
- package/lib/__templates__/nextjs/src/components/ui/sonner.tsx +40 -0
- package/lib/__templates__/nextjs/src/components/ui/spinner.tsx +16 -0
- package/lib/__templates__/nextjs/src/components/ui/switch.tsx +31 -0
- package/lib/__templates__/nextjs/src/components/ui/table.tsx +116 -0
- package/lib/__templates__/nextjs/src/components/ui/tabs.tsx +66 -0
- package/lib/__templates__/nextjs/src/components/ui/textarea.tsx +18 -0
- package/lib/__templates__/nextjs/src/components/ui/toggle-group.tsx +83 -0
- package/lib/__templates__/nextjs/src/components/ui/toggle.tsx +47 -0
- package/lib/__templates__/nextjs/src/components/ui/tooltip.tsx +61 -0
- package/lib/__templates__/nextjs/src/hooks/use-mobile.ts +19 -0
- package/lib/__templates__/nextjs/src/lib/utils.ts +6 -0
- package/lib/__templates__/nextjs/template.config.js +85 -0
- package/lib/__templates__/nextjs/tsconfig.json +34 -0
- package/lib/__templates__/templates.json +87 -0
- package/lib/__templates__/vite/.coze +12 -0
- package/lib/__templates__/vite/README.md +239 -0
- package/lib/__templates__/vite/_gitignore +66 -0
- package/lib/__templates__/vite/_npmrc +23 -0
- package/lib/__templates__/vite/eslint.config.mjs +9 -0
- package/lib/__templates__/vite/index.html +13 -0
- package/lib/__templates__/vite/package.json +28 -0
- package/lib/__templates__/vite/pnpm-lock.yaml +4716 -0
- package/lib/__templates__/vite/postcss.config.js +6 -0
- package/lib/__templates__/vite/scripts/build.sh +14 -0
- package/lib/__templates__/vite/scripts/dev.sh +32 -0
- package/lib/__templates__/vite/scripts/prepare.sh +9 -0
- package/lib/__templates__/vite/scripts/start.sh +15 -0
- package/lib/__templates__/vite/src/index.css +21 -0
- package/lib/__templates__/vite/src/index.ts +5 -0
- package/lib/__templates__/vite/src/main.ts +64 -0
- package/lib/__templates__/vite/tailwind.config.js +9 -0
- package/lib/__templates__/vite/template.config.js +90 -0
- package/lib/__templates__/vite/tsconfig.json +16 -0
- package/lib/__templates__/vite/vite.config.ts +15 -0
- package/lib/cli.js +1916 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@/screens/demo'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@/screens/demo';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ExpoConfig, ConfigContext } from 'expo/config';
|
|
2
|
+
|
|
3
|
+
const appName = 'My App';
|
|
4
|
+
const slugAppName = 'my-app';
|
|
5
|
+
|
|
6
|
+
export default ({ config }: ConfigContext): ExpoConfig => {
|
|
7
|
+
return {
|
|
8
|
+
...config,
|
|
9
|
+
"name": appName,
|
|
10
|
+
"slug": slugAppName,
|
|
11
|
+
"version": "1.0.0",
|
|
12
|
+
"orientation": "portrait",
|
|
13
|
+
"icon": "./assets/images/icon.png",
|
|
14
|
+
"scheme": "myapp",
|
|
15
|
+
"userInterfaceStyle": "automatic",
|
|
16
|
+
"newArchEnabled": true,
|
|
17
|
+
"ios": {
|
|
18
|
+
"supportsTablet": true
|
|
19
|
+
},
|
|
20
|
+
"android": {
|
|
21
|
+
"adaptiveIcon": {
|
|
22
|
+
"foregroundImage": "./assets/images/adaptive-icon.png",
|
|
23
|
+
"backgroundColor": "#ffffff"
|
|
24
|
+
},
|
|
25
|
+
"package": "com.anonymous.myapp"
|
|
26
|
+
},
|
|
27
|
+
"web": {
|
|
28
|
+
"bundler": "metro",
|
|
29
|
+
"output": "single",
|
|
30
|
+
"favicon": "./assets/images/favicon.png"
|
|
31
|
+
},
|
|
32
|
+
"plugins": [
|
|
33
|
+
process.env.EXPO_PUBLIC_BACKEND_BASE_URL ? [
|
|
34
|
+
"expo-router",
|
|
35
|
+
{
|
|
36
|
+
"origin": process.env.EXPO_PUBLIC_BACKEND_BASE_URL
|
|
37
|
+
}
|
|
38
|
+
] : 'expo-router',
|
|
39
|
+
[
|
|
40
|
+
"expo-splash-screen",
|
|
41
|
+
{
|
|
42
|
+
"image": "./assets/images/splash-icon.png",
|
|
43
|
+
"imageWidth": 200,
|
|
44
|
+
"resizeMode": "contain",
|
|
45
|
+
"backgroundColor": "#ffffff"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"expo-image-picker",
|
|
50
|
+
{
|
|
51
|
+
"photosPermission": `允许${appName}访问您的相册,以便您上传或保存图片。`,
|
|
52
|
+
"cameraPermission": `允许${appName}使用您的相机,以便您直接拍摄照片上传。`,
|
|
53
|
+
"microphonePermission": `允许${appName}访问您的麦克风,以便您拍摄带有声音的视频。`
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"expo-location",
|
|
58
|
+
{
|
|
59
|
+
"locationWhenInUsePermission": `${appName}需要访问您的位置以提供周边服务及导航功能。`
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"expo-camera",
|
|
64
|
+
{
|
|
65
|
+
"cameraPermission": `${appName}需要访问相机以拍摄照片和视频。`,
|
|
66
|
+
"microphonePermission": `${appName}需要访问麦克风以录制视频声音。`,
|
|
67
|
+
"recordAudioAndroid": true
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
],
|
|
71
|
+
"experiments": {
|
|
72
|
+
"typedRoutes": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
ScrollView,
|
|
6
|
+
View,
|
|
7
|
+
TouchableWithoutFeedback,
|
|
8
|
+
Keyboard,
|
|
9
|
+
ViewStyle,
|
|
10
|
+
FlatList,
|
|
11
|
+
SectionList,
|
|
12
|
+
Modal,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useSafeAreaInsets, Edge } from 'react-native-safe-area-context';
|
|
15
|
+
import { StatusBar } from 'expo-status-bar';
|
|
16
|
+
// 引入 KeyboardAware 系列组件
|
|
17
|
+
import {
|
|
18
|
+
KeyboardAwareScrollView,
|
|
19
|
+
KeyboardAwareFlatList,
|
|
20
|
+
KeyboardAwareSectionList
|
|
21
|
+
} from 'react-native-keyboard-aware-scroll-view';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* # Screen 组件使用指南
|
|
25
|
+
*
|
|
26
|
+
* 核心原则:统一使用手动安全区管理 (padding),支持沉浸式布局,解决 iOS/Android 状态栏一致性问题。
|
|
27
|
+
*
|
|
28
|
+
* ## 1. 普通页面 (默认)
|
|
29
|
+
* - 场景:标准的白底或纯色背景页面,Header 在安全区下方。
|
|
30
|
+
* - 用法:`<Screen>{children}</Screen>`
|
|
31
|
+
* - 行为:自动处理上下左右安全区,状态栏文字黑色。
|
|
32
|
+
*
|
|
33
|
+
* ## 2. 沉浸式 Header (推荐)
|
|
34
|
+
* - 场景:Header 背景色/图片需要延伸到状态栏 (如首页、个人中心)。
|
|
35
|
+
* - 用法:`<Screen safeAreaEdges={['left', 'right', 'bottom']}>` (❌ 去掉 'top')
|
|
36
|
+
* - 配合:页面内部 Header 组件必须手动添加 paddingTop:
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const insets = useSafeAreaInsets();
|
|
39
|
+
* <View style={{ paddingTop: insets.top + 12, backgroundColor: '...' }}>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* ## 3. 底部有 TabBar 或 悬浮按钮
|
|
43
|
+
* - 场景:页面底部有固定导航栏,或者需要精细控制底部留白。
|
|
44
|
+
* - 用法:`<Screen safeAreaEdges={['top', 'left', 'right']}>` (❌ 去掉 'bottom')
|
|
45
|
+
* - 配合:
|
|
46
|
+
* - 若是滚动页:`<ScrollView contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}>`
|
|
47
|
+
* - 若是固定页:`<View style={{ paddingBottom: insets.bottom + 60 }}>`
|
|
48
|
+
*
|
|
49
|
+
* ## 4. 滚动列表/表单
|
|
50
|
+
* - 场景:长内容,需要键盘避让。
|
|
51
|
+
* - 用法:`<Screen>{children}</Screen>`
|
|
52
|
+
* - 行为:若子树不包含 ScrollView/FlatList/SectionList,则外层自动使用 ScrollView,
|
|
53
|
+
* 自动处理键盘遮挡,底部安全区会自动加在内容末尾。
|
|
54
|
+
*/
|
|
55
|
+
interface ScreenProps {
|
|
56
|
+
children: React.ReactNode;
|
|
57
|
+
/** 背景色,默认 #fff */
|
|
58
|
+
backgroundColor?: string;
|
|
59
|
+
/**
|
|
60
|
+
* 状态栏样式
|
|
61
|
+
* - 'dark': 黑色文字 (默认)
|
|
62
|
+
* - 'light': 白色文字 (深色背景时用)
|
|
63
|
+
*/
|
|
64
|
+
statusBarStyle?: 'auto' | 'inverted' | 'light' | 'dark';
|
|
65
|
+
/**
|
|
66
|
+
* 状态栏背景色
|
|
67
|
+
* - 默认 'transparent' 以支持沉浸式
|
|
68
|
+
* - Android 下如果需要不透明,可传入具体颜色
|
|
69
|
+
*/
|
|
70
|
+
statusBarColor?: string;
|
|
71
|
+
/**
|
|
72
|
+
* 安全区控制 (关键属性)
|
|
73
|
+
* - 默认: ['top', 'left', 'right', 'bottom'] (全避让)
|
|
74
|
+
* - 沉浸式 Header: 去掉 'top'
|
|
75
|
+
* - 自定义底部: 去掉 'bottom'
|
|
76
|
+
*/
|
|
77
|
+
safeAreaEdges?: Edge[];
|
|
78
|
+
/** 自定义容器样式 */
|
|
79
|
+
style?: ViewStyle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type KeyboardAwareProps = {
|
|
83
|
+
element: React.ReactElement<any, any>;
|
|
84
|
+
extraPadding: number;
|
|
85
|
+
contentInsetBehaviorIOS: 'automatic' | 'never';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const KeyboardAwareScrollable = ({
|
|
89
|
+
element,
|
|
90
|
+
extraPadding,
|
|
91
|
+
contentInsetBehaviorIOS,
|
|
92
|
+
}: KeyboardAwareProps) => {
|
|
93
|
+
// 获取原始组件的 props
|
|
94
|
+
const childAttrs: any = (element as any).props || {};
|
|
95
|
+
const originStyle = childAttrs['contentContainerStyle'];
|
|
96
|
+
const styleArray = Array.isArray(originStyle) ? originStyle : originStyle ? [originStyle] : [];
|
|
97
|
+
const merged = Object.assign({}, ...styleArray);
|
|
98
|
+
const currentPB = typeof merged.paddingBottom === 'number' ? merged.paddingBottom : 0;
|
|
99
|
+
|
|
100
|
+
// 合并 paddingBottom (安全区 + 额外留白)
|
|
101
|
+
const enhancedContentStyle = [{ ...merged, paddingBottom: currentPB + extraPadding }];
|
|
102
|
+
|
|
103
|
+
// 基础配置 props,用于传递给 KeyboardAware 组件
|
|
104
|
+
const commonProps = {
|
|
105
|
+
...childAttrs,
|
|
106
|
+
contentContainerStyle: enhancedContentStyle,
|
|
107
|
+
keyboardShouldPersistTaps: childAttrs['keyboardShouldPersistTaps'] ?? 'handled',
|
|
108
|
+
keyboardDismissMode: childAttrs['keyboardDismissMode'] ?? 'on-drag',
|
|
109
|
+
enableOnAndroid: true,
|
|
110
|
+
// 类似于原代码中的 setTimeout/scrollToEnd 逻辑,这里设置额外的滚动高度确保输入框可见
|
|
111
|
+
extraHeight: 100,
|
|
112
|
+
// 禁用自带的 ScrollView 自动 inset,由外部 padding 控制
|
|
113
|
+
enableAutomaticScroll: true,
|
|
114
|
+
...(Platform.OS === 'ios'
|
|
115
|
+
? { contentInsetAdjustmentBehavior: childAttrs['contentInsetAdjustmentBehavior'] ?? contentInsetBehaviorIOS }
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const t = (element as any).type;
|
|
120
|
+
|
|
121
|
+
// 根据组件类型返回对应的 KeyboardAware 版本
|
|
122
|
+
// 注意:不再使用 KeyboardAvoidingView,直接替换为增强版 ScrollView
|
|
123
|
+
if (t === ScrollView) {
|
|
124
|
+
return <KeyboardAwareScrollView {...commonProps} />;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (t === FlatList) {
|
|
128
|
+
return <KeyboardAwareFlatList {...commonProps} />;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (t === SectionList) {
|
|
132
|
+
return <KeyboardAwareSectionList {...commonProps} />;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 理论上不应运行到这里,如果是非标准组件则原样返回,仅修改样式
|
|
136
|
+
return React.cloneElement(element, {
|
|
137
|
+
contentContainerStyle: enhancedContentStyle,
|
|
138
|
+
keyboardShouldPersistTaps: childAttrs['keyboardShouldPersistTaps'] ?? 'handled',
|
|
139
|
+
keyboardDismissMode: childAttrs['keyboardDismissMode'] ?? 'on-drag',
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const Screen = ({
|
|
144
|
+
children,
|
|
145
|
+
backgroundColor = '#fff',
|
|
146
|
+
statusBarStyle = 'dark',
|
|
147
|
+
statusBarColor = 'transparent',
|
|
148
|
+
safeAreaEdges = ['top', 'left', 'right', 'bottom'],
|
|
149
|
+
style,
|
|
150
|
+
}: ScreenProps) => {
|
|
151
|
+
const insets = useSafeAreaInsets();
|
|
152
|
+
const [keyboardShown, setKeyboardShown] = React.useState(false);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
156
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
157
|
+
const s1 = Keyboard.addListener(showEvent, () => setKeyboardShown(true));
|
|
158
|
+
const s2 = Keyboard.addListener(hideEvent, () => setKeyboardShown(false));
|
|
159
|
+
return () => { s1.remove(); s2.remove(); };
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// 自动检测:若子树中包含 ScrollView/FlatList/SectionList,则认为页面自身处理滚动
|
|
163
|
+
const isNodeScrollable = (node: React.ReactNode): boolean => {
|
|
164
|
+
const isScrollableElement = (el: unknown): boolean => {
|
|
165
|
+
if (!React.isValidElement(el)) return false;
|
|
166
|
+
const element = el as React.ReactElement<any, any>;
|
|
167
|
+
const t = element.type;
|
|
168
|
+
// 不递归检查 Modal 内容,避免将弹窗内的 ScrollView 误判为页面已具备垂直滚动
|
|
169
|
+
if (t === Modal) return false;
|
|
170
|
+
const props = element.props as Record<string, unknown> | undefined;
|
|
171
|
+
// 仅识别“垂直”滚动容器;横向滚动不视为页面已处理垂直滚动
|
|
172
|
+
// eslint-disable-next-line react/prop-types
|
|
173
|
+
const isHorizontal = !!(props && (props as any).horizontal === true);
|
|
174
|
+
if ((t === ScrollView || t === FlatList || t === SectionList) && !isHorizontal) return true;
|
|
175
|
+
const c: React.ReactNode | undefined = props && 'children' in props
|
|
176
|
+
? (props.children as React.ReactNode)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (Array.isArray(c)) return c.some(isScrollableElement);
|
|
179
|
+
return c ? isScrollableElement(c) : false;
|
|
180
|
+
};
|
|
181
|
+
if (Array.isArray(node)) return node.some(isScrollableElement);
|
|
182
|
+
return isScrollableElement(node);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const childIsNativeScrollable = isNodeScrollable(children);
|
|
186
|
+
|
|
187
|
+
// 说明:避免双重补白
|
|
188
|
+
// KeyboardAwareScrollView 内部会自动处理键盘高度。
|
|
189
|
+
// 我们主要关注非键盘状态下的 Safe Area 管理。
|
|
190
|
+
|
|
191
|
+
// 解析安全区设置
|
|
192
|
+
const hasTop = safeAreaEdges.includes('top');
|
|
193
|
+
const hasBottom = safeAreaEdges.includes('bottom');
|
|
194
|
+
const hasLeft = safeAreaEdges.includes('left');
|
|
195
|
+
const hasRight = safeAreaEdges.includes('right');
|
|
196
|
+
|
|
197
|
+
// 强制禁用 iOS 自动调整内容区域,完全由手动 padding 控制,消除系统自动计算带来的多余空白
|
|
198
|
+
const contentInsetBehaviorIOS = 'never';
|
|
199
|
+
|
|
200
|
+
// 1. 外层容器样式
|
|
201
|
+
// 负责:背景色、Top/Left/Right 安全区、以及非滚动模式下的 Bottom 安全区
|
|
202
|
+
const childArray = React.Children.toArray(children);
|
|
203
|
+
let firstChild: React.ReactElement<any, any> | null = null;
|
|
204
|
+
for (let i = 0; i < childArray.length; i++) {
|
|
205
|
+
const el = childArray[i];
|
|
206
|
+
if (React.isValidElement(el)) { firstChild = el as React.ReactElement<any, any>; break; }
|
|
207
|
+
}
|
|
208
|
+
const firstChildHasInlinePaddingTop = (() => {
|
|
209
|
+
if (!firstChild) return false;
|
|
210
|
+
const st: any = (firstChild as any).props?.style;
|
|
211
|
+
const arr = Array.isArray(st) ? st : st ? [st] : [];
|
|
212
|
+
return arr.some((s) => s && typeof s === 'object' && typeof (s as any).paddingTop === 'number' && (s as any).paddingTop > 10);
|
|
213
|
+
})();
|
|
214
|
+
const applyTopInset = hasTop && !firstChildHasInlinePaddingTop;
|
|
215
|
+
|
|
216
|
+
const wrapperStyle: ViewStyle = {
|
|
217
|
+
flex: 1,
|
|
218
|
+
backgroundColor,
|
|
219
|
+
paddingTop: applyTopInset ? insets.top : 0,
|
|
220
|
+
paddingLeft: hasLeft ? insets.left : 0,
|
|
221
|
+
paddingRight: hasRight ? insets.right : 0,
|
|
222
|
+
// 当页面不使用外层 ScrollView 时(子树本身可滚动),由外层 View 负责底部安全区
|
|
223
|
+
paddingBottom: (childIsNativeScrollable && hasBottom)
|
|
224
|
+
? (keyboardShown ? 0 : insets.bottom)
|
|
225
|
+
: 0,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// 若子树不可滚动,则外层使用 KeyboardAwareScrollView 提供“全局页面滑动”能力
|
|
229
|
+
const useScrollContainer = !childIsNativeScrollable;
|
|
230
|
+
|
|
231
|
+
// 2. 滚动容器配置
|
|
232
|
+
// 如果使用滚动容器,则使用 KeyboardAwareScrollView 替代原有的 ScrollView
|
|
233
|
+
const Container = useScrollContainer ? KeyboardAwareScrollView : View;
|
|
234
|
+
|
|
235
|
+
const containerProps = useScrollContainer ? {
|
|
236
|
+
contentContainerStyle: {
|
|
237
|
+
flexGrow: 1,
|
|
238
|
+
// 滚动模式下,Bottom 安全区由内容容器处理,保证内容能完整显示且不被 Home Indicator 遮挡,同时背景色能延伸到底部
|
|
239
|
+
paddingBottom: hasBottom ? (keyboardShown ? 0 : insets.bottom) : 0,
|
|
240
|
+
},
|
|
241
|
+
keyboardShouldPersistTaps: 'handled' as const,
|
|
242
|
+
showsVerticalScrollIndicator: false,
|
|
243
|
+
keyboardDismissMode: 'on-drag' as const,
|
|
244
|
+
enableOnAndroid: true,
|
|
245
|
+
extraHeight: 100, // 替代原代码手动计算的 offset
|
|
246
|
+
// iOS 顶部白条修复:强制不自动添加顶部安全区
|
|
247
|
+
...(Platform.OS === 'ios'
|
|
248
|
+
? { contentInsetAdjustmentBehavior: contentInsetBehaviorIOS }
|
|
249
|
+
: {}),
|
|
250
|
+
} : {};
|
|
251
|
+
|
|
252
|
+
// 3. 若子元素自身包含滚动容器,给该滚动容器单独添加键盘避让,不影响其余固定元素(如底部栏)
|
|
253
|
+
const wrapScrollableWithKeyboardAvoid = (nodes: React.ReactNode): React.ReactNode => {
|
|
254
|
+
const isVerticalScrollable = (el: React.ReactElement<any, any>): boolean => {
|
|
255
|
+
const t = el.type;
|
|
256
|
+
const elementProps = (el as any).props || {};
|
|
257
|
+
const isHorizontal = !!(elementProps as any).horizontal;
|
|
258
|
+
return (t === ScrollView || t === FlatList || t === SectionList) && !isHorizontal;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const wrapIfNeeded = (el: React.ReactElement<any, any>, idx?: number): React.ReactElement => {
|
|
262
|
+
if (isVerticalScrollable(el)) {
|
|
263
|
+
return (
|
|
264
|
+
<KeyboardAwareScrollable
|
|
265
|
+
key={el.key ?? idx}
|
|
266
|
+
element={el}
|
|
267
|
+
extraPadding={keyboardShown ? 0 : (hasBottom ? insets.bottom : 0)}
|
|
268
|
+
contentInsetBehaviorIOS={contentInsetBehaviorIOS}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return el;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (Array.isArray(nodes)) {
|
|
276
|
+
return nodes.map((n, idx) => {
|
|
277
|
+
if (React.isValidElement(n)) {
|
|
278
|
+
return wrapIfNeeded(n as React.ReactElement<any, any>, idx);
|
|
279
|
+
}
|
|
280
|
+
return n;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (React.isValidElement(nodes)) {
|
|
284
|
+
return wrapIfNeeded(nodes as React.ReactElement<any, any>, 0);
|
|
285
|
+
}
|
|
286
|
+
return nodes;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
// 核心原则:严禁使用 SafeAreaView,统一使用 View + padding 手动管理
|
|
291
|
+
<View style={wrapperStyle}>
|
|
292
|
+
{/* 状态栏配置:强制透明背景 + 沉浸式,以支持背景图延伸 */}
|
|
293
|
+
<StatusBar
|
|
294
|
+
style={statusBarStyle}
|
|
295
|
+
backgroundColor={statusBarColor}
|
|
296
|
+
translucent
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
{/* 键盘避让:仅当外层使用 ScrollView 时启用,避免固定底部栏随键盘上移 */}
|
|
300
|
+
{useScrollContainer ? (
|
|
301
|
+
// 替换为 KeyboardAwareScrollView,移除原先的 KeyboardAvoidingView 包裹
|
|
302
|
+
// 因为 KeyboardAwareScrollView 已经内置了处理逻辑
|
|
303
|
+
<Container style={[styles.innerContainer, style]} {...containerProps}>
|
|
304
|
+
{children}
|
|
305
|
+
</Container>
|
|
306
|
+
) : (
|
|
307
|
+
// 页面自身已处理滚动,不启用全局键盘避让,保证固定底部栏不随键盘上移
|
|
308
|
+
childIsNativeScrollable ? (
|
|
309
|
+
<View style={[styles.innerContainer, style]}>
|
|
310
|
+
{wrapScrollableWithKeyboardAvoid(children)}
|
|
311
|
+
</View>
|
|
312
|
+
) : (
|
|
313
|
+
<TouchableWithoutFeedback onPress={Keyboard.dismiss} disabled={Platform.OS === 'web'}>
|
|
314
|
+
<View style={[styles.innerContainer, style]}>
|
|
315
|
+
{children}
|
|
316
|
+
</View>
|
|
317
|
+
</TouchableWithoutFeedback>
|
|
318
|
+
)
|
|
319
|
+
)}
|
|
320
|
+
</View>
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const styles = StyleSheet.create({
|
|
325
|
+
innerContainer: {
|
|
326
|
+
flex: 1,
|
|
327
|
+
// 确保内部容器透明,避免背景色遮挡
|
|
328
|
+
backgroundColor: 'transparent',
|
|
329
|
+
},
|
|
330
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Keyboard,
|
|
8
|
+
Platform,
|
|
9
|
+
useColorScheme,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
TextStyle
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import DateTimePickerModal from 'react-native-modal-datetime-picker';
|
|
14
|
+
import dayjs from 'dayjs';
|
|
15
|
+
import { FontAwesome6 } from '@expo/vector-icons';
|
|
16
|
+
|
|
17
|
+
// --------------------------------------------------------
|
|
18
|
+
// 1. 配置 Dayjs
|
|
19
|
+
// --------------------------------------------------------
|
|
20
|
+
// 即使服务端返回 '2023-10-20T10:00:00Z' (UTC),
|
|
21
|
+
// dayjs(utcString).format() 会自动转为手机当前的本地时区显示。
|
|
22
|
+
// 如果需要传回给后端,我们再转回 ISO 格式。
|
|
23
|
+
|
|
24
|
+
interface SmartDateInputProps {
|
|
25
|
+
label?: string; // 表单标题 (可选)
|
|
26
|
+
value?: string | null; // 服务端返回的时间字符串 (ISO 8601, 带 T)
|
|
27
|
+
onChange: (isoDate: string) => void; // 回调给父组件的值,依然是标准 ISO 字符串
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
mode?: 'date' | 'time' | 'datetime'; // 支持日期、时间、或两者
|
|
30
|
+
displayFormat?: string; // UI展示的格式,默认 YYYY-MM-DD
|
|
31
|
+
error?: string; // 错误信息
|
|
32
|
+
|
|
33
|
+
// 样式自定义(可选)
|
|
34
|
+
containerStyle?: ViewStyle; // 外层容器样式
|
|
35
|
+
inputStyle?: ViewStyle; // 输入框样式
|
|
36
|
+
textStyle?: TextStyle; // 文字样式
|
|
37
|
+
labelStyle?: TextStyle; // 标签样式
|
|
38
|
+
placeholderTextStyle?: TextStyle; // 占位符文字样式
|
|
39
|
+
errorTextStyle?: TextStyle; // 错误信息文字样式
|
|
40
|
+
iconColor?: string; // 图标颜色
|
|
41
|
+
iconSize?: number; // 图标大小
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SmartDateInput = ({
|
|
45
|
+
label,
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
placeholder = '请选择',
|
|
49
|
+
mode = 'date',
|
|
50
|
+
displayFormat,
|
|
51
|
+
error,
|
|
52
|
+
containerStyle,
|
|
53
|
+
inputStyle,
|
|
54
|
+
textStyle,
|
|
55
|
+
labelStyle,
|
|
56
|
+
placeholderTextStyle,
|
|
57
|
+
errorTextStyle,
|
|
58
|
+
iconColor,
|
|
59
|
+
iconSize = 18
|
|
60
|
+
}: SmartDateInputProps) => {
|
|
61
|
+
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
|
62
|
+
const colorScheme = useColorScheme();
|
|
63
|
+
const isDark = colorScheme === 'dark';
|
|
64
|
+
|
|
65
|
+
// 默认展示格式
|
|
66
|
+
const format = displayFormat || (mode === 'time' ? 'HH:mm' : 'YYYY-MM-DD');
|
|
67
|
+
|
|
68
|
+
// --------------------------------------------------------
|
|
69
|
+
// 2. 核心:数据转换逻辑
|
|
70
|
+
// --------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
// 解析服务端值,确保无效值不传给控件;time 模式兼容仅时间字符串
|
|
73
|
+
const parsedValue = useMemo(() => {
|
|
74
|
+
if (!value) return null;
|
|
75
|
+
|
|
76
|
+
const direct = dayjs(value);
|
|
77
|
+
if (direct.isValid()) return direct;
|
|
78
|
+
|
|
79
|
+
if (mode === 'time') {
|
|
80
|
+
const timeOnly = dayjs(`1970-01-01T${value}`);
|
|
81
|
+
if (timeOnly.isValid()) return timeOnly;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}, [value, mode]);
|
|
86
|
+
|
|
87
|
+
// A. 将字符串转为 JS Date 对象给控件使用
|
|
88
|
+
// 如果 value 是空或无效,回退到当前时间
|
|
89
|
+
const dateObjectForPicker = useMemo(() => {
|
|
90
|
+
return parsedValue ? parsedValue.toDate() : new Date();
|
|
91
|
+
}, [parsedValue]);
|
|
92
|
+
|
|
93
|
+
// B. 将 Date 对象转为展示字符串
|
|
94
|
+
const displayString = useMemo(() => {
|
|
95
|
+
if (!parsedValue) return '';
|
|
96
|
+
return parsedValue.format(format);
|
|
97
|
+
}, [parsedValue, format]);
|
|
98
|
+
|
|
99
|
+
// --------------------------------------------------------
|
|
100
|
+
// 3. 核心:交互逻辑 (解决键盘遮挡/无法收起)
|
|
101
|
+
// --------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const showDatePicker = () => {
|
|
104
|
+
// 【关键点】打开日期控件前,必须强制收起键盘!
|
|
105
|
+
// 否则键盘会遮挡 iOS 的底部滚轮,或者导致 Android 焦点混乱
|
|
106
|
+
Keyboard.dismiss();
|
|
107
|
+
setDatePickerVisibility(true);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const hideDatePicker = () => {
|
|
111
|
+
setDatePickerVisibility(false);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleConfirm = (date: Date) => {
|
|
115
|
+
hideDatePicker();
|
|
116
|
+
// 采用带本地偏移的 ISO 字符串,避免 date 模式在非 UTC 时区出现跨天
|
|
117
|
+
const serverString = dayjs(date).format(format);
|
|
118
|
+
onChange(serverString);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 根据 mode 选择图标
|
|
122
|
+
const iconName = mode === 'time' ? 'clock' : 'calendar';
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View style={[styles.container, containerStyle]}>
|
|
126
|
+
{/* 标题 */}
|
|
127
|
+
{label && <Text style={[styles.label, labelStyle]}>{label}</Text>}
|
|
128
|
+
|
|
129
|
+
{/*
|
|
130
|
+
这里用 TouchableOpacity 模拟 Input。
|
|
131
|
+
模拟组件永远不会唤起键盘。
|
|
132
|
+
*/}
|
|
133
|
+
<TouchableOpacity
|
|
134
|
+
style={[
|
|
135
|
+
styles.inputBox,
|
|
136
|
+
error ? styles.inputBoxError : null,
|
|
137
|
+
inputStyle
|
|
138
|
+
]}
|
|
139
|
+
onPress={showDatePicker}
|
|
140
|
+
activeOpacity={0.7}
|
|
141
|
+
>
|
|
142
|
+
<Text
|
|
143
|
+
style={[
|
|
144
|
+
styles.text,
|
|
145
|
+
textStyle,
|
|
146
|
+
!value && styles.placeholder,
|
|
147
|
+
!value && placeholderTextStyle
|
|
148
|
+
]}
|
|
149
|
+
numberOfLines={1}
|
|
150
|
+
>
|
|
151
|
+
{displayString || placeholder}
|
|
152
|
+
</Text>
|
|
153
|
+
|
|
154
|
+
<FontAwesome6
|
|
155
|
+
name={iconName}
|
|
156
|
+
size={iconSize}
|
|
157
|
+
color={iconColor || (value ? '#4B5563' : '#9CA3AF')}
|
|
158
|
+
style={styles.icon}
|
|
159
|
+
/>
|
|
160
|
+
</TouchableOpacity>
|
|
161
|
+
|
|
162
|
+
{error && <Text style={[styles.errorText, errorTextStyle]}>{error}</Text>}
|
|
163
|
+
|
|
164
|
+
{/*
|
|
165
|
+
DateTimePickerModal 是 React Native Modal。
|
|
166
|
+
它会覆盖在所有 View 之上。
|
|
167
|
+
*/}
|
|
168
|
+
<DateTimePickerModal
|
|
169
|
+
isVisible={isDatePickerVisible}
|
|
170
|
+
mode={mode}
|
|
171
|
+
date={dateObjectForPicker} // 传入 Date 对象
|
|
172
|
+
onConfirm={handleConfirm}
|
|
173
|
+
onCancel={hideDatePicker}
|
|
174
|
+
// iOS 只有用这个 display 样式才最稳,避免乱七八糟的 inline 样式
|
|
175
|
+
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
176
|
+
// 自动适配系统深色模式,或者根据 isDark 变量控制
|
|
177
|
+
isDarkModeEnabled={isDark}
|
|
178
|
+
// 强制使用中文环境
|
|
179
|
+
locale="zh-CN"
|
|
180
|
+
confirmTextIOS="确定"
|
|
181
|
+
cancelTextIOS="取消"
|
|
182
|
+
/>
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// 设计样式
|
|
188
|
+
const styles = StyleSheet.create({
|
|
189
|
+
container: {
|
|
190
|
+
marginBottom: 20,
|
|
191
|
+
},
|
|
192
|
+
label: {
|
|
193
|
+
fontSize: 14,
|
|
194
|
+
fontWeight: '600',
|
|
195
|
+
color: '#374151', // Gray 700
|
|
196
|
+
marginBottom: 8,
|
|
197
|
+
marginLeft: 2,
|
|
198
|
+
},
|
|
199
|
+
inputBox: {
|
|
200
|
+
height: 52, // 增加高度提升触控体验
|
|
201
|
+
backgroundColor: '#FFFFFF',
|
|
202
|
+
borderRadius: 12, // 更圆润的角
|
|
203
|
+
flexDirection: 'row',
|
|
204
|
+
alignItems: 'center',
|
|
205
|
+
justifyContent: 'space-between',
|
|
206
|
+
paddingHorizontal: 16,
|
|
207
|
+
borderWidth: 1,
|
|
208
|
+
borderColor: '#E5E7EB', // Gray 200
|
|
209
|
+
// 增加轻微阴影提升层次感 (iOS)
|
|
210
|
+
shadowColor: '#000',
|
|
211
|
+
shadowOffset: { width: 0, height: 1 },
|
|
212
|
+
shadowOpacity: 0.05,
|
|
213
|
+
shadowRadius: 2,
|
|
214
|
+
// Android
|
|
215
|
+
elevation: 1,
|
|
216
|
+
},
|
|
217
|
+
inputBoxError: {
|
|
218
|
+
borderColor: '#EF4444', // Red 500
|
|
219
|
+
backgroundColor: '#FEF2F2', // Red 50
|
|
220
|
+
},
|
|
221
|
+
text: {
|
|
222
|
+
fontSize: 16,
|
|
223
|
+
color: '#111827', // Gray 900
|
|
224
|
+
flex: 1,
|
|
225
|
+
},
|
|
226
|
+
placeholder: {
|
|
227
|
+
color: '#9CA3AF', // Gray 400 - 标准占位符颜色
|
|
228
|
+
},
|
|
229
|
+
icon: {
|
|
230
|
+
marginLeft: 12,
|
|
231
|
+
},
|
|
232
|
+
errorText: {
|
|
233
|
+
marginTop: 4,
|
|
234
|
+
marginLeft: 2,
|
|
235
|
+
fontSize: 12,
|
|
236
|
+
color: '#EF4444',
|
|
237
|
+
}
|
|
238
|
+
});
|