diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift index c0584a7..4aa2712 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -85,6 +85,14 @@ class UserService { func removeDevice(id: String) async throws -> GenericSuccessResponse { return try await client.request("/users/me/devices/\(id)", method: "DELETE") } + + func fetchAssets() async throws -> AssetsResponse { + return try await client.request("/users/me/assets-summary") + } + + func fetchStorage() async throws -> StorageResponse { + return try await client.request("/users/me/storage") + } } struct UserDevice: Codable, Identifiable { diff --git a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift index 0202158..a0266c0 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/ProfileView.swift @@ -44,7 +44,7 @@ struct ProfileView: View { ZXProfileMenuRow(icon: "bubble.left.fill", title: "帮助与反馈", desc: "问题报告 · 功能建议") }.foregroundColor(.primary) }.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) - achievementsSection.padding(.bottom, 120) + assetsSection.padding(.bottom, 120) }.padding(.horizontal, 20) }.scrollIndicators(.hidden) } @@ -94,10 +94,37 @@ struct ProfileView: View { .accessibilityLabel("编辑个人资料,\(profile?.nickname ?? "学习者")") .accessibilityHint("双击查看和编辑个人资料") } - private var achievementsSection: some View { - VStack(alignment: .leading, spacing: 12) { - Text("成就").font(.system(size: 15, weight: .bold)).foregroundColor(Color.zxF0) - HStack(spacing: 8) { ZXAchievementBadge(icon: "flame.fill", label: "连续 14 天", color: Color.zxOrange); ZXAchievementBadge(icon: "brain.head.profile", label: "费曼达人", color: Color.zxPurple); ZXAchievementBadge(icon: "books.vertical.fill", label: "知识收藏家", color: Color.zxTeal); ZXAchievementBadge(icon: "bolt.fill", label: "速学者", color: Color.zxYellow) } + private var assetsSection: some View { + VStack(spacing: 16) { + // 学习资产 + VStack(spacing: 0) { + NavigationLink(value: Route.libraryCreate) { + HStack { + Image(systemName: "books.vertical.fill").font(.system(size: 18)).foregroundColor(Color.zxPurple).frame(width: 36, height: 36).background(Color.zxPurpleBG(0.12)).clipShape(RoundedRectangle(cornerRadius: 10)) + Text("学习资产").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + Spacer() + Text("\(viewModel.kbCount)库 \(viewModel.itemCount)项 \(viewModel.cardCount)卡").font(.system(size: 12)).foregroundColor(Color.zxF04) + Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.horizontal, 16).padding(.vertical, 14) + }.foregroundColor(.primary) + ZXProfileDivider() + NavigationLink(value: Route.notificationList) { + HStack { + Image(systemName: "bell.fill").font(.system(size: 18)).foregroundColor(Color.zxOrange).frame(width: 36, height: 36).background(Color.zxOrange.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 10)) + Text("消息中心").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.horizontal, 16).padding(.vertical, 14) + }.foregroundColor(.primary) + ZXProfileDivider() + HStack { + Image(systemName: "externaldrive.fill").font(.system(size: 18)).foregroundColor(Color.zxTeal).frame(width: 36, height: 36).background(Color.zxTeal.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 10)) + VStack(alignment: .leading, spacing: 2) { + Text("存储空间").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0) + Text(viewModel.formattedStorage).font(.system(size: 11)).foregroundColor(Color.zxF04) + } + Spacer(); Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03) + }.padding(.horizontal, 16).padding(.vertical, 14) + }.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) } } } diff --git a/AIStudyApp/AIStudyApp/Features/Profile/ProfileViewModel.swift b/AIStudyApp/AIStudyApp/Features/Profile/ProfileViewModel.swift index 9bbf478..ed4f18e 100644 --- a/AIStudyApp/AIStudyApp/Features/Profile/ProfileViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Profile/ProfileViewModel.swift @@ -7,49 +7,49 @@ class ProfileViewModel: ObservableObject { @Published var preferences: UserPreferences? @Published var profileData: UserProfileData? @Published var summary: ActivitySummary? + @Published var kbCount = 0, itemCount = 0, cardCount = 0 + @Published var usedBytes = 0, totalBytes = 1073741824 @Published var isLoading = false @Published var errorMessage: String? + var formattedStorage: String { + if usedBytes < 1024 * 1024 { return "\(usedBytes / 1024) KB / \(totalBytes / 1024 / 1024 / 1024) GB" } + return String(format: "%.0f MB / %d GB", Double(usedBytes) / 1024 / 1024, totalBytes / 1024 / 1024 / 1024) + } + func loadAll() async { - isLoading = true - errorMessage = nil + isLoading = true; errorMessage = nil await loadProfile() isLoading = false Task { await loadActivitySummary() } + Task { await loadStats() } } func loadProfile() async { - do { - let profile = try await UserService.shared.myProfile() - userProfile = profile - preferences = profile.preferences - profileData = profile.profile - } catch { - if userProfile == nil { errorMessage = "加载用户信息失败" } - } + do { let p = try await UserService.shared.myProfile(); userProfile = p; preferences = p.preferences; profileData = p.profile } catch { if userProfile == nil { errorMessage = "加载用户信息失败" } } } + func updatePreferences(_ dto: UpdatePreferencesRequest) async { do { preferences = try await UserService.shared.updatePreferences(dto) } catch { errorMessage = "保存设置失败" } } + func updateProfileDetail(_ dto: UpdateProfileDataRequest) async { do { profileData = try await UserService.shared.updateProfileDetail(dto) } catch { errorMessage = "保存学习档案失败" } } + private func loadActivitySummary() async { do { summary = try await ActivityService.shared.summary() } catch {} } - func updatePreferences(_ dto: UpdatePreferencesRequest) async { - do { - preferences = try await UserService.shared.updatePreferences(dto) - } catch { - errorMessage = "保存设置失败" - } - } - - func updateProfileDetail(_ dto: UpdateProfileDataRequest) async { - do { - profileData = try await UserService.shared.updateProfileDetail(dto) - } catch { - errorMessage = "保存学习档案失败" - } - } - - private func loadActivitySummary() async { - do { - summary = try await ActivityService.shared.summary() - } catch { - // non-critical, stats remain at 0 - } + private func loadStats() async { + async let a: AssetsResponse? = try? UserService.shared.fetchAssets() + async let s: StorageResponse? = try? UserService.shared.fetchStorage() + if let assets = await a { kbCount = assets.knowledgeBaseCount; itemCount = assets.knowledgeItemCount; cardCount = assets.reviewCardCount } + if let storage = await s { usedBytes = storage.usedBytes; totalBytes = storage.totalBytes } } } + +// MARK: - Raw API models + +struct AssetsResponse: Codable { + let knowledgeBaseCount: Int + let knowledgeItemCount: Int + let reviewCardCount: Int +} + +struct StorageResponse: Codable { + let usedBytes: Int + let totalBytes: Int + let fileCount: Int +}