feat: 实现减脂体重管理App完整功能
- 实现拍照识别食物功能(集成大语言模型视觉能力) - 实现智能对话功能(集成大语言模型流式输出) - 实现食物记录和卡路里管理功能 - 实现体重记录和统计功能 - 实现健康数据管理页面 - 配置数据库表结构(用户、食物记录、体重记录) - 实现Express后端API路由 - 配置Tab导航和前端页面 - 采用健康运动配色方案
This commit is contained in:
48
client/hooks/useColorScheme.tsx
Normal file
48
client/hooks/useColorScheme.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react';
|
||||
import { ColorSchemeName, useColorScheme as useReactNativeColorScheme, Platform } from 'react-native';
|
||||
|
||||
const ColorSchemeContext = createContext<'light' | 'dark' | null | undefined>(null);
|
||||
|
||||
const ColorSchemeProvider = function ({ children }: { children?: ReactNode }) {
|
||||
const systemColorScheme = useReactNativeColorScheme();
|
||||
const [colorScheme, setColorScheme] = useState(systemColorScheme);
|
||||
|
||||
useEffect(() => {
|
||||
setColorScheme(systemColorScheme);
|
||||
}, [systemColorScheme]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage(e: MessageEvent<{ event: string; colorScheme: ColorSchemeName; } | undefined>) {
|
||||
if (e.data?.event === 'coze.workbench.colorScheme') {
|
||||
const cs = e.data.colorScheme;
|
||||
if (typeof cs === 'string' && typeof setColorScheme === 'function') {
|
||||
setColorScheme(cs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
window.addEventListener('message', handleMessage, false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (Platform.OS === 'web') {
|
||||
window.removeEventListener('message', handleMessage, false);
|
||||
}
|
||||
}
|
||||
}, [setColorScheme]);
|
||||
|
||||
return <ColorSchemeContext.Provider value={colorScheme}>
|
||||
{children}
|
||||
</ColorSchemeContext.Provider>
|
||||
};
|
||||
|
||||
function useColorScheme() {
|
||||
const colorScheme = useContext(ColorSchemeContext);
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
export {
|
||||
ColorSchemeProvider,
|
||||
useColorScheme,
|
||||
}
|
||||
152
client/hooks/useSafeRouter.ts
Normal file
152
client/hooks/useSafeRouter.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 安全路由 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;
|
||||
}
|
||||
33
client/hooks/useTheme.ts
Normal file
33
client/hooks/useTheme.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Colors } from '@/constants/theme';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
enum COLOR_SCHEME_CHOICE {
|
||||
FOLLOW_SYSTEM = 'follow-system', // 跟随系统自动变化
|
||||
DARK = 'dark', // 固定为 dark 主题,不随系统变化
|
||||
LIGHT = 'light', // 固定为 light 主题,不随系统变化
|
||||
};
|
||||
|
||||
const userPreferColorScheme: COLOR_SCHEME_CHOICE = COLOR_SCHEME_CHOICE.FOLLOW_SYSTEM;
|
||||
|
||||
function getTheme(colorScheme?: 'dark' | 'light' | null) {
|
||||
const isDark = colorScheme === 'dark';
|
||||
const theme = Colors[colorScheme ?? 'light'];
|
||||
|
||||
return {
|
||||
theme,
|
||||
isDark,
|
||||
};
|
||||
}
|
||||
|
||||
function useTheme() {
|
||||
const systemColorScheme = useColorScheme()
|
||||
const colorScheme = userPreferColorScheme === COLOR_SCHEME_CHOICE.FOLLOW_SYSTEM ?
|
||||
systemColorScheme :
|
||||
userPreferColorScheme;
|
||||
|
||||
return getTheme(colorScheme);
|
||||
}
|
||||
|
||||
export {
|
||||
useTheme,
|
||||
}
|
||||
Reference in New Issue
Block a user