feat: 更新依赖项并优化组件结构

- 在 package.json 和 package-lock.json 中新增 @sentry/react-native、react-native-device-info 和 react-native-purchases 依赖
- 更新统计页面,替换 CircularRing 组件为 FitnessRingsCard,增强健身数据展示
- 在布局文件中引入 ToastProvider,优化用户通知体验
- 新增 SuccessToast 组件,提供全局成功提示功能
- 更新健康数据获取逻辑,支持健身圆环数据的提取
- 优化多个组件的样式和交互,提升用户体验
This commit is contained in:
richarjiang
2025-08-21 09:51:25 +08:00
parent 19b92547e1
commit 78620f18ee
21 changed files with 2494 additions and 108 deletions

View File

@@ -1,6 +1,6 @@
import { AnimatedNumber } from '@/components/AnimatedNumber';
import { BMICard } from '@/components/BMICard';
import { CircularRing } from '@/components/CircularRing';
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar';
import { StressMeter } from '@/components/StressMeter';
@@ -52,13 +52,21 @@ export default function ExploreScreen() {
// 日期条自动滚动到选中项
const daysScrollRef = useRef<import('react-native').ScrollView | null>(null);
const [scrollWidth, setScrollWidth] = useState(0);
const DAY_PILL_WIDTH = 68;
const DAY_PILL_SPACING = 12;
const DAY_PILL_WIDTH = 48;
const DAY_PILL_SPACING = 8;
const scrollToIndex = (index: number, animated = true) => {
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING);
if (!daysScrollRef.current || scrollWidth === 0) return;
const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING;
const baseOffset = index * itemWidth;
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
// 确保不会滚动超出边界
const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth);
const finalOffset = Math.min(centerOffset, maxScrollOffset);
daysScrollRef.current.scrollTo({ x: finalOffset, animated });
};
useEffect(() => {
@@ -68,6 +76,14 @@ export default function ExploreScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollWidth]);
// 当选中索引变化时,滚动到对应位置
useEffect(() => {
if (scrollWidth > 0) {
scrollToIndex(selectedIndex, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedIndex]);
// HealthKit: 每次页面聚焦都拉取今日数据
const [stepCount, setStepCount] = useState<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null);
@@ -76,6 +92,15 @@ export default function ExploreScreen() {
// HRV数据
const [hrvValue, setHrvValue] = useState<number>(0);
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date());
// 健身圆环数据
const [fitnessRingsData, setFitnessRingsData] = useState({
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12
});
const [isLoading, setIsLoading] = useState(false);
// 用于触发动画重置的 token当日期或数据变化时更新
@@ -124,6 +149,15 @@ export default function ExploreScreen() {
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
setSleepDuration(data.sleepDuration);
// 更新健身圆环数据
setFitnessRingsData({
activeCalories: data.activeCalories,
activeCaloriesGoal: data.activeCaloriesGoal,
exerciseMinutes: data.exerciseMinutes,
exerciseMinutesGoal: data.exerciseMinutesGoal,
standHours: data.standHours,
standHoursGoal: data.standHoursGoal
});
const hrv = data.hrv ?? 0;
setHrvValue(hrv);
@@ -195,7 +229,6 @@ export default function ExploreScreen() {
// 日期点击时,加载对应日期数据
const onSelectDate = (index: number) => {
setSelectedIndex(index);
scrollToIndex(index);
const target = days[index]?.date?.toDate();
if (target) {
loadHealthData(target);
@@ -320,19 +353,17 @@ export default function ExploreScreen() {
// compact={true}
/>
<View style={[styles.masonryCard, styles.trainingCard]}>
<Text style={styles.cardTitleSecondary}></Text>
<View style={styles.trainingContent}>
<CircularRing
size={120}
strokeWidth={12}
trackColor="#E2D9FD"
progressColor="#8B74F3"
progress={trainingProgress}
resetToken={animToken}
/>
</View>
</View>
<FitnessRingsCard
activeCalories={fitnessRingsData.activeCalories}
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
exerciseMinutes={fitnessRingsData.exerciseMinutes}
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
standHours={fitnessRingsData.standHours}
standHoursGoal={fitnessRingsData.standHoursGoal}
resetToken={animToken}
style={styles.masonryCard}
/>
<View style={[styles.masonryCard, styles.sleepCard]}>
<View style={styles.cardHeaderRow}>
@@ -396,13 +427,13 @@ const styles = StyleSheet.create({
},
dayItemWrapper: {
alignItems: 'center',
width: 68,
marginRight: 12,
width: 48,
marginRight: 8,
},
dayPill: {
width: 68,
height: 68,
borderRadius: 18,
width: 48,
height: 48,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
@@ -413,16 +444,16 @@ const styles = StyleSheet.create({
backgroundColor: lightColors.datePickerSelected,
},
dayLabel: {
fontSize: 16,
fontSize: 12,
fontWeight: '700',
color: '#192126',
marginBottom: 2,
marginBottom: 1,
},
dayLabelSelected: {
color: '#FFFFFF',
},
dayDate: {
fontSize: 16,
fontSize: 12,
fontWeight: '800',
color: '#192126',
},
@@ -430,12 +461,12 @@ const styles = StyleSheet.create({
color: '#FFFFFF',
},
selectedDot: {
width: 8,
height: 8,
borderRadius: 4,
width: 5,
height: 5,
borderRadius: 2.5,
backgroundColor: lightColors.datePickerSelected,
marginTop: 10,
marginBottom: 4,
marginTop: 6,
marginBottom: 2,
alignSelf: 'center',
},
sectionTitle: {
@@ -481,13 +512,13 @@ const styles = StyleSheet.create({
cardTitleSecondary: {
color: '#9AA3AE',
fontSize: 14,
fontSize: 10,
fontWeight: '600',
marginBottom: 10,
},
caloriesValue: {
color: '#192126',
fontSize: 22,
fontSize: 18,
fontWeight: '800',
},
trainingContent: {
@@ -569,8 +600,8 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
iconSquare: {
width: 30,
height: 30,
width: 24,
height: 24,
borderRadius: 8,
backgroundColor: '#FFFFFF',
alignItems: 'center',
@@ -578,7 +609,7 @@ const styles = StyleSheet.create({
marginRight: 10,
},
cardTitle: {
fontSize: 18,
fontSize: 14,
fontWeight: '800',
color: '#192126',
},
@@ -606,7 +637,7 @@ const styles = StyleSheet.create({
backgroundColor: '#FFE4B8',
},
stepsValue: {
fontSize: 16,
fontSize: 14,
color: '#7A6A42',
fontWeight: '700',
marginBottom: 8,