feat(widget): 增强Widget数据同步机制并优化UI设计

- 在useWaterData中统一处理数据变更后的Widget同步逻辑
- 新增数组类型数据存取方法支持更复杂数据结构
- 重构Widget UI为圆形进度条设计,提升视觉体验
- 修复数据同步时可能存在的竞态条件问题
- 优化错误处理,确保Widget同步失败不影响主功能
This commit is contained in:
richarjiang
2025-09-11 10:38:54 +08:00
parent 62690ee3fc
commit 35d6b74451
5 changed files with 148 additions and 55 deletions

View File

@@ -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();

View File

@@ -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()
// 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 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)
// 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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -150,7 +150,7 @@ export const clearWidgetData = async (): Promise<void> => {
* Fallback: 使用AsyncStorage存储Widget数据
*/
const saveWidgetDataToAsyncStorage = async (data: WidgetWaterData): Promise<void> => {
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()],