feat: 添加食物分析结果页面的图片预览功能,优化记录栏显示逻辑

This commit is contained in:
richarjiang
2025-09-04 15:12:39 +08:00
parent 5e00cb7788
commit 05a643a9e6
6 changed files with 495 additions and 99 deletions

View File

@@ -6,11 +6,13 @@ import { useAppSelector } from '@/hooks/redux';
import { addDietRecord, type CreateDietRecordDto, type MealType } from '@/services/dietRecords';
import { selectFoodRecognitionResult } from '@/store/foodRecognitionSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
BackHandler,
Image,
Modal,
ScrollView,
@@ -19,7 +21,7 @@ import {
TouchableOpacity,
View
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import ImageViewing from 'react-native-image-viewing';
// 模拟食物摄入列表数据
@@ -73,17 +75,36 @@ export default function FoodAnalysisResultScreen() {
imageUri?: string;
mealType?: string;
recognitionId?: string;
hideRecordBar?: string;
}>();
const [foodItems, setFoodItems] = useState(mockFoodItems);
const [currentMealType, setCurrentMealType] = useState<MealType>((params.mealType as MealType) || 'breakfast');
const [showMealSelector, setShowMealSelector] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const { imageUri, recognitionId } = params;
const [showImagePreview, setShowImagePreview] = useState(false);
const [animationTrigger, setAnimationTrigger] = useState(0);
const { imageUri, recognitionId, hideRecordBar } = params;
// 判断是否隐藏记录栏(默认显示)
const shouldHideRecordBar = hideRecordBar === 'true';
// 从 Redux 获取识别结果
const recognitionResult = useAppSelector(recognitionId ? selectFoodRecognitionResult(recognitionId) : () => null);
// 处理Android返回键关闭图片预览
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
if (showImagePreview) {
setShowImagePreview(false);
return true; // 阻止默认返回行为
}
return false;
});
return () => backHandler.remove();
}, [showImagePreview]);
// 处理识别结果数据
useEffect(() => {
if (recognitionResult) {
@@ -116,9 +137,19 @@ export default function FoodAnalysisResultScreen() {
setCurrentMealType(mealTypeFromApi);
}
}
// 触发营养圆环动画
setAnimationTrigger(prev => prev + 1);
}
}, [recognitionResult]);
// 当食物项发生变化时也触发动画
useEffect(() => {
if (foodItems.length > 0) {
setAnimationTrigger(prev => prev + 1);
}
}, [foodItems]);
const handleSaveToDiary = async () => {
if (isRecording || foodItems.length === 0) return;
@@ -188,17 +219,17 @@ export default function FoodAnalysisResultScreen() {
{ key: 'snack' as const, label: '加餐', color: '#FF9800' },
];
if (!imageUri) {
if (!imageUri && !recognitionResult) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<HeaderBar
title="分析结果"
onBack={() => router.back()}
/>
<View style={styles.errorContainer}>
<Text style={styles.errorText}></Text>
<Text style={styles.errorText}></Text>
</View>
</View>
</SafeAreaView>
);
}
@@ -206,17 +237,12 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#ffffffff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<View style={styles.decorativeCircle3} />
<HeaderBar
title="分析结果"
onBack={() => router.back()}
@@ -226,18 +252,36 @@ export default function FoodAnalysisResultScreen() {
<ScrollView style={styles.scrollContainer} showsVerticalScrollIndicator={false}>
{/* 食物主图 */}
<View style={styles.imageContainer}>
{imageUri ? (
<TouchableOpacity
onPress={() => setShowImagePreview(true)}
activeOpacity={0.9}
>
<Image
source={{ uri: imageUri }}
style={styles.foodImage}
resizeMode="cover"
/>
{/* 预览提示图标 */}
<View style={styles.previewHint}>
<Ionicons name="expand-outline" size={20} color="#FFF" />
</View>
</TouchableOpacity>
) : (
<View style={styles.placeholderContainer}>
<View style={styles.placeholderContent}>
<Ionicons name="restaurant-outline" size={48} color="#666" />
<Text style={styles.placeholderText}></Text>
</View>
</View>
)}
{/* 识别信息气泡 */}
<View style={styles.descriptionBubble}>
<Text style={styles.descriptionText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
'2025年9月4日'
dayjs().format('YYYY年M月D日')
}
</Text>
</View>
@@ -259,6 +303,7 @@ export default function FoodAnalysisResultScreen() {
unit="克"
percentage={Math.min(100, proteinPercentage)}
color="#4CAF50"
resetToken={animationTrigger}
/>
<NutritionRing
label="脂肪"
@@ -266,6 +311,7 @@ export default function FoodAnalysisResultScreen() {
unit="克"
percentage={Math.min(100, fatPercentage)}
color="#FF9800"
resetToken={animationTrigger}
/>
<NutritionRing
label="碳水"
@@ -273,6 +319,7 @@ export default function FoodAnalysisResultScreen() {
unit="克"
percentage={Math.min(100, carbohydratePercentage)}
color="#2196F3"
resetToken={animationTrigger}
/>
</View>
@@ -320,15 +367,15 @@ export default function FoodAnalysisResultScreen() {
<View style={styles.foodIntakeCalories}>
<Text style={styles.foodIntakeCaloriesValue}>{item.calories}</Text>
<TouchableOpacity style={styles.editButton}>
{shouldHideRecordBar ? null : <TouchableOpacity style={styles.editButton}>
<Ionicons name="create-outline" size={16} color="#666" />
</TouchableOpacity>
<TouchableOpacity
</TouchableOpacity>}
{shouldHideRecordBar ? null : <TouchableOpacity
style={styles.deleteButton}
onPress={() => handleRemoveFood(item.id)}
>
<Ionicons name="trash-outline" size={16} color="#666" />
</TouchableOpacity>
</TouchableOpacity>}
</View>
</View>
))}
@@ -337,7 +384,8 @@ export default function FoodAnalysisResultScreen() {
</ScrollView>
{/* 底部餐次选择和记录按钮 */}
{recognitionResult && !recognitionResult.isFoodDetected ? (
{!shouldHideRecordBar && (
recognitionResult && !recognitionResult.isFoodDetected ? (
// 非食物检测情况显示重新拍照按钮
<View style={styles.bottomContainer}>
<TouchableOpacity
@@ -379,6 +427,7 @@ export default function FoodAnalysisResultScreen() {
)}
</TouchableOpacity>
</View>
)
)}
{/* 餐次选择弹窗 */}
@@ -426,6 +475,39 @@ export default function FoodAnalysisResultScreen() {
</View>
</View>
</Modal>
{/* 图片预览 */}
<ImageViewing
images={imageUri ? [{ uri: imageUri }] : []}
imageIndex={0}
visible={showImagePreview}
onRequestClose={() => {
console.log('ImageViewing onRequestClose called');
setShowImagePreview(false);
}}
swipeToCloseEnabled={true}
doubleTapToZoomEnabled={true}
HeaderComponent={() => (
<View style={styles.imageViewerHeader}>
<Text style={styles.imageViewerHeaderText}>
{recognitionResult ?
`置信度: ${recognitionResult.confidence}%` :
dayjs().format('YYYY年M月D日 HH:mm')
}
</Text>
</View>
)}
FooterComponent={() => (
<View style={styles.imageViewerFooter}>
<TouchableOpacity
style={styles.imageViewerFooterButton}
onPress={() => setShowImagePreview(false)}
>
<Text style={styles.imageViewerFooterButtonText}></Text>
</TouchableOpacity>
</View>
)}
/>
</View>
);
}
@@ -436,24 +518,28 @@ function NutritionRing({
value,
unit,
percentage,
color
color,
resetToken
}: {
label: string;
value: string | number;
unit?: string;
percentage: number;
color: string;
resetToken?: unknown;
}) {
return (
<View style={styles.nutritionRingContainer}>
<View style={styles.ringWrapper}>
<CircularRing
size={60}
strokeWidth={4}
strokeWidth={6}
trackColor="#E2E8F0"
progressColor={color}
progress={percentage / 100}
showCenterText={false}
resetToken={resetToken}
durationMs={1200}
/>
<View style={styles.ringCenter}>
<Text style={styles.ringCenterText}>{percentage}%</Text>
@@ -547,7 +633,8 @@ const styles = StyleSheet.create({
backgroundColor: Colors.light.background,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
marginTop: -60,
height: '100%',
marginTop: -24,
paddingTop: 24,
paddingHorizontal: 20,
paddingBottom: 40,
@@ -882,4 +969,74 @@ const styles = StyleSheet.create({
fontWeight: '600',
letterSpacing: 0.5,
},
placeholderContainer: {
width: '100%',
height: '100%',
backgroundColor: '#F5F5F5',
alignItems: 'center',
justifyContent: 'center',
},
placeholderContent: {
alignItems: 'center',
},
placeholderText: {
fontSize: 16,
color: '#666',
fontWeight: '500',
marginTop: 8,
},
// 预览提示图标样式
previewHint: {
position: 'absolute',
top: 16,
right: 16,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 20,
padding: 8,
},
// ImageViewing 组件样式
imageViewerHeader: {
position: 'absolute',
top: 60,
left: 20,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
zIndex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
imageViewerCloseButton: {
padding: 4,
},
imageViewerHeaderText: {
color: '#FFF',
fontSize: 14,
fontWeight: '500',
flex: 1,
textAlign: 'center',
marginLeft: -28, // 补偿关闭按钮的宽度,保持文字居中
},
imageViewerFooter: {
position: 'absolute',
bottom: 60,
left: 20,
right: 20,
alignItems: 'center',
zIndex: 1,
},
imageViewerFooterButton: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 20,
},
imageViewerFooterButtonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '500',
},
});

View File

@@ -159,21 +159,23 @@ export default function FoodCameraScreen() {
<View style={styles.cameraFrameContainer}>
<Text style={styles.hintText}></Text>
{/* 相机取景框包装器 */}
<View style={styles.cameraWrapper}>
{/* 相机取景框 */}
<View style={styles.cameraFrame}>
<CameraView
ref={cameraRef}
style={styles.cameraView}
facing={facing}
>
{/* 取景框装饰 */}
/>
</View>
{/* 取景框装饰 - 放在外层避免被截断 */}
<View style={styles.viewfinderOverlay}>
<View style={[styles.corner, styles.topLeft]} />
<View style={[styles.corner, styles.topRight]} />
<View style={[styles.corner, styles.bottomLeft]} />
<View style={[styles.corner, styles.bottomRight]} />
</View>
</CameraView>
</View>
</View>
@@ -294,21 +296,29 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: 20,
},
cameraWrapper: {
width: 300,
height: 300,
position: 'relative',
},
cameraFrame: {
width: 300,
height: 300,
borderRadius: 20,
overflow: 'hidden',
borderWidth: 3,
borderColor: '#FFF',
backgroundColor: '#000',
},
cameraView: {
flex: 1,
},
viewfinderOverlay: {
flex: 1,
position: 'relative',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
pointerEvents: 'none',
},
camera: {
flex: 1,

View File

@@ -1,11 +1,14 @@
import { CalorieRingChart } from '@/components/CalorieRingChart';
import { DateSelector } from '@/components/DateSelector';
import { FloatingFoodOverlay } from '@/components/FloatingFoodOverlay';
import { NutritionRecordCard } from '@/components/NutritionRecordCard';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { DietRecord } from '@/services/dietRecords';
import { type FoodRecognitionResponse } from '@/services/foodRecognition';
import { saveRecognitionResult } from '@/store/foodRecognitionSlice';
import { selectHealthDataByDate } from '@/store/healthSlice';
import {
deleteNutritionRecord,
@@ -70,6 +73,9 @@ export default function NutritionRecordsScreen() {
const [hasMoreData, setHasMoreData] = useState(true);
const [page, setPage] = useState(1);
// 食物添加弹窗状态
const [showFoodOverlay, setShowFoodOverlay] = useState(false);
// 根据视图模式选择使用的数据
const displayRecords = viewMode === 'daily' ? nutritionRecords : allRecords;
const loading = viewMode === 'daily' ? nutritionLoading.records : allRecordsLoading;
@@ -249,6 +255,51 @@ export default function NutritionRecordsScreen() {
}
};
// 处理营养记录卡片点击
const handleRecordPress = (record: DietRecord) => {
// 将 DietRecord 转换为 FoodRecognitionResponse 格式
const recognitionResult: FoodRecognitionResponse = {
items: [{
id: record.id.toString(),
label: record.foodName,
foodName: record.foodName,
portion: record.portionDescription || `${record.estimatedCalories || 0}g`,
calories: record.estimatedCalories || 0,
mealType: record.mealType,
nutritionData: {
proteinGrams: record.proteinGrams || 0,
carbohydrateGrams: record.carbohydrateGrams || 0,
fatGrams: record.fatGrams || 0,
fiberGrams: 0, // DietRecord 中没有纤维数据设为0
}
}],
analysisText: record.foodDescription || `${record.foodName} - ${record.portionDescription}`,
confidence: 95, // 设置一个默认置信度
isFoodDetected: true,
nonFoodMessage: undefined
};
// 生成唯一的识别ID
const recognitionId = `record-${record.id}-${Date.now()}`;
// 保存到 Redux
dispatch(saveRecognitionResult({
id: recognitionId,
result: recognitionResult
}));
// 跳转到分析结果页面
router.push({
pathname: '/food/analysis-result',
params: {
imageUri: record.imageUrl || '',
mealType: record.mealType,
recognitionId: recognitionId,
hideRecordBar: 'true'
}
});
};
// 渲染视图模式切换器
const renderViewModeToggle = () => (
<View style={[styles.viewModeContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
@@ -294,8 +345,12 @@ export default function NutritionRecordsScreen() {
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={(index, date) => setSelectedIndex(index)}
showMonthTitle={false}
showMonthTitle={true}
disableFutureDates={true}
showCalendarIcon={true}
containerStyle={{
paddingHorizontal: 16
}}
/>
);
};
@@ -317,6 +372,7 @@ export default function NutritionRecordsScreen() {
const renderRecord = ({ item, index }: { item: DietRecord; index: number }) => (
<NutritionRecordCard
record={item}
onPress={() => handleRecordPress(item)}
onDelete={() => handleDeleteRecord(item.id)}
/>
);
@@ -362,8 +418,7 @@ export default function NutritionRecordsScreen() {
// 添加食物的处理函数
const handleAddFood = () => {
const mealType = getCurrentMealType();
router.push(`/food-library?mealType=${mealType}`);
setShowFoodOverlay(true);
};
// 渲染右侧添加按钮
@@ -385,7 +440,7 @@ export default function NutritionRecordsScreen() {
right={renderRightButton()}
/>
{renderViewModeToggle()}
{/* {renderViewModeToggle()} */}
{renderDateSelector()}
{/* Calorie Ring Chart */}
@@ -425,6 +480,13 @@ export default function NutritionRecordsScreen() {
onEndReachedThreshold={0.1}
/>
)}
{/* 食物添加悬浮窗 */}
<FloatingFoodOverlay
visible={showFoodOverlay}
onClose={() => setShowFoodOverlay(false)}
mealType={getCurrentMealType()}
/>
</View>
);
}

View File

@@ -115,9 +115,6 @@ export function CalorieRingChart({
<ThemedText style={[styles.centerValue, { color: textColor }]}>
{Math.round(canEat)}
</ThemedText>
<ThemedText style={[styles.centerPercentage, { color: textSecondaryColor }]}>
{Math.round(progressPercentage)}%
</ThemedText>
</View>
</View>
@@ -187,7 +184,7 @@ const styles = StyleSheet.create({
borderRadius: 16,
padding: 16,
marginHorizontal: 16,
marginBottom: 16,
marginBottom: 8,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,

View File

@@ -1,7 +1,12 @@
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
import dayjs from 'dayjs';
import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
@@ -28,6 +33,8 @@ export interface DateSelectorProps {
dayItemStyle?: any;
/** 是否自动滚动到选中项 */
autoScrollToSelected?: boolean;
/** 是否显示日历图标 */
showCalendarIcon?: boolean;
}
export const DateSelector: React.FC<DateSelectorProps> = ({
@@ -40,14 +47,16 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
containerStyle,
dayItemStyle,
autoScrollToSelected = true,
showCalendarIcon = true,
}) => {
// 内部状态管理
const [internalSelectedIndex, setInternalSelectedIndex] = useState(getTodayIndexInMonth());
const [currentMonth, setCurrentMonth] = useState(dayjs()); // 当前显示的月份
const selectedIndex = externalSelectedIndex ?? internalSelectedIndex;
// 获取日期数据
const days = getMonthDaysZh();
const monthTitle = externalMonthTitle ?? getMonthTitleZh();
const days = getMonthDaysZh(currentMonth);
const monthTitle = externalMonthTitle ?? getMonthTitleZh(currentMonth);
// 滚动相关
const daysScrollRef = useRef<ScrollView | null>(null);
@@ -55,6 +64,10 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
const DAY_PILL_WIDTH = 48;
const DAY_PILL_SPACING = 8;
// 日历弹窗相关
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [pickerDate, setPickerDate] = useState<Date>(new Date());
// 滚动到指定索引
const scrollToIndex = (index: number, animated = true) => {
if (!daysScrollRef.current || scrollWidth === 0) return;
@@ -103,6 +116,17 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
}
}, [selectedIndex, autoScrollToSelected]);
// 当月份变化时,重新滚动到选中位置
useEffect(() => {
if (scrollWidth > 0 && autoScrollToSelected) {
const timer = setTimeout(() => {
scrollToIndex(selectedIndex, true);
}, 100);
return () => clearTimeout(timer);
}
}, [currentMonth, scrollWidth, autoScrollToSelected]);
// 处理日期选择
const handleDateSelect = (index: number) => {
const targetDate = days[index]?.date?.toDate();
@@ -127,10 +151,61 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
onDateSelect?.(index, targetDate);
};
// 日历弹窗相关函数
const openDatePicker = () => {
const currentSelectedDate = days[selectedIndex]?.date?.toDate() || new Date();
setPickerDate(currentSelectedDate);
setDatePickerVisible(true);
};
const closeDatePicker = () => setDatePickerVisible(false);
const onConfirmDate = (date: Date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const picked = new Date(date);
picked.setHours(0, 0, 0, 0);
// 如果禁用未来日期,则限制选择
const finalDate = (disableFutureDates && picked > today) ? today : picked;
closeDatePicker();
// 更新当前月份为选中日期的月份
const selectedMonth = dayjs(finalDate);
setCurrentMonth(selectedMonth);
// 计算选中日期在新月份中的索引
const newMonthDays = getMonthDaysZh(selectedMonth);
const selectedDay = selectedMonth.date();
const newSelectedIndex = newMonthDays.findIndex(day => day.dayOfMonth === selectedDay);
// 更新内部状态(如果使用外部控制则不更新)
if (externalSelectedIndex === undefined && newSelectedIndex !== -1) {
setInternalSelectedIndex(newSelectedIndex);
}
// 调用统一的日期选择回调
if (newSelectedIndex !== -1) {
onDateSelect?.(newSelectedIndex, finalDate);
}
};
return (
<View style={[styles.container, containerStyle]}>
{showMonthTitle && (
<View style={styles.monthTitleContainer}>
<Text style={styles.monthTitle}>{monthTitle}</Text>
{showCalendarIcon && (
<TouchableOpacity
onPress={openDatePicker}
style={styles.calendarIconButton}
activeOpacity={0.7}
>
<Ionicons name="calendar-outline" size={14} color="#666666" />
</TouchableOpacity>
)}
</View>
)}
<ScrollView
@@ -176,6 +251,49 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
);
})}
</ScrollView>
{/* 日历选择弹窗 */}
<Modal
visible={datePickerVisible}
transparent
animationType="fade"
onRequestClose={closeDatePicker}
>
<Pressable style={styles.modalBackdrop} onPress={closeDatePicker} />
<View style={styles.modalSheet}>
<DateTimePicker
value={pickerDate}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
minimumDate={dayjs().subtract(6, 'month').toDate()}
maximumDate={disableFutureDates ? new Date() : undefined}
{...(Platform.OS === 'ios' ? { locale: 'zh-CN' } : {})}
onChange={(event, date) => {
if (Platform.OS === 'ios') {
if (date) setPickerDate(date);
} else {
if (event.type === 'set' && date) {
onConfirmDate(date);
} else {
closeDatePicker();
}
}
}}
/>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<Pressable onPress={closeDatePicker} style={[styles.modalBtn]}>
<Text style={styles.modalBtnText}></Text>
</Pressable>
<Pressable onPress={() => {
onConfirmDate(pickerDate);
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}></Text>
</Pressable>
</View>
)}
</View>
</Modal>
</View>
);
};
@@ -183,12 +301,22 @@ export const DateSelector: React.FC<DateSelectorProps> = ({
const styles = StyleSheet.create({
container: {
},
monthTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
marginBottom: 8,
},
monthTitle: {
fontSize: 18,
fontSize: 20,
fontWeight: '800',
color: '#192126',
marginBottom: 14,
},
calendarIconButton: {
padding: 4,
borderRadius: 6,
marginLeft: 4
},
daysContainer: {
paddingBottom: 8,
@@ -243,4 +371,41 @@ const styles = StyleSheet.create({
dayDateDisabled: {
color: 'gray',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
modalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: 16,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 8,
gap: 12,
},
modalBtn: {
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 10,
backgroundColor: '#F1F5F9',
},
modalBtnPrimary: {
backgroundColor: '#7a5af8',
},
modalBtnText: {
color: '#334155',
fontWeight: '700',
},
modalBtnTextPrimary: {
color: '#FFFFFF',
fontWeight: '700',
},
});

View File

@@ -5,9 +5,10 @@ import { NutritionSummary } from '@/services/dietRecords';
import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import * as Haptics from 'expo-haptics';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Animated, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
@@ -137,6 +138,10 @@ export function NutritionRadarCard({
});
const handleNavigateToRecords = () => {
// ios 下震动反馈
if (Platform.OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
router.push(ROUTES.NUTRITION_RECORDS);
};