feat(ios): 添加用药计划Widget小组件支持
- 创建medicineExtension小组件,支持iOS桌面显示用药计划 - 实现App Group数据共享机制,支持主应用与小组件数据同步 - 添加AppGroupUserDefaultsManager原生模块,提供跨应用数据访问能力 - 添加WidgetManager和WidgetCenterHelper,实现小组件刷新控制 - 在medications页面和Redux store中集成小组件数据同步逻辑 - 支持实时同步今日用药状态(待服用/已服用/已错过)到小组件 - 配置App Group entitlements (group.com.anonymous.digitalpilates) - 更新Xcode项目配置,添加WidgetKit和SwiftUI框架支持
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import dayjs from 'dayjs';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
// Widget数据同步服务
|
||||
@@ -16,6 +17,13 @@ const WIDGET_DATA_KEYS = {
|
||||
PENDING_WATER_RECORDS: 'widget_pending_water_records',
|
||||
} as const;
|
||||
|
||||
// 用药计划Widget数据存储键
|
||||
const MEDICATION_WIDGET_KEYS = {
|
||||
MEDICATION_DATA: 'widget_medication_data',
|
||||
PENDING_MEDICATION_ACTIONS: 'widget_pending_medication_actions',
|
||||
MEDICATION_LAST_SYNC: 'widget_medication_last_sync',
|
||||
} as const;
|
||||
|
||||
export interface WidgetWaterData {
|
||||
currentIntake: number;
|
||||
dailyGoal: number;
|
||||
@@ -31,6 +39,31 @@ const DEFAULT_WIDGET_DATA: WidgetWaterData = {
|
||||
lastSyncTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 用药计划小组件数据接口
|
||||
export interface WidgetMedicationData {
|
||||
medications: WidgetMedicationItem[];
|
||||
lastSyncTime: string;
|
||||
date: string; // YYYY-MM-DD格式的日期
|
||||
}
|
||||
|
||||
export interface WidgetMedicationItem {
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
scheduledTime: string; // HH:mm格式
|
||||
status: 'upcoming' | 'taken' | 'missed';
|
||||
medicationId: string;
|
||||
recordId?: string;
|
||||
image?: string; // 图片URI或默认图片资源名
|
||||
}
|
||||
|
||||
// 默认用药数据
|
||||
const DEFAULT_MEDICATION_WIDGET_DATA: WidgetMedicationData = {
|
||||
medications: [],
|
||||
lastSyncTime: new Date().toISOString(),
|
||||
date: new Date().toISOString().split('T')[0], // 格式化为YYYY-MM-DD
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建Native Module来访问App Group UserDefaults
|
||||
* 这需要在iOS原生代码中实现
|
||||
@@ -54,7 +87,14 @@ export interface PendingWaterRecord {
|
||||
}
|
||||
|
||||
// 尝试获取原生模块,如果不存在则使用fallback
|
||||
const AppGroupDefaults: AppGroupUserDefaults | null = NativeModules.AppGroupUserDefaults || null;
|
||||
const AppGroupDefaults: AppGroupUserDefaults | null = NativeModules.AppGroupUserDefaultsManager || null;
|
||||
|
||||
// 添加调试日志
|
||||
if (AppGroupDefaults) {
|
||||
console.log('✅ AppGroupUserDefaultsManager native module loaded successfully');
|
||||
} else {
|
||||
console.warn('⚠️ AppGroupUserDefaultsManager native module not found, will use AsyncStorage fallback');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将饮水数据同步到Widget
|
||||
@@ -281,4 +321,211 @@ export const markWidgetDataSynced = async (): Promise<void> => {
|
||||
} catch (error) {
|
||||
console.error('Failed to mark widget data as synced:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 用药计划数据同步 ====================
|
||||
|
||||
/**
|
||||
* 将用药数据同步到Widget
|
||||
*/
|
||||
export const syncMedicationDataToWidget = async (data: WidgetMedicationData): Promise<void> => {
|
||||
try {
|
||||
console.log('🔄 [Widget Sync] Starting medication data sync...');
|
||||
console.log('🔄 [Widget Sync] Data to sync:', {
|
||||
medicationsCount: data.medications.length,
|
||||
date: data.date,
|
||||
lastSyncTime: data.lastSyncTime,
|
||||
medications: data.medications.map(m => ({
|
||||
name: m.name,
|
||||
scheduledTime: m.scheduledTime,
|
||||
status: m.status
|
||||
}))
|
||||
});
|
||||
|
||||
if (!AppGroupDefaults) {
|
||||
console.warn('⚠️ [Widget Sync] AppGroupUserDefaults native module not available, falling back to AsyncStorage');
|
||||
// Fallback到AsyncStorage
|
||||
const dataJson = JSON.stringify(data);
|
||||
await AsyncStorage.setItem(MEDICATION_WIDGET_KEYS.MEDICATION_DATA, dataJson);
|
||||
await AsyncStorage.setItem(MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC, data.lastSyncTime);
|
||||
console.log('✅ [Widget Sync] Data saved to AsyncStorage (fallback)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用App Group UserDefaults
|
||||
const dataJson = JSON.stringify(data);
|
||||
console.log('📝 [Widget Sync] JSON payload length:', dataJson.length);
|
||||
|
||||
await AppGroupDefaults.setString(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_DATA, dataJson);
|
||||
await AppGroupDefaults.setString(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC, data.lastSyncTime);
|
||||
|
||||
console.log('✅ [Widget Sync] Medication widget data synced successfully:', data.medications.length, 'medications');
|
||||
|
||||
// 验证数据是否写入成功
|
||||
const verifyJson = await AppGroupDefaults.getString(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_DATA);
|
||||
if (verifyJson) {
|
||||
console.log('✅ [Widget Sync] Verification successful - data exists in App Group');
|
||||
} else {
|
||||
console.warn('⚠️ [Widget Sync] Verification failed - data not found in App Group');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Widget Sync] Failed to sync medication data to widget:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从Widget获取用药数据
|
||||
*/
|
||||
export const getMedicationDataFromWidget = async (): Promise<WidgetMedicationData> => {
|
||||
try {
|
||||
if (!AppGroupDefaults) {
|
||||
console.warn('AppGroupUserDefaults native module not available, falling back to AsyncStorage');
|
||||
// Fallback从AsyncStorage读取
|
||||
const dataJson = await AsyncStorage.getItem(MEDICATION_WIDGET_KEYS.MEDICATION_DATA);
|
||||
const lastSyncTime = await AsyncStorage.getItem(MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC);
|
||||
|
||||
if (dataJson) {
|
||||
const data = JSON.parse(dataJson);
|
||||
return {
|
||||
...data,
|
||||
lastSyncTime: lastSyncTime || DEFAULT_MEDICATION_WIDGET_DATA.lastSyncTime,
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_MEDICATION_WIDGET_DATA;
|
||||
}
|
||||
|
||||
// 从App Group UserDefaults读取数据
|
||||
const dataJson = await AppGroupDefaults.getString(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_DATA);
|
||||
const lastSyncTime = await AppGroupDefaults.getString(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC);
|
||||
|
||||
if (dataJson) {
|
||||
const data = JSON.parse(dataJson);
|
||||
return {
|
||||
...data,
|
||||
lastSyncTime: lastSyncTime || DEFAULT_MEDICATION_WIDGET_DATA.lastSyncTime,
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_MEDICATION_WIDGET_DATA;
|
||||
} catch (error) {
|
||||
console.error('Failed to get medication widget data:', error);
|
||||
return DEFAULT_MEDICATION_WIDGET_DATA;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除用药Widget数据
|
||||
*/
|
||||
export const clearMedicationWidgetData = async (): Promise<void> => {
|
||||
try {
|
||||
if (!AppGroupDefaults) {
|
||||
await AsyncStorage.multiRemove([
|
||||
MEDICATION_WIDGET_KEYS.MEDICATION_DATA,
|
||||
MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC,
|
||||
MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除App Group UserDefaults中的数据
|
||||
await Promise.all([
|
||||
AppGroupDefaults.removeKey(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_DATA),
|
||||
AppGroupDefaults.removeKey(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.MEDICATION_LAST_SYNC),
|
||||
AppGroupDefaults.removeKey(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS),
|
||||
]);
|
||||
|
||||
console.log('Medication widget data cleared successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to clear medication widget data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Widget用药操作接口
|
||||
*/
|
||||
export interface WidgetMedicationAction {
|
||||
type: 'take' | 'skip';
|
||||
recordId: string;
|
||||
medicationId: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Widget获取待处理的用药操作
|
||||
*/
|
||||
export const getPendingMedicationActions = async (): Promise<WidgetMedicationAction[]> => {
|
||||
try {
|
||||
if (!AppGroupDefaults) {
|
||||
// Fallback: 从 AsyncStorage 读取
|
||||
const pendingActionsJson = await AsyncStorage.getItem(MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS);
|
||||
return pendingActionsJson ? JSON.parse(pendingActionsJson) : [];
|
||||
}
|
||||
|
||||
// 从 App Group UserDefaults 读取
|
||||
const pendingActions = await AppGroupDefaults.getArray(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS);
|
||||
return pendingActions || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get pending medication actions:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除待处理的用药操作
|
||||
*/
|
||||
export const clearPendingMedicationActions = async (): Promise<void> => {
|
||||
try {
|
||||
if (!AppGroupDefaults) {
|
||||
// Fallback: 从 AsyncStorage 清除
|
||||
await AsyncStorage.removeItem(MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 App Group UserDefaults 清除
|
||||
await AppGroupDefaults.removeKey(APP_GROUP_ID, MEDICATION_WIDGET_KEYS.PENDING_MEDICATION_ACTIONS);
|
||||
console.log('Pending medication actions cleared successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to clear pending medication actions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将Redux数据转换为Widget数据格式
|
||||
*/
|
||||
export const convertMedicationDataToWidget = (
|
||||
records: import('@/types/medication').MedicationRecord[],
|
||||
medications: import('@/types/medication').Medication[],
|
||||
date: string
|
||||
): WidgetMedicationData => {
|
||||
const medicationMap = new Map(medications.map(m => [m.id, m]));
|
||||
|
||||
const widgetItems: WidgetMedicationItem[] = records
|
||||
.map(record => {
|
||||
const medication = medicationMap.get(record.medicationId);
|
||||
if (!medication || !medication.isActive) return null;
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
name: medication.name,
|
||||
dosage: `${medication.dosageValue} ${medication.dosageUnit}`,
|
||||
scheduledTime: dayjs(record.scheduledTime).format('HH:mm'),
|
||||
status: record.status,
|
||||
medicationId: medication.id,
|
||||
recordId: record.id,
|
||||
image: medication.photoUrl,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as WidgetMedicationItem[];
|
||||
|
||||
// 按时间排序
|
||||
widgetItems.sort((a, b) => a.scheduledTime.localeCompare(b.scheduledTime));
|
||||
|
||||
return {
|
||||
medications: widgetItems,
|
||||
lastSyncTime: new Date().toISOString(),
|
||||
date,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user