feat(health): 重构营养卡片数据获取逻辑,支持基础代谢与运动消耗分离
- 新增 fetchCompleteNutritionCardData 异步 action,统一拉取营养、健康与基础代谢数据 - NutritionRadarCard 改用 Redux 数据源,移除 props 透传,自动根据日期刷新 - BasalMetabolismCard 新增详情弹窗,展示 BMR 计算公式、正常区间及提升策略 - StepsCard 与 StepsCardOptimized 引入 InteractionManager 与动画懒加载,减少 UI 阻塞 - HealthKitManager 新增饮水读写接口,支持将饮水记录同步至 HealthKit - 移除 statistics 页面冗余 mock 与 nutrition/health 重复请求,缓存时间统一为 5 分钟
This commit is contained in:
@@ -68,4 +68,13 @@ RCT_EXTERN_METHOD(getHourlyStandHours:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
// Water Intake Methods
|
||||
RCT_EXTERN_METHOD(saveWaterIntakeToHealthKit:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(getWaterIntakeFromHealthKit:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
@end
|
||||
@@ -36,18 +36,20 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
||||
static let appleStandTime = HKObjectType.categoryType(forIdentifier: .appleStandHour)!
|
||||
static let oxygenSaturation = HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!
|
||||
static let activitySummary = HKObjectType.activitySummaryType()
|
||||
static let dietaryWater = HKObjectType.quantityType(forIdentifier: .dietaryWater)!
|
||||
|
||||
static var all: Set<HKObjectType> {
|
||||
return [sleep, stepCount, heartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation, activitySummary]
|
||||
return [sleep, stepCount, heartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation, activitySummary, dietaryWater]
|
||||
}
|
||||
}
|
||||
|
||||
/// For writing (if needed)
|
||||
private struct WriteTypes {
|
||||
static let bodyMass = HKObjectType.quantityType(forIdentifier: .bodyMass)!
|
||||
static let dietaryWater = HKObjectType.quantityType(forIdentifier: .dietaryWater)!
|
||||
|
||||
static var all: Set<HKSampleType> {
|
||||
return [bodyMass]
|
||||
return [bodyMass, dietaryWater]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,4 +1335,150 @@ class HealthKitManager: NSObject, RCTBridgeModule {
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
// MARK: - Water Intake Methods
|
||||
|
||||
@objc
|
||||
func saveWaterIntakeToHealthKit(
|
||||
_ 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()
|
||||
}
|
||||
|
||||
let waterType = WriteTypes.dietaryWater
|
||||
|
||||
// Create quantity sample
|
||||
let quantity = HKQuantity(unit: HKUnit.literUnit(with: .milli), doubleValue: amount)
|
||||
let sample = HKQuantitySample(
|
||||
type: waterType,
|
||||
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 water intake: \(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 water intake", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func getWaterIntakeFromHealthKit(
|
||||
_ 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
|
||||
}
|
||||
|
||||
let waterType = HKObjectType.quantityType(forIdentifier: .dietaryWater)!
|
||||
|
||||
// Parse date range
|
||||
let startDate: Date
|
||||
if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
|
||||
startDate = d
|
||||
} else {
|
||||
startDate = Calendar.current.startOfDay(for: Date())
|
||||
}
|
||||
|
||||
let endDate: Date
|
||||
if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
|
||||
endDate = d
|
||||
} else {
|
||||
endDate = Date()
|
||||
}
|
||||
|
||||
let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
|
||||
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
|
||||
|
||||
let query = HKSampleQuery(sampleType: waterType,
|
||||
predicate: predicate,
|
||||
limit: limit,
|
||||
sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
rejecter("QUERY_ERROR", "Failed to query water intake: \(error.localizedDescription)", error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let waterSamples = samples as? [HKQuantitySample] else {
|
||||
resolver([
|
||||
"data": [],
|
||||
"totalAmount": 0,
|
||||
"count": 0,
|
||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
let waterData = waterSamples.map { sample in
|
||||
[
|
||||
"id": sample.uuid.uuidString,
|
||||
"startDate": self?.dateToISOString(sample.startDate) ?? "",
|
||||
"endDate": self?.dateToISOString(sample.endDate) ?? "",
|
||||
"value": sample.quantity.doubleValue(for: HKUnit.literUnit(with: .milli)),
|
||||
"source": [
|
||||
"name": sample.sourceRevision.source.name,
|
||||
"bundleIdentifier": sample.sourceRevision.source.bundleIdentifier
|
||||
],
|
||||
"metadata": sample.metadata ?? [:]
|
||||
] as [String : Any]
|
||||
}
|
||||
|
||||
let totalAmount = waterSamples.reduce(0.0) { total, sample in
|
||||
return total + sample.quantity.doubleValue(for: HKUnit.literUnit(with: .milli))
|
||||
}
|
||||
|
||||
let result: [String: Any] = [
|
||||
"data": waterData,
|
||||
"totalAmount": totalAmount,
|
||||
"count": waterData.count,
|
||||
"startDate": self?.dateToISOString(startDate) ?? "",
|
||||
"endDate": self?.dateToISOString(endDate) ?? ""
|
||||
]
|
||||
resolver(result)
|
||||
}
|
||||
}
|
||||
healthStore.execute(query)
|
||||
}
|
||||
|
||||
} // end class
|
||||
|
||||
Reference in New Issue
Block a user