wangdl 4ebb70c036 feat: 图标线型化 + 首页重设计 + 知识库卡片优化 + 知识点列表重构
- 所有 SF Symbol .fill 图标替换为线性版本
- 自定义加载动画全部替换为原生 ProgressView/refreshable
- StudyHomeView 重设计:优先级驱动主行动卡片
- ZLibraryCard 重新设计:封面图自适应、信息布局优化
- LibraryDetailPage:顶部KB信息区、···菜单、排序、长按操作
- 知识点列表:文件类型图标、学习时长、分割线样式
- 弥散渐变顶部背景
- 新增 icon-folder、icon-xmark SVG

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 20:07:15 +08:00

140 lines
9.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) }
}