feat(challenges): 排行榜支持单位显示与健身圆环自动上报进度
- ChallengeRankingItem 新增 unit 字段,支持按单位格式化今日进度 - FitnessRingsCard 监听圆环闭合,自动向进行中的运动挑战上报 1 次进度 - 过滤已结束挑战,确保睡眠、喝水、运动进度仅上报进行中活动 - 移除 StressMeter 调试日志与 challengesSlice 多余打印
This commit is contained in:
@@ -473,7 +473,13 @@ export default function ChallengeDetailScreen() {
|
||||
<View style={styles.rankingCard}>
|
||||
{rankingData.length ? (
|
||||
rankingData.map((item, index) => (
|
||||
<ChallengeRankingItem key={item.id ?? index} item={item} index={index} showDivider={index > 0} />
|
||||
<ChallengeRankingItem
|
||||
key={item.id ?? index}
|
||||
item={item}
|
||||
index={index}
|
||||
showDivider={index > 0}
|
||||
unit={challenge?.unit}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View style={styles.emptyRanking}>
|
||||
|
||||
@@ -177,7 +177,13 @@ export default function ChallengeLeaderboardScreen() {
|
||||
</View>
|
||||
) : rankingData.length ? (
|
||||
rankingData.map((item, index) => (
|
||||
<ChallengeRankingItem key={item.id ?? index} item={item} index={index} showDivider={index > 0} />
|
||||
<ChallengeRankingItem
|
||||
key={item.id ?? index}
|
||||
item={item}
|
||||
index={index}
|
||||
showDivider={index > 0}
|
||||
unit={challenge?.unit}
|
||||
/>
|
||||
))
|
||||
) : rankingError ? (
|
||||
<View style={styles.emptyRanking}>
|
||||
|
||||
@@ -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<ActivityRingsData | null>(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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<View style={[styles.rankingRow, showDivider && styles.rankingRowDivider]}>
|
||||
<View style={styles.rankingOrderCircle}>
|
||||
@@ -28,6 +65,11 @@ export function ChallengeRankingItem({ item, index, showDivider = false }: Chall
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.rankingMetric}>{item.metric}</Text>
|
||||
{progressLabel ? (
|
||||
<Text style={styles.rankingProgress} numberOfLines={1}>
|
||||
{progressLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{item.badge ? <Text style={styles.rankingBadge}>{item.badge}</Text> : null}
|
||||
</View>
|
||||
@@ -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',
|
||||
|
||||
@@ -24,7 +24,7 @@ const SleepCard: React.FC<SleepCardProps> = ({
|
||||
const [sleepDuration, setSleepDuration] = useState<number | null>(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);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export type ChallengeListItemDto = {
|
||||
periodLabel?: string;
|
||||
durationLabel: string;
|
||||
requirementLabel: string;
|
||||
unit?: string;
|
||||
status: ChallengeStatus;
|
||||
participantsCount: number;
|
||||
rankingDescription?: string;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user