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:
parent
5ff979cc97
commit
90ad19aad1
@ -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 {
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
preferences = profile.preferences
|
|
||||||
profileData = profile.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 {
|
private func loadStats() async {
|
||||||
do {
|
async let a: AssetsResponse? = try? UserService.shared.fetchAssets()
|
||||||
preferences = try await UserService.shared.updatePreferences(dto)
|
async let s: StorageResponse? = try? UserService.shared.fetchStorage()
|
||||||
} catch {
|
if let assets = await a { kbCount = assets.knowledgeBaseCount; itemCount = assets.knowledgeItemCount; cardCount = assets.reviewCardCount }
|
||||||
errorMessage = "保存设置失败"
|
if let storage = await s { usedBytes = storage.usedBytes; totalBytes = storage.totalBytes }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user