feat(auth): 添加登录验证到食物记录相关功能
- 在食物拍照、语音记录和营养成分分析功能中添加登录验证 - 使用 ensureLoggedIn 方法确保用户已登录后再调用服务端接口 - 使用 pushIfAuthedElseLogin 方法处理需要登录的页面导航 - 添加新的营养图标资源 - 在路由常量中添加 FOOD_CAMERA 路由定义 - 更新 Memory Bank 任务文档,记录登录验证和路由常量管理的实现模式
This commit is contained in:
@@ -146,3 +146,201 @@ const styles = StyleSheet.create({
|
|||||||
- `app/food/nutrition-analysis-history.tsx` - 删除按钮实现
|
- `app/food/nutrition-analysis-history.tsx` - 删除按钮实现
|
||||||
- `components/glass/button.tsx` - 通用 Glass 按钮组件
|
- `components/glass/button.tsx` - 通用 Glass 按钮组件
|
||||||
- `app/(tabs)/_layout.tsx` - 标签栏按钮实现
|
- `app/(tabs)/_layout.tsx` - 标签栏按钮实现
|
||||||
|
|
||||||
|
## 登录验证实现模式
|
||||||
|
|
||||||
|
**最后更新**: 2025-10-16
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
在应用中实现需要登录才能访问的功能时,需要判断用户是否已登录,未登录时先跳转到登录页面。
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
使用 `useAuthGuard` hook 中的 `pushIfAuthedElseLogin` 方法处理需要登录验证的导航操作,使用 `ensureLoggedIn` 方法处理需要登录验证的功能实现。
|
||||||
|
|
||||||
|
### 权限校验原则
|
||||||
|
**重要**: 功能实现如果包含服务端接口的调用,需要使用 `ensureLoggedIn` 来判断用户是否登录。
|
||||||
|
|
||||||
|
### 实现模式
|
||||||
|
|
||||||
|
#### 1. 导入必要的 hook
|
||||||
|
```typescript
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 在组件中获取方法
|
||||||
|
```typescript
|
||||||
|
const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 替换导航操作
|
||||||
|
```typescript
|
||||||
|
// ❌ 原来的写法 - 没有登录验证
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.push('/food/nutrition-analysis-history')}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
|
||||||
|
// ✅ 修改后的写法 - 带登录验证
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 服务端接口调用的登录验证
|
||||||
|
对于需要调用服务端接口的功能,使用 `ensureLoggedIn` 进行登录验证:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 原来的写法 - 没有登录验证
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => startNewAnalysis(imageUri)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
|
||||||
|
// ✅ 修改后的写法 - 带登录验证
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={async () => {
|
||||||
|
// 先验证登录状态
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
startNewAnalysis(imageUri);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 完整示例(包含 Liquid Glass 兼容性处理)
|
||||||
|
```typescript
|
||||||
|
{isLiquidGlassAvailable() ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<GlassView
|
||||||
|
style={styles.historyButton}
|
||||||
|
glassEffectStyle="clear"
|
||||||
|
tintColor="rgba(255, 255, 255, 0.2)"
|
||||||
|
isInteractive={true}
|
||||||
|
>
|
||||||
|
<Ionicons name="time-outline" size={24} color="#333" />
|
||||||
|
</GlassView>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
|
||||||
|
style={[styles.historyButton, styles.fallbackBackground]}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Ionicons name="time-outline" size={24} color="#333" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重要注意事项
|
||||||
|
1. **统一体验**:使用 `pushIfAuthedElseLogin` 可以确保登录后自动跳转到目标页面
|
||||||
|
2. **参数传递**:该方法支持传递路由参数,格式为 `pushIfAuthedElseLogin('/path', { param: value })`
|
||||||
|
3. **登录重定向**:登录页面会接收 `redirectTo` 和 `redirectParams` 参数用于登录后跳转
|
||||||
|
4. **兼容性**:与 Liquid Glass 设计风格完全兼容,可以同时使用
|
||||||
|
5. **服务端接口调用**:所有调用服务端接口的功能必须使用 `ensureLoggedIn` 进行登录验证
|
||||||
|
6. **异步处理**:`ensureLoggedIn` 是异步函数,需要使用 `await` 等待结果
|
||||||
|
|
||||||
|
### 其他可用方法
|
||||||
|
- `ensureLoggedIn()` - 检查登录状态,未登录时跳转到登录页面,返回布尔值表示是否已登录
|
||||||
|
- `guardHandler(fn, options)` - 包装一个函数,在执行前确保用户已登录
|
||||||
|
- `isLoggedIn` - 布尔值,表示当前用户是否已登录
|
||||||
|
|
||||||
|
### 使用场景选择
|
||||||
|
- **页面导航**:使用 `pushIfAuthedElseLogin` 处理页面跳转
|
||||||
|
- **服务端接口调用**:使用 `ensureLoggedIn` 验证登录状态后再执行功能
|
||||||
|
- **函数包装**:使用 `guardHandler` 包装需要登录验证的函数
|
||||||
|
|
||||||
|
### 参考实现
|
||||||
|
- `app/food/nutrition-label-analysis.tsx` - 成分表分析功能登录验证
|
||||||
|
- `app/(tabs)/personal.tsx` - 个人中心编辑按钮
|
||||||
|
- `hooks/useAuthGuard.ts` - 完整的认证守卫实现
|
||||||
|
|
||||||
|
## 路由常量管理
|
||||||
|
|
||||||
|
**最后更新**: 2025-10-16
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
在应用开发中,所有路由路径都应该使用常量定义,而不是硬编码字符串。这样可以确保路由的一致性,便于维护和重构。
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
将所有路由路径定义在 `constants/Routes.ts` 文件中,并在组件中使用这些常量。
|
||||||
|
|
||||||
|
### 实现模式
|
||||||
|
|
||||||
|
#### 1. 添加新路由常量
|
||||||
|
在 `constants/Routes.ts` 文件中添加新的路由常量:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const ROUTES = {
|
||||||
|
// 现有路由...
|
||||||
|
|
||||||
|
// 新增路由
|
||||||
|
FOOD_CAMERA: '/food/camera',
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 在组件中使用路由常量
|
||||||
|
导入并使用路由常量,而不是硬编码路径:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
|
// ❌ 错误写法 - 硬编码路径
|
||||||
|
router.push('/food/camera?mealType=dinner');
|
||||||
|
|
||||||
|
// ✅ 正确写法 - 使用路由常量
|
||||||
|
router.push(`${ROUTES.FOOD_CAMERA}?mealType=dinner`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 结合登录验证使用
|
||||||
|
对于需要登录验证的路由,结合 `pushIfAuthedElseLogin` 使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
|
||||||
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
|
|
||||||
|
// 在需要登录验证的路由中使用
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => pushIfAuthedElseLogin(`${ROUTES.FOOD_CAMERA}?mealType=${currentMealType}`)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重要注意事项
|
||||||
|
1. **统一管理**:所有路由路径都必须在 `constants/Routes.ts` 中定义
|
||||||
|
2. **命名规范**:使用大写字母和下划线,如 `FOOD_CAMERA`
|
||||||
|
3. **路径一致性**:常量名应该清晰表达路由的用途
|
||||||
|
4. **参数处理**:查询参数和路径参数在使用时动态拼接
|
||||||
|
5. **类型安全**:使用 `as const` 确保类型推导
|
||||||
|
|
||||||
|
### 路由分类
|
||||||
|
按照功能模块对路由进行分组:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const ROUTES = {
|
||||||
|
// Tab路由
|
||||||
|
TAB_EXPLORE: '/explore',
|
||||||
|
TAB_COACH: '/coach',
|
||||||
|
|
||||||
|
// 营养相关路由
|
||||||
|
NUTRITION_RECORDS: '/nutrition/records',
|
||||||
|
FOOD_LIBRARY: '/food-library',
|
||||||
|
FOOD_CAMERA: '/food/camera',
|
||||||
|
|
||||||
|
// 用户相关路由
|
||||||
|
AUTH_LOGIN: '/auth/login',
|
||||||
|
PROFILE_EDIT: '/profile/edit',
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参考实现
|
||||||
|
- `constants/Routes.ts` - 路由常量定义
|
||||||
|
- `components/NutritionRadarCard.tsx` - 使用路由常量和登录验证
|
||||||
|
- `app/food/camera.tsx` - 食物拍照页面实现
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
import { CameraType, CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
@@ -24,6 +25,7 @@ export default function FoodCameraScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useLocalSearchParams<{ mealType?: string }>();
|
const params = useLocalSearchParams<{ mealType?: string }>();
|
||||||
const cameraRef = useRef<CameraView>(null);
|
const cameraRef = useRef<CameraView>(null);
|
||||||
|
const { ensureLoggedIn } = useAuthGuard();
|
||||||
|
|
||||||
const [currentMealType, setCurrentMealType] = useState<MealType>(
|
const [currentMealType, setCurrentMealType] = useState<MealType>(
|
||||||
(params.mealType as MealType) || 'dinner'
|
(params.mealType as MealType) || 'dinner'
|
||||||
@@ -103,10 +105,13 @@ export default function FoodCameraScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (photo) {
|
if (photo) {
|
||||||
// 跳转到食物识别页面
|
// 先验证登录状态,再跳转到食物识别页面
|
||||||
console.log('照片拍摄成功:', photo.uri);
|
console.log('照片拍摄成功:', photo.uri);
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
router.replace(`/food/food-recognition?imageUri=${encodeURIComponent(photo.uri)}&mealType=${currentMealType}`);
|
router.replace(`/food/food-recognition?imageUri=${encodeURIComponent(photo.uri)}&mealType=${currentMealType}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('拍照失败:', error);
|
console.error('拍照失败:', error);
|
||||||
Alert.alert('拍照失败', '请重试');
|
Alert.alert('拍照失败', '请重试');
|
||||||
@@ -127,8 +132,12 @@ export default function FoodCameraScreen() {
|
|||||||
if (!result.canceled && result.assets[0]) {
|
if (!result.canceled && result.assets[0]) {
|
||||||
const imageUri = result.assets[0].uri;
|
const imageUri = result.assets[0].uri;
|
||||||
console.log('从相册选择的照片:', imageUri);
|
console.log('从相册选择的照片:', imageUri);
|
||||||
|
// 先验证登录状态,再跳转到食物识别页面
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
router.push(`/food/food-recognition?imageUri=${encodeURIComponent(imageUri)}&mealType=${currentMealType}`);
|
router.push(`/food/food-recognition?imageUri=${encodeURIComponent(imageUri)}&mealType=${currentMealType}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('选择照片失败:', error);
|
console.error('选择照片失败:', error);
|
||||||
Alert.alert('选择失败', '请重试');
|
Alert.alert('选择失败', '请重试');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +31,7 @@ import ImageViewing from 'react-native-image-viewing';
|
|||||||
export default function NutritionLabelAnalysisScreen() {
|
export default function NutritionLabelAnalysisScreen() {
|
||||||
const safeAreaTop = useSafeAreaTop();
|
const safeAreaTop = useSafeAreaTop();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();
|
||||||
const { upload, uploading: uploadingToCos, progress: uploadProgress } = useCosUpload({
|
const { upload, uploading: uploadingToCos, progress: uploadProgress } = useCosUpload({
|
||||||
prefix: 'nutrition-labels'
|
prefix: 'nutrition-labels'
|
||||||
});
|
});
|
||||||
@@ -186,7 +188,7 @@ export default function NutritionLabelAnalysisScreen() {
|
|||||||
right={
|
right={
|
||||||
isLiquidGlassAvailable() ? (
|
isLiquidGlassAvailable() ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push('/food/nutrition-analysis-history')}
|
onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<GlassView
|
<GlassView
|
||||||
@@ -200,7 +202,7 @@ export default function NutritionLabelAnalysisScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push('/food/nutrition-analysis-history')}
|
onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
|
||||||
style={[styles.historyButton, styles.fallbackBackground]}
|
style={[styles.historyButton, styles.fallbackBackground]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@@ -241,7 +243,13 @@ export default function NutritionLabelAnalysisScreen() {
|
|||||||
{!isAnalyzing && !isUploading && !newAnalysisResult && (
|
{!isAnalyzing && !isUploading && !newAnalysisResult && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.analyzeButton}
|
style={styles.analyzeButton}
|
||||||
onPress={() => startNewAnalysis(imageUri)}
|
onPress={async () => {
|
||||||
|
// 先验证登录状态
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
startNewAnalysis(imageUri);
|
||||||
|
}
|
||||||
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Ionicons name="search-outline" size={20} color="#FFF" />
|
<Ionicons name="search-outline" size={20} color="#FFF" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||||
import { analyzeFoodFromText } from '@/services/foodRecognition';
|
import { analyzeFoodFromText } from '@/services/foodRecognition';
|
||||||
@@ -28,6 +29,7 @@ export default function VoiceRecordScreen() {
|
|||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>();
|
const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { ensureLoggedIn } = useAuthGuard();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [recordState, setRecordState] = useState<VoiceRecordState>('idle');
|
const [recordState, setRecordState] = useState<VoiceRecordState>('idle');
|
||||||
@@ -222,6 +224,12 @@ export default function VoiceRecordScreen() {
|
|||||||
|
|
||||||
// 开始录音
|
// 开始录音
|
||||||
const startRecording = async () => {
|
const startRecording = async () => {
|
||||||
|
// 先验证登录状态
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 重置状态
|
// 重置状态
|
||||||
setRecognizedText('');
|
setRecognizedText('');
|
||||||
@@ -292,6 +300,12 @@ export default function VoiceRecordScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先验证登录状态
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
triggerHapticFeedback('impactMedium');
|
triggerHapticFeedback('impactMedium');
|
||||||
setRecordState('analyzing');
|
setRecordState('analyzing');
|
||||||
|
|||||||
BIN
assets/images/icons/icon-yingyang.png
Normal file
BIN
assets/images/icons/icon-yingyang.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -250,7 +250,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionItem}
|
style={styles.foodOptionItem}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
triggerLightHaptic();
|
triggerLightHaptic();
|
||||||
pushIfAuthedElseLogin(`/food/camera?mealType=${currentMealType}`);
|
router.push(`${ROUTES.FOOD_CAMERA}?mealType=${currentMealType}`);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@@ -284,7 +284,7 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionItem}
|
style={styles.foodOptionItem}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
triggerLightHaptic();
|
triggerLightHaptic();
|
||||||
pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`);
|
router.push(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
@@ -301,13 +301,13 @@ export function NutritionRadarCard({
|
|||||||
style={styles.foodOptionItem}
|
style={styles.foodOptionItem}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
triggerLightHaptic();
|
triggerLightHaptic();
|
||||||
pushIfAuthedElseLogin(`${ROUTES.NUTRITION_LABEL_ANALYSIS}?mealType=${currentMealType}`);
|
router.push(`${ROUTES.NUTRITION_LABEL_ANALYSIS}?mealType=${currentMealType}`);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={[styles.foodOptionIcon]}>
|
<View style={[styles.foodOptionIcon]}>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/icons/icon-recommend.png')}
|
source={require('@/assets/images/icons/icon-yingyang.png')}
|
||||||
style={styles.foodOptionImage}
|
style={styles.foodOptionImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const ROUTES = {
|
|||||||
FOOD_LIBRARY: '/food-library',
|
FOOD_LIBRARY: '/food-library',
|
||||||
VOICE_RECORD: '/voice-record',
|
VOICE_RECORD: '/voice-record',
|
||||||
NUTRITION_LABEL_ANALYSIS: '/food/nutrition-label-analysis',
|
NUTRITION_LABEL_ANALYSIS: '/food/nutrition-label-analysis',
|
||||||
|
FOOD_CAMERA: '/food/camera',
|
||||||
|
|
||||||
// 体重记录相关路由
|
// 体重记录相关路由
|
||||||
WEIGHT_RECORDS: '/weight-records',
|
WEIGHT_RECORDS: '/weight-records',
|
||||||
|
|||||||
Reference in New Issue
Block a user