diff --git a/AreYouSleeping/Views/AgreementView.swift b/AreYouSleeping/Views/AgreementView.swift index de92db2..984a494 100644 --- a/AreYouSleeping/Views/AgreementView.swift +++ b/AreYouSleeping/Views/AgreementView.swift @@ -4,73 +4,82 @@ struct AgreementView: View { @EnvironmentObject var store: Store var body: some View { - VStack(spacing: 0) { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - Text("用户协议与隐私说明") - .font(.title).bold() + ZStack { + LinearGradient( + colors: [Color.white, Color(red: 0.84, green: 0.84, blue: 0.87)], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() - Text("欢迎使用「睡了吗」。\n\n本协议说明应用功能与数据使用方式,请仔细阅读。") - .font(.body).foregroundColor(.secondary) + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("用户协议与隐私说明") + .font(.title).bold() - section("一、应用功能") { - Text("1. 睡眠监督:在您设定的就寝时段,提醒您按时入睡。\n(注:iOS 系统限制,本应用无法直接屏蔽其他应用,仅提供通知提醒。)\n2. 作息记录:记录您的入睡与起床时间,生成睡眠统计。\n3. 控制码功能(见第二条)。") + Text("欢迎使用「睡了吗」。\n\n本协议说明应用功能与数据使用方式,请仔细阅读。") + .font(.body).foregroundColor(.secondary) + + agreementSection("一、应用功能") { + Text("1. 睡眠监督:在您设定的就寝时段,提醒您按时入睡。\n(注:iOS 系统限制,本应用无法直接屏蔽其他应用,仅提供通知提醒。)\n2. 作息记录:记录您的入睡与起床时间,生成睡眠统计。\n3. 控制码功能(见第二条)。") + } + + agreementSection("二、控制码功能") { + Text("本应用会为您的手机生成一个独一无二的控制码。\n\n• 控制码绑定本机,卸载重装后才会变更。\n• 您可以将控制码分享给朋友。朋友输入您的控制码后可以远程设置您的就寝/起床时间、提醒您入睡/起床。\n• 您可以随时在设置中停止分享。\n• 请仅将控制码分享给您信任的人。") + } + + agreementSection("三、数据与隐私") { + Text("• 您的作息设置、睡眠记录会同步到服务器,以便朋友通过控制码查看和远程操作。\n• 服务器仅存储与功能相关的必要数据,不会出售或分享给第三方。\n• 您可以在应用设置中随时停止监督服务,断开与服务器同步。") + } + + agreementSection("四、所需权限") { + Text("• 通知:发送就寝提醒与朋友远程消息\n• 网络访问:与服务器同步作息数据、接收朋友远程指令") + } + + Spacer().frame(height: 16) + + VStack(spacing: 4) { + Text("解释权归 JGZ_YES 所有") + .font(.footnote).foregroundColor(.secondary) + Text("(c) 2026 JGZ_YES") + .font(.footnote).foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) } - - section("二、控制码功能") { - Text("本应用会为您的手机生成一个独一无二的控制码。\n\n• 控制码绑定本机,卸载重装后才会变更。\n• 您可以将控制码分享给朋友。朋友输入您的控制码后可以远程设置您的就寝/起床时间、提醒您入睡/起床。\n• 您可以随时在设置中停止分享。\n• 请仅将控制码分享给您信任的人。") - } - - section("三、数据与隐私") { - Text("• 您的作息设置、睡眠记录会同步到服务器,以便朋友通过控制码查看和远程操作。\n• 服务器仅存储与功能相关的必要数据,不会出售或分享给第三方。\n• 您可以在应用设置中随时停止监督服务,断开与服务器同步。") - } - - section("四、所需权限") { - Text("• 通知:发送就寝提醒与朋友远程消息\n• 网络访问:与服务器同步作息数据、接收朋友远程指令") - } - - Spacer().frame(height: 16) - - VStack(spacing: 4) { - Text("解释权归 JGZ_YES 所有") - .font(.footnote).foregroundColor(.secondary) - Text("(c) 2026 JGZ_YES") - .font(.footnote).foregroundColor(.secondary) - } - .frame(maxWidth: .infinity) + .padding(24) } - .padding(24) + + // 底部按钮 + HStack(spacing: 12) { + Button("不同意") { + exit(0) + } + .buttonStyle(.bordered) + .tint(.secondary) + .controlSize(.large) + + Button("同意并继续") { + store.agreed = true + store.ensureControlCode() + store.save() + Task { + _ = await ApiClient.bind(code: store.controlCode, nickname: store.nickname) + } + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.large) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(.regularMaterial) } - - // 底部按钮 - HStack(spacing: 12) { - Button("不同意") { - exit(0) - } - .buttonStyle(.bordered) - .tint(.secondary) - .controlSize(.large) - - Button("同意并继续") { - store.agreed = true - store.ensureControlCode() - store.save() - Task { - _ = await ApiClient.bind(code: store.controlCode, nickname: store.nickname) - } - } - .buttonStyle(.borderedProminent) - .tint(.blue) - .controlSize(.large) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(.regularMaterial) } } @ViewBuilder - private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { + private func agreementSection(_ title: String, @ViewBuilder content: () -> some View) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title).font(.headline) content().font(.subheadline).foregroundColor(.secondary) diff --git a/AreYouSleeping/Views/ContentView.swift b/AreYouSleeping/Views/ContentView.swift index 8431ab9..2f345b7 100644 --- a/AreYouSleeping/Views/ContentView.swift +++ b/AreYouSleeping/Views/ContentView.swift @@ -3,31 +3,41 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var store: Store @State private var selectedTab = 0 - @State private var settingsRefresh = UUID() var body: some View { - TabView(selection: $selectedTab) { - DashboardView() - .tabItem { - Image(systemName: "house.fill") - Text("仪表盘") - } - .tag(0) + ZStack { + // 渐变背景 + LinearGradient( + colors: [Color.white, Color(red: 0.84, green: 0.84, blue: 0.87)], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() - StatsView() - .tabItem { - Image(systemName: "chart.bar.fill") - Text("统计") - } - .tag(1) + TabView(selection: $selectedTab) { + DashboardView() + .tabItem { + Image(systemName: "house.fill") + Text("仪表盘") + } + .tag(0) - SettingsView() - .tabItem { - Image(systemName: "gearshape.fill") - Text("设置") - } - .tag(2) + StatsView() + .tabItem { + Image(systemName: "chart.bar.fill") + Text("统计") + } + .tag(1) + + SettingsView() + .tabItem { + Image(systemName: "gearshape.fill") + Text("设置") + } + .tag(2) + } + .tint(.blue) + .toolbarBackground(.ultraThinMaterial, for: .tabBar) } - .tint(.blue) } } diff --git a/AreYouSleeping/Views/DashboardView.swift b/AreYouSleeping/Views/DashboardView.swift index fd98a5a..933573c 100644 --- a/AreYouSleeping/Views/DashboardView.swift +++ b/AreYouSleeping/Views/DashboardView.swift @@ -6,79 +6,80 @@ struct DashboardView: View { private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { - NavigationStack { - ScrollView { - VStack(spacing: 20) { + ScrollView { + VStack(spacing: 16) { + // 时钟 + 日期 + VStack(spacing: 4) { + Text(timeString) + .font(.system(size: 56, weight: .thin, design: .rounded)) + Text(dateString) + .font(.subheadline).foregroundColor(.secondary) + } + .padding(.top, 20) - // 时钟 + 日期 - VStack(spacing: 4) { - Text(timeString) - .font(.system(size: 56, weight: .thin, design: .rounded)) - Text(dateString) - .font(.subheadline).foregroundColor(.secondary) - } - .padding(.top, 20) + // 问候语 + Text(greeting) + .font(.title2) - // 问候语 - Text(greeting) - .font(.title2) - - // 倒计时 - VStack(spacing: 8) { - Text(statusTitle) - .font(.headline) - Text(countdownText) - .font(.system(size: 28, weight: .medium)) - .foregroundColor(.blue) - } - .padding() - .frame(maxWidth: .infinity) - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 16)) - - // 睡眠状态卡片 - HStack(spacing: 16) { - statusCard(title: "就寝时间", value: TimeUtil.fmt(store.bedtimeMinutes)) - statusCard(title: "起床时间", value: TimeUtil.fmt(store.waketimeMinutes)) - } - - // 监督状态 - HStack { - Image(systemName: store.supervisionEnabled ? "shield.checkered" : "shield.slash") - .foregroundColor(store.supervisionEnabled ? .green : .red) - Text(store.supervisionEnabled ? "监督运行中" : "监督已关闭") - .font(.subheadline) - } - .padding(.horizontal) - - // 入睡/起床按钮 - if store.isSleeping { - Button { - store.endSleep() - } label: { - Label("我起床了", systemImage: "sunrise.fill") - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(.orange) - } else { - Button { - store.startSleep() - } label: { - Label("我要睡觉了", systemImage: "moon.zzz.fill") - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(.indigo) - } + // 倒计时卡片 + VStack(spacing: 8) { + Text(statusTitle) + .font(.headline) + Text(countdownText) + .font(.system(size: 28, weight: .medium)) + .foregroundColor(.blue) } .padding() + .frame(maxWidth: .infinity) + .glassCard() + + // 就寝/起床时间卡片 + HStack(spacing: 12) { + infoCard(title: "就寝时间", value: TimeUtil.fmt(store.bedtimeMinutes), icon: "moon.fill") + infoCard(title: "起床时间", value: TimeUtil.fmt(store.waketimeMinutes), icon: "sunrise.fill") + } + + // 监督状态 + HStack { + Image(systemName: store.supervisionEnabled ? "shield.checkered" : "shield.slash") + .foregroundColor(store.supervisionEnabled ? .green : .secondary) + Text(store.supervisionEnabled ? "监督运行中" : "监督已关闭") + .font(.subheadline) + } + + // 入睡/起床按钮 + if store.isSleeping { + Button { + store.endSleep() + } label: { + Label("我起床了", systemImage: "sunrise.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.orange) + } else { + Button { + store.startSleep() + } label: { + Label("我要睡觉了", systemImage: "moon.zzz.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.indigo) + } } - .navigationTitle("睡了吗") - .onReceive(timer) { t in now = t } + .padding(.horizontal, 16) + .padding(.bottom, 16) } + .toolbar { + ToolbarItem(placement: .principal) { + Text("睡了吗") + .font(.headline) + } + } + .onReceive(timer) { t in now = t } } private var timeString: String { @@ -121,14 +122,28 @@ struct DashboardView: View { } @ViewBuilder - private func statusCard(title: String, value: String) -> some View { - VStack(spacing: 4) { - Text(title).font(.caption).foregroundColor(.secondary) + private func infoCard(title: String, value: String, icon: String) -> some View { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.title3) + .foregroundColor(.blue) Text(value).font(.title3).bold() + Text(title).font(.caption).foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding() - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .glassCard() + } +} + +// MARK: - 玻璃卡片 Modifier +extension View { + func glassCard(radius: CGFloat = 16) -> some View { + self + .background( + RoundedRectangle(cornerRadius: radius) + .fill(.thinMaterial) + .shadow(color: .black.opacity(0.06), radius: 8, x: 0, y: 2) + ) } } diff --git a/AreYouSleeping/Views/FriendControlView.swift b/AreYouSleeping/Views/FriendControlView.swift index 9468d44..70c91fa 100644 --- a/AreYouSleeping/Views/FriendControlView.swift +++ b/AreYouSleeping/Views/FriendControlView.swift @@ -8,60 +8,95 @@ struct FriendControlView: View { @State private var errorMsg: String? var body: some View { - Form { - Section { - HStack { - TextField("输入朋友的控制码", text: $friendCode) - .disableAutocorrection(true) - .textCase(.uppercase) - Button("查询") { lookupFriend() } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .disabled(friendCode.isEmpty || isLoading) - } - } - - if let info = friendInfo { - Section("朋友信息") { - Text(info).font(.headline) - } - } - - if let error = errorMsg { - Section { Text(error).foregroundColor(.red) } - } - - if friendInfo != nil { - Section("操作") { - Button { triggerFriend("sleep") } label: { - Label("催朋友睡觉", systemImage: "moon.zzz.fill") + ScrollView { + VStack(spacing: 16) { + // 查询卡片 + VStack(spacing: 12) { + HStack { + TextField("输入朋友的控制码", text: $friendCode) + .disableAutocorrection(true) + .textCase(.uppercase) + Button("查询") { lookupFriend() } + .buttonStyle(.borderedProminent) + .controlSize(.small) + .disabled(friendCode.isEmpty || isLoading) } - Button { triggerFriend("wake") } label: { - Label("叫朋友起床", systemImage: "sunrise.fill") + + if isLoading { + ProgressView() + } + + if let info = friendInfo { + Text(info) + .font(.headline) + .foregroundColor(.green) + } + + if let error = errorMsg { + Text(error) + .font(.subheadline) + .foregroundColor(.red) } } + .padding() + .glassCard() + + // 远程控制卡片 + if friendInfo != nil { + VStack(spacing: 8) { + Button { + sendCommand("sleep") + } label: { + Label("提醒入睡", systemImage: "moon.zzz.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.indigo) + + Button { + sendCommand("wake") + } label: { + Label("提醒起床", systemImage: "sunrise.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.orange) + } + } + } + .padding(16) + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("控制朋友").font(.headline) } } - .navigationTitle("控制朋友") - .overlay { if isLoading { ProgressView() } } } private func lookupFriend() { - isLoading = true; errorMsg = nil; friendInfo = nil + isLoading = true + errorMsg = nil + friendInfo = nil Task { - if let res = await ApiClient.friendInfo(code: friendCode) { - friendInfo = (res["nickname"] as? String) ?? friendCode - } else { - errorMsg = "未找到该用户" - } + let result = await ApiClient.query(code: friendCode) isLoading = false + switch result { + case .success(let info): + friendInfo = info + case .failure(let e): + errorMsg = "查询失败:\(e.localizedDescription)" + } } } - private func triggerFriend(_ action: String) { + private func sendCommand(_ cmd: String) { Task { - let res = await ApiClient.friendTrigger(friendCode: friendCode, action: action) - if !res.ok { errorMsg = res.error ?? "操作失败" } + let result = await ApiClient.command(code: friendCode, cmd: cmd) + await MainActor.run { + if case .failure(let e) = result { + errorMsg = "指令失败:\(e.localizedDescription)" + } + } } } } diff --git a/AreYouSleeping/Views/SettingsView.swift b/AreYouSleeping/Views/SettingsView.swift index 8e58c1a..db11178 100644 --- a/AreYouSleeping/Views/SettingsView.swift +++ b/AreYouSleeping/Views/SettingsView.swift @@ -3,101 +3,108 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var store: Store @State private var showAgreement = false - @State private var showStyleSettings = false @State private var showTimePicker = false @State private var isBedPicker = false @State private var pickerDate = Date() var body: some View { NavigationStack { - Form { - // MARK: - 控制码 - Section { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("我的控制码").font(.caption).foregroundColor(.secondary) - Text(store.controlCode.isEmpty ? "----" : store.controlCode) - .font(.title3).bold().foregroundColor(.blue) + ScrollView { + VStack(spacing: 12) { + // 控制码卡片 + cardSection(header: "控制码") { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("我的控制码").font(.caption).foregroundColor(.secondary) + Text(store.controlCode.isEmpty ? "----" : store.controlCode) + .font(.title3).bold().foregroundColor(.blue) + } + Spacer() + if !store.controlCode.isEmpty { + ShareLink(item: shareText) { + Image(systemName: "square.and.arrow.up") + .font(.title3) + } + } + } + Divider() + Toggle("启用控制码功能", isOn: $store.controlCodeEnabled) + .onChange(of: store.controlCodeEnabled) { _ in store.save() } } - Spacer() - if !store.controlCode.isEmpty { - ShareLink(item: shareText) { - Image(systemName: "square.and.arrow.up") + } + + // 作息卡片 + cardSection(header: "作息") { + VStack(spacing: 0) { + timeRow(title: "目标就寝时间", minutes: store.bedtimeMinutes) { + showTimePickerSheet(isBed: true) + } + Divider().padding(.leading, 16) + timeRow(title: "目标起床时间", minutes: store.waketimeMinutes) { + showTimePickerSheet(isBed: false) } } } - Toggle("启用控制码功能", isOn: $store.controlCodeEnabled) - .onChange(of: store.controlCodeEnabled) { _ in store.save() } - } header: { Text("控制码") } - // MARK: - 作息 - Section { - HStack { - Text("目标就寝时间") - Spacer() - Text(TimeUtil.fmt(store.bedtimeMinutes)).foregroundColor(.blue) + // 提醒卡片 + cardSection(header: "提醒", footer: "睡觉时间前发送通知提醒你准备休息") { + Toggle("睡前 30 分钟提醒", isOn: $store.bedtimeReminderEnabled) + .onChange(of: store.bedtimeReminderEnabled) { _ in + store.save() + BedtimeReminder.schedule(store: store) + } } - .contentShape(Rectangle()) - .onTapGesture { showTimePickerSheet(isBed: true) } - HStack { - Text("目标起床时间") - Spacer() - Text(TimeUtil.fmt(store.waketimeMinutes)).foregroundColor(.blue) + // 监督卡片 + cardSection(header: "监督", footer: "就寝时段提醒你放下手机") { + Toggle("监督模式", isOn: $store.supervisionEnabled) + .onChange(of: store.supervisionEnabled) { _ in store.save() } } - .contentShape(Rectangle()) - .onTapGesture { showTimePickerSheet(isBed: false) } - } header: { Text("作息") } - // MARK: - 提醒 - Section { - Toggle("睡前 30 分钟提醒", isOn: $store.bedtimeReminderEnabled) - .onChange(of: store.bedtimeReminderEnabled) { _ in - store.save() - BedtimeReminder.schedule(store: store) + // 后台运行卡片 + cardSection(header: "后台运行", footer: "关闭后应用切到后台将停止服务") { + Toggle("允许后台运行", isOn: $store.backgroundRunning) + .onChange(of: store.backgroundRunning) { _ in store.save() } + } + + // 外观 + cardSection(header: "外观") { + NavigationLink { + StyleSettingsView() + } label: { + HStack { + Text("样式") + Spacer() + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } } - } header: { Text("提醒") } footer: { - Text("睡觉时间前发送通知提醒你准备休息") - } - - // MARK: - 监督 - Section { - Toggle("监督模式", isOn: $store.supervisionEnabled) - .onChange(of: store.supervisionEnabled) { _ in store.save() } - } header: { Text("监督") } footer: { - Text("就寝时段提醒你放下手机") - } - - // MARK: - 后台运行 - Section { - Toggle("允许后台运行", isOn: $store.backgroundRunning) - .onChange(of: store.backgroundRunning) { _ in store.save() } - } header: { Text("后台运行") } footer: { - Text("关闭后应用切到后台将停止服务") - } - - // MARK: - 样式 - Section { - NavigationLink { - StyleSettingsView() - } label: { - Text("样式") + .foregroundColor(.primary) } - } header: { Text("外观") } - // MARK: - 朋友 - Section { - NavigationLink("控制朋友") { - FriendControlView() + // 朋友 + cardSection(header: "朋友") { + NavigationLink("控制朋友") { + FriendControlView() + } + .foregroundColor(.primary) } - } header: { Text("朋友") } - // MARK: - 其他 - Section { - Button("用户协议与隐私说明") { showAgreement = true } + // 其他 + cardSection { + Button("用户协议与隐私说明") { showAgreement = true } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("设置").font(.headline) } } - .navigationTitle("设置") .sheet(isPresented: $showAgreement) { AgreementView() } @@ -108,7 +115,7 @@ struct SettingsView: View { } } - // MARK: - 时间选择器(纯 SwiftUI) + // MARK: - 时间选择器 private var timePickerSheet: some View { VStack(spacing: 16) { HStack { @@ -135,7 +142,6 @@ struct SettingsView: View { .padding(.horizontal) DatePicker("", selection: $pickerDate, displayedComponents: .hourAndMinute) - .datePickerStyle(.wheel) .labelsHidden() } .padding(.top, 12) @@ -161,4 +167,43 @@ struct SettingsView: View { \(webLink) """ } + + // MARK: - 组件 + + @ViewBuilder + private func timeRow(title: String, minutes: Int, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack { + Text(title) + Spacer() + Text(TimeUtil.fmt(minutes)) + .foregroundColor(.blue) + } + .padding(.vertical, 12) + .padding(.horizontal, 16) + .foregroundColor(.primary) + } + } + + @ViewBuilder + private func cardSection(header: String? = nil, footer: String? = nil, @ViewBuilder content: () -> some View) -> some View { + VStack(alignment: .leading, spacing: 4) { + if let h = header { + Text(h) + .font(.caption) + .foregroundColor(.secondary) + .padding(.bottom, 2) + } + content() + .padding(.horizontal, 16) + .padding(.vertical, 8) + .glassCard(radius: 14) + if let f = footer { + Text(f) + .font(.caption2) + .foregroundColor(.secondary) + .padding(.horizontal, 4) + } + } + } } diff --git a/AreYouSleeping/Views/StatsView.swift b/AreYouSleeping/Views/StatsView.swift index 8dea86c..b30aec5 100644 --- a/AreYouSleeping/Views/StatsView.swift +++ b/AreYouSleeping/Views/StatsView.swift @@ -6,32 +6,42 @@ struct StatsView: View { private var records: [SleepRecord] { store.recentRecords(7) } var body: some View { - NavigationStack { - ScrollView { - VStack(spacing: 20) { - // KPI 卡片 - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { - kpiCard(title: "平均睡眠", value: avgHours, unit: "小时", color: .blue) - kpiCard(title: "连续达标", value: "\(streakDays)", unit: "天", color: .green) - kpiCard(title: "准时入睡", value: "\(onTimeCount)/\(max(records.count, 1))", unit: "晚", color: .orange) - kpiCard(title: "达标率", value: String(format: "%.0f%%", goalRate * 100), unit: "", color: .purple) - } + ScrollView { + VStack(spacing: 16) { + // KPI 卡片 + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + StatKpiCard(title: "平均睡眠", value: avgHours, unit: "小时", color: .blue) + StatKpiCard(title: "连续达标", value: "\(streakDays)", unit: "天", color: .green) + StatKpiCard(title: "准时入睡", value: "\(onTimeCount)/\(max(records.count, 1))", unit: "晚", color: .orange) + StatKpiCard(title: "达标率", value: String(format: "%.0f%%", goalRate * 100), unit: "", color: .purple) + } - // 最近记录 - VStack(alignment: .leading, spacing: 8) { - Text("最近 7 晚").font(.headline).padding(.horizontal) - if records.isEmpty { - Text("暂无睡眠记录").foregroundColor(.secondary).padding() - } else { - ForEach(records) { rec in - recordRow(rec) - } + // 最近记录 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("最近 7 晚") + .font(.headline) + Spacer() + } + if records.isEmpty { + Text("暂无睡眠记录") + .foregroundColor(.secondary) + .padding() + .frame(maxWidth: .infinity) + .glassCard() + } else { + ForEach(records) { rec in + RecordRow(rec: rec) } } } - .padding() } - .navigationTitle("统计") + .padding(16) + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("统计").font(.headline) + } } } @@ -56,36 +66,46 @@ struct StatsView: View { private var onTimeCount: Int { records.filter { $0.bedMinutes <= store.bedtimeMinutes + 30 }.count } +} - @ViewBuilder - private func kpiCard(title: String, value: String, unit: String, color: Color) -> some View { +struct StatKpiCard: View { + let title: String + let value: String + let unit: String + let color: Color + + var body: some View { VStack(spacing: 4) { Text(title).font(.caption).foregroundColor(.secondary) HStack(alignment: .lastTextBaseline, spacing: 2) { Text(value).font(.title2).bold().foregroundColor(color) - if !unit.isEmpty { Text(unit).font(.caption).foregroundColor(.secondary) } + if !unit.isEmpty { + Text(unit).font(.caption).foregroundColor(.secondary) + } } } .frame(maxWidth: .infinity) .padding() - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .glassCard(radius: 14) } +} - @ViewBuilder - private func recordRow(_ rec: SleepRecord) -> some View { +struct RecordRow: View { + let rec: SleepRecord + + var body: some View { HStack { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 2) { Text(rec.nightDate).font(.subheadline).bold() Text("\(TimeUtil.fmt(rec.bedMinutes)) → \(TimeUtil.fmt(rec.wakeMinutes))") - .font(.caption).foregroundColor(.secondary) + .font(.caption).foregroundColor(.secondary) } Spacer() Text(String(format: "%.1f 小时", rec.sleepHours)) - .font(.headline).foregroundColor(rec.sleepHours >= 7 ? .green : .orange) + .font(.headline) + .foregroundColor(rec.sleepHours >= 7 ? .green : .orange) } .padding() - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .glassCard(radius: 14) } } diff --git a/AreYouSleeping/Views/StyleSettingsView.swift b/AreYouSleeping/Views/StyleSettingsView.swift index 2b07b70..a2ece15 100644 --- a/AreYouSleeping/Views/StyleSettingsView.swift +++ b/AreYouSleeping/Views/StyleSettingsView.swift @@ -4,60 +4,97 @@ struct StyleSettingsView: View { @EnvironmentObject var store: Store var body: some View { - Form { - Section { - Toggle("暗色模式", isOn: $store.darkMode) - .onChange(of: store.darkMode) { _ in store.save() } - } + ScrollView { + VStack(spacing: 12) { + // 暗色模式 + styleCard { + Toggle("暗色模式", isOn: $store.darkMode) + .onChange(of: store.darkMode) { _ in store.save() } + } header: { Text("主题") } - Section { - Picker("导航栏样式", selection: $store.navStyle) { - Text("默认").tag(0) - Text("无边框").tag(1) - } - .pickerStyle(.segmented) - .onChange(of: store.navStyle) { _ in store.save() } - } header: { Text("导航栏") } - - Section { - Picker("字体风格", selection: $store.fontStyle) { - Text("默认").tag(0) - Text("圆体").tag(1) - Text("等宽").tag(2) - } - .pickerStyle(.segmented) - .onChange(of: store.fontStyle) { _ in store.save() } - - VStack { - HStack { - Text("字体大小: \(String(format: "%.0f%%", store.fontScale * 100))") - Spacer() - Button("重置") { store.fontScale = 1.0; store.save() } - .buttonStyle(.borderless) + // 导航栏 + styleCard { + VStack(spacing: 8) { + Picker("导航栏样式", selection: $store.navStyle) { + Text("默认").tag(0) + Text("无边框").tag(1) + } + .pickerStyle(.segmented) + .onChange(of: store.navStyle) { _ in store.save() } } - Slider(value: $store.fontScale, in: 0.85...1.30, step: 0.05) - .onChange(of: store.fontScale) { _ in store.save() } - } - } header: { Text("字体") } + } header: { Text("导航栏") } - Section { - Picker("卡片圆角", selection: $store.cardRadius) { - Text("小 (4)").tag(4) - Text("中 (18)").tag(18) - Text("大 (28)").tag(28) - } - .onChange(of: store.cardRadius) { _ in store.save() } - } header: { Text("卡片") } + // 字体 + styleCard { + VStack(spacing: 12) { + Picker("字体风格", selection: $store.fontStyle) { + Text("默认").tag(0) + Text("圆体").tag(1) + Text("等宽").tag(2) + } + .pickerStyle(.segmented) + .onChange(of: store.fontStyle) { _ in store.save() } - Section { - Picker("模糊强度", selection: $store.blurStrength) { - Text("无").tag(0) - Text("轻微").tag(1) - Text("强烈").tag(2) - } - .onChange(of: store.blurStrength) { _ in store.save() } - } header: { Text("模糊") } + VStack(spacing: 4) { + HStack { + Text("字体大小: \(String(format: "%.0f%%", store.fontScale * 100))") + .font(.subheadline) + Spacer() + Button("重置") { store.fontScale = 1.0; store.save() } + .buttonStyle(.borderless) + .font(.caption) + } + Slider(value: $store.fontScale, in: 0.85...1.30, step: 0.05) + .onChange(of: store.fontScale) { _ in store.save() } + } + } + } header: { Text("字体") } + + // 卡片 + styleCard { + Picker("卡片圆角", selection: $store.cardRadius) { + Text("小 (4)").tag(4) + Text("中 (18)").tag(18) + Text("大 (28)").tag(28) + } + .pickerStyle(.segmented) + .onChange(of: store.cardRadius) { _ in store.save() } + } header: { Text("卡片") } + + // 模糊 + styleCard { + Picker("模糊强度", selection: $store.blurStrength) { + Text("无").tag(0) + Text("轻微").tag(1) + Text("强烈").tag(2) + } + .pickerStyle(.segmented) + .onChange(of: store.blurStrength) { _ in store.save() } + } header: { Text("模糊") } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("样式设置").font(.headline) + } + } + } + + @ViewBuilder + private func styleCard( + @ViewBuilder content: () -> Content, + header: () -> Header + ) -> some View { + VStack(alignment: .leading, spacing: 4) { + header() + .font(.caption) + .foregroundColor(.secondary) + content() + .padding(.horizontal, 16) + .padding(.vertical, 8) + .glassCard(radius: 14) } - .navigationTitle("样式设置") } }