@coze-arch/cli 0.0.1-alpha.4c5c53 → 0.0.1-alpha.531ccd

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 (38) hide show
  1. package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +12 -4
  2. package/lib/__templates__/expo/README.md +7 -5
  3. package/lib/__templates__/expo/client/app/+not-found.tsx +15 -0
  4. package/lib/__templates__/expo/client/app/_layout.tsx +14 -12
  5. package/lib/__templates__/expo/client/app.config.ts +2 -2
  6. package/lib/__templates__/expo/client/components/Screen.tsx +1 -17
  7. package/lib/__templates__/expo/client/eslint.config.mjs +20 -0
  8. package/lib/__templates__/expo/client/global.css +76 -0
  9. package/lib/__templates__/expo/client/hooks/useSafeRouter.ts +152 -0
  10. package/lib/__templates__/expo/client/metro.config.js +8 -1
  11. package/lib/__templates__/expo/client/package.json +11 -7
  12. package/lib/__templates__/expo/client/screens/demo/index.tsx +6 -12
  13. package/lib/__templates__/expo/client/uniwind-types.d.ts +10 -0
  14. package/lib/__templates__/expo/eslint-plugins/fontawesome6/names.js +1886 -2483
  15. package/lib/__templates__/expo/eslint-plugins/fontawesome6/rule.js +20 -1
  16. package/lib/__templates__/expo/eslint-plugins/fontawesome6/v5-only-names.js +388 -0
  17. package/lib/__templates__/expo/eslint-plugins/react-native/index.js +9 -0
  18. package/lib/__templates__/expo/eslint-plugins/react-native/rule.js +64 -0
  19. package/lib/__templates__/expo/eslint-plugins/reanimated/index.js +9 -0
  20. package/lib/__templates__/expo/eslint-plugins/reanimated/rule.js +88 -0
  21. package/lib/__templates__/expo/package.json +3 -0
  22. package/lib/__templates__/expo/patches/expo@54.0.32.patch +44 -0
  23. package/lib/__templates__/expo/pnpm-lock.yaml +1924 -1846
  24. package/lib/__templates__/nextjs/.babelrc +15 -0
  25. package/lib/__templates__/nextjs/package.json +9 -1
  26. package/lib/__templates__/nextjs/pnpm-lock.yaml +2603 -1728
  27. package/lib/__templates__/nextjs/src/app/layout.tsx +5 -3
  28. package/lib/__templates__/nextjs/template.config.js +5 -5
  29. package/lib/__templates__/vite/package.json +6 -1
  30. package/lib/__templates__/vite/pnpm-lock.yaml +504 -982
  31. package/lib/__templates__/vite/template.config.js +6 -4
  32. package/lib/cli.js +819 -105
  33. package/package.json +1 -1
  34. package/lib/__templates__/expo/client/components/ThemedText.tsx +0 -33
  35. package/lib/__templates__/expo/client/components/ThemedView.tsx +0 -38
  36. package/lib/__templates__/expo/client/constants/theme.ts +0 -854
  37. package/lib/__templates__/expo/client/hooks/useColorScheme.ts +0 -34
  38. package/lib/__templates__/expo/client/hooks/useTheme.ts +0 -33
@@ -15,13 +15,14 @@ EXPO_PORT="5000"
15
15
  WEB_URL="${COZE_PROJECT_DOMAIN_DEFAULT:-http://127.0.0.1:${SERVER_PORT}}"
16
16
  ASSUME_YES="1"
17
17
  EXPO_PUBLIC_BACKEND_BASE_URL="${EXPO_PUBLIC_BACKEND_BASE_URL:-$WEB_URL}"
18
-
18
+ EXPO_PUBLIC_COZE_PROJECT_ID="${COZE_PROJECT_ID:-}"
19
19
 
20
20
  EXPO_PACKAGER_PROXY_URL="${EXPO_PUBLIC_BACKEND_BASE_URL}"
21
- export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL
21
+ export EXPO_PUBLIC_BACKEND_BASE_URL EXPO_PACKAGER_PROXY_URL EXPO_PUBLIC_COZE_PROJECT_ID
22
22
  # 运行时变量(为避免 set -u 的未绑定错误,预置为空)
23
23
  SERVER_PID=""
24
24
  EXPO_PID=""
25
+
25
26
  # ==================== 工具函数 ====================
26
27
  check_command() {
27
28
  if ! command -v "$1" &> /dev/null; then
@@ -114,10 +115,10 @@ start_expo() {
114
115
  pushd "$ROOT_DIR/client"
115
116
 
116
117
  if [ "$offline" = "1" ]; then
117
- ( EXPO_OFFLINE=1 EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" \
118
+ ( EXPO_OFFLINE=1 EXPO_NO_DEPENDENCY_VALIDATION=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" EXPO_PUBLIC_COZE_PROJECT_ID="$EXPO_PUBLIC_COZE_PROJECT_ID" \
118
119
  nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$ROOT_DIR/logs/client.log" ) &
119
120
  else
120
- ( EXPO_NO_DOCTOR=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" \
121
+ ( EXPO_NO_DEPENDENCY_VALIDATION=1 EXPO_PUBLIC_BACKEND_BASE_URL="$EXPO_PUBLIC_BACKEND_BASE_URL" EXPO_PACKAGER_PROXY_URL="$EXPO_PACKAGER_PROXY_URL" EXPO_PUBLIC_COZE_PROJECT_ID="$EXPO_PUBLIC_COZE_PROJECT_ID" \
121
122
  nohup npx expo start --clear --port "$EXPO_PORT" 2>&1 | pipe_to_log "CLIENT" "$ROOT_DIR/logs/client.log" ) &
122
123
  fi
123
124
  EXPO_PID=$!
@@ -150,6 +151,12 @@ if [ -f "$PREVIEW_DIR/pre_install.py" ]; then
150
151
  python "$PREVIEW_DIR/pre_install.py" || echo "pre_install.py 执行失败"
151
152
  fi
152
153
 
154
+ echo "检查根目录 post_install.py"
155
+ if [ -f "$PREVIEW_DIR/post_install.py" ]; then
156
+ echo "执行:python $PREVIEW_DIR/post_install.py"
157
+ python "$PREVIEW_DIR/post_install.py" || echo "post_install.py 执行失败"
158
+ fi
159
+
153
160
  echo "==================== 开始启动 ===================="
154
161
  echo "开始执行服务启动脚本(start_dev.sh)..."
155
162
  echo "正在检查依赖命令和目录是否存在..."
@@ -188,6 +195,7 @@ fi
188
195
  echo "Expo 环境变量配置:"
189
196
  echo "EXPO_PUBLIC_BACKEND_BASE_URL=${EXPO_PUBLIC_BACKEND_BASE_URL}"
190
197
  echo "EXPO_PACKAGER_PROXY_URL=${EXPO_PACKAGER_PROXY_URL}"
198
+ echo "EXPO_PUBLIC_COZE_PROJECT_ID=${EXPO_PUBLIC_COZE_PROJECT_ID}"
191
199
  if [ -z "${EXPO_PID}" ]; then
192
200
  echo "无法获取 Expo 后台进程 PID"
193
201
  fi
@@ -15,18 +15,16 @@
15
15
  | └── package.json # 服务端 package.json
16
16
  ├── client/ # React Native 前端代码
17
17
  │ ├── app/ # Expo Router 路由目录(仅路由配置)
18
- │ │ ├── _layout.tsx # 根布局文件(必需)
18
+ │ │ ├── _layout.tsx # 根布局文件(必需,务必阅读)
19
19
  │ │ ├── home.tsx # 首页
20
20
  │ │ └── index.tsx # re-export home.tsx
21
21
  │ ├── screens/ # 页面实现目录(与 app/ 路由对应)
22
- │ │ └── home/
23
- │ │ ├── index.tsx # 页面组件实现
24
- │ │ └── styles.ts # 页面样式
22
+ │ │ └── demo/ # demo 示例页面
23
+ │ │ └── index.tsx # 页面组件实现
25
24
  │ ├── components/ # 可复用组件
26
25
  │ │ └── Screen.tsx # 页面容器组件(必用)
27
26
  │ ├── hooks/ # 自定义 Hooks
28
27
  │ ├── contexts/ # React Context 代码
29
- │ ├── constants/ # 常量定义(如主题配置)
30
28
  │ ├── utils/ # 工具函数
31
29
  │ ├── assets/ # 静态资源
32
30
  | └── package.json # Expo 应用 package.json
@@ -34,6 +32,10 @@
34
32
  ├── .cozeproj # 预置脚手架脚本(禁止修改)
35
33
  └── .coze # 配置文件(禁止修改)
36
34
 
35
+ ## 样式方案(TailwindCSS v4)
36
+
37
+ 本项目通过 uniwind 实现了 react-native 对 TailwindCSS v4 的支持,在开始开发前,应先查看 client/global.css 了解必要的信息
38
+
37
39
  ## 安装依赖
38
40
 
39
41
  ### 命令
@@ -0,0 +1,15 @@
1
+ import { View, Text } from 'react-native';
2
+ import { Link } from 'expo-router';
3
+
4
+ export default function NotFoundScreen() {
5
+ return (
6
+ <View className="flex-1 justify-center items-center bg-background">
7
+ <Text className="text-foreground">
8
+ 页面不存在
9
+ </Text>
10
+ <Link href="/" className="text-accent mt-6">
11
+ 返回首页
12
+ </Link>
13
+ </View>
14
+ );
15
+ }
@@ -6,6 +6,8 @@ import { LogBox } from 'react-native';
6
6
  import Toast from 'react-native-toast-message';
7
7
  import { AuthProvider } from "@/contexts/AuthContext";
8
8
 
9
+ import '../global.css';
10
+
9
11
  LogBox.ignoreLogs([
10
12
  "TurboModuleRegistry.getEnforcing(...): 'RNMapsAirModule' could not be found",
11
13
  // 添加其它想暂时忽略的错误或警告信息
@@ -15,18 +17,18 @@ export default function RootLayout() {
15
17
  return (
16
18
  <AuthProvider>
17
19
  <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="index" options={{ title: "" }} />
28
- </Stack>
29
- <Toast />
20
+ <StatusBar style="dark"></StatusBar>
21
+ <Stack screenOptions={{
22
+ // 设置所有页面的切换动画为从右侧滑入,适用于iOS 和 Android
23
+ animation: 'slide_from_right',
24
+ gestureEnabled: true,
25
+ gestureDirection: 'horizontal',
26
+ // 隐藏自带的头部
27
+ headerShown: false
28
+ }}>
29
+ <Stack.Screen name="index" options={{ title: "" }} />
30
+ </Stack>
31
+ <Toast />
30
32
  </GestureHandlerRootView>
31
33
  </AuthProvider>
32
34
  );
@@ -1,7 +1,7 @@
1
1
  import { ExpoConfig, ConfigContext } from 'expo/config';
2
2
 
3
- const appName = process.env.EXPO_PUBLIC_COZE_PROJECT_NAME || '应用';
4
- const projectId = process.env.EXPO_PUBLIC_COZE_PROJECT_ID;
3
+ const appName = process.env.COZE_PROJECT_NAME || process.env.EXPO_PUBLIC_COZE_PROJECT_NAME || '应用';
4
+ const projectId = process.env.COZE_PROJECT_ID || process.env.EXPO_PUBLIC_COZE_PROJECT_ID;
5
5
  const slugAppName = projectId ? `app${projectId}` : 'myapp';
6
6
 
7
7
  export default ({ config }: ConfigContext): ExpoConfig => {
@@ -197,26 +197,10 @@ export const Screen = ({
197
197
  // 强制禁用 iOS 自动调整内容区域,完全由手动 padding 控制,消除系统自动计算带来的多余空白
198
198
  const contentInsetBehaviorIOS = 'never';
199
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
200
  const wrapperStyle: ViewStyle = {
217
201
  flex: 1,
218
202
  backgroundColor,
219
- paddingTop: applyTopInset ? insets.top : 0,
203
+ paddingTop: hasTop ? insets.top : 0,
220
204
  paddingLeft: hasLeft ? insets.left : 0,
221
205
  paddingRight: hasRight ? insets.right : 0,
222
206
  // 当页面不使用外层 ScrollView 时(子树本身可滚动),由外层 View 负责底部安全区
@@ -6,6 +6,8 @@ import reactHooks from 'eslint-plugin-react-hooks';
6
6
  import regexp from 'eslint-plugin-regexp';
7
7
  import pluginImport from 'eslint-plugin-import';
8
8
  import fontawesome6 from '../eslint-plugins/fontawesome6/index.js';
9
+ import reanimated from '../eslint-plugins/reanimated/index.js';
10
+ import reactnative from '../eslint-plugins/react-native/index.js';
9
11
 
10
12
  export default [
11
13
  {
@@ -16,6 +18,8 @@ export default [
16
18
  'src/api/**', // 排除 src 下的自动生成 API
17
19
  '.expo/**', // 排除 Expo 自动生成的文件
18
20
  'tailwind.config.js', // 排除 Tailwind 配置文件
21
+ '**/*.d.ts',
22
+ 'eslint.config.*',
19
23
  ],
20
24
  },
21
25
  regexp.configs["flat/recommended"],
@@ -54,6 +58,8 @@ export default [
54
58
  plugins: {
55
59
  import: pluginImport,
56
60
  fontawesome6,
61
+ reanimated,
62
+ reactnative,
57
63
  },
58
64
  rules: {
59
65
  // 关闭代码风格规则
@@ -73,6 +79,20 @@ export default [
73
79
  'react/react-in-jsx-scope': 'off',
74
80
  'react/jsx-uses-react': 'off',
75
81
  'fontawesome6/valid-name': 'error',
82
+ 'reanimated/ban-mix-use': 'error',
83
+ // 禁止使用 via.placeholder.com 服务
84
+ 'no-restricted-syntax': [
85
+ 'error',
86
+ {
87
+ 'selector': 'Literal[value=/via\\.placeholder\\.com/]',
88
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
89
+ },
90
+ {
91
+ 'selector': 'TemplateLiteral > TemplateElement[value.raw=/via\\.placeholder\\.com/]',
92
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
93
+ },
94
+ ],
95
+ 'reactnative/wrap-horizontal-scrollview-inside-view': ['error'],
76
96
  },
77
97
  },
78
98
 
@@ -0,0 +1,76 @@
1
+ @import 'tailwindcss';
2
+ @import 'uniwind';
3
+
4
+ :root,
5
+ .light,
6
+ .default,
7
+ [data-theme="light"],
8
+ [data-theme="default"] {
9
+ /* Theme Colors (Light Mode) */
10
+ --accent: oklch(55.00% 0.2500 254.00);
11
+ --accent-foreground: oklch(99.11% 0 0);
12
+ --background: oklch(97.02% 0.0040 254.00);
13
+ --border: oklch(90.00% 0.0040 254.00);
14
+ --danger: oklch(65.32% 0.2347 25.76);
15
+ --danger-foreground: oklch(99.11% 0 0);
16
+ --default: oklch(94.00% 0.0040 254.00);
17
+ --default-foreground: oklch(21.03% 0.0059 254.00);
18
+ --field-background: oklch(100.00% 0.0020 254.00);
19
+ --field-foreground: oklch(21.03% 0.0059 254.00);
20
+ --field-placeholder: oklch(55.17% 0.0081 254.00);
21
+ --focus: oklch(55.00% 0.2500 254.00);
22
+ --foreground: oklch(21.03% 0.0059 254.00);
23
+ --muted: oklch(55.17% 0.0081 254.00);
24
+ --overlay: oklch(100.00% 0.0012 254.00);
25
+ --overlay-foreground: oklch(21.03% 0.0059 254.00);
26
+ --scrollbar: oklch(87.10% 0.0040 254.00);
27
+ --segment: oklch(100.00% 0.0040 254.00);
28
+ --segment-foreground: oklch(21.03% 0.0059 254.00);
29
+ --separator: oklch(92.00% 0.0040 254.00);
30
+ --success: oklch(73.29% 0.1951 150.83);
31
+ --success-foreground: oklch(21.03% 0.0059 150.83);
32
+ --surface: oklch(100.00% 0.0020 254.00);
33
+ --surface-foreground: oklch(21.03% 0.0059 254.00);
34
+ --warning: oklch(78.19% 0.1598 72.35);
35
+ --warning-foreground: oklch(21.03% 0.0059 72.35);
36
+
37
+ /* Border Radius */
38
+ --radius: 0.75rem;
39
+ --field-radius: 0.5rem;
40
+
41
+ /* Font Family */
42
+ /* Make sure to load Google Sans font in your app */
43
+ --font-sans: var(--font-google-sans);
44
+ }
45
+
46
+ .dark,
47
+ [data-theme="dark"] {
48
+ color-scheme: dark;
49
+ /* Theme Colors (Dark Mode) */
50
+ --accent: oklch(55.00% 0.2500 254.00);
51
+ --accent-foreground: oklch(99.11% 0 0);
52
+ --background: oklch(12.00% 0.0040 254.00);
53
+ --border: oklch(28.00% 0.0040 254.00);
54
+ --danger: oklch(59.40% 0.1983 24.65);
55
+ --danger-foreground: oklch(99.11% 0 0);
56
+ --default: oklch(27.40% 0.0040 254.00);
57
+ --default-foreground: oklch(99.11% 0 0);
58
+ --field-background: oklch(21.03% 0.0081 254.00);
59
+ --field-foreground: oklch(99.11% 0.0000 0.00);
60
+ --field-placeholder: oklch(70.50% 0.0081 254.00);
61
+ --focus: oklch(55.00% 0.2500 254.00);
62
+ --foreground: oklch(99.11% 0.0000 0.00);
63
+ --muted: oklch(70.50% 0.0081 254.00);
64
+ --overlay: oklch(21.03% 0.0081 254.00);
65
+ --overlay-foreground: oklch(99.11% 0.0000 0.00);
66
+ --scrollbar: oklch(70.50% 0.0040 254.00);
67
+ --segment: oklch(39.64% 0.0040 254.00);
68
+ --segment-foreground: oklch(99.11% 0.0000 0.00);
69
+ --separator: oklch(25.00% 0.0040 254.00);
70
+ --success: oklch(73.29% 0.1951 150.83);
71
+ --success-foreground: oklch(21.03% 0.0059 150.83);
72
+ --surface: oklch(21.03% 0.0081 254.00);
73
+ --surface-foreground: oklch(99.11% 0.0000 0.00);
74
+ --warning: oklch(82.03% 0.1399 76.36);
75
+ --warning-foreground: oklch(21.03% 0.0059 76.36);
76
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 安全路由 Hook - 完全代替原生的 useRouter 和 useLocalSearchParams
3
+ *
4
+ * 提供的 Hook:
5
+ * - useSafeRouter: 代替 useRouter,包含所有路由方法,并对 push/replace/navigate/setParams 进行安全编码
6
+ * - useSafeSearchParams: 代替 useLocalSearchParams,获取路由参数
7
+ *
8
+ * 解决的问题:
9
+ * 1. URI 编解码不对称 - useLocalSearchParams 会自动解码,但 router.push 不会自动编码,
10
+ * 当参数包含 % 等特殊字符时会拿到错误的值
11
+ * 2. 类型丢失 - URL 参数全是 string,Number/Boolean 类型会丢失
12
+ * 3. 嵌套对象无法传递 - URL search params 不支持嵌套结构
13
+ *
14
+ * 解决方案:
15
+ * 采用 Payload 模式,将所有参数打包成 JSON 并 Base64 编码后传递,
16
+ * 接收时再解码还原,确保数据完整性和类型安全。
17
+ *
18
+ * 优点:
19
+ * 1. 自动处理所有特殊字符(如 %、&、=、中文、Emoji 等)
20
+ * 2. 保留数据类型(Number、Boolean 不会变成 String)
21
+ * 3. 支持嵌套对象和数组传递
22
+ * 4. 三端兼容(iOS、Android、Web)
23
+ *
24
+ * 使用方式:
25
+ * ```tsx
26
+ * // 发送端 - 使用 useSafeRouter 代替 useRouter
27
+ * const router = useSafeRouter();
28
+ * router.push('/detail', { id: 123, uri: 'file:///path/%40test.mp3' });
29
+ * router.replace('/home', { tab: 'settings' });
30
+ * router.navigate('/profile', { userId: 456 });
31
+ * router.back();
32
+ * if (router.canGoBack()) { ... }
33
+ * router.setParams({ updated: true });
34
+ *
35
+ * // 接收端 - 使用 useSafeSearchParams 代替 useLocalSearchParams
36
+ * const { id, uri } = useSafeSearchParams<{ id: number; uri: string }>();
37
+ * ```
38
+ */
39
+ import { useMemo } from 'react';
40
+ import { useRouter as useExpoRouter, useLocalSearchParams as useExpoParams } from 'expo-router';
41
+ import { Base64 } from 'js-base64';
42
+
43
+ const PAYLOAD_KEY = '__safeRouterPayload__';
44
+ const LOG_PREFIX = '[SafeRouter]';
45
+
46
+
47
+ const getCurrentParams = (rawParams: Record<string, string | string[]>): Record<string, unknown> => {
48
+ const payload = rawParams[PAYLOAD_KEY];
49
+ if (payload && typeof payload === 'string') {
50
+ const decoded = deserializeParams<Record<string, unknown>>(payload);
51
+ if (decoded && Object.keys(decoded).length > 0) {
52
+ return decoded;
53
+ }
54
+ }
55
+ const { [PAYLOAD_KEY]: _, ...rest } = rawParams;
56
+ return rest as Record<string, unknown>;
57
+ };
58
+
59
+ const serializeParams = (params: Record<string, unknown>): string => {
60
+ try {
61
+ const jsonStr = JSON.stringify(params);
62
+ return Base64.encode(jsonStr);
63
+ } catch (error) {
64
+ console.error(LOG_PREFIX, 'serialize failed:', error instanceof Error ? error.message : 'Unknown error');
65
+ return '';
66
+ }
67
+ };
68
+
69
+ const deserializeParams = <T = Record<string, unknown>>(
70
+ payload: string | string[] | undefined
71
+ ): T | null => {
72
+ if (!payload || typeof payload !== 'string') {
73
+ return null;
74
+ }
75
+ try {
76
+ const jsonStr = Base64.decode(payload);
77
+ return JSON.parse(jsonStr) as T;
78
+ } catch (error) {
79
+ console.error(LOG_PREFIX, 'deserialize failed:', error instanceof Error ? error.message : 'Unknown error');
80
+ return null;
81
+ }
82
+ };
83
+
84
+
85
+ /**
86
+ * 安全路由 Hook,用于页面跳转,代替 useRouter
87
+ * @returns 路由方法(继承 useRouter 所有方法,并对以下方法进行安全编码)
88
+ * - push(pathname, params) - 入栈新页面
89
+ * - replace(pathname, params) - 替换当前页面
90
+ * - navigate(pathname, params) - 智能跳转(已存在则返回,否则 push)
91
+ * - setParams(params) - 更新当前页面参数(合并现有参数)
92
+ */
93
+ export function useSafeRouter() {
94
+ const router = useExpoRouter();
95
+ const rawParams = useExpoParams<Record<string, string | string[]>>();
96
+
97
+ const push = (pathname: string, params: Record<string, unknown> = {}) => {
98
+ const encodedPayload = serializeParams(params);
99
+ router.push({
100
+ pathname: pathname as `/${string}`,
101
+ params: { [PAYLOAD_KEY]: encodedPayload },
102
+ });
103
+ };
104
+
105
+ const replace = (pathname: string, params: Record<string, unknown> = {}) => {
106
+ const encodedPayload = serializeParams(params);
107
+ router.replace({
108
+ pathname: pathname as `/${string}`,
109
+ params: { [PAYLOAD_KEY]: encodedPayload },
110
+ });
111
+ };
112
+
113
+ const navigate = (pathname: string, params: Record<string, unknown> = {}) => {
114
+ const encodedPayload = serializeParams(params);
115
+ router.navigate({
116
+ pathname: pathname as `/${string}`,
117
+ params: { [PAYLOAD_KEY]: encodedPayload },
118
+ });
119
+ };
120
+
121
+ const setParams = (params: Record<string, unknown>) => {
122
+ const currentParams = getCurrentParams(rawParams);
123
+ const mergedParams = { ...currentParams, ...params };
124
+ const encodedPayload = serializeParams(mergedParams);
125
+ router.setParams({ [PAYLOAD_KEY]: encodedPayload });
126
+ };
127
+
128
+ return {
129
+ ...router,
130
+ push,
131
+ replace,
132
+ navigate,
133
+ setParams,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * 安全获取路由参数 Hook,用于接收方,代替 useLocalSearchParams
139
+ * 兼容两种跳转方式:
140
+ * 1. useSafeRouter 跳转 - 自动解码 Payload
141
+ * 2. 外部跳转(深链接、浏览器直接访问等)- 回退到原始参数
142
+ * @returns 解码后的参数对象,类型安全
143
+ */
144
+ export function useSafeSearchParams<T = Record<string, unknown>>(): T {
145
+ const rawParams = useExpoParams<Record<string, string | string[]>>();
146
+
147
+ const decodedParams = useMemo(() => {
148
+ return getCurrentParams(rawParams) as T;
149
+ }, [rawParams]);
150
+
151
+ return decodedParams;
152
+ }
@@ -1,6 +1,7 @@
1
1
  const { getDefaultConfig } = require('expo/metro-config');
2
2
  const { createProxyMiddleware } = require('http-proxy-middleware');
3
3
  const connect = require('connect');
4
+ const { withUniwindConfig } = require('uniwind/metro');
4
5
 
5
6
  const config = getDefaultConfig(__dirname);
6
7
 
@@ -118,4 +119,10 @@ config.server = {
118
119
  },
119
120
  };
120
121
 
121
- module.exports = config;
122
+ module.exports = withUniwindConfig(config, {
123
+ // relative path to your global.css file (from previous step)
124
+ cssEntryFile: './global.css',
125
+ // (optional) path where we gonna auto-generate typings
126
+ // defaults to project's root
127
+ dtsFile: './uniwind-types.d.ts'
128
+ });
@@ -25,26 +25,28 @@
25
25
  "@react-navigation/bottom-tabs": "^7.2.0",
26
26
  "@react-navigation/native": "^7.0.14",
27
27
  "dayjs": "^1.11.19",
28
- "expo": "^54.0.7",
28
+ "expo": "54.0.32",
29
29
  "expo-auth-session": "^7.0.9",
30
30
  "expo-av": "~16.0.6",
31
31
  "expo-blur": "~15.0.6",
32
32
  "expo-camera": "~17.0.10",
33
33
  "expo-constants": "~18.0.8",
34
34
  "expo-crypto": "^15.0.7",
35
+ "expo-file-system": "~19.0.21",
35
36
  "expo-font": "~14.0.7",
36
37
  "expo-haptics": "~15.0.6",
38
+ "expo-image": "^3.0.11",
37
39
  "expo-image-picker": "~17.0.7",
38
40
  "expo-linear-gradient": "~15.0.6",
39
41
  "expo-linking": "~8.0.7",
40
42
  "expo-location": "~19.0.7",
41
- "expo-image": "^3.0.11",
42
43
  "expo-router": "~6.0.0",
43
44
  "expo-splash-screen": "~31.0.8",
44
45
  "expo-status-bar": "~3.0.7",
45
46
  "expo-symbols": "~1.0.6",
46
47
  "expo-system-ui": "~6.0.9",
47
48
  "expo-web-browser": "~15.0.10",
49
+ "js-base64": "^3.7.7",
48
50
  "react": "19.1.0",
49
51
  "react-dom": "19.1.0",
50
52
  "react-native": "0.81.5",
@@ -60,17 +62,20 @@
60
62
  "react-native-web": "^0.21.2",
61
63
  "react-native-webview": "~13.15.0",
62
64
  "react-native-worklets": "0.5.1",
65
+ "tailwindcss": "^4.1.18",
66
+ "uniwind": "^1.2.7",
63
67
  "zod": "^4.2.1"
64
68
  },
65
69
  "devDependencies": {
66
70
  "@babel/core": "^7.25.2",
67
- "babel-plugin-module-resolver": "^5.0.2",
68
- "babel-preset-expo": "^54.0.9",
69
71
  "@eslint/js": "^9.27.0",
70
72
  "@types/jest": "^29.5.12",
71
73
  "@types/react": "~19.1.0",
72
74
  "@types/react-test-renderer": "19.1.0",
75
+ "babel-plugin-module-resolver": "^5.0.2",
76
+ "babel-preset-expo": "^54.0.9",
73
77
  "chalk": "^4.1.2",
78
+ "connect": "^3.7.0",
74
79
  "depcheck": "^1.4.7",
75
80
  "esbuild": "0.27.2",
76
81
  "eslint": "^9.39.2",
@@ -81,13 +86,12 @@
81
86
  "eslint-plugin-react-hooks": "^7.0.1",
82
87
  "eslint-plugin-regexp": "^2.10.0",
83
88
  "globals": "^16.1.0",
89
+ "http-proxy-middleware": "^3.0.5",
84
90
  "jest": "^29.2.1",
85
91
  "jest-expo": "~54.0.10",
86
92
  "react-test-renderer": "19.1.0",
87
93
  "tsx": "^4.21.0",
88
94
  "typescript": "^5.8.3",
89
- "typescript-eslint": "^8.32.1",
90
- "connect": "^3.7.0",
91
- "http-proxy-middleware": "^3.0.5"
95
+ "typescript-eslint": "^8.32.1"
92
96
  }
93
97
  }
@@ -1,24 +1,18 @@
1
1
  import { View, Text } from 'react-native';
2
2
  import { Image } from 'expo-image';
3
3
 
4
- import { useTheme } from '@/hooks/useTheme';
5
4
  import { Screen } from '@/components/Screen';
6
- import { styles } from './styles';
7
5
 
8
6
  export default function DemoPage() {
9
- const { theme, isDark } = useTheme();
10
-
11
7
  return (
12
- <Screen backgroundColor={theme.backgroundRoot} statusBarStyle={isDark ? 'light' : 'dark'}>
13
- <View
14
- style={styles.container}
15
- >
8
+ <Screen backgroundColor="var(--background)" statusBarStyle="auto">
9
+ <View className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
16
10
  <Image
17
- style={styles.logo}
11
+ className="w-[130px] h-[109px]"
18
12
  source="https://lf-coze-web-cdn.coze.cn/obj/eden-cn/lm-lgvj/ljhwZthlaukjlkulzlp/coze-coding/expo/coze-loading.gif"
19
- ></Image>
20
- <Text style={{...styles.title, color: theme.textPrimary}}>APP 开发中</Text>
21
- <Text style={{...styles.description, color: theme.textSecondary}}>即将为您呈现应用界面</Text>
13
+ />
14
+ <Text className="text-base font-bold text-foreground">APP 开发中</Text>
15
+ <Text className="text-sm mt-2 text-muted">即将为您呈现应用界面</Text>
22
16
  </View>
23
17
  </Screen>
24
18
  );
@@ -0,0 +1,10 @@
1
+ // NOTE: This file is generated by uniwind and it should not be edited manually.
2
+ /// <reference types="uniwind/types" />
3
+
4
+ declare module 'uniwind' {
5
+ export interface UniwindConfig {
6
+ themes: readonly ['light', 'dark']
7
+ }
8
+ }
9
+
10
+ export {}