Files
kangkang/康康/Features/Me/RemindersListView.swift
link2026 77a4ee1c37 缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
2026-06-07 14:17:18 +08:00

339 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import SwiftData
struct RemindersListView: View {
@Environment(\.modelContext) private var ctx
@Environment(\.dismiss) private var dismiss
@Query(sort: \CustomReminder.updatedAt, order: .reverse)
private var customReminders: [CustomReminder]
@Query(sort: \MetricReminder.updatedAt, order: .reverse)
private var reminders: [MetricReminder]
/// sheet ();
/// push , false
var presentedAsSheet = false
@State private var editingId: String?
@State private var creatingNew = false
@State private var editingCustom: CustomReminder?
private var isEmpty: Bool { customReminders.isEmpty && reminders.isEmpty }
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
header
createButton
if isEmpty {
emptyState
} else {
ForEach(customReminders) { r in
CustomReminderRow(
reminder: r,
onTapEdit: { editingCustom = r },
onToggle: { Task { await syncCustom(r) } }
)
}
if !reminders.isEmpty {
sectionLabel(String(appLoc: "指标记录提醒"))
ForEach(reminders) { r in
ReminderRow(
reminder: r,
isEditing: editingId == r.metricId,
onTapEdit: { toggleEdit(r.metricId) },
onChange: { Task { await sync(r) } },
onDelete: { delete(r) }
)
}
}
}
}
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 32)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle("提醒")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if presentedAsSheet {
ToolbarItem(placement: .topBarTrailing) {
Button(String(appLoc: "完成")) { dismiss() }
}
}
}
.sheet(isPresented: $creatingNew) {
CustomReminderEditSheet()
}
.sheet(item: $editingCustom) { r in
CustomReminderEditSheet(reminder: r)
}
}
private var header: some View {
Text("新建提醒,或在记录指标时开启")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var createButton: some View {
Button { creatingNew = true } label: {
Label(String(appLoc: "新建提醒"), systemImage: "plus")
.frame(maxWidth: .infinity)
}
.buttonStyle(TjPrimaryButton(height: 46, fontSize: 14))
}
private func sectionLabel(_ text: String) -> some View {
Text(text)
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 8)
}
private var emptyState: some View {
VStack(spacing: 12) {
Spacer(minLength: 40)
TjPlaceholder(label: String(appLoc: "还没有提醒,点上方新建"))
.frame(width: 240, height: 140)
Spacer()
}
.frame(maxWidth: .infinity)
}
// MARK: -
private func syncCustom(_ r: CustomReminder) async {
r.updatedAt = .now
try? ctx.save()
await ReminderService.sync(r)
}
// MARK: - (沿)
private func toggleEdit(_ id: String) {
editingId = (editingId == id) ? nil : id
}
private func sync(_ r: MetricReminder) async {
r.updatedAt = .now
try? ctx.save()
await ReminderService.sync(r)
}
private func delete(_ r: MetricReminder) {
ReminderService.cancel(metricId: r.metricId)
ctx.delete(r)
try? ctx.save()
}
}
/// : sheet; Toggle
private struct CustomReminderRow: View {
@Bindable var reminder: CustomReminder
let onTapEdit: () -> Void
let onToggle: () -> Void
var body: some View {
HStack(spacing: 12) {
Button(action: onTapEdit) {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(reminder.enabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.tjScaled( 16))
.foregroundStyle(reminder.enabled ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminder.title)
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
Text("\(reminder.timeLabel) · \(reminder.frequencyLabel)")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer(minLength: 0)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Toggle("", isOn: $reminder.enabled)
.labelsHidden()
.tint(Tj.Palette.ink)
.onChange(of: reminder.enabled) { _, _ in onToggle() }
// 28×28 , Toggle
Image(systemName: "chevron.right")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(width: 28, height: 28)
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
}
private struct ReminderRow: View {
@Bindable var reminder: MetricReminder
let isEditing: Bool
let onTapEdit: () -> Void
let onChange: () -> Void
let onDelete: () -> Void
@State private var pickedTime: Date = .now
@State private var hydrated = false
var body: some View {
VStack(spacing: 12) {
headerRow
if isEditing {
editingPanel
}
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
}
private var headerRow: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(reminder.enabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.tjScaled( 16))
.foregroundStyle(reminder.enabled ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminder.displayName)
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("\(reminder.timeLabel) · \(reminder.frequencyLabel)")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
Toggle("", isOn: $reminder.enabled)
.labelsHidden()
.tint(Tj.Palette.ink)
.onChange(of: reminder.enabled) { _, _ in onChange() }
Button {
onTapEdit()
} label: {
Image(systemName: isEditing ? "chevron.up" : "chevron.down")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(width: 28, height: 28)
}
.buttonStyle(.plain)
}
}
private var editingPanel: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("时间").font(.tjScaled( 13)).foregroundStyle(Tj.Palette.text2)
Spacer()
DatePicker("", selection: $pickedTime, displayedComponents: .hourAndMinute)
.datePickerStyle(.compact)
.labelsHidden()
.onChange(of: pickedTime) { _, new in
let cal = Calendar.current
reminder.hour = cal.component(.hour, from: new)
reminder.minute = cal.component(.minute, from: new)
onChange()
}
}
weekdayRow
HStack {
Spacer()
Button(role: .destructive) {
onDelete()
} label: {
Label("删除提醒", systemImage: "trash")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
}
.buttonStyle(.plain)
}
}
.onAppear {
if !hydrated {
pickedTime = Calendar.current.date(
bySettingHour: reminder.hour, minute: reminder.minute, second: 0, of: .now
) ?? .now
hydrated = true
}
}
}
private var weekdayRow: some View {
let names = [
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""),
]
let weekdayValues = [2, 3, 4, 5, 6, 7, 1]
return HStack(spacing: 6) {
ForEach(Array(weekdayValues.enumerated()), id: \.offset) { idx, w in
Button {
var s = Set(reminder.weekdays)
if s.contains(w) { s.remove(w) } else { s.insert(w) }
reminder.weekdays = s.sorted()
onChange()
} label: {
Text(names[idx])
.font(.tjScaled( 13,
weight: reminder.weekdays.contains(w) ? .semibold : .regular))
.foregroundStyle(reminder.weekdays.contains(w) ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 30)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(reminder.weekdays.contains(w) ? Tj.Palette.ink : Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(Tj.Palette.line,
lineWidth: reminder.weekdays.contains(w) ? 0 : 1)
)
}
.buttonStyle(.plain)
}
}
}
}
#Preview {
NavigationStack {
RemindersListView()
}
.modelContainer(for: [MetricReminder.self, CustomReminder.self], inMemory: true)
}