@coze-arch/cli 0.0.1-alpha.f9be02 → 0.0.1-alpha.fd3d56

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 (84) hide show
  1. package/lib/__templates__/expo/.coze +1 -1
  2. package/lib/__templates__/expo/.cozeproj/scripts/dev_build.sh +19 -82
  3. package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +75 -86
  4. package/lib/__templates__/expo/.cozeproj/scripts/prod_build.sh +2 -2
  5. package/lib/__templates__/expo/.cozeproj/scripts/prod_run.sh +2 -2
  6. package/lib/__templates__/expo/.cozeproj/scripts/server_dev_run.sh +45 -0
  7. package/lib/__templates__/expo/README.md +68 -7
  8. package/lib/__templates__/expo/client/app/+not-found.tsx +30 -0
  9. package/lib/__templates__/expo/client/{src/app → app}/_layout.tsx +15 -12
  10. package/lib/__templates__/expo/client/app/index.tsx +1 -0
  11. package/lib/__templates__/expo/client/app.config.ts +65 -60
  12. package/lib/__templates__/expo/client/{src/components → components}/Screen.tsx +1 -17
  13. package/lib/__templates__/expo/client/{src/components → components}/ThemedView.tsx +1 -2
  14. package/lib/__templates__/expo/client/constants/theme.ts +177 -0
  15. package/lib/__templates__/expo/client/declarations.d.ts +5 -0
  16. package/lib/__templates__/expo/client/eslint.config.mjs +30 -10
  17. package/lib/__templates__/expo/client/hooks/useColorScheme.tsx +48 -0
  18. package/lib/__templates__/expo/client/hooks/useSafeRouter.ts +152 -0
  19. package/lib/__templates__/expo/client/hooks/useTheme.ts +33 -0
  20. package/lib/__templates__/expo/client/package.json +6 -3
  21. package/lib/__templates__/expo/client/screens/demo/index.tsx +25 -0
  22. package/lib/__templates__/expo/client/screens/demo/styles.ts +28 -0
  23. package/lib/__templates__/expo/client/scripts/install-missing-deps.js +1 -0
  24. package/lib/__templates__/expo/client/tsconfig.json +1 -1
  25. package/lib/__templates__/expo/client/{src/utils → utils}/index.ts +22 -0
  26. package/lib/__templates__/expo/eslint-plugins/fontawesome6/index.js +9 -0
  27. package/lib/__templates__/expo/eslint-plugins/fontawesome6/names.js +1889 -0
  28. package/lib/__templates__/expo/eslint-plugins/fontawesome6/rule.js +174 -0
  29. package/lib/__templates__/expo/eslint-plugins/fontawesome6/v5-only-names.js +388 -0
  30. package/lib/__templates__/expo/eslint-plugins/reanimated/index.js +9 -0
  31. package/lib/__templates__/expo/eslint-plugins/reanimated/rule.js +88 -0
  32. package/lib/__templates__/expo/package.json +7 -98
  33. package/lib/__templates__/expo/patches/expo@54.0.32.patch +44 -0
  34. package/lib/__templates__/expo/pnpm-lock.yaml +2001 -2124
  35. package/lib/__templates__/expo/server/build.js +21 -0
  36. package/lib/__templates__/expo/server/package.json +19 -4
  37. package/lib/__templates__/expo/server/src/index.ts +9 -2
  38. package/lib/__templates__/expo/server/tsconfig.json +24 -0
  39. package/lib/__templates__/expo/template.config.js +1 -0
  40. package/lib/__templates__/nextjs/.babelrc +15 -0
  41. package/lib/__templates__/nextjs/.coze +1 -0
  42. package/lib/__templates__/nextjs/_npmrc +1 -0
  43. package/lib/__templates__/nextjs/next.config.ts +12 -0
  44. package/lib/__templates__/nextjs/package.json +10 -11
  45. package/lib/__templates__/nextjs/pnpm-lock.yaml +2543 -1747
  46. package/lib/__templates__/nextjs/scripts/prepare.sh +9 -0
  47. package/lib/__templates__/nextjs/src/app/globals.css +10 -2
  48. package/lib/__templates__/nextjs/src/app/layout.tsx +5 -14
  49. package/lib/__templates__/nextjs/src/app/page.tsx +35 -23
  50. package/lib/__templates__/nextjs/src/components/ui/resizable.tsx +29 -22
  51. package/lib/__templates__/nextjs/src/components/ui/sidebar.tsx +228 -230
  52. package/lib/__templates__/nextjs/template.config.js +30 -0
  53. package/lib/__templates__/templates.json +61 -43
  54. package/lib/__templates__/vite/.coze +1 -0
  55. package/lib/__templates__/vite/_npmrc +1 -0
  56. package/lib/__templates__/vite/eslint.config.mjs +9 -0
  57. package/lib/__templates__/vite/package.json +10 -1
  58. package/lib/__templates__/vite/pnpm-lock.yaml +3115 -126
  59. package/lib/__templates__/vite/scripts/prepare.sh +9 -0
  60. package/lib/__templates__/vite/src/main.ts +1 -2
  61. package/lib/__templates__/vite/template.config.js +30 -4
  62. package/lib/cli.js +691 -130
  63. package/package.json +5 -3
  64. package/lib/__templates__/expo/client/src/app/index.ts +0 -1
  65. package/lib/__templates__/expo/client/src/constants/theme.ts +0 -850
  66. package/lib/__templates__/expo/client/src/hooks/useColorScheme.ts +0 -1
  67. package/lib/__templates__/expo/client/src/hooks/useTheme.ts +0 -13
  68. package/lib/__templates__/expo/client/src/screens/home/index.tsx +0 -50
  69. package/lib/__templates__/expo/client/src/screens/home/styles.ts +0 -60
  70. package/lib/__templates__/nextjs/.vscode/settings.json +0 -121
  71. package/lib/__templates__/vite/.vscode/settings.json +0 -7
  72. /package/lib/__templates__/expo/client/{src/assets → assets}/fonts/SpaceMono-Regular.ttf +0 -0
  73. /package/lib/__templates__/expo/client/{src/assets → assets}/images/adaptive-icon.png +0 -0
  74. /package/lib/__templates__/expo/client/{src/assets → assets}/images/default-avatar.png +0 -0
  75. /package/lib/__templates__/expo/client/{src/assets → assets}/images/favicon.png +0 -0
  76. /package/lib/__templates__/expo/client/{src/assets → assets}/images/icon.png +0 -0
  77. /package/lib/__templates__/expo/client/{src/assets → assets}/images/partial-react-logo.png +0 -0
  78. /package/lib/__templates__/expo/client/{src/assets → assets}/images/react-logo.png +0 -0
  79. /package/lib/__templates__/expo/client/{src/assets → assets}/images/react-logo@2x.png +0 -0
  80. /package/lib/__templates__/expo/client/{src/assets → assets}/images/react-logo@3x.png +0 -0
  81. /package/lib/__templates__/expo/client/{src/assets → assets}/images/splash-icon.png +0 -0
  82. /package/lib/__templates__/expo/client/{src/components → components}/SmartDateInput.tsx +0 -0
  83. /package/lib/__templates__/expo/client/{src/components → components}/ThemedText.tsx +0 -0
  84. /package/lib/__templates__/expo/client/{src/contexts → contexts}/AuthContext.tsx +0 -0
@@ -1,71 +1,76 @@
1
1
  import { ExpoConfig, ConfigContext } from 'expo/config';
2
2
 
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
+ const slugAppName = projectId ? `app${projectId}` : 'myapp';
6
+
3
7
  export default ({ config }: ConfigContext): ExpoConfig => {
4
8
  return {
5
9
  ...config,
6
- "name": "${app_name}",
7
- "slug": "${slug}",
8
- "version": "1.0.0",
9
- "orientation": "portrait",
10
- "icon": "./assets/images/icon.png",
11
- "scheme": "myapp",
12
- "userInterfaceStyle": "automatic",
13
- "newArchEnabled": true,
14
- "ios": {
15
- "supportsTablet": true
10
+ "name": appName,
11
+ "slug": slugAppName,
12
+ "version": "1.0.0",
13
+ "orientation": "portrait",
14
+ "icon": "./assets/images/icon.png",
15
+ "scheme": "myapp",
16
+ "userInterfaceStyle": "automatic",
17
+ "newArchEnabled": true,
18
+ "ios": {
19
+ "supportsTablet": true
20
+ },
21
+ "android": {
22
+ "adaptiveIcon": {
23
+ "foregroundImage": "./assets/images/adaptive-icon.png",
24
+ "backgroundColor": "#ffffff"
16
25
  },
17
- "android": {
18
- "adaptiveIcon": {
19
- "foregroundImage": "./assets/images/adaptive-icon.png",
26
+ "package": `com.anonymous.x${projectId || '0'}`
27
+ },
28
+ "web": {
29
+ "bundler": "metro",
30
+ "output": "single",
31
+ "favicon": "./assets/images/favicon.png"
32
+ },
33
+ "plugins": [
34
+ process.env.EXPO_PUBLIC_BACKEND_BASE_URL ? [
35
+ "expo-router",
36
+ {
37
+ "origin": process.env.EXPO_PUBLIC_BACKEND_BASE_URL
38
+ }
39
+ ] : 'expo-router',
40
+ [
41
+ "expo-splash-screen",
42
+ {
43
+ "image": "./assets/images/splash-icon.png",
44
+ "imageWidth": 200,
45
+ "resizeMode": "contain",
20
46
  "backgroundColor": "#ffffff"
21
47
  }
22
- },
23
- "web": {
24
- "bundler": "metro",
25
- "output": "single",
26
- "favicon": "./assets/images/favicon.png"
27
- },
28
- "plugins": [
29
- process.env.EXPO_PUBLIC_BACKEND_BASE_URL ? [
30
- "expo-router",
31
- {
32
- "origin": process.env.EXPO_PUBLIC_BACKEND_BASE_URL
33
- }
34
- ] : 'expo-router',
35
- [
36
- "expo-splash-screen",
37
- {
38
- "image": "./assets/images/splash-icon.png",
39
- "imageWidth": 200,
40
- "resizeMode": "contain",
41
- "backgroundColor": "#ffffff"
42
- }
43
- ],
44
- [
45
- "expo-image-picker",
46
- {
47
- "photosPermission": "允许${app_name}访问您的相册,以便您上传或保存图片。",
48
- "cameraPermission": "允许${app_name}使用您的相机,以便您直接拍摄照片上传。",
49
- "microphonePermission": "允许${app_name}访问您的麦克风,以便您拍摄带有声音的视频。"
50
- }
51
- ],
52
- [
53
- "expo-location",
54
- {
55
- "locationWhenInUsePermission": "${app_name}需要访问您的位置以提供周边服务及导航功能。"
56
- }
57
- ],
58
- [
59
- "expo-camera",
60
- {
61
- "cameraPermission": "${app_name}需要访问相机以拍摄照片和视频。",
62
- "microphonePermission": "${app_name}需要访问麦克风以录制视频声音。",
63
- "recordAudioAndroid": true
64
- }
65
- ]
66
48
  ],
67
- "experiments": {
68
- "typedRoutes": true
69
- }
49
+ [
50
+ "expo-image-picker",
51
+ {
52
+ "photosPermission": `允许${appName}访问您的相册,以便您上传或保存图片。`,
53
+ "cameraPermission": `允许${appName}使用您的相机,以便您直接拍摄照片上传。`,
54
+ "microphonePermission": `允许${appName}访问您的麦克风,以便您拍摄带有声音的视频。`
55
+ }
56
+ ],
57
+ [
58
+ "expo-location",
59
+ {
60
+ "locationWhenInUsePermission": `${appName}需要访问您的位置以提供周边服务及导航功能。`
61
+ }
62
+ ],
63
+ [
64
+ "expo-camera",
65
+ {
66
+ "cameraPermission": `${appName}需要访问相机以拍摄照片和视频。`,
67
+ "microphonePermission": `${appName}需要访问麦克风以录制视频声音。`,
68
+ "recordAudioAndroid": true
69
+ }
70
+ ]
71
+ ],
72
+ "experiments": {
73
+ "typedRoutes": true
74
+ }
70
75
  }
71
76
  }
@@ -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 负责底部安全区
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { View, ViewProps, ViewStyle } from 'react-native';
3
3
  import { useTheme } from '@/hooks/useTheme';
4
4
 
5
- type BackgroundLevel = 'root' | 'default' | 'secondary' | 'tertiary';
5
+ type BackgroundLevel = 'root' | 'default' | 'tertiary';
6
6
 
7
7
  interface ThemedViewProps extends ViewProps {
8
8
  level?: BackgroundLevel;
@@ -12,7 +12,6 @@ interface ThemedViewProps extends ViewProps {
12
12
  const backgroundMap: Record<BackgroundLevel, string> = {
13
13
  root: 'backgroundRoot',
14
14
  default: 'backgroundDefault',
15
- secondary: 'backgroundSecondary',
16
15
  tertiary: 'backgroundTertiary',
17
16
  };
18
17
 
@@ -0,0 +1,177 @@
1
+ export const Colors = {
2
+ light: {
3
+ textPrimary: "#1C1917",
4
+ textSecondary: "#78716c",
5
+ textMuted: "#9CA3AF",
6
+ primary: "#4F46E5", // Indigo-600 - 品牌主色,代表科技与智能
7
+ accent: "#8B5CF6", // Violet-500 - 辅助色,代表创造力
8
+ success: "#10B981", // Emerald-500
9
+ error: "#EF4444",
10
+ backgroundRoot: "#FAFAFA",
11
+ backgroundDefault: "#FFFFFF",
12
+ backgroundTertiary: "#F9FAFB", // 更浅的背景色,用于去线留白
13
+ buttonPrimaryText: "#FFFFFF",
14
+ tabIconSelected: "#4F46E5",
15
+ border: "#E5E7EB",
16
+ borderLight: "#F3F4F6",
17
+ },
18
+ dark: {
19
+ textPrimary: "#FAFAF9",
20
+ textSecondary: "#A8A29E",
21
+ textMuted: "#6F767E",
22
+ primary: "#818CF8", // Indigo-400 - 暗色模式品牌主色
23
+ accent: "#A78BFA", // Violet-400
24
+ success: "#34D399",
25
+ error: "#F87171",
26
+ backgroundRoot: "#09090B", // 更深的背景色
27
+ backgroundDefault: "#1C1C1E",
28
+ backgroundTertiary: "#1F1F22", // 暗色模式去线留白背景
29
+ buttonPrimaryText: "#09090B",
30
+ tabIconSelected: "#818CF8",
31
+ border: "#3F3F46",
32
+ borderLight: "#27272A",
33
+ },
34
+ };
35
+
36
+ export const Spacing = {
37
+ xs: 4,
38
+ sm: 8,
39
+ md: 12,
40
+ lg: 16,
41
+ xl: 20,
42
+ "2xl": 24,
43
+ "3xl": 32,
44
+ "4xl": 40,
45
+ "5xl": 48,
46
+ "6xl": 64,
47
+ };
48
+
49
+ export const BorderRadius = {
50
+ xs: 4,
51
+ sm: 8,
52
+ md: 12,
53
+ lg: 16,
54
+ xl: 20,
55
+ "2xl": 24,
56
+ "3xl": 28,
57
+ "4xl": 32,
58
+ full: 9999,
59
+ };
60
+
61
+ export const Typography = {
62
+ display: {
63
+ fontSize: 112,
64
+ lineHeight: 112,
65
+ fontWeight: "200" as const,
66
+ letterSpacing: -4,
67
+ },
68
+ displayLarge: {
69
+ fontSize: 112,
70
+ lineHeight: 112,
71
+ fontWeight: "200" as const,
72
+ letterSpacing: -2,
73
+ },
74
+ displayMedium: {
75
+ fontSize: 48,
76
+ lineHeight: 56,
77
+ fontWeight: "200" as const,
78
+ },
79
+ h1: {
80
+ fontSize: 32,
81
+ lineHeight: 40,
82
+ fontWeight: "700" as const,
83
+ },
84
+ h2: {
85
+ fontSize: 28,
86
+ lineHeight: 36,
87
+ fontWeight: "700" as const,
88
+ },
89
+ h3: {
90
+ fontSize: 24,
91
+ lineHeight: 32,
92
+ fontWeight: "300" as const,
93
+ },
94
+ h4: {
95
+ fontSize: 20,
96
+ lineHeight: 28,
97
+ fontWeight: "600" as const,
98
+ },
99
+ title: {
100
+ fontSize: 18,
101
+ lineHeight: 24,
102
+ fontWeight: "700" as const,
103
+ },
104
+ body: {
105
+ fontSize: 16,
106
+ lineHeight: 24,
107
+ fontWeight: "400" as const,
108
+ },
109
+ bodyMedium: {
110
+ fontSize: 16,
111
+ lineHeight: 24,
112
+ fontWeight: "500" as const,
113
+ },
114
+ small: {
115
+ fontSize: 14,
116
+ lineHeight: 20,
117
+ fontWeight: "400" as const,
118
+ },
119
+ smallMedium: {
120
+ fontSize: 14,
121
+ lineHeight: 20,
122
+ fontWeight: "500" as const,
123
+ },
124
+ caption: {
125
+ fontSize: 12,
126
+ lineHeight: 16,
127
+ fontWeight: "400" as const,
128
+ },
129
+ captionMedium: {
130
+ fontSize: 12,
131
+ lineHeight: 16,
132
+ fontWeight: "500" as const,
133
+ },
134
+ label: {
135
+ fontSize: 14,
136
+ lineHeight: 20,
137
+ fontWeight: "500" as const,
138
+ letterSpacing: 2,
139
+ textTransform: "uppercase" as const,
140
+ },
141
+ labelSmall: {
142
+ fontSize: 12,
143
+ lineHeight: 16,
144
+ fontWeight: "500" as const,
145
+ letterSpacing: 1,
146
+ textTransform: "uppercase" as const,
147
+ },
148
+ labelTitle: {
149
+ fontSize: 14,
150
+ lineHeight: 20,
151
+ fontWeight: "700" as const,
152
+ letterSpacing: 2,
153
+ textTransform: "uppercase" as const,
154
+ },
155
+ link: {
156
+ fontSize: 16,
157
+ lineHeight: 24,
158
+ fontWeight: "400" as const,
159
+ },
160
+ stat: {
161
+ fontSize: 30,
162
+ lineHeight: 36,
163
+ fontWeight: "300" as const,
164
+ },
165
+ tiny: {
166
+ fontSize: 10,
167
+ lineHeight: 14,
168
+ fontWeight: "400" as const,
169
+ },
170
+ navLabel: {
171
+ fontSize: 10,
172
+ lineHeight: 14,
173
+ fontWeight: "500" as const,
174
+ },
175
+ };
176
+
177
+ export type Theme = typeof Colors.light;
@@ -0,0 +1,5 @@
1
+ // declarations.d.ts
2
+
3
+ declare module 'expo-file-system/legacy' {
4
+ export * from 'expo-file-system';
5
+ }
@@ -1,10 +1,12 @@
1
- import js from "@eslint/js";
2
- import globals from "globals";
3
- import tseslint from "typescript-eslint";
4
- import pluginReact from "eslint-plugin-react";
5
- import reactHooks from "eslint-plugin-react-hooks";
6
- import regexp from "eslint-plugin-regexp";
7
- import pluginImport from "eslint-plugin-import";
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import tseslint from 'typescript-eslint';
4
+ import pluginReact from 'eslint-plugin-react';
5
+ import reactHooks from 'eslint-plugin-react-hooks';
6
+ import regexp from 'eslint-plugin-regexp';
7
+ import pluginImport from 'eslint-plugin-import';
8
+ import fontawesome6 from '../eslint-plugins/fontawesome6/index.js';
9
+ import reanimated from '../eslint-plugins/reanimated/index.js'
8
10
 
9
11
  export default [
10
12
  {
@@ -15,19 +17,21 @@ export default [
15
17
  'src/api/**', // 排除 src 下的自动生成 API
16
18
  '.expo/**', // 排除 Expo 自动生成的文件
17
19
  'tailwind.config.js', // 排除 Tailwind 配置文件
20
+ '**/*.d.ts',
21
+ 'eslint.config.*',
18
22
  ],
19
23
  },
20
24
  regexp.configs["flat/recommended"],
21
25
  js.configs.recommended,
22
26
  ...tseslint.configs.recommended,
23
-
27
+
24
28
  // React 的推荐配置
25
29
  pluginReact.configs.flat.recommended,
26
30
  pluginReact.configs.flat['jsx-runtime'],
27
31
  reactHooks.configs.flat.recommended,
28
32
  {
29
33
  files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
30
-
34
+
31
35
  // 语言选项:设置全局变量
32
36
  languageOptions: {
33
37
  globals: {
@@ -52,6 +56,8 @@ export default [
52
56
 
53
57
  plugins: {
54
58
  import: pluginImport,
59
+ fontawesome6,
60
+ reanimated,
55
61
  },
56
62
  rules: {
57
63
  // 关闭代码风格规则
@@ -70,6 +76,20 @@ export default [
70
76
  'no-prototype-builtins': 'off',
71
77
  'react/react-in-jsx-scope': 'off',
72
78
  'react/jsx-uses-react': 'off',
79
+ 'fontawesome6/valid-name': 'error',
80
+ 'reanimated/ban-mix-use': 'error',
81
+ // 禁止使用 via.placeholder.com 服务
82
+ 'no-restricted-syntax': [
83
+ 'error',
84
+ {
85
+ 'selector': 'Literal[value=/via\\.placeholder\\.com/]',
86
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
87
+ },
88
+ {
89
+ 'selector': 'TemplateLiteral > TemplateElement[value.raw=/via\\.placeholder\\.com/]',
90
+ 'message': 'via.placeholder.com 服务不可用,禁止在代码中使用',
91
+ },
92
+ ],
73
93
  },
74
94
  },
75
95
 
@@ -91,7 +111,7 @@ export default [
91
111
  // 在 .js 文件中关闭 TS 规则
92
112
  '@typescript-eslint/no-require-imports': 'off',
93
113
  // 在 Node.js 文件中允许 require
94
- '@typescript-eslint/no-var-requires': 'off',
114
+ '@typescript-eslint/no-var-requires': 'off',
95
115
  'no-undef': 'off',
96
116
  },
97
117
  },
@@ -0,0 +1,48 @@
1
+ import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react';
2
+ import { ColorSchemeName, useColorScheme as useReactNativeColorScheme, Platform } from 'react-native';
3
+
4
+ const ColorSchemeContext = createContext<'light' | 'dark' | null | undefined>(null);
5
+
6
+ const ColorSchemeProvider = function ({ children }: { children?: ReactNode }) {
7
+ const systemColorScheme = useReactNativeColorScheme();
8
+ const [colorScheme, setColorScheme] = useState(systemColorScheme);
9
+
10
+ useEffect(() => {
11
+ setColorScheme(systemColorScheme);
12
+ }, [systemColorScheme]);
13
+
14
+ useEffect(() => {
15
+ function handleMessage(e: MessageEvent<{ event: string; colorScheme: ColorSchemeName; } | undefined>) {
16
+ if (e.data?.event === 'coze.workbench.colorScheme') {
17
+ const cs = e.data.colorScheme;
18
+ if (typeof cs === 'string' && typeof setColorScheme === 'function') {
19
+ setColorScheme(cs);
20
+ }
21
+ }
22
+ }
23
+
24
+ if (Platform.OS === 'web') {
25
+ window.addEventListener('message', handleMessage, false);
26
+ }
27
+
28
+ return () => {
29
+ if (Platform.OS === 'web') {
30
+ window.removeEventListener('message', handleMessage, false);
31
+ }
32
+ }
33
+ }, [setColorScheme]);
34
+
35
+ return <ColorSchemeContext.Provider value={colorScheme}>
36
+ {children}
37
+ </ColorSchemeContext.Provider>
38
+ };
39
+
40
+ function useColorScheme() {
41
+ const colorScheme = useContext(ColorSchemeContext);
42
+ return colorScheme;
43
+ }
44
+
45
+ export {
46
+ ColorSchemeProvider,
47
+ useColorScheme,
48
+ }
@@ -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
+ }
@@ -0,0 +1,33 @@
1
+ import { Colors } from '@/constants/theme';
2
+ import { useColorScheme } from '@/hooks/useColorScheme';
3
+
4
+ enum COLOR_SCHEME_CHOICE {
5
+ FOLLOW_SYSTEM = 'follow-system', // 跟随系统自动变化
6
+ DARK = 'dark', // 固定为 dark 主题,不随系统变化
7
+ LIGHT = 'light', // 固定为 light 主题,不随系统变化
8
+ };
9
+
10
+ const userPreferColorScheme: COLOR_SCHEME_CHOICE = COLOR_SCHEME_CHOICE.FOLLOW_SYSTEM;
11
+
12
+ function getTheme(colorScheme?: 'dark' | 'light' | null) {
13
+ const isDark = colorScheme === 'dark';
14
+ const theme = Colors[colorScheme ?? 'light'];
15
+
16
+ return {
17
+ theme,
18
+ isDark,
19
+ };
20
+ }
21
+
22
+ function useTheme() {
23
+ const systemColorScheme = useColorScheme()
24
+ const colorScheme = userPreferColorScheme === COLOR_SCHEME_CHOICE.FOLLOW_SYSTEM ?
25
+ systemColorScheme :
26
+ userPreferColorScheme;
27
+
28
+ return getTheme(colorScheme);
29
+ }
30
+
31
+ export {
32
+ useTheme,
33
+ }