WangDL 5e19bd740e feat(ios): 鉴权系统完善 + 前后端打通 + 模型对齐 + ViewModel 创建
- 新增 AuthManager (ObservableObject) 集中管理鉴权状态:
  - session 恢复 → token 验证 → 自动刷新
  - 登出自动重定向到登录页
  - NotificationCenter 监听 401 实现全局踢回
- APIClient 新增 401 自动 refresh + 单次重试
- App.swift 重构鉴权门控:
  - 去掉 hasCompletedOnboarding 绕过鉴权漏洞
  - 拆分为 SplashScreen / PreLoginFlow / PostLoginOnboardingFlow / ContentView
  - LoginPage 移除"跳过"按钮
- KeychainHelper 实现 token 安全存储
- APIModels 对齐后端 Prisma schema (UserProfile/KnowledgeBase/ReviewCard 等)
- APIService 简化 AuthService,token 管理迁移至 AuthManager
- 新增 8 个 ViewModel 接入 API:
  ProfileViewModel, LibraryViewModel, StudyViewModel,
  ActiveRecallViewModel, AIAnalysisViewModel, ReviewViewModel, ActivityViewModel
- 新增 EditProfilePage 编辑资料页
- 新增 NotificationListView 通知列表页
- AIHomeView 修复"检测中"卡住 (改用公开 GET / 健康检查)
- SettingsView 登出调用 AuthManager.signOut() 实现重定向
- 修复 NotificationItem 命名冲突、Combine import 缺失等编译错误

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:06:23 +08:00

114 lines
5.5 KiB
Swift

import SwiftUI
struct EditProfilePage: View {
@StateObject private var viewModel = ProfileViewModel()
@State private var nickname: String = ""
@State private var learningIdentity: String = ""
@State private var learningDirection: String = ""
@State private var bio: String = ""
@State private var currentGoal: String = ""
@State private var saved = false
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
ScrollView {
VStack(spacing: 16) {
sectionHeader("基本信息")
VStack(spacing: 0) {
ZXEditField(title: "昵称", text: $nickname, placeholder: "你的昵称")
ZXSettingDivider()
ZXEditField(title: "学习身份", text: $learningIdentity, placeholder: "如:考研学生、软件工程师")
}
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
sectionHeader("学习档案")
VStack(spacing: 0) {
ZXEditField(title: "学习方向", text: $learningDirection, placeholder: "如:机器学习、公考申论")
ZXSettingDivider()
ZXEditField(title: "当前目标", text: $currentGoal, placeholder: "如:通过 6 月 CFA 一级")
}
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
sectionHeader("个人简介")
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 8) {
Text("简介").font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035)
TextEditor(text: $bio)
.frame(minHeight: 100)
.scrollContentBackground(.hidden)
.padding(12)
.background(Color.zxFill003)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1))
.tint(Color.zxPurple)
}.padding(.horizontal, 16).padding(.vertical, 14)
}
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
Button {
Task {
_ = try? await UserService.shared.updateProfile(UpdateProfileRequest(
nickname: nickname.isEmpty ? nil : nickname, avatarUrl: nil
))
_ = try? await UserService.shared.updateProfileDetail(UpdateProfileDataRequest(
learningIdentity: learningIdentity.isEmpty ? nil : learningIdentity,
learningDirection: learningDirection.isEmpty ? nil : learningDirection,
bio: bio.isEmpty ? nil : bio,
currentGoal: currentGoal.isEmpty ? nil : currentGoal
))
saved = true
}
} label: {
Text(saved ? "已保存" : "保存修改")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(ZXGradient.ctaPurple)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 80)
}
.scrollIndicators(.hidden)
}
.navigationTitle("编辑资料")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.task {
await viewModel.loadProfile()
nickname = viewModel.userProfile?.nickname ?? ""
learningIdentity = viewModel.profileData?.learningIdentity ?? ""
learningDirection = viewModel.profileData?.learningDirection ?? ""
bio = viewModel.profileData?.bio ?? ""
currentGoal = viewModel.profileData?.currentGoal ?? ""
}
}
private func sectionHeader(_ text: String) -> some View {
Text(text).font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5).padding(.top, 4)
}
}
struct ZXEditField: View {
let title: String
@Binding var text: String
let placeholder: String
var body: some View {
HStack(spacing: 12) {
Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).frame(width: 72, alignment: .leading)
TextField(placeholder, text: $text)
.font(.system(size: 14))
.tint(Color.zxPurple)
.foregroundColor(Color.zxF0)
Spacer()
}
.padding(.horizontal, 16).padding(.vertical, 14)
}
}