feat(ios): IOS-M1-05 我的页面资产摘要 + 存储空间

- ProfileView 新增学习资产行(KB数/知识点/复习卡)
- 新增存储空间行(已用/总量)
- 新增消息中心快捷入口
- UserService 新增 fetchAssets()/fetchStorage()
- ProfileViewModel 新增 loadStats()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-29 19:50:26 +08:00
parent 5ff979cc97
commit 90ad19aad1
3 changed files with 72 additions and 37 deletions

View File

@ -85,6 +85,14 @@ class UserService {
func removeDevice(id: String) async throws -> GenericSuccessResponse { func removeDevice(id: String) async throws -> GenericSuccessResponse {
return try await client.request("/users/me/devices/\(id)", method: "DELETE") 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 { struct UserDevice: Codable, Identifiable {

View File

@ -44,7 +44,7 @@ struct ProfileView: View {
ZXProfileMenuRow(icon: "bubble.left.fill", title: "帮助与反馈", desc: "问题报告 · 功能建议") ZXProfileMenuRow(icon: "bubble.left.fill", title: "帮助与反馈", desc: "问题报告 · 功能建议")
}.foregroundColor(.primary) }.foregroundColor(.primary)
}.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1)) }.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) }.padding(.horizontal, 20)
}.scrollIndicators(.hidden) }.scrollIndicators(.hidden)
} }
@ -94,10 +94,37 @@ struct ProfileView: View {
.accessibilityLabel("编辑个人资料,\(profile?.nickname ?? "学习者")") .accessibilityLabel("编辑个人资料,\(profile?.nickname ?? "学习者")")
.accessibilityHint("双击查看和编辑个人资料") .accessibilityHint("双击查看和编辑个人资料")
} }
private var achievementsSection: some View { private var assetsSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(spacing: 16) {
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) } 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))
} }
} }
} }

View File

@ -7,49 +7,49 @@ class ProfileViewModel: ObservableObject {
@Published var preferences: UserPreferences? @Published var preferences: UserPreferences?
@Published var profileData: UserProfileData? @Published var profileData: UserProfileData?
@Published var summary: ActivitySummary? @Published var summary: ActivitySummary?
@Published var kbCount = 0, itemCount = 0, cardCount = 0
@Published var usedBytes = 0, totalBytes = 1073741824
@Published var isLoading = false @Published var isLoading = false
@Published var errorMessage: String? @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 { func loadAll() async {
isLoading = true isLoading = true; errorMessage = nil
errorMessage = nil
await loadProfile() await loadProfile()
isLoading = false isLoading = false
Task { await loadActivitySummary() } Task { await loadActivitySummary() }
Task { await loadStats() }
} }
func loadProfile() async { func loadProfile() async {
do { do { let p = try await UserService.shared.myProfile(); userProfile = p; preferences = p.preferences; profileData = p.profile } catch { if userProfile == nil { errorMessage = "加载用户信息失败" } }
let profile = try await UserService.shared.myProfile() }
userProfile = profile func updatePreferences(_ dto: UpdatePreferencesRequest) async { do { preferences = try await UserService.shared.updatePreferences(dto) } catch { errorMessage = "保存设置失败" } }
preferences = profile.preferences func updateProfileDetail(_ dto: UpdateProfileDataRequest) async { do { profileData = try await UserService.shared.updateProfileDetail(dto) } catch { errorMessage = "保存学习档案失败" } }
profileData = profile.profile private func loadActivitySummary() async { do { summary = try await ActivityService.shared.summary() } catch {} }
} catch {
if userProfile == nil { errorMessage = "加载用户信息失败" } 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 }
} }
} }
func updatePreferences(_ dto: UpdatePreferencesRequest) async { // MARK: - Raw API models
do {
preferences = try await UserService.shared.updatePreferences(dto) struct AssetsResponse: Codable {
} catch { let knowledgeBaseCount: Int
errorMessage = "保存设置失败" let knowledgeItemCount: Int
} let reviewCardCount: Int
} }
func updateProfileDetail(_ dto: UpdateProfileDataRequest) async { struct StorageResponse: Codable {
do { let usedBytes: Int
profileData = try await UserService.shared.updateProfileDetail(dto) let totalBytes: Int
} catch { let fileCount: Int
errorMessage = "保存学习档案失败"
}
}
private func loadActivitySummary() async {
do {
summary = try await ActivityService.shared.summary()
} catch {
// non-critical, stats remain at 0
}
}
} }