From 35d6b7445133a6dcf8349e40e2ddb849023efb86 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 11 Sep 2025 10:38:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(widget):=20=E5=A2=9E=E5=BC=BAWidget?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E6=9C=BA=E5=88=B6=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96UI=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在useWaterData中统一处理数据变更后的Widget同步逻辑 - 新增数组类型数据存取方法支持更复杂数据结构 - 重构Widget UI为圆形进度条设计,提升视觉体验 - 修复数据同步时可能存在的竞态条件问题 - 优化错误处理,确保Widget同步失败不影响主功能 --- hooks/useWaterData.ts | 56 +++++++++-- ios/WaterWidget/WaterWidget.swift | 99 ++++++++++--------- ios/digitalpilates/AppGroupUserDefaults.m | 11 +++ ios/digitalpilates/AppGroupUserDefaults.swift | 35 +++++++ utils/widgetDataSync.ts | 2 +- 5 files changed, 148 insertions(+), 55 deletions(-) diff --git a/hooks/useWaterData.ts b/hooks/useWaterData.ts index 02320de..eb6b8ea 100644 --- a/hooks/useWaterData.ts +++ b/hooks/useWaterData.ts @@ -101,17 +101,16 @@ export const useWaterData = () => { // HealthKit 同步失败不影响主要功能 } - // 重新获取今日统计 - dispatch(fetchTodayWaterStats()); + // 重新获取今日统计并等待完成 + const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap(); // 同步数据到Widget try { - const newCurrentIntake = (todayStats?.totalAmount || 0) + amount; const quickAddAmount = await getQuickWaterAmount(); await syncWaterDataToWidget({ - currentIntake: newCurrentIntake, - dailyGoal: dailyWaterGoal || 2000, + currentIntake: updatedStats.totalAmount, + dailyGoal: updatedStats.dailyGoal, quickAddAmount, }); @@ -159,8 +158,25 @@ export const useWaterData = () => { try { await dispatch(updateWaterRecordAction(dto)).unwrap(); - // 重新获取今日统计 - dispatch(fetchTodayWaterStats()); + // 重新获取今日统计并等待完成 + const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap(); + + // 同步数据到Widget + try { + const quickAddAmount = await getQuickWaterAmount(); + + await syncWaterDataToWidget({ + currentIntake: updatedStats.totalAmount, + dailyGoal: updatedStats.dailyGoal, + quickAddAmount, + }); + + // 刷新Widget + await refreshWidget(); + } catch (widgetError) { + console.error('Widget 更新同步错误:', widgetError); + // Widget 同步失败不影响主要功能 + } return true; } catch (error: any) { @@ -200,8 +216,25 @@ export const useWaterData = () => { } } - // 重新获取今日统计 - dispatch(fetchTodayWaterStats()); + // 重新获取今日统计并等待完成 + const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap(); + + // 同步数据到Widget + try { + const quickAddAmount = await getQuickWaterAmount(); + + await syncWaterDataToWidget({ + currentIntake: updatedStats.totalAmount, + dailyGoal: updatedStats.dailyGoal, + quickAddAmount, + }); + + // 刷新Widget + await refreshWidget(); + } catch (widgetError) { + console.error('Widget 删除同步错误:', widgetError); + // Widget 同步失败不影响主要功能 + } return true; } catch (error: any) { @@ -222,12 +255,15 @@ export const useWaterData = () => { try { await dispatch(updateWaterGoalAction(goal)).unwrap(); + // 重新获取今日统计以确保数据一致性 + const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap(); + // 同步目标到Widget try { const quickAddAmount = await getQuickWaterAmount(); await syncWaterDataToWidget({ dailyGoal: goal, - currentIntake: todayStats?.totalAmount || 0, + currentIntake: updatedStats.totalAmount, quickAddAmount, }); await refreshWidget(); diff --git a/ios/WaterWidget/WaterWidget.swift b/ios/WaterWidget/WaterWidget.swift index e02e5a8..2df580d 100644 --- a/ios/WaterWidget/WaterWidget.swift +++ b/ios/WaterWidget/WaterWidget.swift @@ -84,63 +84,74 @@ struct WaterWidgetEntryView : View { var entry: Provider.Entry var body: some View { - VStack(spacing: 0) { + VStack(spacing: 8) { // Header with title and add button - HStack { - Text("喝水") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(Color(red: 0.098, green: 0.129, blue: 0.149)) + HStack() { + Text("饮水") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - Spacer() // Quick add water button Button(intent: AddWaterIntent(amount: entry.waterData.quickAddAmount)) { - HStack(spacing: 2) { - Text("+") - Text("\(entry.waterData.quickAddAmount)ml") - } - .font(.system(size: 10, weight: .bold)) - .foregroundColor(Color(red: 0.388, green: 0.4, blue: 0.945)) - .padding(.horizontal, 6) - .padding(.vertical, 5) - .background(Color(red: 0.882, green: 0.906, blue: 1.0)) - .cornerRadius(16) + Text("+\(entry.waterData.quickAddAmount)") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background( + LinearGradient( + gradient: Gradient(colors: [ + Color(red: 0.3, green: 0.7, blue: 1.0), + Color(red: 0.2, green: 0.6, blue: 0.9) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .cornerRadius(10) } .buttonStyle(PlainButtonStyle()) } - .padding(.bottom, 8) - Spacer() - - // Progress visualization - simplified bar chart representation - HStack(spacing: 2) { - ForEach(0..<12, id: \.self) { index in - RoundedRectangle(cornerRadius: 1) - .fill(index < Int(entry.waterData.progressPercentage * 12) ? - Color(red: 0.49, green: 0.827, blue: 0.988) : - Color(red: 0.941, green: 0.976, blue: 1.0)) - .frame(width: 3, height: 12) + // Main content - left right layout + HStack(alignment: .center, spacing: 12) { + // Left side - Progress circle + ZStack { + // Background circle + Circle() + .stroke(Color(red: 0.95, green: 0.95, blue: 0.95), lineWidth: 4) + .frame(width: 45, height: 45) + + // Progress circle + Circle() + .trim(from: 0, to: entry.waterData.progressPercentage) + .stroke( + LinearGradient( + gradient: Gradient(colors: [ + Color(red: 0.3, green: 0.8, blue: 1.0), + Color(red: 0.1, green: 0.6, blue: 0.9) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + style: StrokeStyle(lineWidth: 4, lineCap: .round) + ) + .frame(width: 45, height: 45) + .rotationEffect(.degrees(-90)) + + // Progress percentage in center + Text("\(Int(entry.waterData.progressPercentage * 100))%") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(Color(red: 0.3, green: 0.7, blue: 1.0)) } - } - .padding(.bottom, 8) - - // Water intake stats - HStack(alignment: .firstTextBaseline, spacing: 2) { - Text("\(entry.waterData.currentIntake)") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color(red: 0.098, green: 0.129, blue: 0.149)) - Text("ml") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color(red: 0.098, green: 0.129, blue: 0.149)) - - Text("/ \(entry.waterData.targetIntake)ml") - .font(.system(size: 12)) - .foregroundColor(Color(red: 0.42, green: 0.447, blue: 0.502)) } } - .padding(16) - .containerBackground(.fill.tertiary, for: .widget) + .padding(12) + .background(Color.white) + .cornerRadius(16) + .containerBackground(Color.clear, for: .widget) } } diff --git a/ios/digitalpilates/AppGroupUserDefaults.m b/ios/digitalpilates/AppGroupUserDefaults.m index efd4a5d..685c2a5 100644 --- a/ios/digitalpilates/AppGroupUserDefaults.m +++ b/ios/digitalpilates/AppGroupUserDefaults.m @@ -31,6 +31,17 @@ RCT_EXTERN_METHOD(getNumber:(NSString *)groupId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(setArray:(NSString *)groupId + key:(NSString *)key + value:(NSArray *)value + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(getArray:(NSString *)groupId + key:(NSString *)key + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + RCT_EXTERN_METHOD(removeKey:(NSString *)groupId key:(NSString *)key resolver:(RCTPromiseResolveBlock)resolve diff --git a/ios/digitalpilates/AppGroupUserDefaults.swift b/ios/digitalpilates/AppGroupUserDefaults.swift index abf7010..cb7f2e8 100644 --- a/ios/digitalpilates/AppGroupUserDefaults.swift +++ b/ios/digitalpilates/AppGroupUserDefaults.swift @@ -90,6 +90,41 @@ class AppGroupUserDefaults: NSObject, RCTBridgeModule { } } + // MARK: - Array Methods + + @objc + func setArray(_ groupId: String, key: String, value: [Any], resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { + DispatchQueue.global(qos: .background).async { + guard let userDefaults = UserDefaults(suiteName: groupId) else { + rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil) + return + } + + userDefaults.set(value, forKey: key) + userDefaults.synchronize() + + DispatchQueue.main.async { + resolver(nil) + } + } + } + + @objc + func getArray(_ groupId: String, key: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { + DispatchQueue.global(qos: .background).async { + guard let userDefaults = UserDefaults(suiteName: groupId) else { + rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil) + return + } + + let value = userDefaults.array(forKey: key) + + DispatchQueue.main.async { + resolver(value) + } + } + } + // MARK: - Remove Key Method @objc diff --git a/utils/widgetDataSync.ts b/utils/widgetDataSync.ts index e0d2c4b..50383c8 100644 --- a/utils/widgetDataSync.ts +++ b/utils/widgetDataSync.ts @@ -150,7 +150,7 @@ export const clearWidgetData = async (): Promise => { * Fallback: 使用AsyncStorage存储Widget数据 */ const saveWidgetDataToAsyncStorage = async (data: WidgetWaterData): Promise => { - const dataToStore = [ + const dataToStore: [string, string][] = [ [WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE, data.currentIntake.toString()], [WIDGET_DATA_KEYS.DAILY_WATER_GOAL, data.dailyGoal.toString()], [WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT, data.quickAddAmount.toString()],