From ab2d843d2d451696847013e8a7abfa3345c42dce Mon Sep 17 00:00:00 2001 From: qiangxue Date: Sun, 10 May 2026 15:03:27 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(AlarmListView):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=97=B9=E9=92=9F=E6=98=BE=E7=A4=BA=E6=8E=92=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整时间字体大小从68到60 - 添加重复规则显示 - 调整标签字体大小和间距 --- Alarm/Views/AlarmListView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Alarm/Views/AlarmListView.swift b/Alarm/Views/AlarmListView.swift index f6b8bbc..f13e005 100644 --- a/Alarm/Views/AlarmListView.swift +++ b/Alarm/Views/AlarmListView.swift @@ -140,14 +140,18 @@ struct AlarmListView: View { } } - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 4) { Text(item.time.alarmTimeText()) - .font(.system(size: 68, weight: .light)) + .font(.system(size: 60, weight: .light)) .foregroundStyle(item.isEnabled ? Color(white: 0.1) : Color(white: 0.6)) Text(item.label) - .font(.system(size: 17, weight: .regular)) + .font(.system(size: 15, weight: .regular)) .foregroundStyle(Color(white: 0.5)) + + Text(item.repeatRule.displayText) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(Color(white: 0.6)) } Spacer() From 534fd3aa2545dbf34c46cb4e3a2aaebf2923e70a Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 10 May 2026 15:07:17 +0800 Subject: [PATCH 2/5] Improve alarm list guidance --- Alarm/Views/AlarmListView.swift | 39 ++++++++++++++++++++++++++++----- README.md | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 README.md diff --git a/Alarm/Views/AlarmListView.swift b/Alarm/Views/AlarmListView.swift index f13e005..171f97b 100644 --- a/Alarm/Views/AlarmListView.swift +++ b/Alarm/Views/AlarmListView.swift @@ -113,10 +113,7 @@ struct AlarmListView: View { .overlay(Color(white: 0.84)) if store.alarms.isEmpty { - Text("暂无闹钟") - .font(.system(size: 17, weight: .regular)) - .foregroundStyle(Color(white: 0.5)) - .padding(.vertical, 10) + emptyStateCard } else { ForEach(displayAlarms) { item in alarmRow(item) @@ -148,10 +145,15 @@ struct AlarmListView: View { Text(item.label) .font(.system(size: 15, weight: .regular)) .foregroundStyle(Color(white: 0.5)) - - Text(item.repeatRule.displayText) + + Text(repeatSummary(for: item)) .font(.system(size: 13, weight: .regular)) .foregroundStyle(Color(white: 0.6)) + + Text(store.nextTriggerText(for: item)) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(Color(white: 0.42)) + .lineLimit(2) } Spacer() @@ -184,6 +186,31 @@ struct AlarmListView: View { private var enabledAlarmCount: Int { store.alarms.filter(\.isEnabled).count } + + private var emptyStateCard: some View { + VStack(alignment: .leading, spacing: 8) { + Text("暂无闹钟") + .font(.system(size: 20, weight: .semibold)) + .foregroundStyle(Color(white: 0.18)) + + Text("点击右上角 + 创建第一个闹钟,列表会在这里显示重复规则和下一次提醒。") + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(Color(white: 0.48)) + .fixedSize(horizontal: false, vertical: true) + } + .padding(18) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 22) + .fill(Color.white) + ) + .padding(.top, 6) + } + + private func repeatSummary(for item: AlarmItem) -> String { + let holidayText = item.skipHolidayEnabled ? "节假日跳过" : "节假日照常" + return "\(item.repeatRule.displayText) · \(holidayText)" + } } #Preview { diff --git a/README.md b/README.md new file mode 100644 index 0000000..834c23c --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Alarm + +一个基于 SwiftUI 的本地闹钟原型,当前重点覆盖工作日重复、节假日跳过、调休补班和基础计时能力。 + +## 当前分支说明 + +当前分支 `Codex/optimize-alarm-display` 侧重闹钟列表的显示优化: + +- 强化空状态提示,首次打开时更清楚地引导创建闹钟 +- 在列表中同时展示重复规则、节假日处理方式和下一次提醒时间 +- 保持现有交互结构不变,只做轻量视觉与信息层级调整 + +## 主要能力 + +- 新增、编辑、删除闹钟 +- 每天、工作日、自定义重复 +- 节假日跳过与调休工作日识别 +- 稍后提醒开关与时长设置 +- 秒表与计时器基础页面 + +## 项目结构 + +- `Alarm/Views`: 闹钟列表、编辑页、节假日设置、秒表、计时器 +- `Alarm/Services`: 存储、调度、节假日计算 +- `Alarm/Models`: 闹钟、应用设置、节假日数据模型 +- `Alarm/Utilities`: 日期格式化辅助方法 + +## 运行说明 + +1. 使用 Xcode 打开 `Alarm.xcodeproj` +2. 选择 `Alarm` scheme +3. 在模拟器或真机运行 + +## 已知限制 + +- 当前无单元测试 target +- `AlarmKit` 可用性依赖签名能力与设备环境 +- 节假日相关结果依赖本地日历和配置数据 From 692a59a578662c469eb479529f1dd480e9f4dddf Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 10 May 2026 15:09:09 +0800 Subject: [PATCH 3/5] Expand README for recent collaborator updates --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 834c23c..ed5d06d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,16 @@ - 在列表中同时展示重复规则、节假日处理方式和下一次提醒时间 - 保持现有交互结构不变,只做轻量视觉与信息层级调整 +## 近期协作更新 + +最近几轮由不同提交者补充的内容,当前分支已经包含: + +- 闹钟列表排版优化:补充启用数量、重复规则摘要和更清晰的信息层级 +- 闹钟编辑页样式统一:列表页与编辑页的视觉风格已基本对齐 +- 重复规则交互增强:支持每天、工作日、自定义日期组合,并优化选择流程 +- 节假日配置能力:可配置法定节假日、调休工作日、自定义跳过日期和自定义工作日 +- 秒表与计时器基础体验:已提供基础页面,秒表含空状态和运行状态文案 + ## 主要能力 - 新增、编辑、删除闹钟 @@ -18,6 +28,13 @@ - 稍后提醒开关与时长设置 - 秒表与计时器基础页面 +## 页面说明 + +- `闹钟列表`: 查看全部闹钟、启用状态、重复规则、节假日策略和下一次提醒 +- `闹钟编辑`: 调整时间、标签、铃声、重复规则、稍后提醒和节假日跳过开关 +- `节假日配置`: 维护法定节假日、调休工作日和自定义日期 +- `秒表 / 计时器`: 提供基础计时功能,用于后续继续补强 + ## 项目结构 - `Alarm/Views`: 闹钟列表、编辑页、节假日设置、秒表、计时器 @@ -36,3 +53,9 @@ - 当前无单元测试 target - `AlarmKit` 可用性依赖签名能力与设备环境 - 节假日相关结果依赖本地日历和配置数据 + +## 后续可补强方向 + +- 为闹钟触发时间计算补单元测试,重点覆盖工作日、节假日和调休交叉场景 +- 为列表和编辑页补首次授权与失败提示说明 +- 继续完善秒表和计时器在后台恢复、完成提醒等细节体验 From 67546ccaa83821fe2a12e63c48eef0696599395b Mon Sep 17 00:00:00 2001 From: qiangxue Date: Sun, 10 May 2026 15:21:27 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(AlarmListView,=20AlarmEditView,=20?= =?UTF-8?q?ContentView):=20=E4=BC=98=E5=8C=96=E9=97=B9=E9=92=9F=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=8E=92=E7=89=88=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为闹钟项添加卡片样式(白色背景、圆角、阴影) - 将标签和重复规则放在同一行,节省垂直空间 - 编辑模式删除按钮添加确认对话框 - 统一AlarmEditView设置卡片行高 - 为展开/折叠的“稍后提醒时长”添加动画过渡 - 为TabView选中状态添加更明显的视觉反馈 - 为禁用状态的闹钟添加更明显的视觉区分 --- Alarm/ContentView.swift | 1 + Alarm/Views/AlarmEditView.swift | 2 ++ Alarm/Views/AlarmListView.swift | 48 ++++++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/Alarm/ContentView.swift b/Alarm/ContentView.swift index 910f845..f24d33c 100644 --- a/Alarm/ContentView.swift +++ b/Alarm/ContentView.swift @@ -25,6 +25,7 @@ struct ContentView: View { Label("计时器", systemImage: "timer") } } + .tint(.orange) .preferredColorScheme(.light) } } diff --git a/Alarm/Views/AlarmEditView.swift b/Alarm/Views/AlarmEditView.swift index e11f183..5bb0cc4 100644 --- a/Alarm/Views/AlarmEditView.swift +++ b/Alarm/Views/AlarmEditView.swift @@ -179,6 +179,8 @@ struct AlarmEditView: View { } .pickerStyle(.wheel) .frame(height: 160) + .transition(.opacity) + .animation(.easeInOut(duration: 0.2), value: showSnoozePicker) } divider diff --git a/Alarm/Views/AlarmListView.swift b/Alarm/Views/AlarmListView.swift index 171f97b..55dae12 100644 --- a/Alarm/Views/AlarmListView.swift +++ b/Alarm/Views/AlarmListView.swift @@ -6,6 +6,7 @@ struct AlarmListView: View { @State private var isPresentingAdd = false @State private var editingItem: AlarmItem? @State private var isEditing = false + @State private var itemToDelete: AlarmItem? var body: some View { NavigationStack { @@ -54,6 +55,22 @@ struct AlarmListView: View { } ) } + .alert("确认删除", isPresented: Binding( + get: { itemToDelete != nil }, + set: { if !$0 { itemToDelete = nil } } + )) { + Button("删除", role: .destructive) { + if let item = itemToDelete { + store.removeAlarm(id: item.id) + itemToDelete = nil + } + } + Button("取消", role: .cancel) { + itemToDelete = nil + } + } message: { + Text("确定要删除这个闹钟吗?") + } } .preferredColorScheme(.light) } @@ -117,8 +134,6 @@ struct AlarmListView: View { } else { ForEach(displayAlarms) { item in alarmRow(item) - Divider() - .overlay(Color(white: 0.84)) } } } @@ -129,7 +144,7 @@ struct AlarmListView: View { HStack(spacing: 10) { if isEditing { Button { - store.removeAlarm(id: item.id) + itemToDelete = item } label: { Image(systemName: "minus.circle.fill") .font(.system(size: 22)) @@ -142,13 +157,19 @@ struct AlarmListView: View { .font(.system(size: 60, weight: .light)) .foregroundStyle(item.isEnabled ? Color(white: 0.1) : Color(white: 0.6)) - Text(item.label) - .font(.system(size: 15, weight: .regular)) - .foregroundStyle(Color(white: 0.5)) - - Text(repeatSummary(for: item)) - .font(.system(size: 13, weight: .regular)) - .foregroundStyle(Color(white: 0.6)) + HStack(spacing: 4) { + Text(item.label) + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(Color(white: 0.5)) + + Text("·") + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(Color(white: 0.6)) + + Text(repeatSummary(for: item)) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(Color(white: 0.6)) + } Text(store.nextTriggerText(for: item)) .font(.system(size: 13, weight: .regular)) @@ -166,6 +187,13 @@ struct AlarmListView: View { .toggleStyle(.switch) .onTapGesture {} } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.white) + .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 2) + ) + .opacity(item.isEnabled ? 1.0 : 0.6) .contentShape(Rectangle()) .onTapGesture { guard !isEditing else { return } From 95a6d8137bc1a6a1785fc9b3fd17f76af7edb768 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 10 May 2026 18:19:09 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(alarm-list):=20=E4=BC=98=E5=85=88?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E6=9C=80=E8=BF=91=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Alarm/Services/AlarmStore.swift | 6 +- Alarm/Views/AlarmListView.swift | 97 +++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/Alarm/Services/AlarmStore.swift b/Alarm/Services/AlarmStore.swift index f2c8ec0..036d5af 100644 --- a/Alarm/Services/AlarmStore.swift +++ b/Alarm/Services/AlarmStore.swift @@ -112,13 +112,17 @@ final class AlarmStore: ObservableObject { func nextTriggerText(for item: AlarmItem) -> String { guard item.isEnabled else { return "已关闭" } - guard let nextDate = nextTriggerDate(for: item, from: Date()) else { + guard let nextDate = nextTriggerDate(for: item) else { return "暂无下次提醒" } return "下次:\(nextDate.alarmDisplayText()) · 节假日跳过 \(item.skipHolidayEnabled ? "🟢" : "🔴")" } + func nextTriggerDate(for item: AlarmItem) -> Date? { + nextTriggerDate(for: item, from: Date()) + } + private func rescheduleEnabledAlarms() async { for item in alarms where item.isEnabled { await scheduleIfNeeded(item) diff --git a/Alarm/Views/AlarmListView.swift b/Alarm/Views/AlarmListView.swift index 55dae12..ae14b95 100644 --- a/Alarm/Views/AlarmListView.swift +++ b/Alarm/Views/AlarmListView.swift @@ -132,6 +132,12 @@ struct AlarmListView: View { if store.alarms.isEmpty { emptyStateCard } else { + if let upcomingAlarm = upcomingAlarm { + upcomingAlarmCard(for: upcomingAlarm) + .padding(.top, 6) + .padding(.bottom, 4) + } + ForEach(displayAlarms) { item in alarmRow(item) } @@ -203,11 +209,27 @@ struct AlarmListView: View { private var displayAlarms: [AlarmItem] { store.alarms.sorted { - let lhs = Calendar.current.dateComponents([.hour, .minute], from: $0.time) - let rhs = Calendar.current.dateComponents([.hour, .minute], from: $1.time) - let lhsValue = (lhs.hour ?? 0) * 60 + (lhs.minute ?? 0) - let rhsValue = (rhs.hour ?? 0) * 60 + (rhs.minute ?? 0) - return lhsValue < rhsValue + if $0.isEnabled != $1.isEnabled { + return $0.isEnabled && !$1.isEnabled + } + + let lhsNext = store.nextTriggerDate(for: $0) + let rhsNext = store.nextTriggerDate(for: $1) + + switch (lhsNext, rhsNext) { + case let (lhsDate?, rhsDate?): + if lhsDate != rhsDate { + return lhsDate < rhsDate + } + case (_?, nil): + return true + case (nil, _?): + return false + case (nil, nil): + break + } + + return alarmTimeValue(for: $0) < alarmTimeValue(for: $1) } } @@ -215,6 +237,17 @@ struct AlarmListView: View { store.alarms.filter(\.isEnabled).count } + private var upcomingAlarm: AlarmItem? { + store.alarms + .filter(\.isEnabled) + .compactMap { item in + guard let nextDate = store.nextTriggerDate(for: item) else { return nil } + return (item, nextDate) + } + .min { $0.1 < $1.1 }? + .0 + } + private var emptyStateCard: some View { VStack(alignment: .leading, spacing: 8) { Text("暂无闹钟") @@ -235,10 +268,64 @@ struct AlarmListView: View { .padding(.top, 6) } + private func upcomingAlarmCard(for item: AlarmItem) -> some View { + let nextDate = store.nextTriggerDate(for: item) + let title = item.label == "闹钟" ? "最近提醒" : "最近提醒 · \(item.label)" + + return VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(Color.orange) + + Text(item.time.alarmTimeText()) + .font(.system(size: 40, weight: .light)) + .foregroundStyle(Color(white: 0.12)) + + if let nextDate { + Text("预计在 \(nextDate.alarmDisplayText()) 响铃") + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(Color(white: 0.42)) + + Text(relativeTimeText(to: nextDate)) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(Color(white: 0.58)) + } else { + Text("当前规则下暂无下次提醒") + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(Color(white: 0.48)) + } + } + .padding(18) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 22) + .fill(Color.white) + ) + } + private func repeatSummary(for item: AlarmItem) -> String { let holidayText = item.skipHolidayEnabled ? "节假日跳过" : "节假日照常" return "\(item.repeatRule.displayText) · \(holidayText)" } + + private func alarmTimeValue(for item: AlarmItem) -> Int { + let components = Calendar.current.dateComponents([.hour, .minute], from: item.time) + return (components.hour ?? 0) * 60 + (components.minute ?? 0) + } + + private func relativeTimeText(to date: Date) -> String { + let interval = max(Int(date.timeIntervalSinceNow), 0) + let hours = interval / 3600 + let minutes = (interval % 3600) / 60 + + if hours == 0 { + return "\(max(minutes, 1)) 分钟后" + } + if minutes == 0 { + return "\(hours) 小时后" + } + return "\(hours) 小时 \(minutes) 分钟后" + } } #Preview {