feat: 更新健康数据权限描述,添加HRV数据获取测试功能,优化后台任务配置,调整压力计显示单位
This commit is contained in:
7
app.json
7
app.json
@@ -17,8 +17,11 @@
|
|||||||
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
|
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
|
||||||
|
"NSUserNotificationsUsageDescription": "应用需要发送通知以提醒您喝水和站立活动。",
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
"processing"
|
"processing",
|
||||||
|
"fetch",
|
||||||
|
"remote-notification"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -55,7 +58,7 @@
|
|||||||
"react-native-health",
|
"react-native-health",
|
||||||
{
|
{
|
||||||
"enableHealthAPI": true,
|
"enableHealthAPI": true,
|
||||||
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。",
|
"healthSharePermission": "应用需要访问您的健康数据(步数、能量消耗、心率变异性等)以展示运动统计和压力分析。",
|
||||||
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/mo
|
|||||||
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||||
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
||||||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
|
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
|
||||||
import { getTestHealthData } from '@/utils/mockHealthData';
|
import { getTestHealthData } from '@/utils/mockHealthData';
|
||||||
import { calculateNutritionGoals } from '@/utils/nutrition';
|
import { calculateNutritionGoals } from '@/utils/nutrition';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
@@ -460,6 +460,7 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* 开发环境调试按钮 */}
|
{/* 开发环境调试按钮 */}
|
||||||
{__DEV__ && (
|
{__DEV__ && (
|
||||||
|
<View style={styles.debugButtonsContainer}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.debugButton}
|
style={styles.debugButton}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
@@ -469,6 +470,17 @@ export default function ExploreScreen() {
|
|||||||
>
|
>
|
||||||
<Text style={styles.debugButtonText}>🔧</Text>
|
<Text style={styles.debugButtonText}>🔧</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.debugButton, styles.hrvTestButton]}
|
||||||
|
onPress={async () => {
|
||||||
|
console.log('🫀 测试HRV数据获取...');
|
||||||
|
await testHRVDataFetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.debugButtonText}>🫀</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -665,6 +677,10 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
|
debugButtonsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
debugButton: {
|
debugButton: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -681,6 +697,9 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
|
hrvTestButton: {
|
||||||
|
backgroundColor: '#8B5CF6',
|
||||||
|
},
|
||||||
debugButtonText: {
|
debugButtonText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
|||||||
{/* 数值显示区域 */}
|
{/* 数值显示区域 */}
|
||||||
<View style={styles.valueSection}>
|
<View style={styles.valueSection}>
|
||||||
<Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
|
<Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
|
||||||
<Text style={styles.unit}>ms</Text>
|
<Text style={styles.unit}>%</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 进度条区域 */}
|
{/* 进度条区域 */}
|
||||||
|
|||||||
5
ios/WaterWidgetExtension.entitlements
Normal file
5
ios/WaterWidgetExtension.entitlements
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.anonymous.digitalpilates.backgroundfetch</string>
|
<string>background-health-reminders</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.7</string>
|
<string>1.0.8</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -54,13 +54,15 @@
|
|||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>应用需要使用相机以实现相关功能</string>
|
<string>应用需要使用相机以实现相关功能</string>
|
||||||
<key>NSHealthShareUsageDescription</key>
|
<key>NSHealthShareUsageDescription</key>
|
||||||
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
<string>应用需要访问您的健康数据(步数、能量消耗、心率变异性等)以展示运动统计和压力分析。</string>
|
||||||
<key>NSHealthUpdateUsageDescription</key>
|
<key>NSHealthUpdateUsageDescription</key>
|
||||||
<string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string>
|
<string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
|
<string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
|
||||||
|
<key>NSUserNotificationsUsageDescription</key>
|
||||||
|
<string>应用需要发送通知以提醒您喝水和站立活动。</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>processing</string>
|
<string>processing</string>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notifica
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import * as BackgroundTask from 'expo-background-task';
|
import * as BackgroundTask from 'expo-background-task';
|
||||||
import * as TaskManager from 'expo-task-manager';
|
import * as TaskManager from 'expo-task-manager';
|
||||||
|
import { TaskManagerTaskBody } from 'expo-task-manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台任务标识符
|
* 后台任务标识符
|
||||||
@@ -14,7 +15,7 @@ export const BACKGROUND_TASK_IDS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 定义后台任务
|
// 定义后台任务
|
||||||
TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async () => {
|
TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async (body: TaskManagerTaskBody) => {
|
||||||
try {
|
try {
|
||||||
console.log('[BackgroundTask] 后台任务执行');
|
console.log('[BackgroundTask] 后台任务执行');
|
||||||
await executeBackgroundTasks();
|
await executeBackgroundTasks();
|
||||||
|
|||||||
@@ -452,22 +452,43 @@ async function fetchSleepDuration(options: HealthDataOptions): Promise<number> {
|
|||||||
|
|
||||||
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
|
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
console.log('=== 开始获取HRV数据 ===');
|
||||||
|
console.log('查询选项:', options);
|
||||||
|
|
||||||
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
|
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
|
||||||
|
console.log('HRV API调用结果:', { err, res });
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
logError('HRV数据', err);
|
logError('HRV数据', err);
|
||||||
|
console.error('HRV获取错误详情:', err);
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res || !Array.isArray(res) || res.length === 0) {
|
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||||
logWarning('HRV', '为空或格式错误');
|
logWarning('HRV', '为空或格式错误');
|
||||||
|
console.warn('HRV数据为空,原始响应:', res);
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
logSuccess('HRV', res);
|
logSuccess('HRV', res);
|
||||||
|
console.log('HRV数据样本数量:', res.length);
|
||||||
|
|
||||||
|
// 打印最新的几个样本用于调试
|
||||||
|
const latestSamples = res.slice(-3);
|
||||||
|
console.log('最新的HRV样本:', latestSamples.map(sample => ({
|
||||||
|
value: sample.value,
|
||||||
|
startDate: sample.startDate,
|
||||||
|
endDate: sample.endDate
|
||||||
|
})));
|
||||||
|
|
||||||
const latestHrv = res[res.length - 1];
|
const latestHrv = res[res.length - 1];
|
||||||
if (latestHrv && latestHrv.value) {
|
if (latestHrv && latestHrv.value !== undefined && latestHrv.value !== null) {
|
||||||
// HealthKit 中的 HRV 数据已经是毫秒单位,无需转换
|
// HealthKit 中的 HRV 数据已经是毫秒单位,无需转换
|
||||||
resolve(Math.round(latestHrv.value));
|
const hrvValue = Math.round(latestHrv.value);
|
||||||
|
console.log('最终HRV值:', hrvValue);
|
||||||
|
resolve(hrvValue);
|
||||||
} else {
|
} else {
|
||||||
|
console.warn('HRV样本值无效:', latestHrv);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -681,6 +702,41 @@ export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | nu
|
|||||||
return fetchHeartRateVariability(options);
|
return fetchHeartRateVariability(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试HRV数据获取功能
|
||||||
|
export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise<void> {
|
||||||
|
console.log('=== 开始测试HRV数据获取 ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 首先确保权限
|
||||||
|
const hasPermission = await ensureHealthPermissions();
|
||||||
|
if (!hasPermission) {
|
||||||
|
console.error('没有健康数据权限,无法测试HRV');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('权限检查通过,开始获取HRV数据...');
|
||||||
|
|
||||||
|
// 测试不同时间范围的HRV数据
|
||||||
|
const options = createDateRange(date);
|
||||||
|
|
||||||
|
// 获取今日HRV
|
||||||
|
const todayHRV = await fetchHeartRateVariability(options);
|
||||||
|
console.log('今日HRV结果:', todayHRV);
|
||||||
|
|
||||||
|
// 获取最近2小时HRV
|
||||||
|
const recentHRV = await fetchRecentHRV(2);
|
||||||
|
console.log('最近2小时HRV结果:', recentHRV);
|
||||||
|
|
||||||
|
// 获取指定日期HRV
|
||||||
|
const dateHRV = await fetchHRVForDate(date);
|
||||||
|
console.log('指定日期HRV结果:', dateHRV);
|
||||||
|
|
||||||
|
console.log('=== HRV数据测试完成 ===');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('HRV测试过程中出现错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新healthkit中的体重
|
// 更新healthkit中的体重
|
||||||
export async function updateWeight(weight: number) {
|
export async function updateWeight(weight: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user