- 所有 SF Symbol .fill 图标替换为线性版本 - 自定义加载动画全部替换为原生 ProgressView/refreshable - StudyHomeView 重设计:优先级驱动主行动卡片 - ZLibraryCard 重新设计:封面图自适应、信息布局优化 - LibraryDetailPage:顶部KB信息区、···菜单、排序、长按操作 - 知识点列表:文件类型图标、学习时长、分割线样式 - 弥散渐变顶部背景 - 新增 icon-folder、icon-xmark SVG Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
140 lines
9.2 KiB
Swift
140 lines
9.2 KiB
Swift
import SwiftUI
|
||
|
||
struct ProfileView: View {
|
||
@StateObject private var viewModel = ProfileViewModel()
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
ZXGradient.page.ignoresSafeArea()
|
||
ScrollView {
|
||
VStack(spacing: 16) {
|
||
HStack {
|
||
Text("我的").font(.system(size: 22, weight: .heavy)).foregroundColor(Color.zxF0).tracking(-0.5)
|
||
Spacer()
|
||
NavigationLink(value: Route.notificationList) {
|
||
Image("icon-notifications").resizable().scaledToFit().frame(width: 22, height: 22).foregroundColor(Color.zxF05)
|
||
}
|
||
.accessibilityLabel("通知中心")
|
||
NavigationLink(value: Route.settings) {
|
||
Image("icon-settings").resizable().scaledToFit().frame(width: 22, height: 22).foregroundColor(Color.zxF05)
|
||
}
|
||
.accessibilityLabel("设置")
|
||
}.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 4)
|
||
profileCard
|
||
VStack(spacing: 0) {
|
||
NavigationLink(value: Route.goalSetting) {
|
||
ZXProfileMenuRow(icon: "target", title: "学习目标设置", desc: "调整你的学习目标")
|
||
}.foregroundColor(.primary)
|
||
ZXProfileDivider()
|
||
NavigationLink(value: Route.settings) {
|
||
ZXProfileMenuRow(icon: "bell", title: "复习提醒", desc: "间隔复习通知设置")
|
||
}.foregroundColor(.primary)
|
||
ZXProfileDivider()
|
||
NavigationLink(value: Route.methodPreference) {
|
||
ZXProfileMenuRow(icon: "puzzlepiece", title: "学习方法偏好", desc: "回忆 · 费曼 · 间隔")
|
||
}.foregroundColor(.primary)
|
||
ZXProfileDivider()
|
||
NavigationLink(value: Route.feedbackForm) {
|
||
ZXProfileMenuRow(icon: "bubble.left", title: "帮助与反馈", desc: "问题报告 · 功能建议")
|
||
}.foregroundColor(.primary)
|
||
}.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20)).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
||
assetsSection.padding(.bottom, 120)
|
||
}.padding(.horizontal, 20)
|
||
}.scrollIndicators(.hidden)
|
||
}
|
||
.task { await viewModel.loadAll() }
|
||
.navigationDestination(for: Route.self) { $0.destination }
|
||
}
|
||
|
||
private var defaultAvatarIcon: some View {
|
||
ZStack {
|
||
Circle().fill(Color.zxPurpleBG(0.2)).frame(width: 80, height: 80)
|
||
Image("icon-brain")
|
||
.font(.system(size: 36))
|
||
.foregroundColor(Color.zxPurple)
|
||
}
|
||
}
|
||
|
||
private var profileCard: some View {
|
||
let profile = viewModel.userProfile
|
||
return NavigationLink(value: Route.editProfile) {
|
||
VStack(spacing: 16) {
|
||
HStack {
|
||
ZStack {
|
||
if let urlStr = profile?.avatarUrl, let url = URL(string: urlStr) {
|
||
AsyncImage(url: url) { phase in
|
||
switch phase {
|
||
case .success(let img):
|
||
img.resizable().scaledToFill()
|
||
default:
|
||
defaultAvatarIcon
|
||
}
|
||
}
|
||
.frame(width: 80, height: 80)
|
||
.clipShape(Circle())
|
||
} else {
|
||
defaultAvatarIcon
|
||
}
|
||
}
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(profile?.nickname ?? "学习者").font(.system(size: 20, weight: .bold)).foregroundColor(Color.zxF0)
|
||
Text(profile?.email ?? "").font(.system(size: 12)).foregroundColor(Color.zxF04)
|
||
}
|
||
Spacer(); Image("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03)
|
||
}
|
||
HStack(spacing: 0) { ZXProfileStat(value: "\(viewModel.summary?.activeDays ?? 0)", label: "活跃天", color: Color.zxOrange); ZXProfileStat(value: "\(viewModel.summary?.totalCardsReviewed ?? 0)", label: "复习卡片", color: Color.zxPurple); ZXProfileStat(value: "\(viewModel.summary?.totalMinutes ?? 0)", label: "分钟", color: Color.zxTeal) }
|
||
}.padding(20).background(ZXGradient.profileCard).overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "#7C6EFA", opacity: 0.2), lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 20))
|
||
}.foregroundColor(.primary)
|
||
.accessibilityLabel("编辑个人资料,\(profile?.nickname ?? "学习者")")
|
||
.accessibilityHint("双击查看和编辑个人资料")
|
||
}
|
||
private var assetsSection: some View {
|
||
VStack(spacing: 16) {
|
||
// 学习资产
|
||
VStack(spacing: 0) {
|
||
NavigationLink(value: Route.libraryCreate) {
|
||
HStack {
|
||
Image("icon-books").resizable().scaledToFit().frame(width: 20, height: 20).foregroundColor(Color.zxF05)
|
||
|
||
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("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03)
|
||
}.padding(.horizontal, 16).padding(.vertical, 14)
|
||
}.foregroundColor(.primary)
|
||
ZXProfileDivider()
|
||
NavigationLink(value: Route.notificationList) {
|
||
HStack {
|
||
Image("icon-bell-on").resizable().scaledToFit().frame(width: 20, height: 20).foregroundColor(Color.zxF05)
|
||
|
||
Text("消息中心").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
|
||
Spacer(); Image("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03)
|
||
}.padding(.horizontal, 16).padding(.vertical, 14)
|
||
}.foregroundColor(.primary)
|
||
ZXProfileDivider()
|
||
HStack {
|
||
Image("icon-storage").resizable().scaledToFit().frame(width: 20, height: 20).foregroundColor(Color.zxF05)
|
||
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text("存储空间").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
|
||
Text(viewModel.formattedStorage).font(.system(size: 12)).foregroundColor(Color.zxF04)
|
||
}
|
||
Spacer(); Image("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).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))
|
||
}
|
||
}
|
||
}
|
||
struct ZXProfileStat: View { let v: String; let l: String; let c: Color; var body: some View { VStack(spacing: 2) { Text(v).font(.system(size: 18, weight: .bold)).foregroundColor(c); Text(l).font(.system(size: 12)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
|
||
init(value: String, label: String, color: Color) { self.v = value; self.l = label; self.c = color }
|
||
}
|
||
struct ZXProfileMenuRow: View { let icon: String; let title: String; let desc: String
|
||
var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(Color.zxF05).frame(width: 36, height: 36).background(Color.zxFill006).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF03) }; Spacer(); Image("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03) }.padding(.horizontal, 16).padding(.vertical, 14).accessibilityLabel("\(title):\(desc)") }
|
||
}
|
||
struct ZXProfileDivider: View {
|
||
var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 64) }
|
||
}
|
||
struct ZXAchievementBadge: View { let icon: String; let label: String; let color: Color
|
||
var body: some View { VStack(spacing: 6) { Image(systemName: icon).font(.system(size: 22)).foregroundColor(color).frame(width: 48, height: 48).background(color.opacity(0.12)).clipShape(RoundedRectangle(cornerRadius: 14)).overlay(RoundedRectangle(cornerRadius: 14).stroke(color.opacity(0.25), lineWidth: 1)); Text(label).font(.system(size: 10, weight: .semibold)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity) }
|
||
}
|