Add comprehensive app update checking functionality with: - New VersionCheckContext for managing update detection and notifications - VersionUpdateModal UI component for presenting update information - Version service API integration with platform-specific update URLs - Version check menu item in personal settings with manual/automatic checking Enhance internationalization across workout features: - Complete workout type translations for English and Chinese - Localized workout detail modal with proper date/time formatting - Locale-aware date formatting in fitness rings detail - Workout notification improvements with deep linking to specific workout details Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
891 lines
26 KiB
TypeScript
891 lines
26 KiB
TypeScript
import { CircularRing } from '@/components/CircularRing';
|
||
import { ThemedView } from '@/components/ThemedView';
|
||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import { useI18n } from '@/hooks/useI18n';
|
||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||
import {
|
||
fetchActivityRingsForDate,
|
||
fetchHourlyActiveCaloriesForDate,
|
||
fetchHourlyExerciseMinutesForDate,
|
||
fetchHourlyStandHoursForDate,
|
||
type ActivityRingsData,
|
||
type HourlyActivityData,
|
||
type HourlyExerciseData,
|
||
type HourlyStandData
|
||
} from '@/utils/health';
|
||
import { getFitnessExerciseMinutesInfoDismissed, setFitnessExerciseMinutesInfoDismissed } from '@/utils/userPreferences';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||
import dayjs from 'dayjs';
|
||
import timezone from 'dayjs/plugin/timezone';
|
||
import utc from 'dayjs/plugin/utc';
|
||
import weekday from 'dayjs/plugin/weekday';
|
||
import { router } from 'expo-router';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import {
|
||
Animated,
|
||
Modal,
|
||
Platform,
|
||
Pressable,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import 'dayjs/locale/en';
|
||
import 'dayjs/locale/zh-cn';
|
||
|
||
// 配置 dayjs 插件
|
||
dayjs.extend(utc);
|
||
dayjs.extend(timezone);
|
||
dayjs.extend(weekday);
|
||
|
||
// 设置默认时区为中国时区
|
||
dayjs.tz.setDefault('Asia/Shanghai');
|
||
|
||
type WeekData = {
|
||
date: Date;
|
||
data: ActivityRingsData | null;
|
||
isToday: boolean;
|
||
dayName: string;
|
||
};
|
||
|
||
export default function FitnessRingsDetailScreen() {
|
||
const { t, i18n } = useI18n();
|
||
const safeAreaTop = useSafeAreaTop();
|
||
const colorScheme = useColorScheme();
|
||
const [weekData, setWeekData] = useState<WeekData[]>([]);
|
||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||
const [selectedDayData, setSelectedDayData] = useState<ActivityRingsData | null>(null);
|
||
const [datePickerVisible, setDatePickerVisible] = useState(false);
|
||
const [pickerDate, setPickerDate] = useState<Date>(new Date());
|
||
// 每小时数据状态
|
||
const [hourlyCaloriesData, setHourlyCaloriesData] = useState<HourlyActivityData[]>([]);
|
||
const [hourlyExerciseData, setHourlyExerciseData] = useState<HourlyExerciseData[]>([]);
|
||
const [hourlyStandData, setHourlyStandData] = useState<HourlyStandData[]>([]);
|
||
const [showExerciseInfo, setShowExerciseInfo] = useState(true);
|
||
const exerciseInfoAnim = useRef(new Animated.Value(1)).current;
|
||
|
||
useEffect(() => {
|
||
// 加载周数据和选中日期的详细数据
|
||
loadWeekData(selectedDate);
|
||
loadSelectedDayData();
|
||
loadExerciseInfoPreference();
|
||
}, [selectedDate]);
|
||
|
||
const loadExerciseInfoPreference = async () => {
|
||
try {
|
||
const dismissed = await getFitnessExerciseMinutesInfoDismissed();
|
||
setShowExerciseInfo(!dismissed);
|
||
if (!dismissed) {
|
||
exerciseInfoAnim.setValue(1);
|
||
} else {
|
||
exerciseInfoAnim.setValue(0);
|
||
}
|
||
} catch (error) {
|
||
console.error(t('fitnessRingsDetail.errors.loadExerciseInfoPreference'), error);
|
||
}
|
||
};
|
||
|
||
const loadWeekData = async (targetDate: Date) => {
|
||
const target = dayjs(targetDate).tz('Asia/Shanghai');
|
||
const today = dayjs().tz('Asia/Shanghai');
|
||
const weekDays = [];
|
||
|
||
// 获取目标日期所在周的数据 (周一到周日)
|
||
// 使用 weekday() 确保周一为一周的开始 (0=Monday, 6=Sunday)
|
||
const startOfWeek = target.weekday(0); // 周一开始
|
||
|
||
for (let i = 0; i < 7; i++) {
|
||
const currentDay = startOfWeek.add(i, 'day');
|
||
const isToday = currentDay.isSame(today, 'day');
|
||
const dayNames = [
|
||
t('fitnessRingsDetail.weekDays.monday'),
|
||
t('fitnessRingsDetail.weekDays.tuesday'),
|
||
t('fitnessRingsDetail.weekDays.wednesday'),
|
||
t('fitnessRingsDetail.weekDays.thursday'),
|
||
t('fitnessRingsDetail.weekDays.friday'),
|
||
t('fitnessRingsDetail.weekDays.saturday'),
|
||
t('fitnessRingsDetail.weekDays.sunday')
|
||
];
|
||
|
||
try {
|
||
const activityRingsData = await fetchActivityRingsForDate(currentDay.toDate());
|
||
weekDays.push({
|
||
date: currentDay.toDate(),
|
||
data: activityRingsData,
|
||
isToday,
|
||
dayName: dayNames[i]
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to fetch activity rings data for', currentDay.format('YYYY-MM-DD'), error);
|
||
weekDays.push({
|
||
date: currentDay.toDate(),
|
||
data: null,
|
||
isToday,
|
||
dayName: dayNames[i]
|
||
});
|
||
}
|
||
}
|
||
|
||
setWeekData(weekDays);
|
||
};
|
||
|
||
const loadSelectedDayData = async () => {
|
||
try {
|
||
// 并行获取活动圆环数据和每小时详细数据
|
||
const [activityRingsData, hourlyCalories, hourlyExercise, hourlyStand] = await Promise.all([
|
||
fetchActivityRingsForDate(selectedDate),
|
||
fetchHourlyActiveCaloriesForDate(selectedDate),
|
||
fetchHourlyExerciseMinutesForDate(selectedDate),
|
||
fetchHourlyStandHoursForDate(selectedDate)
|
||
]);
|
||
|
||
setSelectedDayData(activityRingsData);
|
||
setHourlyCaloriesData(hourlyCalories);
|
||
setHourlyExerciseData(hourlyExercise);
|
||
setHourlyStandData(hourlyStand);
|
||
} catch (error) {
|
||
console.error('Failed to fetch selected day activity rings data', error);
|
||
setSelectedDayData(null);
|
||
setHourlyCaloriesData([]);
|
||
setHourlyExerciseData([]);
|
||
setHourlyStandData([]);
|
||
}
|
||
};
|
||
|
||
// 日期选择器相关函数
|
||
const openDatePicker = () => {
|
||
setPickerDate(selectedDate);
|
||
setDatePickerVisible(true);
|
||
};
|
||
|
||
const closeDatePicker = () => setDatePickerVisible(false);
|
||
|
||
const onConfirmDate = async (date: Date) => {
|
||
const today = dayjs().tz('Asia/Shanghai').startOf('day');
|
||
const picked = dayjs(date).tz('Asia/Shanghai').startOf('day');
|
||
const finalDate = picked.isAfter(today) ? today.toDate() : picked.toDate();
|
||
|
||
setSelectedDate(finalDate);
|
||
closeDatePicker();
|
||
};
|
||
|
||
// 格式化头部显示的日期
|
||
const formatHeaderDate = (date: Date) => {
|
||
const dayJsDate = dayjs(date).tz('Asia/Shanghai').locale(i18n.language === 'zh' ? 'zh-cn' : 'en');
|
||
const dateFormat = t('fitnessRingsDetail.dateFormats.header', { defaultValue: 'YYYY年MM月DD日' });
|
||
return dayJsDate.format(dateFormat);
|
||
};
|
||
|
||
const renderWeekRingItem = (item: WeekData, index: number) => {
|
||
const isSelected = dayjs(item.date).tz('Asia/Shanghai').isSame(dayjs(selectedDate).tz('Asia/Shanghai'), 'day');
|
||
|
||
// 使用默认值确保即使没有数据也能显示圆环
|
||
const data = item.data || {
|
||
activeEnergyBurned: 0,
|
||
activeEnergyBurnedGoal: 350,
|
||
appleExerciseTime: 0,
|
||
appleExerciseTimeGoal: 30,
|
||
appleStandHours: 0,
|
||
appleStandHoursGoal: 12,
|
||
};
|
||
|
||
const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data;
|
||
|
||
// 计算进度百分比
|
||
const caloriesProgress = Math.min(1, Math.max(0, activeEnergyBurned / activeEnergyBurnedGoal));
|
||
const exerciseProgress = Math.min(1, Math.max(0, appleExerciseTime / appleExerciseTimeGoal));
|
||
const standProgress = Math.min(1, Math.max(0, appleStandHours / appleStandHoursGoal));
|
||
|
||
// 检查是否完成了所有目标
|
||
const isComplete = caloriesProgress >= 1 && exerciseProgress >= 1 && standProgress >= 1;
|
||
|
||
return (
|
||
<TouchableOpacity
|
||
key={index}
|
||
style={[styles.weekRingItem, isSelected && styles.weekRingItemSelected]}
|
||
onPress={() => setSelectedDate(item.date)}
|
||
>
|
||
<View style={styles.weekRingContainer}>
|
||
{/* {isComplete && (
|
||
<View style={styles.weekStarContainer}>
|
||
<Text style={styles.weekStarIcon}>✓</Text>
|
||
</View>
|
||
)} */}
|
||
|
||
<View style={styles.weekRingsWrapper}>
|
||
{/* 外圈 - 活动卡路里 (红色) */}
|
||
<View style={styles.ringPosition}>
|
||
<CircularRing
|
||
size={50}
|
||
strokeWidth={3}
|
||
trackColor="rgba(255, 59, 48, 0.15)"
|
||
progressColor="#FF3B30"
|
||
progress={caloriesProgress}
|
||
showCenterText={false}
|
||
startAngleDeg={-90}
|
||
/>
|
||
</View>
|
||
|
||
{/* 中圈 - 锻炼分钟 (橙色) */}
|
||
<View style={styles.ringPosition}>
|
||
<CircularRing
|
||
size={36}
|
||
strokeWidth={2.5}
|
||
trackColor="rgba(255, 149, 0, 0.15)"
|
||
progressColor="#FF9500"
|
||
progress={exerciseProgress}
|
||
showCenterText={false}
|
||
startAngleDeg={-90}
|
||
/>
|
||
</View>
|
||
|
||
{/* 内圈 - 站立小时 (蓝色) */}
|
||
<View style={styles.ringPosition}>
|
||
<CircularRing
|
||
size={22}
|
||
strokeWidth={2}
|
||
trackColor="rgba(0, 122, 255, 0.15)"
|
||
progressColor="#007AFF"
|
||
progress={standProgress}
|
||
showCenterText={false}
|
||
startAngleDeg={-90}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<Text style={[
|
||
styles.weekDayNumber,
|
||
item.isToday && styles.weekTodayNumber,
|
||
isSelected && styles.weekSelectedNumber,
|
||
{ color: isSelected ? '#007AFF' : (item.isToday ? '#007AFF' : Colors[colorScheme ?? 'light'].text) }
|
||
]}>
|
||
{dayjs(item.date).tz('Asia/Shanghai').date()}
|
||
</Text>
|
||
</View>
|
||
|
||
<Text style={[
|
||
styles.weekDayLabel,
|
||
item.isToday && styles.weekTodayLabel,
|
||
isSelected && styles.weekSelectedLabel,
|
||
{ color: isSelected ? '#007AFF' : (item.isToday ? '#007AFF' : Colors[colorScheme ?? 'light'].tabIconDefault) }
|
||
]}>
|
||
{item.dayName}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
const getClosedRingCount = () => {
|
||
let count = 0;
|
||
weekData.forEach(item => {
|
||
// 使用默认值处理空数据情况
|
||
const data = item.data || {
|
||
activeEnergyBurned: 0,
|
||
activeEnergyBurnedGoal: 350,
|
||
appleExerciseTime: 0,
|
||
appleExerciseTimeGoal: 30,
|
||
appleStandHours: 0,
|
||
appleStandHoursGoal: 12,
|
||
};
|
||
|
||
const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data;
|
||
const caloriesComplete = activeEnergyBurned >= activeEnergyBurnedGoal;
|
||
const exerciseComplete = appleExerciseTime >= appleExerciseTimeGoal;
|
||
const standComplete = appleStandHours >= appleStandHoursGoal;
|
||
|
||
if (caloriesComplete && exerciseComplete && standComplete) {
|
||
count++;
|
||
}
|
||
});
|
||
return count;
|
||
};
|
||
|
||
const handleKnowButtonPress = async () => {
|
||
try {
|
||
await setFitnessExerciseMinutesInfoDismissed(true);
|
||
Animated.timing(exerciseInfoAnim, {
|
||
toValue: 0,
|
||
duration: 300,
|
||
useNativeDriver: true,
|
||
}).start(() => {
|
||
setShowExerciseInfo(false);
|
||
});
|
||
} catch (error) {
|
||
console.error(t('fitnessRingsDetail.errors.saveExerciseInfoPreference'), error);
|
||
}
|
||
};
|
||
|
||
// 渲染简单的柱状图
|
||
const renderBarChart = (data: number[], maxValue: number, color: string, unit: string) => {
|
||
// 确保始终有24小时的数据,没有数据时用0填充
|
||
const chartData = Array.from({ length: 24 }, (_, index) => {
|
||
if (data && data.length > index) {
|
||
return data[index] || 0;
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
// 计算最大值,如果所有数据都是0,使用传入的maxValue作为参考
|
||
const maxChartValue = Math.max(...chartData, 1); // 确保最小值为1,避免除零
|
||
const effectiveMaxValue = Math.max(maxChartValue, maxValue);
|
||
|
||
return (
|
||
<View style={styles.chartContainer}>
|
||
<View style={styles.chartBars}>
|
||
{chartData.map((value, index) => {
|
||
const height = Math.max(2, (value / effectiveMaxValue) * 40); // 最小高度2,最大40
|
||
return (
|
||
<View
|
||
key={index}
|
||
style={[
|
||
styles.chartBar,
|
||
{
|
||
flex: 1,
|
||
height: value > 0 ? height : 2, // 没有数据时显示最小高度的灰色条
|
||
backgroundColor: value > 0 ? color : '#E5E5EA',
|
||
opacity: value > 0 ? 1 : 0.5,
|
||
marginHorizontal: 0.5
|
||
}
|
||
]}
|
||
/>
|
||
);
|
||
})}
|
||
</View>
|
||
<View style={styles.chartLabels}>
|
||
{chartData.map((_, index) => {
|
||
// 只在关键时间点显示标签:0点、6点、12点、18点
|
||
if (index === 0 || index === 6 || index === 12 || index === 18) {
|
||
const hour = index;
|
||
return (
|
||
<Text key={index} style={styles.chartLabel}>
|
||
{hour.toString().padStart(2, '0')}:00
|
||
</Text>
|
||
);
|
||
}
|
||
// 对于不显示标签的小时,返回一个占位的View
|
||
return <View key={index} style={styles.chartLabelSpacer} />;
|
||
})}
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const renderSelectedDayDetail = () => {
|
||
// 使用默认值确保即使没有数据也能显示图表
|
||
const data = selectedDayData || {
|
||
activeEnergyBurned: 0,
|
||
activeEnergyBurnedGoal: 350,
|
||
appleExerciseTime: 0,
|
||
appleExerciseTimeGoal: 30,
|
||
appleStandHours: 0,
|
||
appleStandHoursGoal: 12,
|
||
};
|
||
|
||
const { activeEnergyBurned, activeEnergyBurnedGoal, appleExerciseTime, appleExerciseTimeGoal, appleStandHours, appleStandHoursGoal } = data;
|
||
|
||
return (
|
||
<View style={styles.detailContainer}>
|
||
{/* 活动热量卡片 */}
|
||
<View style={styles.metricCard}>
|
||
<View style={styles.cardHeader}>
|
||
<Text style={styles.cardTitle}>{t('fitnessRingsDetail.cards.activeCalories.title')}</Text>
|
||
<TouchableOpacity style={styles.helpButton}>
|
||
<Text style={styles.helpIcon}>?</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.cardValue}>
|
||
<Text style={[styles.valueText, { color: '#FF3B30' }]}>
|
||
{Math.round(activeEnergyBurned)}/{activeEnergyBurnedGoal}
|
||
</Text>
|
||
<Text style={styles.unitText}>{t('fitnessRingsDetail.cards.activeCalories.unit')}</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(activeEnergyBurned)}{t('fitnessRingsDetail.cards.activeCalories.unit')}
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyCaloriesData.map(h => h.calories),
|
||
Math.max(activeEnergyBurnedGoal / 24, 1),
|
||
'#FF3B30',
|
||
t('fitnessRingsDetail.cards.activeCalories.unit')
|
||
)}
|
||
</View>
|
||
|
||
{/* 锻炼分钟卡片 */}
|
||
<View style={styles.metricCard}>
|
||
<View style={styles.cardHeader}>
|
||
<Text style={styles.cardTitle}>{t('fitnessRingsDetail.cards.exerciseMinutes.title')}</Text>
|
||
<TouchableOpacity style={styles.helpButton}>
|
||
<Text style={styles.helpIcon}>?</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.cardValue}>
|
||
<Text style={[styles.valueText, { color: '#FF9500' }]}>
|
||
{Math.round(appleExerciseTime)}/{appleExerciseTimeGoal}
|
||
</Text>
|
||
<Text style={styles.unitText}>{t('fitnessRingsDetail.cards.exerciseMinutes.unit')}</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(appleExerciseTime)}{t('fitnessRingsDetail.cards.exerciseMinutes.unit')}
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyExerciseData.map(h => h.minutes),
|
||
Math.max(appleExerciseTimeGoal / 8, 1),
|
||
'#FF9500',
|
||
t('fitnessRingsDetail.cards.exerciseMinutes.unit')
|
||
)}
|
||
|
||
{/* 锻炼分钟说明 */}
|
||
{showExerciseInfo && (
|
||
<Animated.View
|
||
style={[
|
||
styles.exerciseInfo,
|
||
{
|
||
opacity: exerciseInfoAnim,
|
||
transform: [
|
||
{
|
||
scale: exerciseInfoAnim.interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: [0.95, 1],
|
||
}),
|
||
},
|
||
],
|
||
}
|
||
]}
|
||
>
|
||
<Text style={styles.exerciseTitle}>{t('fitnessRingsDetail.cards.exerciseMinutes.info.title')}</Text>
|
||
<Text style={styles.exerciseDesc}>
|
||
{t('fitnessRingsDetail.cards.exerciseMinutes.info.description')}
|
||
</Text>
|
||
<Text style={styles.exerciseRecommendation}>
|
||
{t('fitnessRingsDetail.cards.exerciseMinutes.info.recommendation')}
|
||
</Text>
|
||
<TouchableOpacity style={styles.knowButton} onPress={handleKnowButtonPress}>
|
||
<Text style={styles.knowButtonText}>{t('fitnessRingsDetail.cards.exerciseMinutes.info.knowButton')}</Text>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 活动小时数卡片 */}
|
||
<View style={styles.metricCard}>
|
||
<View style={styles.cardHeader}>
|
||
<Text style={styles.cardTitle}>{t('fitnessRingsDetail.cards.standHours.title')}</Text>
|
||
<TouchableOpacity style={styles.helpButton}>
|
||
<Text style={styles.helpIcon}>?</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<View style={styles.cardValue}>
|
||
<Text style={[styles.valueText, { color: '#007AFF' }]}>
|
||
{Math.round(appleStandHours)}/{appleStandHoursGoal}
|
||
</Text>
|
||
<Text style={styles.unitText}>{t('fitnessRingsDetail.cards.standHours.unit')}</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(appleStandHours)}{t('fitnessRingsDetail.cards.standHours.unit')}
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyStandData.map(h => h.hasStood),
|
||
1,
|
||
'#007AFF',
|
||
t('fitnessRingsDetail.cards.standHours.unit')
|
||
)}
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<ThemedView style={styles.container}>
|
||
{/* 头部 */}
|
||
<HeaderBar
|
||
title={formatHeaderDate(selectedDate)}
|
||
onBack={() => router.back()}
|
||
right={
|
||
<TouchableOpacity style={styles.calendarButton} onPress={openDatePicker}>
|
||
<Ionicons name="calendar-outline" size={20} color="#666666" />
|
||
</TouchableOpacity>
|
||
}
|
||
withSafeTop={true}
|
||
transparent={true}
|
||
variant="default"
|
||
/>
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={[styles.scrollContent, {
|
||
paddingTop: safeAreaTop
|
||
}]}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* 本周圆环横向滚动 */}
|
||
<View style={styles.weekSection}>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.weekScrollContent}
|
||
style={styles.weekScrollView}
|
||
>
|
||
{weekData.map((item, index) => renderWeekRingItem(item, index))}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* 选中日期的详细数据 */}
|
||
{renderSelectedDayDetail()}
|
||
|
||
{/* 周闭环天数统计 */}
|
||
<View style={styles.statsContainer}>
|
||
<View style={styles.statRow}>
|
||
<Text style={[styles.statLabel, { color: Colors[colorScheme ?? 'light'].text }]}>{t('fitnessRingsDetail.stats.weeklyClosedRings')}</Text>
|
||
<View style={styles.statValue}>
|
||
<Text style={[styles.statNumber, { color: Colors[colorScheme ?? 'light'].text }]}>{getClosedRingCount()}{t('fitnessRingsDetail.stats.daysUnit')}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</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={new Date(2020, 0, 1)}
|
||
maximumDate={new Date()}
|
||
{...(Platform.OS === 'ios' ? { locale: i18n.language === 'zh' ? 'zh-CN' : 'en-US' } : {})}
|
||
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}>{t('fitnessRingsDetail.datePicker.cancel')}</Text>
|
||
</Pressable>
|
||
<Pressable onPress={() => {
|
||
onConfirmDate(pickerDate);
|
||
}} style={[styles.modalBtn, styles.modalBtnPrimary]}>
|
||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>{t('fitnessRingsDetail.datePicker.confirm')}</Text>
|
||
</Pressable>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</Modal>
|
||
</ThemedView>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
},
|
||
calendarButton: {
|
||
width: 32,
|
||
height: 32,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
},
|
||
scrollContent: {
|
||
paddingBottom: 32,
|
||
},
|
||
weekSection: {
|
||
paddingVertical: 20,
|
||
},
|
||
weekScrollView: {
|
||
paddingHorizontal: 16,
|
||
},
|
||
weekScrollContent: {
|
||
paddingHorizontal: 8,
|
||
},
|
||
weekRingItem: {
|
||
alignItems: 'center',
|
||
marginHorizontal: 8,
|
||
padding: 8,
|
||
borderRadius: 12,
|
||
},
|
||
weekRingItemSelected: {
|
||
backgroundColor: 'rgba(0, 122, 255, 0.1)',
|
||
},
|
||
weekRingContainer: {
|
||
position: 'relative',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginBottom: 8,
|
||
},
|
||
weekStarContainer: {
|
||
position: 'absolute',
|
||
top: -8,
|
||
right: -8,
|
||
zIndex: 10,
|
||
},
|
||
weekStarIcon: {
|
||
fontSize: 12,
|
||
},
|
||
weekRingsWrapper: {
|
||
position: 'relative',
|
||
width: 50,
|
||
height: 50,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
ringPosition: {
|
||
position: 'absolute',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
weekDayNumber: {
|
||
fontSize: 11,
|
||
fontWeight: '600',
|
||
marginTop: 6,
|
||
},
|
||
weekTodayNumber: {
|
||
color: '#007AFF',
|
||
},
|
||
weekSelectedNumber: {
|
||
fontWeight: '700',
|
||
},
|
||
weekDayLabel: {
|
||
fontSize: 10,
|
||
fontWeight: '500',
|
||
marginTop: 2,
|
||
},
|
||
weekTodayLabel: {
|
||
color: '#007AFF',
|
||
},
|
||
weekSelectedLabel: {
|
||
fontWeight: '600',
|
||
},
|
||
detailContainer: {
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 20,
|
||
},
|
||
// 卡片样式
|
||
metricCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 20,
|
||
marginBottom: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 2,
|
||
},
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 8,
|
||
elevation: 3,
|
||
},
|
||
cardHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
cardTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
color: '#1C1C1E',
|
||
},
|
||
helpButton: {
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: 12,
|
||
backgroundColor: '#F2F2F7',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
helpIcon: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#8E8E93',
|
||
},
|
||
cardValue: {
|
||
flexDirection: 'row',
|
||
alignItems: 'baseline',
|
||
marginBottom: 8,
|
||
},
|
||
valueText: {
|
||
fontSize: 32,
|
||
fontWeight: '700',
|
||
letterSpacing: -1,
|
||
},
|
||
unitText: {
|
||
fontSize: 18,
|
||
fontWeight: '500',
|
||
color: '#8E8E93',
|
||
marginLeft: 4,
|
||
},
|
||
cardSubtext: {
|
||
fontSize: 14,
|
||
color: '#8E8E93',
|
||
marginBottom: 20,
|
||
},
|
||
// 图表样式
|
||
chartContainer: {
|
||
marginTop: 16,
|
||
},
|
||
chartBars: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-end',
|
||
height: 60,
|
||
marginBottom: 8,
|
||
paddingHorizontal: 2,
|
||
},
|
||
chartBar: {
|
||
borderRadius: 1.5,
|
||
},
|
||
chartLabels: {
|
||
flexDirection: 'row',
|
||
paddingHorizontal: 2,
|
||
justifyContent: 'space-between',
|
||
},
|
||
chartLabel: {
|
||
fontSize: 10,
|
||
color: '#8E8E93',
|
||
fontWeight: '500',
|
||
textAlign: 'center',
|
||
flex: 6, // 给显示标签的元素更多空间
|
||
},
|
||
chartLabelSpacer: {
|
||
flex: 1, // 占位元素使用较少空间
|
||
},
|
||
// 锻炼信息样式
|
||
exerciseInfo: {
|
||
marginTop: 20,
|
||
padding: 16,
|
||
backgroundColor: '#F2F2F7',
|
||
borderRadius: 12,
|
||
},
|
||
exerciseTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: '#1C1C1E',
|
||
marginBottom: 8,
|
||
},
|
||
exerciseDesc: {
|
||
fontSize: 14,
|
||
color: '#3C3C43',
|
||
lineHeight: 20,
|
||
marginBottom: 12,
|
||
},
|
||
exerciseRecommendation: {
|
||
fontSize: 14,
|
||
color: '#3C3C43',
|
||
lineHeight: 20,
|
||
marginBottom: 16,
|
||
},
|
||
knowButton: {
|
||
alignSelf: 'flex-end',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 8,
|
||
backgroundColor: '#007AFF',
|
||
borderRadius: 20,
|
||
},
|
||
knowButtonText: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#FFFFFF',
|
||
},
|
||
noDataText: {
|
||
fontSize: 16,
|
||
textAlign: 'center',
|
||
marginTop: 40,
|
||
},
|
||
statsContainer: {
|
||
marginHorizontal: 16,
|
||
marginTop: 32,
|
||
padding: 16,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||
borderRadius: 12,
|
||
},
|
||
statRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
statLabel: {
|
||
fontSize: 16,
|
||
fontWeight: '500',
|
||
},
|
||
statValue: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
statNumber: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
marginLeft: 4,
|
||
},
|
||
starIcon: {
|
||
fontSize: 16,
|
||
},
|
||
// 日期选择器样式
|
||
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',
|
||
},
|
||
});
|