feat(nutrition): 添加营养数据保存功能到HealthKit,包括蛋白质、脂肪和碳水化合物
This commit is contained in:
@@ -9,7 +9,6 @@ import 'react-native-reanimated';
|
|||||||
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useQuickActions } from '@/hooks/useQuickActions';
|
import { useQuickActions } from '@/hooks/useQuickActions';
|
||||||
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
|
||||||
import { hrvMonitorService } from '@/services/hrvMonitor';
|
import { hrvMonitorService } from '@/services/hrvMonitor';
|
||||||
import { notificationService } from '@/services/notifications';
|
import { notificationService } from '@/services/notifications';
|
||||||
import { setupQuickActions } from '@/services/quickActions';
|
import { setupQuickActions } from '@/services/quickActions';
|
||||||
@@ -137,10 +136,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
await setupQuickActions();
|
await setupQuickActions();
|
||||||
logger.info('✅ 快捷动作初始化完成');
|
logger.info('✅ 快捷动作初始化完成');
|
||||||
|
|
||||||
// 4. 清空 AI 教练会话缓存(轻量操作)
|
|
||||||
clearAiCoachSessionCache();
|
|
||||||
logger.info('✅ AI 教练缓存清理完成');
|
|
||||||
|
|
||||||
// 5. 初始化喝水记录 Bridge
|
// 5. 初始化喝水记录 Bridge
|
||||||
initializeWaterRecordBridge();
|
initializeWaterRecordBridge();
|
||||||
logger.info('✅ 喝水记录 Bridge 初始化完成');
|
logger.info('✅ 喝水记录 Bridge 初始化完成');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { addDietRecord, type CreateDietRecordDto } from '@/services/dietRecords'
|
|||||||
import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi';
|
import { foodLibraryApi, type CreateCustomFoodDto } from '@/services/foodLibraryApi';
|
||||||
import { fetchDailyNutritionData } from '@/store/nutritionSlice';
|
import { fetchDailyNutritionData } from '@/store/nutritionSlice';
|
||||||
import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food';
|
import type { FoodItem, MealType, SelectedFoodItem } from '@/types/food';
|
||||||
|
import { saveNutritionToHealthKit } from '@/utils/health';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
@@ -178,7 +179,27 @@ export default function FoodLibraryScreen() {
|
|||||||
imageUrl: item.food.imageUrl,
|
imageUrl: item.food.imageUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 先保存到后端
|
||||||
await addDietRecord(dietRecordData);
|
await addDietRecord(dietRecordData);
|
||||||
|
|
||||||
|
// 然后尝试同步到 HealthKit(非阻塞)
|
||||||
|
// 提取蛋白质、脂肪和碳水化合物数据
|
||||||
|
const { proteinGrams, fatGrams, carbohydrateGrams, mealTime } = dietRecordData;
|
||||||
|
|
||||||
|
if (proteinGrams !== undefined || fatGrams !== undefined || carbohydrateGrams !== undefined) {
|
||||||
|
// 使用 catch 确保 HealthKit 同步失败不影响后端记录
|
||||||
|
saveNutritionToHealthKit(
|
||||||
|
{
|
||||||
|
proteinGrams: proteinGrams || undefined,
|
||||||
|
fatGrams: fatGrams || undefined,
|
||||||
|
carbohydrateGrams: carbohydrateGrams || undefined
|
||||||
|
},
|
||||||
|
mealTime
|
||||||
|
).catch(error => {
|
||||||
|
// HealthKit 同步失败只记录日志,不影响用户体验
|
||||||
|
console.error('HealthKit 营养数据同步失败(不影响记录):', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录成功后,刷新当天的营养数据
|
// 记录成功后,刷新当天的营养数据
|
||||||
|
|||||||
@@ -78,6 +78,19 @@ RCT_EXTERN_METHOD(getWaterIntakeFromHealthKit:(NSDictionary *)options
|
|||||||
resolver:(RCTPromiseResolveBlock)resolver
|
resolver:(RCTPromiseResolveBlock)resolver
|
||||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||||
|
|
||||||
|
// Nutrition Data Methods
|
||||||
|
RCT_EXTERN_METHOD(saveProteinToHealthKit:(NSDictionary *)options
|
||||||
|
resolver:(RCTPromiseResolveBlock)resolver
|
||||||
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(saveFatToHealthKit:(NSDictionary *)options
|
||||||
|
resolver:(RCTPromiseResolveBlock)resolver
|
||||||
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||||
|
|
||||||
|
RCT_EXTERN_METHOD(saveCarbohydratesToHealthKit:(NSDictionary *)options
|
||||||
|
resolver:(RCTPromiseResolveBlock)resolver
|
||||||
|
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||||
|
|
||||||
// Workout Data Methods
|
// Workout Data Methods
|
||||||
RCT_EXTERN_METHOD(getRecentWorkouts:(NSDictionary *)options
|
RCT_EXTERN_METHOD(getRecentWorkouts:(NSDictionary *)options
|
||||||
resolver:(RCTPromiseResolveBlock)resolver
|
resolver:(RCTPromiseResolveBlock)resolver
|
||||||
|
|||||||
@@ -88,11 +88,23 @@ class HealthKitManager: RCTEventEmitter {
|
|||||||
static var dietaryWater: HKQuantityType? {
|
static var dietaryWater: HKQuantityType? {
|
||||||
return HKObjectType.quantityType(forIdentifier: .dietaryWater)
|
return HKObjectType.quantityType(forIdentifier: .dietaryWater)
|
||||||
}
|
}
|
||||||
|
static var dietaryProtein: HKQuantityType? {
|
||||||
|
return HKObjectType.quantityType(forIdentifier: .dietaryProtein)
|
||||||
|
}
|
||||||
|
static var dietaryFatTotal: HKQuantityType? {
|
||||||
|
return HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)
|
||||||
|
}
|
||||||
|
static var dietaryCarbohydrates: HKQuantityType? {
|
||||||
|
return HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
|
||||||
|
}
|
||||||
|
|
||||||
static var all: Set<HKSampleType> {
|
static var all: Set<HKSampleType> {
|
||||||
var types: Set<HKSampleType> = []
|
var types: Set<HKSampleType> = []
|
||||||
if let bodyMass = bodyMass { types.insert(bodyMass) }
|
if let bodyMass = bodyMass { types.insert(bodyMass) }
|
||||||
if let dietaryWater = dietaryWater { types.insert(dietaryWater) }
|
if let dietaryWater = dietaryWater { types.insert(dietaryWater) }
|
||||||
|
if let dietaryProtein = dietaryProtein { types.insert(dietaryProtein) }
|
||||||
|
if let dietaryFatTotal = dietaryFatTotal { types.insert(dietaryFatTotal) }
|
||||||
|
if let dietaryCarbohydrates = dietaryCarbohydrates { types.insert(dietaryCarbohydrates) }
|
||||||
return types
|
return types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1669,6 +1681,194 @@ class HealthKitManager: RCTEventEmitter {
|
|||||||
healthStore.execute(query)
|
healthStore.execute(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Nutrition Data Methods
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func saveProteinToHealthKit(
|
||||||
|
_ options: NSDictionary,
|
||||||
|
resolver: @escaping RCTPromiseResolveBlock,
|
||||||
|
rejecter: @escaping RCTPromiseRejectBlock
|
||||||
|
) {
|
||||||
|
guard HKHealthStore.isHealthDataAvailable() else {
|
||||||
|
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parameters
|
||||||
|
guard let amount = options["amount"] as? Double else {
|
||||||
|
rejecter("INVALID_PARAMETERS", "Amount is required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordedAt: Date
|
||||||
|
if let recordedAtString = options["recordedAt"] as? String,
|
||||||
|
let date = parseDate(from: recordedAtString) {
|
||||||
|
recordedAt = date
|
||||||
|
} else {
|
||||||
|
recordedAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let proteinType = WriteTypes.dietaryProtein else {
|
||||||
|
rejecter("TYPE_NOT_AVAILABLE", "Protein type is not available", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create quantity sample (protein in grams)
|
||||||
|
let quantity = HKQuantity(unit: HKUnit.gram(), doubleValue: amount)
|
||||||
|
let sample = HKQuantitySample(
|
||||||
|
type: proteinType,
|
||||||
|
quantity: quantity,
|
||||||
|
start: recordedAt,
|
||||||
|
end: recordedAt,
|
||||||
|
metadata: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save to HealthKit
|
||||||
|
healthStore.save(sample) { [weak self] (success, error) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
rejecter("SAVE_ERROR", "Failed to save protein: \(error.localizedDescription)", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
let result: [String: Any] = [
|
||||||
|
"success": true,
|
||||||
|
"amount": amount,
|
||||||
|
"recordedAt": self?.dateToISOString(recordedAt) ?? ""
|
||||||
|
]
|
||||||
|
resolver(result)
|
||||||
|
} else {
|
||||||
|
rejecter("SAVE_FAILED", "Failed to save protein", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func saveFatToHealthKit(
|
||||||
|
_ options: NSDictionary,
|
||||||
|
resolver: @escaping RCTPromiseResolveBlock,
|
||||||
|
rejecter: @escaping RCTPromiseRejectBlock
|
||||||
|
) {
|
||||||
|
guard HKHealthStore.isHealthDataAvailable() else {
|
||||||
|
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parameters
|
||||||
|
guard let amount = options["amount"] as? Double else {
|
||||||
|
rejecter("INVALID_PARAMETERS", "Amount is required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordedAt: Date
|
||||||
|
if let recordedAtString = options["recordedAt"] as? String,
|
||||||
|
let date = parseDate(from: recordedAtString) {
|
||||||
|
recordedAt = date
|
||||||
|
} else {
|
||||||
|
recordedAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let fatType = WriteTypes.dietaryFatTotal else {
|
||||||
|
rejecter("TYPE_NOT_AVAILABLE", "Fat type is not available", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create quantity sample (fat in grams)
|
||||||
|
let quantity = HKQuantity(unit: HKUnit.gram(), doubleValue: amount)
|
||||||
|
let sample = HKQuantitySample(
|
||||||
|
type: fatType,
|
||||||
|
quantity: quantity,
|
||||||
|
start: recordedAt,
|
||||||
|
end: recordedAt,
|
||||||
|
metadata: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save to HealthKit
|
||||||
|
healthStore.save(sample) { [weak self] (success, error) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
rejecter("SAVE_ERROR", "Failed to save fat: \(error.localizedDescription)", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
let result: [String: Any] = [
|
||||||
|
"success": true,
|
||||||
|
"amount": amount,
|
||||||
|
"recordedAt": self?.dateToISOString(recordedAt) ?? ""
|
||||||
|
]
|
||||||
|
resolver(result)
|
||||||
|
} else {
|
||||||
|
rejecter("SAVE_FAILED", "Failed to save fat", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func saveCarbohydratesToHealthKit(
|
||||||
|
_ options: NSDictionary,
|
||||||
|
resolver: @escaping RCTPromiseResolveBlock,
|
||||||
|
rejecter: @escaping RCTPromiseRejectBlock
|
||||||
|
) {
|
||||||
|
guard HKHealthStore.isHealthDataAvailable() else {
|
||||||
|
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parameters
|
||||||
|
guard let amount = options["amount"] as? Double else {
|
||||||
|
rejecter("INVALID_PARAMETERS", "Amount is required", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordedAt: Date
|
||||||
|
if let recordedAtString = options["recordedAt"] as? String,
|
||||||
|
let date = parseDate(from: recordedAtString) {
|
||||||
|
recordedAt = date
|
||||||
|
} else {
|
||||||
|
recordedAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let carbohydratesType = WriteTypes.dietaryCarbohydrates else {
|
||||||
|
rejecter("TYPE_NOT_AVAILABLE", "Carbohydrates type is not available", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create quantity sample (carbohydrates in grams)
|
||||||
|
let quantity = HKQuantity(unit: HKUnit.gram(), doubleValue: amount)
|
||||||
|
let sample = HKQuantitySample(
|
||||||
|
type: carbohydratesType,
|
||||||
|
quantity: quantity,
|
||||||
|
start: recordedAt,
|
||||||
|
end: recordedAt,
|
||||||
|
metadata: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save to HealthKit
|
||||||
|
healthStore.save(sample) { [weak self] (success, error) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
rejecter("SAVE_ERROR", "Failed to save carbohydrates: \(error.localizedDescription)", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
let result: [String: Any] = [
|
||||||
|
"success": true,
|
||||||
|
"amount": amount,
|
||||||
|
"recordedAt": self?.dateToISOString(recordedAt) ?? ""
|
||||||
|
]
|
||||||
|
resolver(result)
|
||||||
|
} else {
|
||||||
|
rejecter("SAVE_FAILED", "Failed to save carbohydrates", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Workout Data Methods
|
// MARK: - Workout Data Methods
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|||||||
108
utils/health.ts
108
utils/health.ts
@@ -1139,6 +1139,114 @@ export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加蛋白质记录到 HealthKit
|
||||||
|
export async function saveProteinToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log('开始保存蛋白质记录到HealthKit...', { grams, recordedAt });
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
amount: grams,
|
||||||
|
recordedAt: recordedAt || new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await HealthKitManager.saveProteinToHealthKit(options);
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('蛋白质记录保存成功:', result);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('蛋白质记录保存失败:', result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加蛋白质记录到 HealthKit 失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加脂肪记录到 HealthKit
|
||||||
|
export async function saveFatToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log('开始保存脂肪记录到HealthKit...', { grams, recordedAt });
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
amount: grams,
|
||||||
|
recordedAt: recordedAt || new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await HealthKitManager.saveFatToHealthKit(options);
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('脂肪记录保存成功:', result);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('脂肪记录保存失败:', result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加脂肪记录到 HealthKit 失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加碳水化合物记录到 HealthKit
|
||||||
|
export async function saveCarbohydratesToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log('开始保存碳水化合物记录到HealthKit...', { grams, recordedAt });
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
amount: grams,
|
||||||
|
recordedAt: recordedAt || new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await HealthKitManager.saveCarbohydratesToHealthKit(options);
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('碳水化合物记录保存成功:', result);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('碳水化合物记录保存失败:', result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加碳水化合物记录到 HealthKit 失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存营养数据到 HealthKit(蛋白质、脂肪和碳水化合物)
|
||||||
|
export async function saveNutritionToHealthKit(
|
||||||
|
nutrition: { proteinGrams?: number; fatGrams?: number; carbohydrateGrams?: number },
|
||||||
|
recordedAt?: string
|
||||||
|
): Promise<{ proteinSaved: boolean; fatSaved: boolean; carbohydrateSaved: boolean }> {
|
||||||
|
try {
|
||||||
|
console.log('开始批量保存营养数据到HealthKit...', { nutrition, recordedAt });
|
||||||
|
|
||||||
|
const results = await Promise.allSettled([
|
||||||
|
nutrition.proteinGrams && nutrition.proteinGrams > 0
|
||||||
|
? saveProteinToHealthKit(nutrition.proteinGrams, recordedAt)
|
||||||
|
: Promise.resolve(false),
|
||||||
|
nutrition.fatGrams && nutrition.fatGrams > 0
|
||||||
|
? saveFatToHealthKit(nutrition.fatGrams, recordedAt)
|
||||||
|
: Promise.resolve(false),
|
||||||
|
nutrition.carbohydrateGrams && nutrition.carbohydrateGrams > 0
|
||||||
|
? saveCarbohydratesToHealthKit(nutrition.carbohydrateGrams, recordedAt)
|
||||||
|
: Promise.resolve(false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const proteinSaved = results[0].status === 'fulfilled' ? results[0].value : false;
|
||||||
|
const fatSaved = results[1].status === 'fulfilled' ? results[1].value : false;
|
||||||
|
const carbohydrateSaved = results[2].status === 'fulfilled' ? results[2].value : false;
|
||||||
|
|
||||||
|
console.log('营养数据批量保存结果:', { proteinSaved, fatSaved, carbohydrateSaved });
|
||||||
|
|
||||||
|
return { proteinSaved, fatSaved, carbohydrateSaved };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量保存营养数据到 HealthKit 失败:', error);
|
||||||
|
return { proteinSaved: false, fatSaved: false, carbohydrateSaved: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 HealthKit 中的饮水记录
|
// 获取 HealthKit 中的饮水记录
|
||||||
export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): Promise<any[]> {
|
export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -43,6 +43,17 @@ export interface SleepDataResult {
|
|||||||
endDate: string;
|
endDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NutritionSaveOptions {
|
||||||
|
amount: number; // Amount in grams
|
||||||
|
recordedAt?: string; // ISO8601 format, defaults to now
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NutritionSaveResult {
|
||||||
|
success: boolean;
|
||||||
|
amount: number;
|
||||||
|
recordedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HealthKitManagerInterface {
|
export interface HealthKitManagerInterface {
|
||||||
/**
|
/**
|
||||||
* Request authorization to access HealthKit data
|
* Request authorization to access HealthKit data
|
||||||
@@ -61,6 +72,24 @@ export interface HealthKitManagerInterface {
|
|||||||
* @param options Query options including date range and limit
|
* @param options Query options including date range and limit
|
||||||
*/
|
*/
|
||||||
getSleepData(options?: SleepDataOptions): Promise<SleepDataResult>;
|
getSleepData(options?: SleepDataOptions): Promise<SleepDataResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save protein intake to HealthKit
|
||||||
|
* @param options Nutrition save options including amount in grams and optional timestamp
|
||||||
|
*/
|
||||||
|
saveProteinToHealthKit(options: NutritionSaveOptions): Promise<NutritionSaveResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save fat intake to HealthKit
|
||||||
|
* @param options Nutrition save options including amount in grams and optional timestamp
|
||||||
|
*/
|
||||||
|
saveFatToHealthKit(options: NutritionSaveOptions): Promise<NutritionSaveResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save carbohydrates intake to HealthKit
|
||||||
|
* @param options Nutrition save options including amount in grams and optional timestamp
|
||||||
|
*/
|
||||||
|
saveCarbohydratesToHealthKit(options: NutritionSaveOptions): Promise<NutritionSaveResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native module interface
|
// Native module interface
|
||||||
|
|||||||
@@ -187,49 +187,6 @@ export class NutritionNotificationHelpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送午餐记录提醒
|
|
||||||
*/
|
|
||||||
static async sendLunchReminder(userName: string) {
|
|
||||||
const coachUrl = buildCoachDeepLink({
|
|
||||||
action: 'diet',
|
|
||||||
subAction: 'card',
|
|
||||||
meal: 'lunch'
|
|
||||||
});
|
|
||||||
|
|
||||||
return notificationService.sendImmediateNotification({
|
|
||||||
title: '午餐记录提醒',
|
|
||||||
body: `${userName},记得记录今天的午餐情况哦!`,
|
|
||||||
data: {
|
|
||||||
type: 'lunch_reminder',
|
|
||||||
meal: '午餐',
|
|
||||||
url: coachUrl
|
|
||||||
},
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消午餐提醒
|
|
||||||
*/
|
|
||||||
static async cancelLunchReminder(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const notifications = await notificationService.getAllScheduledNotifications();
|
|
||||||
|
|
||||||
for (const notification of notifications) {
|
|
||||||
if (notification.content.data?.type === 'lunch_reminder' &&
|
|
||||||
notification.content.data?.isDailyReminder === true) {
|
|
||||||
await notificationService.cancelNotification(notification.identifier);
|
|
||||||
console.log('已取消午餐提醒:', notification.identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消午餐提醒失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安排每日晚餐提醒
|
* 安排每日晚餐提醒
|
||||||
* @param userName 用户名
|
* @param userName 用户名
|
||||||
|
|||||||
Reference in New Issue
Block a user