@coze-arch/cli 0.0.1-beta.5

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 (126) hide show
  1. package/README.md +142 -0
  2. package/bin/main +2 -0
  3. package/lib/__templates__/expo/.coze +7 -0
  4. package/lib/__templates__/expo/.cozeproj/scripts/deploy_build.sh +109 -0
  5. package/lib/__templates__/expo/.cozeproj/scripts/deploy_run.sh +257 -0
  6. package/lib/__templates__/expo/README.md +13 -0
  7. package/lib/__templates__/expo/_gitignore +11 -0
  8. package/lib/__templates__/expo/app.json +63 -0
  9. package/lib/__templates__/expo/babel.config.js +9 -0
  10. package/lib/__templates__/expo/client/app/(tabs)/_layout.tsx +43 -0
  11. package/lib/__templates__/expo/client/app/(tabs)/home.tsx +1 -0
  12. package/lib/__templates__/expo/client/app/(tabs)/index.tsx +7 -0
  13. package/lib/__templates__/expo/client/app/+not-found.tsx +79 -0
  14. package/lib/__templates__/expo/client/app/_layout.tsx +33 -0
  15. package/lib/__templates__/expo/client/assets/fonts/SpaceMono-Regular.ttf +0 -0
  16. package/lib/__templates__/expo/client/assets/images/adaptive-icon.png +0 -0
  17. package/lib/__templates__/expo/client/assets/images/default-avatar.png +0 -0
  18. package/lib/__templates__/expo/client/assets/images/favicon.png +0 -0
  19. package/lib/__templates__/expo/client/assets/images/icon.png +0 -0
  20. package/lib/__templates__/expo/client/assets/images/partial-react-logo.png +0 -0
  21. package/lib/__templates__/expo/client/assets/images/react-logo.png +0 -0
  22. package/lib/__templates__/expo/client/assets/images/react-logo@2x.png +0 -0
  23. package/lib/__templates__/expo/client/assets/images/react-logo@3x.png +0 -0
  24. package/lib/__templates__/expo/client/assets/images/splash-icon.png +0 -0
  25. package/lib/__templates__/expo/client/components/Screen.tsx +330 -0
  26. package/lib/__templates__/expo/client/components/SmartDateInput.tsx +238 -0
  27. package/lib/__templates__/expo/client/constants/theme.ts +118 -0
  28. package/lib/__templates__/expo/client/contexts/AuthContext.tsx +142 -0
  29. package/lib/__templates__/expo/client/hooks/useColorScheme.ts +1 -0
  30. package/lib/__templates__/expo/client/hooks/useTheme.ts +13 -0
  31. package/lib/__templates__/expo/client/index.js +11 -0
  32. package/lib/__templates__/expo/client/screens/home/index.tsx +54 -0
  33. package/lib/__templates__/expo/client/screens/home/styles.ts +332 -0
  34. package/lib/__templates__/expo/client/scripts/install-missing-deps.js +80 -0
  35. package/lib/__templates__/expo/client/utils/index.ts +55 -0
  36. package/lib/__templates__/expo/eslint-formatter-simple.mjs +49 -0
  37. package/lib/__templates__/expo/eslint.config.mjs +98 -0
  38. package/lib/__templates__/expo/metro.config.js +53 -0
  39. package/lib/__templates__/expo/package.json +100 -0
  40. package/lib/__templates__/expo/pnpm-lock.yaml +13978 -0
  41. package/lib/__templates__/expo/src/index.ts +12 -0
  42. package/lib/__templates__/expo/template.config.js +49 -0
  43. package/lib/__templates__/expo/tsconfig.json +24 -0
  44. package/lib/__templates__/nextjs/.coze +11 -0
  45. package/lib/__templates__/nextjs/.vscode/settings.json +121 -0
  46. package/lib/__templates__/nextjs/README.md +36 -0
  47. package/lib/__templates__/nextjs/_gitignore +99 -0
  48. package/lib/__templates__/nextjs/_npmrc +22 -0
  49. package/lib/__templates__/nextjs/eslint.config.mjs +18 -0
  50. package/lib/__templates__/nextjs/next-env.d.ts +6 -0
  51. package/lib/__templates__/nextjs/next.config.ts +7 -0
  52. package/lib/__templates__/nextjs/package.json +32 -0
  53. package/lib/__templates__/nextjs/pnpm-lock.yaml +4061 -0
  54. package/lib/__templates__/nextjs/postcss.config.mjs +7 -0
  55. package/lib/__templates__/nextjs/public/file.svg +1 -0
  56. package/lib/__templates__/nextjs/public/globe.svg +1 -0
  57. package/lib/__templates__/nextjs/public/next.svg +1 -0
  58. package/lib/__templates__/nextjs/public/vercel.svg +1 -0
  59. package/lib/__templates__/nextjs/public/window.svg +1 -0
  60. package/lib/__templates__/nextjs/scripts/build.sh +14 -0
  61. package/lib/__templates__/nextjs/scripts/dev.sh +51 -0
  62. package/lib/__templates__/nextjs/scripts/start.sh +15 -0
  63. package/lib/__templates__/nextjs/src/app/favicon.ico +0 -0
  64. package/lib/__templates__/nextjs/src/app/globals.css +26 -0
  65. package/lib/__templates__/nextjs/src/app/layout.tsx +34 -0
  66. package/lib/__templates__/nextjs/src/app/page.tsx +66 -0
  67. package/lib/__templates__/nextjs/template.config.js +55 -0
  68. package/lib/__templates__/nextjs/tsconfig.json +34 -0
  69. package/lib/__templates__/react-rsbuild/.coze +11 -0
  70. package/lib/__templates__/react-rsbuild/.vscode/settings.json +121 -0
  71. package/lib/__templates__/react-rsbuild/README.md +61 -0
  72. package/lib/__templates__/react-rsbuild/_gitignore +97 -0
  73. package/lib/__templates__/react-rsbuild/_npmrc +22 -0
  74. package/lib/__templates__/react-rsbuild/package.json +31 -0
  75. package/lib/__templates__/react-rsbuild/pnpm-lock.yaml +997 -0
  76. package/lib/__templates__/react-rsbuild/rsbuild.config.ts +13 -0
  77. package/lib/__templates__/react-rsbuild/scripts/build.sh +14 -0
  78. package/lib/__templates__/react-rsbuild/scripts/dev.sh +51 -0
  79. package/lib/__templates__/react-rsbuild/scripts/start.sh +15 -0
  80. package/lib/__templates__/react-rsbuild/src/App.tsx +60 -0
  81. package/lib/__templates__/react-rsbuild/src/index.css +21 -0
  82. package/lib/__templates__/react-rsbuild/src/index.html +12 -0
  83. package/lib/__templates__/react-rsbuild/src/index.tsx +16 -0
  84. package/lib/__templates__/react-rsbuild/tailwind.config.js +9 -0
  85. package/lib/__templates__/react-rsbuild/template.config.js +54 -0
  86. package/lib/__templates__/react-rsbuild/tsconfig.json +17 -0
  87. package/lib/__templates__/rsbuild/.coze +11 -0
  88. package/lib/__templates__/rsbuild/.vscode/settings.json +7 -0
  89. package/lib/__templates__/rsbuild/README.md +61 -0
  90. package/lib/__templates__/rsbuild/_gitignore +97 -0
  91. package/lib/__templates__/rsbuild/_npmrc +22 -0
  92. package/lib/__templates__/rsbuild/package.json +24 -0
  93. package/lib/__templates__/rsbuild/pnpm-lock.yaml +888 -0
  94. package/lib/__templates__/rsbuild/rsbuild.config.ts +12 -0
  95. package/lib/__templates__/rsbuild/scripts/build.sh +14 -0
  96. package/lib/__templates__/rsbuild/scripts/dev.sh +51 -0
  97. package/lib/__templates__/rsbuild/scripts/start.sh +15 -0
  98. package/lib/__templates__/rsbuild/src/index.css +21 -0
  99. package/lib/__templates__/rsbuild/src/index.html +12 -0
  100. package/lib/__templates__/rsbuild/src/index.ts +5 -0
  101. package/lib/__templates__/rsbuild/src/main.ts +65 -0
  102. package/lib/__templates__/rsbuild/tailwind.config.js +9 -0
  103. package/lib/__templates__/rsbuild/template.config.js +54 -0
  104. package/lib/__templates__/rsbuild/tsconfig.json +16 -0
  105. package/lib/__templates__/templates.json +100 -0
  106. package/lib/__templates__/vite/.coze +11 -0
  107. package/lib/__templates__/vite/.vscode/settings.json +7 -0
  108. package/lib/__templates__/vite/README.md +61 -0
  109. package/lib/__templates__/vite/_gitignore +66 -0
  110. package/lib/__templates__/vite/_npmrc +22 -0
  111. package/lib/__templates__/vite/index.html +13 -0
  112. package/lib/__templates__/vite/package.json +24 -0
  113. package/lib/__templates__/vite/pnpm-lock.yaml +1249 -0
  114. package/lib/__templates__/vite/postcss.config.js +6 -0
  115. package/lib/__templates__/vite/scripts/build.sh +14 -0
  116. package/lib/__templates__/vite/scripts/dev.sh +51 -0
  117. package/lib/__templates__/vite/scripts/start.sh +15 -0
  118. package/lib/__templates__/vite/src/index.css +21 -0
  119. package/lib/__templates__/vite/src/index.ts +5 -0
  120. package/lib/__templates__/vite/src/main.ts +65 -0
  121. package/lib/__templates__/vite/tailwind.config.js +9 -0
  122. package/lib/__templates__/vite/template.config.js +55 -0
  123. package/lib/__templates__/vite/tsconfig.json +16 -0
  124. package/lib/__templates__/vite/vite.config.ts +15 -0
  125. package/lib/cli.js +1575 -0
  126. package/package.json +70 -0
@@ -0,0 +1,79 @@
1
+ import { useRouter } from 'expo-router';
2
+ import { StyleSheet, View, Text, Pressable, Platform } from 'react-native';
3
+
4
+ export default function NotFoundScreen() {
5
+ const router = useRouter();
6
+
7
+ const goBack = () => {
8
+ if (router.canGoBack()) {
9
+ router.back();
10
+ }
11
+ };
12
+
13
+ return (
14
+ <View style={styles.container}>
15
+ <Text style={styles.heading}>404</Text>
16
+ <Text style={styles.textStyle}>页面未找到</Text>
17
+ <Text style={styles.subTextStyle}>
18
+ 抱歉!您访问的页面不存在,当前页面功能待完善。
19
+ </Text>
20
+ <Pressable onPress={goBack} style={styles.button}>
21
+ <Text style={styles.buttonText}>返回上一页</Text>
22
+ </Pressable>
23
+ </View>
24
+ );
25
+ }
26
+
27
+ const styles = StyleSheet.create({
28
+ container: {
29
+ flex: 1,
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ backgroundColor: '#f3f5ff',
33
+ padding: 16,
34
+ },
35
+ heading: {
36
+ fontSize: 120,
37
+ fontWeight: '900',
38
+ color: '#4a5568',
39
+ textShadowColor: 'rgba(0, 0, 0, 0.05)',
40
+ textShadowOffset: { width: 0, height: 4 },
41
+ textShadowRadius: 8,
42
+ },
43
+ textStyle: {
44
+ fontSize: 24,
45
+ fontWeight: '500',
46
+ marginTop: -16,
47
+ marginBottom: 16,
48
+ color: '#4a5568',
49
+ },
50
+ subTextStyle: {
51
+ fontSize: 16,
52
+ color: '#718096',
53
+ marginBottom: 32,
54
+ maxWidth: 400,
55
+ textAlign: 'center',
56
+ },
57
+ button: {
58
+ paddingVertical: 12,
59
+ paddingHorizontal: 24,
60
+ backgroundColor: '#6366f1',
61
+ borderRadius: 9999,
62
+ ...Platform.select({
63
+ ios: {
64
+ shadowColor: 'rgba(0, 0, 0, 0.1)',
65
+ shadowOffset: { width: 0, height: 10 },
66
+ shadowOpacity: 1,
67
+ shadowRadius: 15,
68
+ },
69
+ android: {
70
+ elevation: 5,
71
+ },
72
+ }),
73
+ },
74
+ buttonText: {
75
+ fontSize: 16,
76
+ fontWeight: '600',
77
+ color: '#ffffff',
78
+ },
79
+ });
@@ -0,0 +1,33 @@
1
+ import { useEffect } from 'react';
2
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
3
+ import { Stack } from 'expo-router';
4
+ import { StatusBar } from 'expo-status-bar';
5
+ import { LogBox } from 'react-native';
6
+ import Toast from 'react-native-toast-message';
7
+ import { AuthProvider } from "@/contexts/AuthContext";
8
+
9
+ LogBox.ignoreLogs([
10
+ "TurboModuleRegistry.getEnforcing(...): 'RNMapsAirModule' could not be found",
11
+ // 添加其它想暂时忽略的错误或警告信息
12
+ ]);
13
+
14
+ export default function RootLayout() {
15
+ return (
16
+ <AuthProvider>
17
+ <GestureHandlerRootView style={{ flex: 1 }}>
18
+ <StatusBar style="dark"></StatusBar>
19
+ <Stack screenOptions={{
20
+ // 设置所有页面的切换动画为从右侧滑入,适用于iOS 和 Android
21
+ animation: 'slide_from_right',
22
+ gestureEnabled: true,
23
+ gestureDirection: 'horizontal',
24
+ // 隐藏自带的头部
25
+ headerShown: false
26
+ }}>
27
+ <Stack.Screen name="(tabs)" options={{ title: "" }} />
28
+ </Stack>
29
+ <Toast />
30
+ </GestureHandlerRootView>
31
+ </AuthProvider>
32
+ );
33
+ }
@@ -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 = [],
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
+ });