feat: 添加食物分析结果页面的图片预览功能,优化记录栏显示逻辑
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user