// // medicine.swift // medicine // // Created by richard on 2025/11/13. // import WidgetKit import SwiftUI // MARK: - 用药数据模型 struct MedicationItem: Codable, Identifiable { let id: String let name: String let dosage: String let scheduledTime: String let status: String let medicationId: String let recordId: String? let image: String? } struct MedicationWidgetData: Codable { let medications: [MedicationItem] let lastSyncTime: String let date: String } // MARK: - Timeline Provider struct Provider: AppIntentTimelineProvider { func placeholder(in context: Context) -> MedicationEntry { MedicationEntry( date: Date(), medications: [], displayDate: formatDate(Date()), lastSyncTime: Date().timeIntervalSince1970 ) } func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> MedicationEntry { let medicationData = loadMedicationDataFromAppGroup() let lastSyncTimeInterval = parseTimeInterval(from: medicationData.lastSyncTime) return MedicationEntry( date: Date(), medications: medicationData.medications, displayDate: medicationData.date, lastSyncTime: lastSyncTimeInterval ) } func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { let currentDate = Date() let medicationData = loadMedicationDataFromAppGroup() let lastSyncTimeInterval = parseTimeInterval(from: medicationData.lastSyncTime) let entry = MedicationEntry( date: currentDate, medications: medicationData.medications, displayDate: medicationData.date, lastSyncTime: lastSyncTimeInterval ) // 下次更新时间 - 每15分钟更新一次 let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! return Timeline(entries: [entry], policy: .after(nextUpdate)) } } // MARK: - Timeline Entry struct MedicationEntry: TimelineEntry { let date: Date let medications: [MedicationItem] let displayDate: String let lastSyncTime: TimeInterval } // MARK: - Widget View struct medicineEntryView: View { var entry: Provider.Entry var body: some View { VStack(spacing: 8) { // 日期标题 Text(entry.displayDate) .font(.caption) .foregroundColor(.secondary) // 用药列表 if entry.medications.isEmpty { Text("今日无用药计划") .font(.body) .foregroundColor(.secondary) } else { LazyVStack(spacing: 6) { ForEach(entry.medications, id: \.id) { medication in MedicationRowView(medication: medication) } } } Spacer() // 底部信息 Text("最后更新: \(formatTime(from: entry.lastSyncTime))") .font(.caption2) .foregroundColor(Color.secondary) } .padding() } } // MARK: - 用药行视图 struct MedicationRowView: View { let medication: MedicationItem var body: some View { HStack { // 状态指示器 Circle() .fill(statusColor) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: 2) { Text(medication.name) .font(.caption) .fontWeight(.medium) .lineLimit(1) Text("\(medication.scheduledTime) • \(medication.dosage)") .font(.caption2) .foregroundColor(.secondary) } Spacer() // 状态标签 Text(statusText) .font(.caption2) .padding(.horizontal, 8) .padding(.vertical, 2) .background(statusColor.opacity(0.2)) .foregroundColor(statusColor) .cornerRadius(4) } .padding(.vertical, 2) } private var statusColor: Color { switch medication.status { case "taken": return .green case "missed": return .red default: return .blue } } private var statusText: String { switch medication.status { case "taken": return "已服用" case "missed": return "已错过" default: return "待服用" } } } // MARK: - Widget Configuration struct medicine: Widget { let kind: String = "medicine" var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in medicineEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("用药计划") .description("显示今日用药计划和服药状态") } } // MARK: - Helper Functions private func loadMedicationDataFromAppGroup() -> MedicationWidgetData { guard let userDefaults = UserDefaults(suiteName: "group.com.anonymous.digitalpilates") else { print("❌ Failed to initialize UserDefaults with App Group") return MedicationWidgetData( medications: [], lastSyncTime: Date().toISOString(), date: formatDate(Date()) ) } guard let dataJson = userDefaults.string(forKey: "widget_medication_data") else { print("⚠️ No medication data found in App Group UserDefaults") return MedicationWidgetData( medications: [], lastSyncTime: Date().toISOString(), date: formatDate(Date()) ) } print("✅ Found medication data JSON: \(dataJson.prefix(100))...") guard let data = dataJson.data(using: .utf8) else { print("❌ Failed to convert JSON string to Data") return MedicationWidgetData( medications: [], lastSyncTime: Date().toISOString(), date: formatDate(Date()) ) } do { let medicationData = try JSONDecoder().decode(MedicationWidgetData.self, from: data) print("✅ Successfully decoded medication data: \(medicationData.medications.count) medications") return medicationData } catch { print("❌ Failed to decode medication data: \(error)") return MedicationWidgetData( medications: [], lastSyncTime: Date().toISOString(), date: formatDate(Date()) ) } } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "M月d日" return formatter.string(from: date) } private func formatTime(from timeInterval: TimeInterval) -> String { let date = Date(timeIntervalSince1970: timeInterval) let formatter = DateFormatter() formatter.timeStyle = .short formatter.locale = Locale(identifier: "zh_CN") return formatter.string(from: date) } /// 解析时间间隔 - 支持ISO字符串和时间戳 private func parseTimeInterval(from string: String) -> TimeInterval { // 尝试直接转换为数字(时间戳) if let timestamp = Double(string) { return timestamp } // 尝试解析ISO 8601格式 let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = isoFormatter.date(from: string) { return date.timeIntervalSince1970 } // 尝试不带毫秒的ISO格式 isoFormatter.formatOptions = [.withInternetDateTime] if let date = isoFormatter.date(from: string) { return date.timeIntervalSince1970 } // 如果都失败,返回当前时间 return Date().timeIntervalSince1970 } /// 将Date转换为ISO字符串(辅助函数) extension Date { func toISOString() -> String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.string(from: self) } } extension ConfigurationAppIntent { fileprivate static var smiley: ConfigurationAppIntent { let intent = ConfigurationAppIntent() intent.favoriteEmoji = "😀" return intent } fileprivate static var starEyes: ConfigurationAppIntent { let intent = ConfigurationAppIntent() intent.favoriteEmoji = "🤩" return intent } } #Preview(as: .systemSmall) { medicine() } timeline: { MedicationEntry(date: .now, medications: [], displayDate: formatDate(Date()), lastSyncTime: Date().timeIntervalSince1970) MedicationEntry(date: .now, medications: [], displayDate: formatDate(Date()), lastSyncTime: Date().timeIntervalSince1970) }