@coze-arch/cli 0.0.1-alpha.f91253 → 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.
- package/lib/__templates__/expo/.cozeproj/scripts/dev_run.sh +12 -4
- package/lib/__templates__/expo/README.md +2 -2
- package/lib/__templates__/expo/client/app/+not-found.tsx +30 -0
- package/lib/__templates__/expo/client/app/_layout.tsx +11 -8
- package/lib/__templates__/expo/client/app.config.ts +2 -2
- package/lib/__templates__/expo/client/components/ThemedView.tsx +1 -2
- package/lib/__templates__/expo/client/eslint.config.mjs +16 -0
- package/lib/__templates__/expo/client/hooks/{useColorScheme.ts → useColorScheme.tsx} +20 -6
- package/lib/__templates__/expo/client/hooks/useSafeRouter.ts +152 -0
- package/lib/__templates__/expo/client/package.json +3 -1
- package/lib/__templates__/expo/eslint-plugins/fontawesome6/names.js +1886 -2483
- package/lib/__templates__/expo/eslint-plugins/fontawesome6/rule.js +20 -1
- package/lib/__templates__/expo/eslint-plugins/fontawesome6/v5-only-names.js +388 -0
- package/lib/__templates__/expo/eslint-plugins/reanimated/index.js +9 -0
- package/lib/__templates__/expo/eslint-plugins/reanimated/rule.js +88 -0
- package/lib/__templates__/expo/package.json +3 -0
- package/lib/__templates__/expo/patches/expo@54.0.32.patch +44 -0
- package/lib/__templates__/expo/pnpm-lock.yaml +1924 -1846
- package/lib/__templates__/nextjs/next.config.ts +1 -1
- package/lib/__templates__/nextjs/package.json +1 -11
- package/lib/__templates__/nextjs/pnpm-lock.yaml +305 -288
- package/lib/cli.js +267 -87
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
(
|
|
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,11 +15,11 @@
|
|
|
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
|
-
│ │ └──
|
|
22
|
+
│ │ └── demo/ # demo 示例页面
|
|
23
23
|
│ │ ├── index.tsx # 页面组件实现
|
|
24
24
|
│ │ └── styles.ts # 页面样式
|
|
25
25
|
│ ├── components/ # 可复用组件
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
2
|
+
import { Link } from 'expo-router';
|
|
3
|
+
import { useTheme } from '@/hooks/useTheme';
|
|
4
|
+
import { Spacing } from '@/constants/theme';
|
|
5
|
+
|
|
6
|
+
export default function NotFoundScreen() {
|
|
7
|
+
const { theme } = useTheme();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<View style={[styles.container, { backgroundColor: theme.backgroundRoot }]}>
|
|
11
|
+
<Text>
|
|
12
|
+
页面不存在
|
|
13
|
+
</Text>
|
|
14
|
+
<Link href="/" style={[styles.gohome]}>
|
|
15
|
+
返回首页
|
|
16
|
+
</Link>
|
|
17
|
+
</View>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const styles = StyleSheet.create({
|
|
22
|
+
container: {
|
|
23
|
+
flex: 1,
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
},
|
|
27
|
+
gohome: {
|
|
28
|
+
marginTop: Spacing['2xl'],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -5,6 +5,7 @@ import { StatusBar } from 'expo-status-bar';
|
|
|
5
5
|
import { LogBox } from 'react-native';
|
|
6
6
|
import Toast from 'react-native-toast-message';
|
|
7
7
|
import { AuthProvider } from "@/contexts/AuthContext";
|
|
8
|
+
import { ColorSchemeProvider } from '@/hooks/useColorScheme';
|
|
8
9
|
|
|
9
10
|
LogBox.ignoreLogs([
|
|
10
11
|
"TurboModuleRegistry.getEnforcing(...): 'RNMapsAirModule' could not be found",
|
|
@@ -14,20 +15,22 @@ LogBox.ignoreLogs([
|
|
|
14
15
|
export default function RootLayout() {
|
|
15
16
|
return (
|
|
16
17
|
<AuthProvider>
|
|
17
|
-
<
|
|
18
|
+
<ColorSchemeProvider>
|
|
19
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
18
20
|
<StatusBar style="dark"></StatusBar>
|
|
19
21
|
<Stack screenOptions={{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// 设置所有页面的切换动画为从右侧滑入,适用于iOS 和 Android
|
|
23
|
+
animation: 'slide_from_right',
|
|
24
|
+
gestureEnabled: true,
|
|
25
|
+
gestureDirection: 'horizontal',
|
|
26
|
+
// 隐藏自带的头部
|
|
27
|
+
headerShown: false
|
|
26
28
|
}}>
|
|
27
29
|
<Stack.Screen name="index" options={{ title: "" }} />
|
|
28
30
|
</Stack>
|
|
29
31
|
<Toast />
|
|
30
|
-
|
|
32
|
+
</GestureHandlerRootView>
|
|
33
|
+
</ColorSchemeProvider>
|
|
31
34
|
</AuthProvider>
|
|
32
35
|
);
|
|
33
36
|
}
|
|
@@ -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 => {
|
|
@@ -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' | '
|
|
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
|
|
|
@@ -6,6 +6,7 @@ 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'
|
|
9
10
|
|
|
10
11
|
export default [
|
|
11
12
|
{
|
|
@@ -17,6 +18,7 @@ export default [
|
|
|
17
18
|
'.expo/**', // 排除 Expo 自动生成的文件
|
|
18
19
|
'tailwind.config.js', // 排除 Tailwind 配置文件
|
|
19
20
|
'**/*.d.ts',
|
|
21
|
+
'eslint.config.*',
|
|
20
22
|
],
|
|
21
23
|
},
|
|
22
24
|
regexp.configs["flat/recommended"],
|
|
@@ -55,6 +57,7 @@ export default [
|
|
|
55
57
|
plugins: {
|
|
56
58
|
import: pluginImport,
|
|
57
59
|
fontawesome6,
|
|
60
|
+
reanimated,
|
|
58
61
|
},
|
|
59
62
|
rules: {
|
|
60
63
|
// 关闭代码风格规则
|
|
@@ -74,6 +77,19 @@ export default [
|
|
|
74
77
|
'react/react-in-jsx-scope': 'off',
|
|
75
78
|
'react/jsx-uses-react': 'off',
|
|
76
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
|
+
],
|
|
77
93
|
},
|
|
78
94
|
},
|
|
79
95
|
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react';
|
|
2
2
|
import { ColorSchemeName, useColorScheme as useReactNativeColorScheme, Platform } from 'react-native';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const ColorSchemeContext = createContext<'light' | 'dark' | null | undefined>(null);
|
|
5
|
+
|
|
6
|
+
const ColorSchemeProvider = function ({ children }: { children?: ReactNode }) {
|
|
5
7
|
const systemColorScheme = useReactNativeColorScheme();
|
|
6
|
-
const [colorScheme, setColorScheme] = useState
|
|
8
|
+
const [colorScheme, setColorScheme] = useState(systemColorScheme);
|
|
7
9
|
|
|
8
10
|
useEffect(() => {
|
|
9
11
|
setColorScheme(systemColorScheme);
|
|
10
|
-
}, [systemColorScheme])
|
|
12
|
+
}, [systemColorScheme]);
|
|
11
13
|
|
|
12
14
|
useEffect(() => {
|
|
13
15
|
function handleMessage(e: MessageEvent<{ event: string; colorScheme: ColorSchemeName; } | undefined>) {
|
|
14
16
|
if (e.data?.event === 'coze.workbench.colorScheme') {
|
|
15
17
|
const cs = e.data.colorScheme;
|
|
16
|
-
if (typeof cs === 'string') {
|
|
18
|
+
if (typeof cs === 'string' && typeof setColorScheme === 'function') {
|
|
17
19
|
setColorScheme(cs);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -28,7 +30,19 @@ export function useColorScheme() {
|
|
|
28
30
|
window.removeEventListener('message', handleMessage, false);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
|
-
}, []);
|
|
33
|
+
}, [setColorScheme]);
|
|
34
|
+
|
|
35
|
+
return <ColorSchemeContext.Provider value={colorScheme}>
|
|
36
|
+
{children}
|
|
37
|
+
</ColorSchemeContext.Provider>
|
|
38
|
+
};
|
|
32
39
|
|
|
40
|
+
function useColorScheme() {
|
|
41
|
+
const colorScheme = useContext(ColorSchemeContext);
|
|
33
42
|
return colorScheme;
|
|
34
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
|
+
}
|
|
@@ -25,13 +25,14 @@
|
|
|
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": "
|
|
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",
|
|
37
38
|
"expo-image-picker": "~17.0.7",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"expo-linking": "~8.0.7",
|
|
40
41
|
"expo-location": "~19.0.7",
|
|
41
42
|
"expo-image": "^3.0.11",
|
|
43
|
+
"js-base64": "^3.7.7",
|
|
42
44
|
"expo-router": "~6.0.0",
|
|
43
45
|
"expo-splash-screen": "~31.0.8",
|
|
44
46
|
"expo-status-bar": "~3.0.7",
|