feat(challenges): 新增 ChallengeProgressCard 组件并接入喝水挑战进度上报

- 抽离进度卡片为独立组件,支持主题色自定义与复用
- 挑战列表页顶部展示进行中的挑战进度
- 喝水记录自动上报至关联的水挑战
- 移除旧版 challengeSlice 与冗余进度样式
- 统一使用 value 字段上报进度,兼容多类型挑战
This commit is contained in:
richarjiang
2025-09-29 15:14:59 +08:00
parent 9c86b0e565
commit 970a4b8568
9 changed files with 364 additions and 464 deletions

View File

@@ -1,3 +1,6 @@
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { ChallengeType } from '@/services/challengesApi';
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
import { logger } from '@/utils/logger';
import { Toast } from '@/utils/toast.utils';
@@ -41,6 +44,32 @@ function createDateRange(date: string): { startDate: string; endDate: string } {
};
}
const useWaterChallengeProgressReporter = () => {
const dispatch = useAppDispatch();
const allChallenges = useAppSelector(selectChallengeList);
const joinedWaterChallenges = useMemo(
() => allChallenges.filter((challenge) => challenge.type === ChallengeType.WATER && challenge.isJoined),
[allChallenges]
);
return useCallback(
async (value: number) => {
if (!joinedWaterChallenges.length) {
return;
}
for (const challenge of joinedWaterChallenges) {
try {
await dispatch(reportChallengeProgress({ id: challenge.id, value })).unwrap();
} catch (error) {
console.warn('挑战进度上报失败', { error, challengeId: challenge.id });
}
}
},
[dispatch, joinedWaterChallenges]
);
};
export const useWaterData = () => {
// 本地状态管理
const [loading, setLoading] = useState({
@@ -152,46 +181,53 @@ export const useWaterData = () => {
}, [getWaterRecordsByDate]);
// 创建喝水记录
const addWaterRecord = useCallback(async (amount: number, recordedAt?: string) => {
try {
const recordTime = recordedAt || dayjs().toISOString();
const reportWaterChallengeProgress = useWaterChallengeProgressReporter();
// 保存到 HealthKit
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime);
if (!healthKitSuccess) {
Toast.error('保存到 HealthKit 失败');
const addWaterRecord = useCallback(
async (amount: number, recordedAt?: string) => {
try {
const recordTime = recordedAt || dayjs().toISOString();
const date = dayjs(recordTime).format('YYYY-MM-DD');
const isToday = dayjs(recordTime).isSame(dayjs(), 'day');
// 保存到 HealthKit
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime);
if (!healthKitSuccess) {
Toast.error('保存到 HealthKit 失败');
return false;
}
// 重新获取当前日期的数据以刷新界面
const updatedRecords = await getWaterRecordsByDate(date);
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
// 如果是今天的数据更新Widget
if (isToday) {
const quickAddAmount = await getQuickWaterAmount();
try {
await syncWaterDataToWidget({
currentIntake: totalAmount,
dailyGoal: dailyWaterGoal,
quickAddAmount,
});
await refreshWidget();
} catch (widgetError) {
console.error('Widget 同步错误:', widgetError);
}
}
await reportWaterChallengeProgress(totalAmount);
return true;
} catch (error: any) {
console.error('添加喝水记录失败:', error);
Toast.error(error?.message || '添加喝水记录失败');
return false;
}
// 重新获取当前日期的数据以刷新界面
const date = dayjs(recordTime).format('YYYY-MM-DD');
await getWaterRecordsByDate(date);
// 如果是今天的数据更新Widget
if (date === dayjs().format('YYYY-MM-DD')) {
const todayRecords = waterRecords[date] || [];
const totalAmount = todayRecords.reduce((sum, record) => sum + record.amount, 0);
const quickAddAmount = await getQuickWaterAmount();
try {
await syncWaterDataToWidget({
currentIntake: totalAmount,
dailyGoal: dailyWaterGoal,
quickAddAmount,
});
await refreshWidget();
} catch (widgetError) {
console.error('Widget 同步错误:', widgetError);
}
}
return true;
} catch (error: any) {
console.error('添加喝水记录失败:', error);
Toast.error(error?.message || '添加喝水记录失败');
return false;
}
}, [getWaterRecordsByDate, waterRecords, dailyWaterGoal]);
},
[dailyWaterGoal, getWaterRecordsByDate, reportWaterChallengeProgress]
);
// 更新喝水记录HealthKit不支持更新只能删除后重新添加
const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => {
@@ -524,44 +560,51 @@ export const useWaterDataByDate = (targetDate?: string) => {
}, []);
// 创建喝水记录
const addWaterRecord = useCallback(async (amount: number, recordedAt?: string) => {
try {
const recordTime = recordedAt || dayjs().toISOString();
const reportWaterChallengeProgress = useWaterChallengeProgressReporter();
// 保存到 HealthKit
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime);
if (!healthKitSuccess) {
Toast.error('保存到 HealthKit 失败');
const addWaterRecord = useCallback(
async (amount: number, recordedAt?: string) => {
try {
const recordTime = recordedAt || dayjs().toISOString();
// 保存到 HealthKit
const healthKitSuccess = await saveWaterIntakeToHealthKit(amount, recordTime);
if (!healthKitSuccess) {
Toast.error('保存到 HealthKit 失败');
return false;
}
// 重新获取当前日期的数据以刷新界面
const updatedRecords = await getWaterRecordsByDate(dateToUse);
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
// 如果是今天的数据更新Widget
if (dateToUse === dayjs().format('YYYY-MM-DD')) {
const quickAddAmount = await getQuickWaterAmount();
try {
await syncWaterDataToWidget({
currentIntake: totalAmount,
dailyGoal: dailyWaterGoal,
quickAddAmount,
});
await refreshWidget();
} catch (widgetError) {
console.error('Widget 同步错误:', widgetError);
}
}
await reportWaterChallengeProgress(totalAmount);
return true;
} catch (error: any) {
console.error('添加喝水记录失败:', error);
Toast.error(error?.message || '添加喝水记录失败');
return false;
}
// 重新获取当前日期的数据以刷新界面
await getWaterRecordsByDate(dateToUse);
// 如果是今天的数据更新Widget
if (dateToUse === dayjs().format('YYYY-MM-DD')) {
const totalAmount = waterRecords.reduce((sum, record) => sum + record.amount, 0) + amount;
const quickAddAmount = await getQuickWaterAmount();
try {
await syncWaterDataToWidget({
currentIntake: totalAmount,
dailyGoal: dailyWaterGoal,
quickAddAmount,
});
await refreshWidget();
} catch (widgetError) {
console.error('Widget 同步错误:', widgetError);
}
}
return true;
} catch (error: any) {
console.error('添加喝水记录失败:', error);
Toast.error(error?.message || '添加喝水记录失败');
return false;
}
}, [getWaterRecordsByDate, dateToUse, waterRecords, dailyWaterGoal]);
},
[dailyWaterGoal, dateToUse, getWaterRecordsByDate, reportWaterChallengeProgress]
);
// 更新喝水记录
const updateWaterRecord = useCallback(async (id: string, amount?: number, note?: string, recordedAt?: string) => {
@@ -708,4 +751,4 @@ export const useWaterDataByDate = (targetDate?: string) => {
updateWaterGoal,
getWaterRecordsByDate,
};
};
};