feat(widget): 增强Widget数据同步机制并优化UI设计
- 在useWaterData中统一处理数据变更后的Widget同步逻辑 - 新增数组类型数据存取方法支持更复杂数据结构 - 重构Widget UI为圆形进度条设计,提升视觉体验 - 修复数据同步时可能存在的竞态条件问题 - 优化错误处理,确保Widget同步失败不影响主功能
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()],
|
||||
|
||||
Reference in New Issue
Block a user