- ZXSettingRow / ZXSettingToggleRow 移除 background+clipShape - ProfileView 通知/设置图标去掉毛玻璃背景 - 全局清理 .background(xxxBG(0.xx)).clipShape() 图案 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
201 lines
10 KiB
Swift
201 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject var authManager: AuthManager
|
|
@StateObject private var profileVM = ProfileViewModel()
|
|
@State private var language = "zh-CN"
|
|
@State private var appearance = "system"
|
|
@State private var defaultFocusMinutes = 25
|
|
@State private var notificationEnabled = true
|
|
@State private var reviewReminder = true
|
|
@State private var reminderTime = "20:00"
|
|
@State private var intervalDays = "1"
|
|
@State private var iCloudSync = false
|
|
@State private var autoBackup = false
|
|
@State private var showLogoutAlert = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.zxBg0.ignoresSafeArea()
|
|
ScrollView {
|
|
VStack(spacing: 16) {
|
|
sectionHeader("外观与语言")
|
|
VStack(spacing: 0) {
|
|
ZXSettingRow(title: "外观", value: appearanceLabel, icon: appearance == "dark" ? "icon-moon" : "icon-sun", color: Color.zxPurple, isCustom: true)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { cycleAppearance() }
|
|
ZXSettingDivider()
|
|
ZXSettingRow(title: "语言", value: language == "zh-CN" ? "简体中文" : "English", icon: "globe", color: Color.zxTeal)
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
|
|
sectionHeader("学习设置")
|
|
VStack(spacing: 0) {
|
|
NavigationLink(value: Route.goalSetting) {
|
|
ZXSettingRow(title: "学习目标", value: "备考考试", icon: "target", color: Color.zxOrange)
|
|
}.foregroundColor(.primary)
|
|
ZXSettingDivider()
|
|
NavigationLink(value: Route.methodPreference) {
|
|
ZXSettingRow(title: "学习方法偏好", value: "间隔回忆 · 费曼技巧", icon: "brain.head.profile", color: Color.zxPurple)
|
|
}.foregroundColor(.primary)
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
|
|
sectionHeader("复习提醒")
|
|
VStack(spacing: 0) {
|
|
ZXSettingToggleRow(title: "开启复习提醒", icon: "bell.badge.fill", color: Color.zxOrange, isOn: $reviewReminder)
|
|
if reviewReminder {
|
|
ZXSettingDivider()
|
|
ZXSettingPickerRow(title: "提醒时间", value: $reminderTime, options: ["08:00", "12:00", "18:00", "20:00", "21:00"])
|
|
ZXSettingDivider()
|
|
ZXSettingPickerRow(title: "间隔天数", value: $intervalDays, options: ["1", "2", "3", "5", "7"])
|
|
}
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
|
|
sectionHeader("数据")
|
|
VStack(spacing: 0) {
|
|
ZXSettingToggleRow(title: "iCloud 同步", icon: "icloud.fill", color: Color.zxTeal, isOn: $iCloudSync)
|
|
ZXSettingDivider()
|
|
ZXSettingToggleRow(title: "自动备份", icon: "arrow.triangle.2.circlepath", color: Color.zxAccent, isOn: $autoBackup)
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
|
|
VStack(spacing: 0) {
|
|
NavigationLink(value: Route.feedbackForm) {
|
|
ZXSettingRow(title: "帮助与反馈", value: "", icon: "questionmark.circle.fill", color: Color.zxAccent)
|
|
}.foregroundColor(.primary)
|
|
ZXSettingDivider()
|
|
ZXSettingRow(title: "隐私政策", value: "", icon: "hand.raised.fill", color: Color.zxYellow)
|
|
ZXSettingDivider()
|
|
ZXSettingRow(title: "用户协议", value: "", icon: "doc.text.fill", color: Color.zxGreen)
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
|
|
VStack(spacing: 0) {
|
|
Button {
|
|
showLogoutAlert = true
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "rectangle.portrait.and.arrow.right").font(.system(size: 16)).foregroundColor(.red).frame(width: 32, height: 32)
|
|
|
|
Text("退出登录").font(.system(size: 14, weight: .semibold)).foregroundColor(.red)
|
|
Spacer()
|
|
}.padding(.horizontal, 16).padding(.vertical, 14)
|
|
}
|
|
}
|
|
.background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.zxBorder006, lineWidth: 1))
|
|
.alert("退出登录", isPresented: $showLogoutAlert) {
|
|
Button("取消", role: .cancel) {}
|
|
Button("退出", role: .destructive) {
|
|
Task { await authManager.signOut() }
|
|
}
|
|
} message: {
|
|
Text("退出后需要重新登录")
|
|
}
|
|
|
|
sectionHeader("关于知习")
|
|
HStack(spacing: 4) {
|
|
Text("知习 v1.0").font(.system(size: 12)).foregroundColor(Color.zxF03)
|
|
}.padding(.bottom, 100)
|
|
}.padding(.horizontal, 20).padding(.top, 8)
|
|
}
|
|
.scrollIndicators(.hidden)
|
|
}
|
|
.task { await profileVM.loadProfile() }
|
|
.onChange(of: profileVM.preferences) { _, p in
|
|
guard let p else { return }
|
|
appearance = p.appearance ?? "system"
|
|
language = p.language ?? "zh-CN"
|
|
defaultFocusMinutes = p.defaultFocusMinutes ?? 25
|
|
notificationEnabled = p.notificationEnabled ?? true
|
|
reviewReminder = notificationEnabled
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline).animatedTabBarHide().toolbarBackground(.hidden, for: .navigationBar)
|
|
}
|
|
|
|
private func sectionHeader(_ text: String) -> some View {
|
|
Text(text).font(.system(size: 12, weight: .semibold)).foregroundColor(Color.zxF035).tracking(0.5).padding(.top, 4)
|
|
}
|
|
|
|
private var appearanceLabel: String {
|
|
switch appearance { case "system": return "跟随系统"; case "dark": return "深色模式"; default: return "浅色模式" }
|
|
}
|
|
|
|
private func cycleAppearance() {
|
|
switch appearance {
|
|
case "system": appearance = "dark"
|
|
case "dark": appearance = "light"
|
|
default: appearance = "system"
|
|
}
|
|
Task {
|
|
await profileVM.updatePreferences(UpdatePreferencesRequest(
|
|
preferredMethods: nil, defaultFocusMinutes: defaultFocusMinutes,
|
|
aiSuggestionLevel: nil, language: language, appearance: appearance,
|
|
notificationEnabled: notificationEnabled
|
|
))
|
|
}
|
|
}
|
|
|
|
private func saveNotificationSettings() {
|
|
Task {
|
|
await profileVM.updatePreferences(UpdatePreferencesRequest(
|
|
preferredMethods: nil, defaultFocusMinutes: defaultFocusMinutes,
|
|
aiSuggestionLevel: nil, language: language, appearance: appearance,
|
|
notificationEnabled: notificationEnabled
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Setting row components
|
|
|
|
struct ZXSettingRow: View {
|
|
let title: String; let value: String; let icon: String; let color: Color; var isCustom: Bool = false
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
if isCustom { Image(icon).resizable().scaledToFit().frame(width: 20, height: 20).foregroundColor(color) }
|
|
else { Image(systemName: icon).font(.system(size: 18)).foregroundColor(color) }
|
|
Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
|
|
Spacer()
|
|
if !value.isEmpty { Text(value).font(.system(size: 13)).foregroundColor(Color.zxF03) }
|
|
Image(systemName: "chevron.right").font(.system(size: 12)).foregroundColor(Color.zxF03)
|
|
}.padding(.horizontal, 16).padding(.vertical, 14)
|
|
}
|
|
}
|
|
|
|
struct ZXSettingToggleRow: View {
|
|
let title: String; let icon: String; let color: Color; @Binding var isOn: Bool
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: icon).font(.system(size: 18)).foregroundColor(color)
|
|
Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0)
|
|
Spacer()
|
|
Toggle("", isOn: $isOn).labelsHidden().tint(Color.zxPurple)
|
|
}.padding(.horizontal, 16).padding(.vertical, 14)
|
|
}
|
|
}
|
|
|
|
struct ZXSettingPickerRow: View {
|
|
let title: String; @Binding var value: String; let options: [String]
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).opacity(0.6)
|
|
Spacer()
|
|
Picker(title, selection: $value) {
|
|
ForEach(options, id: \.self) { o in Text(o).tag(o) }
|
|
}.pickerStyle(.segmented).frame(width: 200).tint(Color.zxPurple)
|
|
}.padding(.horizontal, 16).padding(.vertical, 10)
|
|
}
|
|
}
|
|
|
|
struct ZXSettingDivider: View {
|
|
var body: some View { Rectangle().fill(Color.zxBorder008).frame(height: 1).padding(.leading, 60) }
|
|
}
|