- 实现拍照识别食物功能(集成大语言模型视觉能力) - 实现智能对话功能(集成大语言模型流式输出) - 实现食物记录和卡路里管理功能 - 实现体重记录和统计功能 - 实现健康数据管理页面 - 配置数据库表结构(用户、食物记录、体重记录) - 实现Express后端API路由 - 配置Tab导航和前端页面 - 采用健康运动配色方案
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
/**
|
||
* 安全路由 Hook - 完全代替原生的 useRouter 和 useLocalSearchParams
|
||
*
|
||
* 提供的 Hook:
|
||
* - useSafeRouter: 代替 useRouter,包含所有路由方法,并对 push/replace/navigate/setParams 进行安全编码
|
||
* - useSafeSearchParams: 代替 useLocalSearchParams,获取路由参数
|
||
*
|
||
* 解决的问题:
|
||
* 1. URI 编解码不对称 - useLocalSearchParams 会自动解码,但 router.push 不会自动编码,
|
||
* 当参数包含 % 等特殊字符时会拿到错误的值
|
||
* 2. 类型丢失 - URL 参数全是 string,Number/Boolean 类型会丢失
|
||
* 3. 嵌套对象无法传递 - URL search params 不支持嵌套结构
|
||
*
|
||
* 解决方案:
|
||
* 采用 Payload 模式,将所有参数打包成 JSON 并 Base64 编码后传递,
|
||
* 接收时再解码还原,确保数据完整性和类型安全。
|
||
*
|
||
* 优点:
|
||
* 1. 自动处理所有特殊字符(如 %、&、=、中文、Emoji 等)
|
||
* 2. 保留数据类型(Number、Boolean 不会变成 String)
|
||
* 3. 支持嵌套对象和数组传递
|
||
* 4. 三端兼容(iOS、Android、Web)
|
||
*
|
||
* 使用方式:
|
||
* ```tsx
|
||
* // 发送端 - 使用 useSafeRouter 代替 useRouter
|
||
* const router = useSafeRouter();
|
||
* router.push('/detail', { id: 123, uri: 'file:///path/%40test.mp3' });
|
||
* router.replace('/home', { tab: 'settings' });
|
||
* router.navigate('/profile', { userId: 456 });
|
||
* router.back();
|
||
* if (router.canGoBack()) { ... }
|
||
* router.setParams({ updated: true });
|
||
*
|
||
* // 接收端 - 使用 useSafeSearchParams 代替 useLocalSearchParams
|
||
* const { id, uri } = useSafeSearchParams<{ id: number; uri: string }>();
|
||
* ```
|
||
*/
|
||
import { useMemo } from 'react';
|
||
import { useRouter as useExpoRouter, useLocalSearchParams as useExpoParams } from 'expo-router';
|
||
import { Base64 } from 'js-base64';
|
||
|
||
const PAYLOAD_KEY = '__safeRouterPayload__';
|
||
const LOG_PREFIX = '[SafeRouter]';
|
||
|
||
|
||
const getCurrentParams = (rawParams: Record<string, string | string[]>): Record<string, unknown> => {
|
||
const payload = rawParams[PAYLOAD_KEY];
|
||
if (payload && typeof payload === 'string') {
|
||
const decoded = deserializeParams<Record<string, unknown>>(payload);
|
||
if (decoded && Object.keys(decoded).length > 0) {
|
||
return decoded;
|
||
}
|
||
}
|
||
const { [PAYLOAD_KEY]: _, ...rest } = rawParams;
|
||
return rest as Record<string, unknown>;
|
||
};
|
||
|
||
const serializeParams = (params: Record<string, unknown>): string => {
|
||
try {
|
||
const jsonStr = JSON.stringify(params);
|
||
return Base64.encode(jsonStr);
|
||
} catch (error) {
|
||
console.error(LOG_PREFIX, 'serialize failed:', error instanceof Error ? error.message : 'Unknown error');
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const deserializeParams = <T = Record<string, unknown>>(
|
||
payload: string | string[] | undefined
|
||
): T | null => {
|
||
if (!payload || typeof payload !== 'string') {
|
||
return null;
|
||
}
|
||
try {
|
||
const jsonStr = Base64.decode(payload);
|
||
return JSON.parse(jsonStr) as T;
|
||
} catch (error) {
|
||
console.error(LOG_PREFIX, 'deserialize failed:', error instanceof Error ? error.message : 'Unknown error');
|
||
return null;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* 安全路由 Hook,用于页面跳转,代替 useRouter
|
||
* @returns 路由方法(继承 useRouter 所有方法,并对以下方法进行安全编码)
|
||
* - push(pathname, params) - 入栈新页面
|
||
* - replace(pathname, params) - 替换当前页面
|
||
* - navigate(pathname, params) - 智能跳转(已存在则返回,否则 push)
|
||
* - setParams(params) - 更新当前页面参数(合并现有参数)
|
||
*/
|
||
export function useSafeRouter() {
|
||
const router = useExpoRouter();
|
||
const rawParams = useExpoParams<Record<string, string | string[]>>();
|
||
|
||
const push = (pathname: string, params: Record<string, unknown> = {}) => {
|
||
const encodedPayload = serializeParams(params);
|
||
router.push({
|
||
pathname: pathname as `/${string}`,
|
||
params: { [PAYLOAD_KEY]: encodedPayload },
|
||
});
|
||
};
|
||
|
||
const replace = (pathname: string, params: Record<string, unknown> = {}) => {
|
||
const encodedPayload = serializeParams(params);
|
||
router.replace({
|
||
pathname: pathname as `/${string}`,
|
||
params: { [PAYLOAD_KEY]: encodedPayload },
|
||
});
|
||
};
|
||
|
||
const navigate = (pathname: string, params: Record<string, unknown> = {}) => {
|
||
const encodedPayload = serializeParams(params);
|
||
router.navigate({
|
||
pathname: pathname as `/${string}`,
|
||
params: { [PAYLOAD_KEY]: encodedPayload },
|
||
});
|
||
};
|
||
|
||
const setParams = (params: Record<string, unknown>) => {
|
||
const currentParams = getCurrentParams(rawParams);
|
||
const mergedParams = { ...currentParams, ...params };
|
||
const encodedPayload = serializeParams(mergedParams);
|
||
router.setParams({ [PAYLOAD_KEY]: encodedPayload });
|
||
};
|
||
|
||
return {
|
||
...router,
|
||
push,
|
||
replace,
|
||
navigate,
|
||
setParams,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 安全获取路由参数 Hook,用于接收方,代替 useLocalSearchParams
|
||
* 兼容两种跳转方式:
|
||
* 1. useSafeRouter 跳转 - 自动解码 Payload
|
||
* 2. 外部跳转(深链接、浏览器直接访问等)- 回退到原始参数
|
||
* @returns 解码后的参数对象,类型安全
|
||
*/
|
||
export function useSafeSearchParams<T = Record<string, unknown>>(): T {
|
||
const rawParams = useExpoParams<Record<string, string | string[]>>();
|
||
|
||
const decodedParams = useMemo(() => {
|
||
return getCurrentParams(rawParams) as T;
|
||
}, [rawParams]);
|
||
|
||
return decodedParams;
|
||
}
|