feat(widget): 增强Widget数据同步机制并优化UI设计
- 在useWaterData中统一处理数据变更后的Widget同步逻辑 - 新增数组类型数据存取方法支持更复杂数据结构 - 重构Widget UI为圆形进度条设计,提升视觉体验 - 修复数据同步时可能存在的竞态条件问题 - 优化错误处理,确保Widget同步失败不影响主功能
This commit is contained in:
@@ -101,17 +101,16 @@ export const useWaterData = () => {
|
|||||||
// HealthKit 同步失败不影响主要功能
|
// HealthKit 同步失败不影响主要功能
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新获取今日统计
|
// 重新获取今日统计并等待完成
|
||||||
dispatch(fetchTodayWaterStats());
|
const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap();
|
||||||
|
|
||||||
// 同步数据到Widget
|
// 同步数据到Widget
|
||||||
try {
|
try {
|
||||||
const newCurrentIntake = (todayStats?.totalAmount || 0) + amount;
|
|
||||||
const quickAddAmount = await getQuickWaterAmount();
|
const quickAddAmount = await getQuickWaterAmount();
|
||||||
|
|
||||||
await syncWaterDataToWidget({
|
await syncWaterDataToWidget({
|
||||||
currentIntake: newCurrentIntake,
|
currentIntake: updatedStats.totalAmount,
|
||||||
dailyGoal: dailyWaterGoal || 2000,
|
dailyGoal: updatedStats.dailyGoal,
|
||||||
quickAddAmount,
|
quickAddAmount,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,8 +158,25 @@ export const useWaterData = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatch(updateWaterRecordAction(dto)).unwrap();
|
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;
|
return true;
|
||||||
} catch (error: any) {
|
} 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;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -222,12 +255,15 @@ export const useWaterData = () => {
|
|||||||
try {
|
try {
|
||||||
await dispatch(updateWaterGoalAction(goal)).unwrap();
|
await dispatch(updateWaterGoalAction(goal)).unwrap();
|
||||||
|
|
||||||
|
// 重新获取今日统计以确保数据一致性
|
||||||
|
const updatedStats = await dispatch(fetchTodayWaterStats()).unwrap();
|
||||||
|
|
||||||
// 同步目标到Widget
|
// 同步目标到Widget
|
||||||
try {
|
try {
|
||||||
const quickAddAmount = await getQuickWaterAmount();
|
const quickAddAmount = await getQuickWaterAmount();
|
||||||
await syncWaterDataToWidget({
|
await syncWaterDataToWidget({
|
||||||
dailyGoal: goal,
|
dailyGoal: goal,
|
||||||
currentIntake: todayStats?.totalAmount || 0,
|
currentIntake: updatedStats.totalAmount,
|
||||||
quickAddAmount,
|
quickAddAmount,
|
||||||
});
|
});
|
||||||
await refreshWidget();
|
await refreshWidget();
|
||||||
|
|||||||
@@ -84,63 +84,74 @@ struct WaterWidgetEntryView : View {
|
|||||||
var entry: Provider.Entry
|
var entry: Provider.Entry
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 8) {
|
||||||
// Header with title and add button
|
// Header with title and add button
|
||||||
HStack {
|
HStack() {
|
||||||
Text("喝水")
|
Text("饮水")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
.foregroundColor(Color(red: 0.098, green: 0.129, blue: 0.149))
|
.foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2))
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Quick add water button
|
// Quick add water button
|
||||||
Button(intent: AddWaterIntent(amount: entry.waterData.quickAddAmount)) {
|
Button(intent: AddWaterIntent(amount: entry.waterData.quickAddAmount)) {
|
||||||
HStack(spacing: 2) {
|
Text("+\(entry.waterData.quickAddAmount)")
|
||||||
Text("+")
|
.font(.system(size: 10, weight: .bold))
|
||||||
Text("\(entry.waterData.quickAddAmount)ml")
|
.foregroundColor(.white)
|
||||||
}
|
.padding(.horizontal, 8)
|
||||||
.font(.system(size: 10, weight: .bold))
|
.padding(.vertical, 5)
|
||||||
.foregroundColor(Color(red: 0.388, green: 0.4, blue: 0.945))
|
.background(
|
||||||
.padding(.horizontal, 6)
|
LinearGradient(
|
||||||
.padding(.vertical, 5)
|
gradient: Gradient(colors: [
|
||||||
.background(Color(red: 0.882, green: 0.906, blue: 1.0))
|
Color(red: 0.3, green: 0.7, blue: 1.0),
|
||||||
.cornerRadius(16)
|
Color(red: 0.2, green: 0.6, blue: 0.9)
|
||||||
|
]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.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
|
// Progress circle
|
||||||
HStack(spacing: 2) {
|
Circle()
|
||||||
ForEach(0..<12, id: \.self) { index in
|
.trim(from: 0, to: entry.waterData.progressPercentage)
|
||||||
RoundedRectangle(cornerRadius: 1)
|
.stroke(
|
||||||
.fill(index < Int(entry.waterData.progressPercentage * 12) ?
|
LinearGradient(
|
||||||
Color(red: 0.49, green: 0.827, blue: 0.988) :
|
gradient: Gradient(colors: [
|
||||||
Color(red: 0.941, green: 0.976, blue: 1.0))
|
Color(red: 0.3, green: 0.8, blue: 1.0),
|
||||||
.frame(width: 3, height: 12)
|
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)
|
.padding(12)
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
.background(Color.white)
|
||||||
|
.cornerRadius(16)
|
||||||
|
.containerBackground(Color.clear, for: .widget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ RCT_EXTERN_METHOD(getNumber:(NSString *)groupId
|
|||||||
resolver:(RCTPromiseResolveBlock)resolve
|
resolver:(RCTPromiseResolveBlock)resolve
|
||||||
rejecter:(RCTPromiseRejectBlock)reject)
|
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
|
RCT_EXTERN_METHOD(removeKey:(NSString *)groupId
|
||||||
key:(NSString *)key
|
key:(NSString *)key
|
||||||
resolver:(RCTPromiseResolveBlock)resolve
|
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
|
// MARK: - Remove Key Method
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export const clearWidgetData = async (): Promise<void> => {
|
|||||||
* Fallback: 使用AsyncStorage存储Widget数据
|
* Fallback: 使用AsyncStorage存储Widget数据
|
||||||
*/
|
*/
|
||||||
const saveWidgetDataToAsyncStorage = async (data: WidgetWaterData): Promise<void> => {
|
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.CURRENT_WATER_INTAKE, data.currentIntake.toString()],
|
||||||
[WIDGET_DATA_KEYS.DAILY_WATER_GOAL, data.dailyGoal.toString()],
|
[WIDGET_DATA_KEYS.DAILY_WATER_GOAL, data.dailyGoal.toString()],
|
||||||
[WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT, data.quickAddAmount.toString()],
|
[WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT, data.quickAddAmount.toString()],
|
||||||
|
|||||||
Reference in New Issue
Block a user