全面重写 iOS 所有页面样式:渐变背景 + 玻璃卡片(.thinMaterial)+ 圆角阴影,匹配 Android 版视觉效果

这个提交包含在:
JGZ_YES
2026-08-12 13:50:20 +08:00
父节点 1dc00c9645
当前提交 e85b0dbde5
修改 7 个文件,包含 520 行新增349 行删除
+67 -58
查看文件
@@ -4,73 +4,82 @@ struct AgreementView: View {
@EnvironmentObject var store: Store @EnvironmentObject var store: Store
var body: some View { var body: some View {
VStack(spacing: 0) { ZStack {
ScrollView { LinearGradient(
VStack(alignment: .leading, spacing: 16) { colors: [Color.white, Color(red: 0.84, green: 0.84, blue: 0.87)],
Text("用户协议与隐私说明") startPoint: .top,
.font(.title).bold() endPoint: .bottom
)
.ignoresSafeArea()
Text("欢迎使用「睡了吗」。\n\n本协议说明应用功能与数据使用方式,请仔细阅读。") VStack(spacing: 0) {
.font(.body).foregroundColor(.secondary) ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("用户协议与隐私说明")
.font(.title).bold()
section("一、应用功能") { Text("欢迎使用「睡了吗」。\n\n本协议说明应用功能与数据使用方式,请仔细阅读。")
Text("1. 睡眠监督:在您设定的就寝时段,提醒您按时入睡。\n(注:iOS 系统限制,本应用无法直接屏蔽其他应用,仅提供通知提醒。)\n2. 作息记录:记录您的入睡与起床时间,生成睡眠统计。\n3. 控制码功能(见第二条)。") .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)
} }
.padding(24)
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)
//
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 @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) { VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline) Text(title).font(.headline)
content().font(.subheadline).foregroundColor(.secondary) content().font(.subheadline).foregroundColor(.secondary)
+31 -21
查看文件
@@ -3,31 +3,41 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@EnvironmentObject var store: Store @EnvironmentObject var store: Store
@State private var selectedTab = 0 @State private var selectedTab = 0
@State private var settingsRefresh = UUID()
var body: some View { var body: some View {
TabView(selection: $selectedTab) { ZStack {
DashboardView() //
.tabItem { LinearGradient(
Image(systemName: "house.fill") colors: [Color.white, Color(red: 0.84, green: 0.84, blue: 0.87)],
Text("仪表盘") startPoint: .top,
} endPoint: .bottom
.tag(0) )
.ignoresSafeArea()
StatsView() TabView(selection: $selectedTab) {
.tabItem { DashboardView()
Image(systemName: "chart.bar.fill") .tabItem {
Text("统计") Image(systemName: "house.fill")
} Text("仪表盘")
.tag(1) }
.tag(0)
SettingsView() StatsView()
.tabItem { .tabItem {
Image(systemName: "gearshape.fill") Image(systemName: "chart.bar.fill")
Text("设置") Text("统计")
} }
.tag(2) .tag(1)
SettingsView()
.tabItem {
Image(systemName: "gearshape.fill")
Text("设置")
}
.tag(2)
}
.tint(.blue)
.toolbarBackground(.ultraThinMaterial, for: .tabBar)
} }
.tint(.blue)
} }
} }
+87 -72
查看文件
@@ -6,79 +6,80 @@ struct DashboardView: View {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View { var body: some View {
NavigationStack { ScrollView {
ScrollView { VStack(spacing: 16) {
VStack(spacing: 20) { // +
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(greeting)
Text(timeString) .font(.title2)
.font(.system(size: 56, weight: .thin, design: .rounded))
Text(dateString)
.font(.subheadline).foregroundColor(.secondary)
}
.padding(.top, 20)
// //
Text(greeting) VStack(spacing: 8) {
.font(.title2) Text(statusTitle)
.font(.headline)
// Text(countdownText)
VStack(spacing: 8) { .font(.system(size: 28, weight: .medium))
Text(statusTitle) .foregroundColor(.blue)
.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)
}
} }
.padding() .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("睡了吗") .padding(.horizontal, 16)
.onReceive(timer) { t in now = t } .padding(.bottom, 16)
} }
.toolbar {
ToolbarItem(placement: .principal) {
Text("睡了吗")
.font(.headline)
}
}
.onReceive(timer) { t in now = t }
} }
private var timeString: String { private var timeString: String {
@@ -121,14 +122,28 @@ struct DashboardView: View {
} }
@ViewBuilder @ViewBuilder
private func statusCard(title: String, value: String) -> some View { private func infoCard(title: String, value: String, icon: String) -> some View {
VStack(spacing: 4) { VStack(spacing: 6) {
Text(title).font(.caption).foregroundColor(.secondary) Image(systemName: icon)
.font(.title3)
.foregroundColor(.blue)
Text(value).font(.title3).bold() Text(value).font(.title3).bold()
Text(title).font(.caption).foregroundColor(.secondary)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding() .padding()
.background(.regularMaterial) .glassCard()
.clipShape(RoundedRectangle(cornerRadius: 12)) }
}
// 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)
)
} }
} }
+75 -40
查看文件
@@ -8,60 +8,95 @@ struct FriendControlView: View {
@State private var errorMsg: String? @State private var errorMsg: String?
var body: some View { var body: some View {
Form { ScrollView {
Section { VStack(spacing: 16) {
HStack { //
TextField("输入朋友的控制码", text: $friendCode) VStack(spacing: 12) {
.disableAutocorrection(true) HStack {
.textCase(.uppercase) TextField("输入朋友的控制码", text: $friendCode)
Button("查询") { lookupFriend() } .disableAutocorrection(true)
.buttonStyle(.borderedProminent) .textCase(.uppercase)
.controlSize(.small) Button("查询") { lookupFriend() }
.disabled(friendCode.isEmpty || isLoading) .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")
} }
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() { private func lookupFriend() {
isLoading = true; errorMsg = nil; friendInfo = nil isLoading = true
errorMsg = nil
friendInfo = nil
Task { Task {
if let res = await ApiClient.friendInfo(code: friendCode) { let result = await ApiClient.query(code: friendCode)
friendInfo = (res["nickname"] as? String) ?? friendCode
} else {
errorMsg = "未找到该用户"
}
isLoading = false 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 { Task {
let res = await ApiClient.friendTrigger(friendCode: friendCode, action: action) let result = await ApiClient.command(code: friendCode, cmd: cmd)
if !res.ok { errorMsg = res.error ?? "操作失败" } await MainActor.run {
if case .failure(let e) = result {
errorMsg = "指令失败:\(e.localizedDescription)"
}
}
} }
} }
} }
+120 -75
查看文件
@@ -3,101 +3,108 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var store: Store @EnvironmentObject var store: Store
@State private var showAgreement = false @State private var showAgreement = false
@State private var showStyleSettings = false
@State private var showTimePicker = false @State private var showTimePicker = false
@State private var isBedPicker = false @State private var isBedPicker = false
@State private var pickerDate = Date() @State private var pickerDate = Date()
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { ScrollView {
// MARK: - VStack(spacing: 12) {
Section { //
HStack { cardSection(header: "控制码") {
VStack(alignment: .leading, spacing: 4) { VStack(spacing: 12) {
Text("我的控制码").font(.caption).foregroundColor(.secondary) HStack {
Text(store.controlCode.isEmpty ? "----" : store.controlCode) VStack(alignment: .leading, spacing: 4) {
.font(.title3).bold().foregroundColor(.blue) 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 { cardSection(header: "提醒", footer: "睡觉时间前发送通知提醒你准备休息") {
HStack { Toggle("睡前 30 分钟提醒", isOn: $store.bedtimeReminderEnabled)
Text("目标就寝时间") .onChange(of: store.bedtimeReminderEnabled) { _ in
Spacer() store.save()
Text(TimeUtil.fmt(store.bedtimeMinutes)).foregroundColor(.blue) BedtimeReminder.schedule(store: store)
}
} }
.contentShape(Rectangle())
.onTapGesture { showTimePickerSheet(isBed: true) }
HStack { //
Text("目标起床时间") cardSection(header: "监督", footer: "就寝时段提醒你放下手机") {
Spacer() Toggle("监督模式", isOn: $store.supervisionEnabled)
Text(TimeUtil.fmt(store.waketimeMinutes)).foregroundColor(.blue) .onChange(of: store.supervisionEnabled) { _ in store.save() }
} }
.contentShape(Rectangle())
.onTapGesture { showTimePickerSheet(isBed: false) }
} header: { Text("作息") }
// MARK: - //
Section { cardSection(header: "后台运行", footer: "关闭后应用切到后台将停止服务") {
Toggle("睡前 30 分钟提醒", isOn: $store.bedtimeReminderEnabled) Toggle("允许后台运行", isOn: $store.backgroundRunning)
.onChange(of: store.bedtimeReminderEnabled) { _ in .onChange(of: store.backgroundRunning) { _ in store.save() }
store.save() }
BedtimeReminder.schedule(store: store)
//
cardSection(header: "外观") {
NavigationLink {
StyleSettingsView()
} label: {
HStack {
Text("样式")
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
} }
} header: { Text("提醒") } footer: { .foregroundColor(.primary)
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("样式")
} }
} header: { Text("外观") }
// MARK: - //
Section { cardSection(header: "朋友") {
NavigationLink("控制朋友") { NavigationLink("控制朋友") {
FriendControlView() FriendControlView()
}
.foregroundColor(.primary)
} }
} header: { Text("朋友") }
// MARK: - //
Section { cardSection {
Button("用户协议与隐私说明") { showAgreement = true } Button("用户协议与隐私说明") { showAgreement = true }
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("设置").font(.headline)
} }
} }
.navigationTitle("设置")
.sheet(isPresented: $showAgreement) { .sheet(isPresented: $showAgreement) {
AgreementView() AgreementView()
} }
@@ -108,7 +115,7 @@ struct SettingsView: View {
} }
} }
// MARK: - SwiftUI // MARK: -
private var timePickerSheet: some View { private var timePickerSheet: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
HStack { HStack {
@@ -135,7 +142,6 @@ struct SettingsView: View {
.padding(.horizontal) .padding(.horizontal)
DatePicker("", selection: $pickerDate, displayedComponents: .hourAndMinute) DatePicker("", selection: $pickerDate, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
.labelsHidden() .labelsHidden()
} }
.padding(.top, 12) .padding(.top, 12)
@@ -161,4 +167,43 @@ struct SettingsView: View {
\(webLink) \(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)
}
}
}
} }
+53 -33
查看文件
@@ -6,32 +6,42 @@ struct StatsView: View {
private var records: [SleepRecord] { store.recentRecords(7) } private var records: [SleepRecord] { store.recentRecords(7) }
var body: some View { var body: some View {
NavigationStack { ScrollView {
ScrollView { VStack(spacing: 16) {
VStack(spacing: 20) { // KPI
// KPI LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { StatKpiCard(title: "平均睡眠", value: avgHours, unit: "小时", color: .blue)
kpiCard(title: "平均睡眠", value: avgHours, unit: "小时", color: .blue) StatKpiCard(title: "连续达标", value: "\(streakDays)", unit: "", color: .green)
kpiCard(title: "连续达标", value: "\(streakDays)", unit: "", color: .green) StatKpiCard(title: "准时入睡", value: "\(onTimeCount)/\(max(records.count, 1))", unit: "", color: .orange)
kpiCard(title: "准时入睡", value: "\(onTimeCount)/\(max(records.count, 1))", unit: "", color: .orange) StatKpiCard(title: "达标率", value: String(format: "%.0f%%", goalRate * 100), unit: "", color: .purple)
kpiCard(title: "达标率", value: String(format: "%.0f%%", goalRate * 100), unit: "", color: .purple) }
}
// //
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("最近 7 晚").font(.headline).padding(.horizontal) HStack {
if records.isEmpty { Text("最近 7 晚")
Text("暂无睡眠记录").foregroundColor(.secondary).padding() .font(.headline)
} else { Spacer()
ForEach(records) { rec in }
recordRow(rec) 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 { private var onTimeCount: Int {
records.filter { $0.bedMinutes <= store.bedtimeMinutes + 30 }.count records.filter { $0.bedMinutes <= store.bedtimeMinutes + 30 }.count
} }
}
@ViewBuilder struct StatKpiCard: View {
private func kpiCard(title: String, value: String, unit: String, color: Color) -> some View { let title: String
let value: String
let unit: String
let color: Color
var body: some View {
VStack(spacing: 4) { VStack(spacing: 4) {
Text(title).font(.caption).foregroundColor(.secondary) Text(title).font(.caption).foregroundColor(.secondary)
HStack(alignment: .lastTextBaseline, spacing: 2) { HStack(alignment: .lastTextBaseline, spacing: 2) {
Text(value).font(.title2).bold().foregroundColor(color) 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) .frame(maxWidth: .infinity)
.padding() .padding()
.background(.regularMaterial) .glassCard(radius: 14)
.clipShape(RoundedRectangle(cornerRadius: 12))
} }
}
@ViewBuilder struct RecordRow: View {
private func recordRow(_ rec: SleepRecord) -> some View { let rec: SleepRecord
var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading, spacing: 2) {
Text(rec.nightDate).font(.subheadline).bold() Text(rec.nightDate).font(.subheadline).bold()
Text("\(TimeUtil.fmt(rec.bedMinutes))\(TimeUtil.fmt(rec.wakeMinutes))") Text("\(TimeUtil.fmt(rec.bedMinutes))\(TimeUtil.fmt(rec.wakeMinutes))")
.font(.caption).foregroundColor(.secondary) .font(.caption).foregroundColor(.secondary)
} }
Spacer() Spacer()
Text(String(format: "%.1f 小时", rec.sleepHours)) Text(String(format: "%.1f 小时", rec.sleepHours))
.font(.headline).foregroundColor(rec.sleepHours >= 7 ? .green : .orange) .font(.headline)
.foregroundColor(rec.sleepHours >= 7 ? .green : .orange)
} }
.padding() .padding()
.background(.regularMaterial) .glassCard(radius: 14)
.clipShape(RoundedRectangle(cornerRadius: 12))
} }
} }
+87 -50
查看文件
@@ -4,60 +4,97 @@ struct StyleSettingsView: View {
@EnvironmentObject var store: Store @EnvironmentObject var store: Store
var body: some View { var body: some View {
Form { ScrollView {
Section { VStack(spacing: 12) {
Toggle("暗色模式", isOn: $store.darkMode) //
.onChange(of: store.darkMode) { _ in store.save() } styleCard {
} Toggle("暗色模式", isOn: $store.darkMode)
.onChange(of: store.darkMode) { _ in store.save() }
} header: { Text("主题") }
Section { //
Picker("导航栏样式", selection: $store.navStyle) { styleCard {
Text("默认").tag(0) VStack(spacing: 8) {
Text("无边框").tag(1) Picker("导航栏样式", selection: $store.navStyle) {
} Text("默认").tag(0)
.pickerStyle(.segmented) Text("无边框").tag(1)
.onChange(of: store.navStyle) { _ in store.save() } }
} header: { Text("导航栏") } .pickerStyle(.segmented)
.onChange(of: store.navStyle) { _ in store.save() }
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)
} }
Slider(value: $store.fontScale, in: 0.85...1.30, step: 0.05) } header: { Text("导航栏") }
.onChange(of: store.fontScale) { _ in store.save() }
}
} header: { Text("字体") }
Section { //
Picker("卡片圆角", selection: $store.cardRadius) { styleCard {
Text("小 (4)").tag(4) VStack(spacing: 12) {
Text("中 (18)").tag(18) Picker("字体风格", selection: $store.fontStyle) {
Text("大 (28)").tag(28) Text("默认").tag(0)
} Text("圆体").tag(1)
.onChange(of: store.cardRadius) { _ in store.save() } Text("等宽").tag(2)
} header: { Text("卡片") } }
.pickerStyle(.segmented)
.onChange(of: store.fontStyle) { _ in store.save() }
Section { VStack(spacing: 4) {
Picker("模糊强度", selection: $store.blurStrength) { HStack {
Text("").tag(0) Text("字体大小: \(String(format: "%.0f%%", store.fontScale * 100))")
Text("轻微").tag(1) .font(.subheadline)
Text("强烈").tag(2) Spacer()
} Button("重置") { store.fontScale = 1.0; store.save() }
.onChange(of: store.blurStrength) { _ in store.save() } .buttonStyle(.borderless)
} header: { Text("模糊") } .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<Content: View, Header: View>(
@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("样式设置")
} }
} }