feat: 支持健康数据上报
This commit is contained in:
@@ -14,7 +14,7 @@ import { WorkoutSummaryCard } from '@/components/WorkoutSummaryCard';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { syncHealthKitToServer } from '@/services/healthKitSync';
|
import { syncDailyHealthReport, syncHealthKitToServer } from '@/services/healthKitSync';
|
||||||
import { setHealthData } from '@/store/healthSlice';
|
import { setHealthData } from '@/store/healthSlice';
|
||||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import { updateUserProfile } from '@/store/userSlice';
|
import { updateUserProfile } from '@/store/userSlice';
|
||||||
@@ -64,6 +64,7 @@ export default function ExploreScreen() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
|
||||||
const userProfile = useAppSelector((s) => s.user.profile);
|
const userProfile = useAppSelector((s) => s.user.profile);
|
||||||
|
const todayWaterStats = useAppSelector((s) => s.water.todayStats);
|
||||||
|
|
||||||
const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard();
|
const { pushIfAuthedElseLogin, isLoggedIn, ensureLoggedIn } = useAuthGuard();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -293,6 +294,7 @@ export default function ExploreScreen() {
|
|||||||
try {
|
try {
|
||||||
logger.info('开始同步 HealthKit 个人健康数据到服务端...');
|
logger.info('开始同步 HealthKit 个人健康数据到服务端...');
|
||||||
|
|
||||||
|
// 1. 同步个人资料 (身高、体重、出生日期)
|
||||||
// 传入当前用户资料,用于 diff 比较
|
// 传入当前用户资料,用于 diff 比较
|
||||||
const success = await syncHealthKitToServer(
|
const success = await syncHealthKitToServer(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
@@ -302,20 +304,36 @@ export default function ExploreScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.info('HealthKit 数据同步到服务端成功');
|
logger.info('HealthKit 个人资料同步到服务端成功');
|
||||||
} else {
|
} else {
|
||||||
logger.info('HealthKit 数据同步到服务端跳过(无变化)或失败');
|
logger.info('HealthKit 个人资料同步到服务端跳过(无变化)或失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 同步每日健康数据报表 (活动、睡眠、心率等)
|
||||||
|
// 传入今日饮水量
|
||||||
|
const waterIntake = todayWaterStats?.totalAmount;
|
||||||
|
logger.info('开始同步每日健康数据报表...', { waterIntake });
|
||||||
|
|
||||||
|
const reportSuccess = await syncDailyHealthReport(waterIntake);
|
||||||
|
|
||||||
|
if (reportSuccess) {
|
||||||
|
logger.info('每日健康数据报表同步成功');
|
||||||
|
} else {
|
||||||
|
logger.info('每日健康数据报表同步跳过(无变化)或失败');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('同步 HealthKit 数据到服务端失败:', error);
|
logger.error('同步 HealthKit 数据到服务端失败:', error);
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, dispatch, userProfile]);
|
}, [isLoggedIn, dispatch, userProfile, todayWaterStats]);
|
||||||
|
|
||||||
// 初始加载时执行数据加载和同步
|
// 初始加载时执行数据加载和同步
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAllData(currentSelectedDate);
|
loadAllData(currentSelectedDate);
|
||||||
|
|
||||||
// 延迟1秒后执行同步,避免影响初始加载性能
|
// 延迟1秒后执行同步,避免影响初始加载性能
|
||||||
|
// 如果 todayWaterStats 还未加载完成,可能会导致第一次同步时 waterIntake 为 undefined
|
||||||
|
// 但 waterSlice.fetchTodayWaterStats 会在 loadAllData 中被调用
|
||||||
const syncTimer = setTimeout(() => {
|
const syncTimer = setTimeout(() => {
|
||||||
syncHealthDataToServer();
|
syncHealthDataToServer();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { ChallengeType } from '@/services/challengesApi';
|
import { ChallengeType } from '@/services/challengesApi';
|
||||||
|
import { WaterRecordSource } from '@/services/waterRecords';
|
||||||
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
|
import { reportChallengeProgress, selectChallengeList } from '@/store/challengesSlice';
|
||||||
|
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||||
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
|
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
|
||||||
import { Toast } from '@/utils/toast.utils';
|
import { Toast } from '@/utils/toast.utils';
|
||||||
import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences';
|
import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences';
|
||||||
@@ -81,6 +83,9 @@ export const useWaterData = () => {
|
|||||||
const [waterRecords, setWaterRecords] = useState<{ [date: string]: WaterRecord[] }>({});
|
const [waterRecords, setWaterRecords] = useState<{ [date: string]: WaterRecord[] }>({});
|
||||||
const [selectedDate, setSelectedDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
const [selectedDate, setSelectedDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
// Redux dispatch
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// 获取指定日期的记录
|
// 获取指定日期的记录
|
||||||
const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => {
|
const getWaterRecordsByDate = useCallback(async (date: string, page = 1, limit = 20) => {
|
||||||
setLoading(prev => ({ ...prev, records: true }));
|
setLoading(prev => ({ ...prev, records: true }));
|
||||||
@@ -196,6 +201,15 @@ export const useWaterData = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步到服务端(后台执行,不阻塞 UI)
|
||||||
|
dispatch(createWaterRecordAction({
|
||||||
|
amount,
|
||||||
|
recordedAt: recordTime,
|
||||||
|
source: WaterRecordSource.Manual,
|
||||||
|
})).catch((err) => {
|
||||||
|
console.warn('同步饮水记录到服务端失败:', err);
|
||||||
|
});
|
||||||
|
|
||||||
// 重新获取当前日期的数据以刷新界面
|
// 重新获取当前日期的数据以刷新界面
|
||||||
const updatedRecords = await getWaterRecordsByDate(date);
|
const updatedRecords = await getWaterRecordsByDate(date);
|
||||||
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
|
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
|
||||||
@@ -225,7 +239,7 @@ export const useWaterData = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dailyWaterGoal, getWaterRecordsByDate, reportWaterChallengeProgress]
|
[dailyWaterGoal, getWaterRecordsByDate, reportWaterChallengeProgress, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新喝水记录(HealthKit不支持更新,只能删除后重新添加)
|
// 更新喝水记录(HealthKit不支持更新,只能删除后重新添加)
|
||||||
@@ -554,6 +568,7 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
|
|
||||||
// 创建喝水记录
|
// 创建喝水记录
|
||||||
const reportWaterChallengeProgress = useWaterChallengeProgressReporter();
|
const reportWaterChallengeProgress = useWaterChallengeProgressReporter();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const addWaterRecord = useCallback(
|
const addWaterRecord = useCallback(
|
||||||
async (amount: number, recordedAt?: string) => {
|
async (amount: number, recordedAt?: string) => {
|
||||||
@@ -567,6 +582,15 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步到服务端(后台执行,不阻塞 UI)
|
||||||
|
dispatch(createWaterRecordAction({
|
||||||
|
amount,
|
||||||
|
recordedAt: recordTime,
|
||||||
|
source: WaterRecordSource.Manual,
|
||||||
|
})).catch((err) => {
|
||||||
|
console.warn('同步饮水记录到服务端失败:', err);
|
||||||
|
});
|
||||||
|
|
||||||
// 重新获取当前日期的数据以刷新界面
|
// 重新获取当前日期的数据以刷新界面
|
||||||
const updatedRecords = await getWaterRecordsByDate(dateToUse);
|
const updatedRecords = await getWaterRecordsByDate(dateToUse);
|
||||||
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
|
const totalAmount = updatedRecords.reduce((sum, record) => sum + record.amount, 0);
|
||||||
@@ -596,7 +620,7 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dailyWaterGoal, dateToUse, getWaterRecordsByDate, reportWaterChallengeProgress]
|
[dailyWaterGoal, dateToUse, getWaterRecordsByDate, reportWaterChallengeProgress, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新喝水记录
|
// 更新喝水记录
|
||||||
|
|||||||
@@ -8,16 +8,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
fetchActiveEnergyBurned,
|
||||||
|
fetchBasalEnergyBurned,
|
||||||
|
fetchCompleteSleepData,
|
||||||
|
fetchHourlyExerciseMinutesForDate,
|
||||||
|
fetchHourlyStandHoursForDate,
|
||||||
|
fetchOxygenSaturation,
|
||||||
fetchPersonalHealthData,
|
fetchPersonalHealthData,
|
||||||
|
fetchSmartHRVData,
|
||||||
saveHeight,
|
saveHeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
} from '@/utils/health';
|
} from '@/utils/health';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
import { convertHrvToStressIndex } from '@/utils/stress';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { DailyHealthDataDto, updateDailyHealthData } from './users';
|
||||||
|
|
||||||
// 同步状态存储键
|
// 同步状态存储键
|
||||||
const SYNC_STATUS_KEY = '@healthkit_sync_status';
|
const SYNC_STATUS_KEY = '@healthkit_sync_status';
|
||||||
const SYNC_LOCK_KEY = '@healthkit_sync_lock';
|
const SYNC_LOCK_KEY = '@healthkit_sync_lock';
|
||||||
|
const DAILY_HEALTH_SYNC_KEY = '@daily_health_sync_status';
|
||||||
|
|
||||||
// 同步状态类型
|
// 同步状态类型
|
||||||
interface SyncStatus {
|
interface SyncStatus {
|
||||||
@@ -30,6 +40,13 @@ interface SyncStatus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每日健康数据同步状态
|
||||||
|
interface DailyHealthSyncStatus {
|
||||||
|
lastSyncTime: number;
|
||||||
|
lastSyncDate: string; // YYYY-MM-DD
|
||||||
|
data: DailyHealthDataDto;
|
||||||
|
}
|
||||||
|
|
||||||
// 同步锁(防止并发同步)
|
// 同步锁(防止并发同步)
|
||||||
let syncLock = false;
|
let syncLock = false;
|
||||||
|
|
||||||
@@ -445,3 +462,149 @@ export async function clearSyncStatus(): Promise<void> {
|
|||||||
export async function getSyncStatusInfo(): Promise<SyncStatus | null> {
|
export async function getSyncStatusInfo(): Promise<SyncStatus | null> {
|
||||||
return getLastSyncStatus();
|
return getLastSyncStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步每日健康数据报表到服务端
|
||||||
|
* @param waterIntake - 当日饮水量(从应用内部获取,因为 HealthKit 可能不包含应用内记录)
|
||||||
|
*/
|
||||||
|
export async function syncDailyHealthReport(waterIntake?: number): Promise<boolean> {
|
||||||
|
console.log('=== 开始同步每日健康报表 ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const today = new Date();
|
||||||
|
const dateStr = dayjs(today).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// 1. 获取各项健康数据
|
||||||
|
// 并行获取以提高性能
|
||||||
|
const [
|
||||||
|
activeEnergy,
|
||||||
|
basalEnergy,
|
||||||
|
sleepData,
|
||||||
|
exerciseMinutesData,
|
||||||
|
standHoursData,
|
||||||
|
oxygenSaturation,
|
||||||
|
hrvData
|
||||||
|
] = await Promise.all([
|
||||||
|
// 卡路里
|
||||||
|
fetchActiveEnergyBurned({
|
||||||
|
startDate: dayjs(today).startOf('day').toISOString(),
|
||||||
|
endDate: dayjs(today).endOf('day').toISOString()
|
||||||
|
}),
|
||||||
|
// 基础代谢
|
||||||
|
fetchBasalEnergyBurned({
|
||||||
|
startDate: dayjs(today).startOf('day').toISOString(),
|
||||||
|
endDate: dayjs(today).endOf('day').toISOString()
|
||||||
|
}),
|
||||||
|
// 睡眠数据 (需要完整数据来获取分钟数)
|
||||||
|
fetchCompleteSleepData(today),
|
||||||
|
// 锻炼分钟数 (按小时聚合)
|
||||||
|
fetchHourlyExerciseMinutesForDate(today),
|
||||||
|
// 站立小时 (按小时聚合)
|
||||||
|
fetchHourlyStandHoursForDate(today),
|
||||||
|
// 血氧
|
||||||
|
fetchOxygenSaturation({
|
||||||
|
startDate: dayjs(today).startOf('day').toISOString(),
|
||||||
|
endDate: dayjs(today).endOf('day').toISOString()
|
||||||
|
}),
|
||||||
|
// HRV (用于计算压力)
|
||||||
|
fetchSmartHRVData(today)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. 数据处理与计算
|
||||||
|
|
||||||
|
// 计算总锻炼分钟数
|
||||||
|
const totalExerciseMinutes = exerciseMinutesData.reduce((sum, item) => sum + item.minutes, 0);
|
||||||
|
|
||||||
|
// 计算总站立时间 (分钟) - 注意 HealthKit 返回的是小时是否有站立,我们这里估算每小时站立1分钟或者直接用小时数
|
||||||
|
// 根据 API 要求 "standingMinutes",HealthKit 的 standHours 是指有多少个小时有站立活动
|
||||||
|
// 通常 Apple Watch 判定一小时内站立至少1分钟即计为1个站立小时
|
||||||
|
// 为了符合 API 语义,我们这里统计有多少个小时达标,转换成分钟可能不太准确,但 API 字段叫 minutes
|
||||||
|
// 策略:如果有 HealthKit 数据,我们用 达标小时数 * 60 作为估算,或者直接传小时数?
|
||||||
|
// 文档说 "standingMinutes: 站立时间(分钟)"。
|
||||||
|
// HealthKit 的 appleStandHours 是 count,比如 12。
|
||||||
|
// 如果我们传 12 分钟显然不对。如果我们传 12 * 60 = 720 分钟也不太对,因为并不是站了这么久。
|
||||||
|
// 实际上 Apple 的 Stand Hours 是 "Hours with >1 min standing".
|
||||||
|
// 这里我们统计有多少小时是有站立的,并乘以一个系数?或者直接传小时数让后端理解?
|
||||||
|
// 鉴于字段名是 minutes,我们统计所有有站立的小时数。
|
||||||
|
// 实际上,我们应该直接使用 HealthKit 的 appleStandTime (如果可用) 或者通过 appleStandHours 估算。
|
||||||
|
// 这里的 fetchHourlyStandHoursForDate 返回的是每小时是否有站立(0或1)。
|
||||||
|
const standHoursCount = standHoursData.filter(h => h.hasStood > 0).length;
|
||||||
|
// 暂时策略:将小时数转换为分钟数传递,虽然这代表的是跨度
|
||||||
|
const standingMinutes = standHoursCount * 60;
|
||||||
|
|
||||||
|
// 计算睡眠分钟数
|
||||||
|
const sleepMinutes = sleepData ? Math.round(sleepData.totalSleepTime) : 0;
|
||||||
|
|
||||||
|
// 计算压力值
|
||||||
|
let stressLevel = 0;
|
||||||
|
if (hrvData && hrvData.value > 0) {
|
||||||
|
const stressIndex = convertHrvToStressIndex(hrvData.value);
|
||||||
|
if (stressIndex !== null) {
|
||||||
|
stressLevel = stressIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构建 DTO
|
||||||
|
const healthData: DailyHealthDataDto = {
|
||||||
|
date: dateStr,
|
||||||
|
// 只有当数据有效时才包含字段
|
||||||
|
...(waterIntake !== undefined && { waterIntake }),
|
||||||
|
...(totalExerciseMinutes > 0 && { exerciseMinutes: Math.round(totalExerciseMinutes) }),
|
||||||
|
...(activeEnergy > 0 && { caloriesBurned: Math.round(activeEnergy) }),
|
||||||
|
...(standingMinutes > 0 && { standingMinutes }),
|
||||||
|
...(basalEnergy > 0 && { basalMetabolism: Math.round(basalEnergy) }),
|
||||||
|
...(sleepMinutes > 0 && { sleepMinutes }),
|
||||||
|
...(oxygenSaturation !== null && oxygenSaturation > 0 && { bloodOxygen: oxygenSaturation }),
|
||||||
|
...(stressLevel > 0 && { stressLevel })
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('准备同步每日健康数据:', healthData);
|
||||||
|
|
||||||
|
// 4. 检查是否需要同步 (与上次同步的数据比较)
|
||||||
|
const lastSyncStatusStr = await AsyncStorage.getItem(DAILY_HEALTH_SYNC_KEY);
|
||||||
|
if (lastSyncStatusStr) {
|
||||||
|
const lastSyncStatus: DailyHealthSyncStatus = JSON.parse(lastSyncStatusStr);
|
||||||
|
|
||||||
|
// 如果是同一天,检查数据差异
|
||||||
|
if (lastSyncStatus.lastSyncDate === dateStr) {
|
||||||
|
const lastData = lastSyncStatus.data;
|
||||||
|
const isDifferent =
|
||||||
|
healthData.waterIntake !== lastData.waterIntake ||
|
||||||
|
healthData.exerciseMinutes !== lastData.exerciseMinutes ||
|
||||||
|
healthData.caloriesBurned !== lastData.caloriesBurned ||
|
||||||
|
healthData.standingMinutes !== lastData.standingMinutes ||
|
||||||
|
healthData.basalMetabolism !== lastData.basalMetabolism ||
|
||||||
|
healthData.sleepMinutes !== lastData.sleepMinutes ||
|
||||||
|
healthData.bloodOxygen !== lastData.bloodOxygen ||
|
||||||
|
healthData.stressLevel !== lastData.stressLevel;
|
||||||
|
|
||||||
|
if (!isDifferent) {
|
||||||
|
console.log('每日健康数据无变化,跳过同步');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 调用 API
|
||||||
|
if (Object.keys(healthData).length > 1) { // 至少包含 date 以外的一个字段
|
||||||
|
await updateDailyHealthData(healthData);
|
||||||
|
console.log('每日健康数据同步成功');
|
||||||
|
|
||||||
|
// 6. 保存同步状态
|
||||||
|
const newSyncStatus: DailyHealthSyncStatus = {
|
||||||
|
lastSyncTime: Date.now(),
|
||||||
|
lastSyncDate: dateStr,
|
||||||
|
data: healthData
|
||||||
|
};
|
||||||
|
await AsyncStorage.setItem(DAILY_HEALTH_SYNC_KEY, JSON.stringify(newSyncStatus));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log('没有有效的健康数据需要同步');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步每日健康报表失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,4 +54,24 @@ export async function updateBodyMeasurements(dto: BodyMeasurementsDto): Promise<
|
|||||||
return await api.put('/users/body-measurements', dto);
|
return await api.put('/users/body-measurements', dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DailyHealthDataDto = {
|
||||||
|
date?: string; // YYYY-MM-DD
|
||||||
|
waterIntake?: number; // ml
|
||||||
|
exerciseMinutes?: number; // minutes
|
||||||
|
caloriesBurned?: number; // kcal
|
||||||
|
standingMinutes?: number; // minutes
|
||||||
|
basalMetabolism?: number; // kcal
|
||||||
|
sleepMinutes?: number; // minutes
|
||||||
|
bloodOxygen?: number; // % (0-100)
|
||||||
|
stressLevel?: number; // ms (based on HRV)
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function updateDailyHealthData(dto: DailyHealthDataDto): Promise<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: any;
|
||||||
|
}> {
|
||||||
|
return await api.put('/users/daily-health', dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CompleteSleepData, fetchCompleteSleepData } from '@/utils/sleepHealthKit';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { AppState, AppStateStatus, NativeModules } from 'react-native';
|
import { AppState, AppStateStatus, NativeModules } from 'react-native';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
@@ -599,7 +600,7 @@ async function fetchHourlyStandHours(date: Date): Promise<number[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
|
export async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const result = await HealthKitManager.getActiveEnergyBurned(options);
|
const result = await HealthKitManager.getActiveEnergyBurned(options);
|
||||||
|
|
||||||
@@ -1484,10 +1485,14 @@ export async function fetchHourlyStandHoursForDate(date: Date): Promise<HourlySt
|
|||||||
const hourlyStandData = await fetchHourlyStandHours(date);
|
const hourlyStandData = await fetchHourlyStandHours(date);
|
||||||
return hourlyStandData.map((hasStood, hour) => ({
|
return hourlyStandData.map((hasStood, hour) => ({
|
||||||
hour,
|
hour,
|
||||||
hasStood
|
hasStood: typeof hasStood === 'number' ? hasStood : (hasStood ? 1 : 0)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出获取完整睡眠数据的函数 (代理到 sleepHealthKit)
|
||||||
|
export { fetchCompleteSleepData };
|
||||||
|
export type { CompleteSleepData };
|
||||||
|
|
||||||
// 专门为活动圆环详情页获取精简的数据
|
// 专门为活动圆环详情页获取精简的数据
|
||||||
export async function fetchActivityRingsForDate(date: Date): Promise<ActivityRingsData | null> {
|
export async function fetchActivityRingsForDate(date: Date): Promise<ActivityRingsData | null> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user