feat: 实现减脂体重管理App完整功能

- 实现拍照识别食物功能(集成大语言模型视觉能力)
- 实现智能对话功能(集成大语言模型流式输出)
- 实现食物记录和卡路里管理功能
- 实现体重记录和统计功能
- 实现健康数据管理页面
- 配置数据库表结构(用户、食物记录、体重记录)
- 实现Express后端API路由
- 配置Tab导航和前端页面
- 采用健康运动配色方案
This commit is contained in:
jaystar
2026-02-02 15:17:50 +08:00
commit 28c4d7b3b4
82 changed files with 21891 additions and 0 deletions

View 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,
}

View File

@@ -0,0 +1,152 @@
/**
* 安全路由 Hook - 完全代替原生的 useRouter 和 useLocalSearchParams
*
* 提供的 Hook
* - useSafeRouter: 代替 useRouter包含所有路由方法并对 push/replace/navigate/setParams 进行安全编码
* - useSafeSearchParams: 代替 useLocalSearchParams获取路由参数
*
* 解决的问题:
* 1. URI 编解码不对称 - useLocalSearchParams 会自动解码,但 router.push 不会自动编码,
* 当参数包含 % 等特殊字符时会拿到错误的值
* 2. 类型丢失 - URL 参数全是 stringNumber/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
View 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,
}