From 2fcf68a64bd7d45589c01fb54ecaccbd6226ef13 Mon Sep 17 00:00:00 2001 From: wangdl Date: Fri, 29 May 2026 19:46:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(ios):=20IOS-M1-03=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotificationListView 改为按时间段分组:今天/本周/更早 - 新增 6 种消息类型图标(导入完成/失败/测验/AI/订阅/系统) - 右上角增加「全部已读」按钮 - 未读蓝点标识 Co-Authored-By: Claude Opus 4.7 --- .../Profile/NotificationListView.swift | 116 ++++++++++++------ 1 file changed, 80 insertions(+), 36 deletions(-) diff --git a/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift b/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift index 2e643bb..06d47a1 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/NotificationListView.swift @@ -5,59 +5,92 @@ struct NotificationListView: View { @State private var isLoading = false @State private var isRefreshing = false + private var todayItems: [NotificationItem] { + notifications.filter { isToday($0.createdAt) } + } + private var weekItems: [NotificationItem] { + notifications.filter { !isToday($0.createdAt) && isThisWeek($0.createdAt) } + } + private var olderItems: [NotificationItem] { + notifications.filter { !isThisWeek($0.createdAt) } + } + var body: some View { ZStack { Color.zxBg0.ignoresSafeArea() ScrollView { VStack(spacing: 0) { if isLoading && notifications.isEmpty { - VStack(spacing: 12) { - ZXLoadingView(size: 36, lineWidth: 3) - Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) - }.padding(.top, 120) + VStack(spacing: 12) { ProgressView().tint(Color.zxPurple); Text("加载中…").font(.system(size: 13)).foregroundColor(Color.zxF04) }.padding(.top, 120) } else if notifications.isEmpty { - VStack(spacing: 12) { - Image(systemName: "bell.slash").font(.system(size: 40)).foregroundColor(Color.zxF03) - Text("暂无通知").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF03) - }.padding(.top, 120) + VStack(spacing: 12) { Image(systemName: "bell.slash").font(.system(size: 40)).foregroundColor(Color.zxF03); Text("暂无通知").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF03) }.padding(.top, 120) } else { - ForEach(Array(notifications.enumerated()), id: \.offset) { i, n in - ZXNotificationItemRow(item: n) { - Task { _ = try? await NotificationService.shared.markRead(id: n.id) } - } - if i < notifications.count - 1 { - ZXSettingDivider() - } - } + if !todayItems.isEmpty { sectionView("今天", items: todayItems) } + if !weekItems.isEmpty { sectionView("本周", items: weekItems) } + if !olderItems.isEmpty { sectionView("更早", items: olderItems) } } - } - .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) - .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) - .padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100) + }.padding(.bottom, 100) } .scrollIndicators(.hidden) .zxPullToRefresh { await refresh() } } + .navigationTitle("消息中心") .navigationBarTitleDisplayMode(.inline).animatedTabBarHide() .toolbarBackground(.hidden, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + Task { + let _ = try? await NotificationService.shared.markAllRead() + await loadNotifications() + } + } label: { + Text("全部已读").font(.system(size: 13)) + } + } + } .task { await loadNotifications() } } + private func sectionView(_ title: String, items: [NotificationItem]) -> some View { + VStack(alignment: .leading, spacing: 0) { + Text(title).font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).padding(.horizontal, 20).padding(.top, 16).padding(.bottom, 8) + VStack(spacing: 0) { + ForEach(Array(items.enumerated()), id: \.offset) { i, n in + ZXNotificationItemRow(item: n) { + Task { _ = try? await NotificationService.shared.markRead(id: n.id) } + } + if i < items.count - 1 { ZXSettingDivider() } + } + } + .background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) + .padding(.horizontal, 20) + } + } + private func loadNotifications() async { isLoading = true - do { - notifications = try await NotificationService.shared.list() - } catch { /* keep empty state */ } + do { notifications = try await NotificationService.shared.list() } catch {} isLoading = false } private func refresh() async { isRefreshing = true - do { - notifications = try await NotificationService.shared.list() - } catch {} + do { notifications = try await NotificationService.shared.list() } catch {} isRefreshing = false } + + private func isToday(_ dateStr: String?) -> Bool { + guard let d = dateStr else { return false } + let today = ISO8601DateFormatter().string(from: Date()) + return d.prefix(10) == today.prefix(10) + } + + private func isThisWeek(_ dateStr: String?) -> Bool { + guard let d = dateStr, let date = ISO8601DateFormatter().date(from: String(d.prefix(19)) + "Z") else { return false } + return Date().timeIntervalSince(date) < 7 * 86400 + } } struct ZXNotificationItemRow: View { @@ -66,18 +99,24 @@ struct ZXNotificationItemRow: View { private var iconName: String { switch item.type { - case "review": return "arrow.triangle.2.circlepath" - case "ai_analysis": return "sparkles" + case "review", "review_reminder": return "arrow.triangle.2.circlepath" + case "import_complete": return "doc.fill.badge.ellipsis" + case "import_failed": return "doc.fill.badge.xmark" + case "quiz_ready": return "questionmark.circle" + case "ai_analysis", "ai_complete": return "brain.head.profile" case "streak": return "flame.fill" + case "subscription", "subscription_update": return "bell.fill" + case "system": return "info.circle" default: return "bell.fill" } } private var iconColor: Color { switch item.type { - case "review": return Color.zxOrange - case "ai_analysis": return Color.zxPurple + case "review", "review_reminder": return Color.zxOrange + case "ai_analysis", "ai_complete": return Color.zxPurple case "streak": return Color.zxGreen + case "import_failed": return Color.zxCoral default: return Color.zxAccent } } @@ -90,18 +129,23 @@ struct ZXNotificationItemRow: View { VStack(alignment: .leading, spacing: 4) { HStack { Text(item.title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) - if item.readAt == nil { - Circle().fill(Color.zxPurple).frame(width: 6, height: 6) - } + if item.readAt == nil { Circle().fill(Color.zxPurple).frame(width: 6, height: 6) } } - Text(item.content ?? "").font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(2) + if let content = item.content { Text(content).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(2) } if let createdAt = item.createdAt { - Text(createdAt.prefix(10).description).font(.system(size: 10)).foregroundColor(Color.zxF03) + Text(formatDate(createdAt)).font(.system(size: 10)).foregroundColor(Color.zxF03) } } Spacer() - Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14) }.foregroundColor(.primary) } + + private func formatDate(_ iso: String) -> String { + if let d = ISO8601DateFormatter().date(from: String(iso.prefix(19)) + "Z") { + let f = DateFormatter(); f.locale = Locale(identifier: "zh_CN") + f.dateFormat = "MM-dd HH:mm"; return f.string(from: d) + } + return String(iso.prefix(10)) + } }