import SwiftUI import SwiftData struct MeView: View { @Environment(\.modelContext) private var ctx @Query private var profiles: [UserProfile] @Query private var customMetrics: [CustomMonitorMetric] @State private var downloadService = ModelDownloadService.shared @State private var appLock = AppLock.shared @State private var lang = LanguageManager.shared // key 必须与 AppLock.enabledKey 一致。 @AppStorage("faceIDLockEnabled") private var lockEnabled = false private var profile: UserProfile? { profiles.first } /// 真实读取 Bundle 版本号,与「关于」页保持一致。 private var appVersionText: String { let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.1" return "v\(short)" } var body: some View { NavigationStack { ScrollView { VStack(spacing: 12) { profileCard customMetricsCard modelManagementCard languageCard faceIDCard NavigationLink { AboutView() } label: { settingsCard(title: String(appLoc: "关于"), detail: appVersionText, icon: "info.circle") } .buttonStyle(.plain) } .padding(.horizontal, 16) .padding(.vertical, 20) } .background(Tj.Palette.sand.ignoresSafeArea()) .navigationTitle("我的") .navigationBarTitleDisplayMode(.large) .onAppear { if profiles.isEmpty { _ = UserProfileStore.loadOrCreate(in: ctx) } downloadService.refreshStates() appLock.refreshAvailability() } } } // MARK: - Cards private var profileCard: some View { NavigationLink { ProfileEditView() } label: { HStack(spacing: 12) { ZStack { Circle() .fill(Tj.Palette.amber.opacity(0.25)) Image(systemName: "person.crop.circle.fill") .font(.system(size: 22)) .foregroundStyle(Tj.Palette.ink) } .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text("个人资料") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Tj.Palette.text) Text(profileLine) .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) .lineLimit(1) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .tjCard() } .buttonStyle(.plain) } private var customMetricsCard: some View { NavigationLink { CustomMetricsListView() } label: { HStack(spacing: 12) { ZStack { Circle() .fill(customMetrics.isEmpty ? Tj.Palette.sand2 : Tj.Palette.leafSoft) Image(systemName: "slider.horizontal.3") .font(.system(size: 18)) .foregroundStyle(customMetrics.isEmpty ? Tj.Palette.text2 : Tj.Palette.ink) } .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text("自定义指标") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Tj.Palette.text) Text(customMetricsLine) .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) .lineLimit(1) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .tjCard() } .buttonStyle(.plain) } private var customMetricsLine: String { if customMetrics.isEmpty { return String(appLoc: "添加你自己的长期监测项") } return String(appLoc: "\(customMetrics.count) 项") } private var modelManagementCard: some View { NavigationLink { ModelManagementView() } label: { settingsCard(title: String(appLoc: "模型管理"), detail: modelDetail, icon: "cpu") } .buttonStyle(.plain) } private var modelDetail: String { let states = downloadService.states if ModelKind.allCases.allSatisfy({ states[$0]?.phase == .ready }) { return String(appLoc: "已就绪") } if downloadService.isAnyDownloading { return String(appLoc: "下载中…") } let readyCount = ModelKind.allCases.filter { states[$0]?.phase == .ready }.count return readyCount == 0 ? String(appLoc: "未下载") : String(appLoc: "\(readyCount)/\(ModelKind.allCases.count) 就绪") } private var languageCard: some View { NavigationLink { LanguageSettingsView() } label: { settingsCard(title: String(appLoc: "语言"), detail: lang.current.displayName, icon: "character.bubble") } .buttonStyle(.plain) } // MARK: - Face ID 启动锁(可交互 Toggle 卡) private var faceIDCard: some View { HStack(spacing: 12) { ZStack { Circle().fill(lockEnabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2) Image(systemName: "faceid") .font(.system(size: 18)) .foregroundStyle(lockEnabled ? Tj.Palette.ink : Tj.Palette.text2) } .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text("Face ID 启动锁") .font(.system(size: 15, weight: .medium)) .foregroundStyle(Tj.Palette.text) Text(faceIDLine) .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) } Spacer() Toggle("", isOn: faceIDBinding) .labelsHidden() .disabled(!appLock.biometryAvailable) } .padding(14) .tjCard() } private var faceIDLine: String { if !appLock.biometryAvailable { return String(appLoc: "本设备未设置 Face ID 或密码") } return lockEnabled ? String(appLoc: "已开启 · \(appLock.biometryLabel)") : String(appLoc: "关闭") } /// 打开 → 先认证一次,成功才置 enabled(失败则开关弹回);关闭 → 直接关。 private var faceIDBinding: Binding { Binding( get: { lockEnabled }, set: { newValue in if newValue { Task { await appLock.enableWithAuth() } } else { appLock.disable() } } ) } private func settingsCard(title: String, detail: String, icon: String) -> some View { HStack(spacing: 12) { ZStack { Circle().fill(Tj.Palette.sand2) Image(systemName: icon) .font(.system(size: 18)) .foregroundStyle(Tj.Palette.text2) } .frame(width: 44, height: 44) Text(title) .font(.system(size: 15, weight: .medium)) .foregroundStyle(Tj.Palette.text) Spacer() Text(detail) .font(.system(size: 12)) .foregroundStyle(Tj.Palette.text3) Image(systemName: "chevron.right") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .tjCard() } private var profileLine: String { guard let p = profile, p.hasAnyBasics else { return String(appLoc: "点这里完善你的资料") } return p.summaryLine } } #Preview { MeView() .modelContainer(for: [ UserProfile.self, Indicator.self, Report.self, DiaryEntry.self, Asset.self, ChatTurn.self, Symptom.self, MetricReminder.self, CustomMonitorMetric.self, ], inMemory: true) }