feat: Update FitnessRingsCard to navigate to fitness rings detail page on press feat: Modify NutritionRadarCard to enhance UI and add haptic feedback on actions feat: Add FITNESS_RINGS_DETAIL route for navigation fix: Adjust minimum fetch interval in BackgroundTaskManager for background tasks feat: Implement haptic feedback utility functions for better user experience feat: Extend health permissions to include Apple Exercise Time and Apple Stand Time feat: Add functions to fetch hourly activity, exercise, and stand data for improved health tracking feat: Enhance user preferences to manage fitness exercise minutes and active hours info dismissal
860 lines
25 KiB
TypeScript
860 lines
25 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 {
|
||
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';
|
||
|
||
// 配置 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 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('加载锻炼分钟说明偏好失败:', 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 = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
|
||
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');
|
||
return `${dayJsDate.format('YYYY年MM月DD日')}`;
|
||
};
|
||
|
||
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('保存锻炼分钟说明偏好失败:', 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,
|
||
{
|
||
height: value > 0 ? height : 2, // 没有数据时显示最小高度的灰色条
|
||
backgroundColor: value > 0 ? color : '#E5E5EA',
|
||
opacity: value > 0 ? 1 : 0.5
|
||
}
|
||
]}
|
||
/>
|
||
);
|
||
})}
|
||
</View>
|
||
<View style={styles.chartLabels}>
|
||
<Text style={styles.chartLabel}>00:00</Text>
|
||
<Text style={styles.chartLabel}>06:00</Text>
|
||
<Text style={styles.chartLabel}>12:00</Text>
|
||
<Text style={styles.chartLabel}>18:00</Text>
|
||
</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}>活动热量</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}>千卡</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(activeEnergyBurned)}千卡
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyCaloriesData.map(h => h.calories),
|
||
Math.max(activeEnergyBurnedGoal / 24, 1),
|
||
'#FF3B30',
|
||
'千卡'
|
||
)}
|
||
</View>
|
||
|
||
{/* 锻炼分钟卡片 */}
|
||
<View style={styles.metricCard}>
|
||
<View style={styles.cardHeader}>
|
||
<Text style={styles.cardTitle}>锻炼分钟数</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}>分钟</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(appleExerciseTime)}分钟
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyExerciseData.map(h => h.minutes),
|
||
Math.max(appleExerciseTimeGoal / 8, 1),
|
||
'#FF9500',
|
||
'分钟'
|
||
)}
|
||
|
||
{/* 锻炼分钟说明 */}
|
||
{showExerciseInfo && (
|
||
<Animated.View
|
||
style={[
|
||
styles.exerciseInfo,
|
||
{
|
||
opacity: exerciseInfoAnim,
|
||
transform: [
|
||
{
|
||
scale: exerciseInfoAnim.interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: [0.95, 1],
|
||
}),
|
||
},
|
||
],
|
||
}
|
||
]}
|
||
>
|
||
<Text style={styles.exerciseTitle}>锻炼分钟数:</Text>
|
||
<Text style={styles.exerciseDesc}>
|
||
进行强度不低于"快走"的运动锻炼,就会积累对应时长的锻炼分钟数。
|
||
</Text>
|
||
<Text style={styles.exerciseRecommendation}>
|
||
世卫组织推荐的成年人每天至少保持30分钟以上的中高强度运动。
|
||
</Text>
|
||
<TouchableOpacity style={styles.knowButton} onPress={handleKnowButtonPress}>
|
||
<Text style={styles.knowButtonText}>知道了</Text>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 活动小时数卡片 */}
|
||
<View style={styles.metricCard}>
|
||
<View style={styles.cardHeader}>
|
||
<Text style={styles.cardTitle}>活动小时数</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}>小时</Text>
|
||
</View>
|
||
|
||
<Text style={styles.cardSubtext}>
|
||
{Math.round(appleStandHours)}小时
|
||
</Text>
|
||
|
||
{renderBarChart(
|
||
hourlyStandData.map(h => h.hasStood),
|
||
1,
|
||
'#007AFF',
|
||
'小时'
|
||
)}
|
||
</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}
|
||
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 }]}>周闭环天数</Text>
|
||
<View style={styles.statValue}>
|
||
<Text style={[styles.statNumber, { color: Colors[colorScheme ?? 'light'].text }]}>{getClosedRingCount()}天</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: '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>
|
||
</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: 4,
|
||
justifyContent: 'space-between',
|
||
},
|
||
chartBar: {
|
||
width: 3,
|
||
borderRadius: 1.5,
|
||
marginHorizontal: 0.5,
|
||
},
|
||
chartLabels: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 4,
|
||
},
|
||
chartLabel: {
|
||
fontSize: 12,
|
||
color: '#8E8E93',
|
||
fontWeight: '500',
|
||
},
|
||
// 锻炼信息样式
|
||
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',
|
||
},
|
||
}); |