diff --git a/app/challenges/[id]/index.tsx b/app/challenges/[id]/index.tsx index 3006aa9..ae2b627 100644 --- a/app/challenges/[id]/index.tsx +++ b/app/challenges/[id]/index.tsx @@ -473,7 +473,13 @@ export default function ChallengeDetailScreen() { {rankingData.length ? ( rankingData.map((item, index) => ( - 0} /> + 0} + unit={challenge?.unit} + /> )) ) : ( diff --git a/app/challenges/[id]/leaderboard.tsx b/app/challenges/[id]/leaderboard.tsx index 6a22772..26ed927 100644 --- a/app/challenges/[id]/leaderboard.tsx +++ b/app/challenges/[id]/leaderboard.tsx @@ -177,7 +177,13 @@ export default function ChallengeLeaderboardScreen() { ) : rankingData.length ? ( rankingData.map((item, index) => ( - 0} /> + 0} + unit={challenge?.unit} + /> )) ) : rankingError ? ( diff --git a/components/FitnessRingsCard.tsx b/components/FitnessRingsCard.tsx index 3c4bc05..65f18d9 100644 --- a/components/FitnessRingsCard.tsx +++ b/components/FitnessRingsCard.tsx @@ -1,10 +1,15 @@ -import React, { useState, useCallback, useRef } from 'react'; -import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'; -import { router } from 'expo-router'; -import { useFocusEffect } from '@react-navigation/native'; -import { CircularRing } from './CircularRing'; import { ROUTES } from '@/constants/Routes'; -import { fetchActivityRingsForDate, ActivityRingsData } from '@/utils/health'; +import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { ChallengeType } from '@/services/challengesApi'; +import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice'; +import { ActivityRingsData, fetchActivityRingsForDate } from '@/utils/health'; +import { logger } from '@/utils/logger'; +import { useFocusEffect } from '@react-navigation/native'; +import dayjs from 'dayjs'; +import { router } from 'expo-router'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { CircularRing } from './CircularRing'; type FitnessRingsCardProps = { style?: any; @@ -21,9 +26,17 @@ export function FitnessRingsCard({ selectedDate, resetToken, }: FitnessRingsCardProps) { + const dispatch = useAppDispatch(); + const challenges = useAppSelector(selectChallengeList); const [activityData, setActivityData] = useState(null); const [loading, setLoading] = useState(false); const loadingRef = useRef(false); + const lastReportedRef = useRef<{ date: string } | null>(null); + + const joinedExerciseChallenges = useMemo( + () => challenges.filter((challenge) => challenge.type === ChallengeType.EXERCISE && challenge.isJoined && challenge.status === 'ongoing'), + [challenges] + ); // 获取健身圆环数据 - 在页面聚焦、日期变化、从后台切换到前台时触发 useFocusEffect( @@ -52,6 +65,63 @@ export function FitnessRingsCard({ }, [selectedDate]) ); + useEffect(() => { + if (!selectedDate || !activityData || !joinedExerciseChallenges.length) { + return; + } + + if (!dayjs(selectedDate).isSame(dayjs(), 'day')) { + return; + } + + const { + activeEnergyBurned, + activeEnergyBurnedGoal, + appleExerciseTime, + appleExerciseTimeGoal, + appleStandHours, + appleStandHoursGoal, + } = activityData; + + if ( + activeEnergyBurnedGoal <= 0 || + appleExerciseTimeGoal <= 0 || + appleStandHoursGoal <= 0 + ) { + return; + } + + const allRingsClosed = + activeEnergyBurned >= activeEnergyBurnedGoal && + appleExerciseTime >= appleExerciseTimeGoal && + appleStandHours >= appleStandHoursGoal; + + if (!allRingsClosed) { + return; + } + + const dateKey = dayjs(selectedDate).format('YYYY-MM-DD'); + if (lastReportedRef.current?.date === dateKey) { + return; + } + + const exerciseChallenge = joinedExerciseChallenges[0]; + if (!exerciseChallenge) { + return; + } + + const reportProgressAsync = async () => { + try { + await dispatch(reportChallengeProgress({ id: exerciseChallenge.id, value: 1 })).unwrap(); + lastReportedRef.current = { date: dateKey }; + } catch (error) { + logger.warn('FitnessRingsCard: 挑战进度上报失败', { error, challengeId: exerciseChallenge.id }); + } + }; + + reportProgressAsync(); + }, [activityData, dispatch, joinedExerciseChallenges, selectedDate]); + // 使用获取到的数据或默认值 const activeCalories = activityData?.activeEnergyBurned ?? 0; const activeCaloriesGoal = activityData?.activeEnergyBurnedGoal ?? 350; @@ -229,4 +299,4 @@ const styles = StyleSheet.create({ minWidth: 25, textAlign: 'right', }, -}); \ No newline at end of file +}); diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx index adbcd65..0491685 100644 --- a/components/StressMeter.tsx +++ b/components/StressMeter.tsx @@ -58,13 +58,6 @@ export function StressMeter({ curDate }: StressMeterProps) { // 使用传入的 hrvValue 进行转换 const stressIndex = convertHrvToStressIndex(hrvValue); - // 调试信息 - console.log('StressMeter 调试:', { - hrvValue, - stressIndex, - progressPercentage: stressIndex !== null ? Math.max(0, Math.min(100, stressIndex)) : 0 - }); - // 计算进度条位置(0-100%) // 压力指数越高,进度条越满(红色区域越多) const progressPercentage = stressIndex !== null ? Math.max(0, Math.min(100, stressIndex)) : 0; diff --git a/components/challenges/ChallengeRankingItem.tsx b/components/challenges/ChallengeRankingItem.tsx index 58c22ca..1081789 100644 --- a/components/challenges/ChallengeRankingItem.tsx +++ b/components/challenges/ChallengeRankingItem.tsx @@ -8,9 +8,46 @@ type ChallengeRankingItemProps = { item: RankingItem; index: number; showDivider?: boolean; + unit?: string; }; -export function ChallengeRankingItem({ item, index, showDivider = false }: ChallengeRankingItemProps) { +const formatNumber = (value: number): string => { + if (Number.isInteger(value)) { + return value.toString(); + } + return value.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); +}; + +const formatMinutes = (value: number): string => { + const safeValue = Math.max(0, Math.round(value)); + const hours = safeValue / 60; + return `${hours.toFixed(1)} 小时`; +}; + +const formatValueWithUnit = (value: number | undefined, unit?: string): string | undefined => { + if (typeof value !== 'number' || Number.isNaN(value)) { + return undefined; + } + if (unit === 'min') { + return formatMinutes(value); + } + const formatted = formatNumber(value); + return unit ? `${formatted} ${unit}` : formatted; +}; + +export function ChallengeRankingItem({ item, index, showDivider = false, unit }: ChallengeRankingItemProps) { + console.log('unit', unit); + + const reportedLabel = formatValueWithUnit(item.todayReportedValue, unit); + const targetLabel = formatValueWithUnit(item.todayTargetValue, unit); + const progressLabel = reportedLabel && targetLabel + ? `今日 ${reportedLabel} / ${targetLabel}` + : reportedLabel + ? `今日 ${reportedLabel}` + : targetLabel + ? `今日目标 ${targetLabel}` + : undefined; + return ( @@ -28,6 +65,11 @@ export function ChallengeRankingItem({ item, index, showDivider = false }: Chall {item.name} {item.metric} + {progressLabel ? ( + + {progressLabel} + + ) : null} {item.badge ? {item.badge} : null} @@ -85,9 +127,14 @@ const styles = StyleSheet.create({ }, rankingMetric: { marginTop: 4, - fontSize: 13, + fontSize: 12, color: '#6f7ba7', }, + rankingProgress: { + marginTop: 2, + fontSize: 10, + color: '#8a94c1', + }, rankingBadge: { fontSize: 12, color: '#A67CFF', diff --git a/components/statistic/SleepCard.tsx b/components/statistic/SleepCard.tsx index 9fb484c..bdeec3e 100644 --- a/components/statistic/SleepCard.tsx +++ b/components/statistic/SleepCard.tsx @@ -24,7 +24,7 @@ const SleepCard: React.FC = ({ const [sleepDuration, setSleepDuration] = useState(null); const [loading, setLoading] = useState(false); const joinedSleepChallenges = useMemo( - () => challenges.filter((challenge) => challenge.type === ChallengeType.SLEEP && challenge.isJoined), + () => challenges.filter((challenge) => challenge.type === ChallengeType.SLEEP && challenge.isJoined && challenge.status === 'ongoing'), [challenges] ); const lastReportedRef = useRef<{ date: string; value: number | null } | null>(null); diff --git a/hooks/useWaterData.ts b/hooks/useWaterData.ts index e2c0ea6..66d9909 100644 --- a/hooks/useWaterData.ts +++ b/hooks/useWaterData.ts @@ -48,7 +48,7 @@ const useWaterChallengeProgressReporter = () => { const dispatch = useAppDispatch(); const allChallenges = useAppSelector(selectChallengeList); const joinedWaterChallenges = useMemo( - () => allChallenges.filter((challenge) => challenge.type === ChallengeType.WATER && challenge.isJoined), + () => allChallenges.filter((challenge) => challenge.type === ChallengeType.WATER && challenge.isJoined && challenge.status === 'ongoing'), [allChallenges] ); diff --git a/services/challengesApi.ts b/services/challengesApi.ts index 130c645..a6b7f3b 100644 --- a/services/challengesApi.ts +++ b/services/challengesApi.ts @@ -37,6 +37,7 @@ export type ChallengeListItemDto = { periodLabel?: string; durationLabel: string; requirementLabel: string; + unit?: string; status: ChallengeStatus; participantsCount: number; rankingDescription?: string; diff --git a/store/challengesSlice.ts b/store/challengesSlice.ts index 29b91d7..9577b45 100644 --- a/store/challengesSlice.ts +++ b/store/challengesSlice.ts @@ -366,8 +366,6 @@ const formatMonthDay = (input: string | undefined): string | undefined => { }; const buildDateRangeLabel = (challenge: ChallengeEntity): string => { - console.log('!!!!!!', challenge); - const startLabel = formatMonthDay(challenge.startAt); const endLabel = formatMonthDay(challenge.endAt); if (startLabel && endLabel) {