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

@@ -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 && (
<Text style={styles.monthTitle}>{monthTitle}</Text>
<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);
};