import Foundation /// 日清日历的展示模式。 enum OfflineCollectionCalendarMode: Sendable, Equatable { case week case month } /// 周/月日历日期运算状态,不依赖 UIKit,便于单元测试。 struct OfflineCollectionCalendarState: Sendable, Equatable { private let calendar: Calendar private(set) var selectedDate: Date private(set) var maximumDate: Date private(set) var mode: OfflineCollectionCalendarMode init( selectedDate: Date, maximumDate: Date, mode: OfflineCollectionCalendarMode = .week, calendar: Calendar = OfflineCollectionDate.calendar ) { self.calendar = calendar self.maximumDate = calendar.startOfDay(for: maximumDate) self.selectedDate = min(calendar.startOfDay(for: selectedDate), self.maximumDate) self.mode = mode } /// 切换周历和月历并保留选中日期。 mutating func toggleMode() { mode = mode == .week ? .month : .week } /// 选择不晚于服务端今日的日期。 mutating func select(_ date: Date) -> Bool { let value = calendar.startOfDay(for: date) guard value <= maximumDate else { return false } selectedDate = value return true } /// 横滑一个周或一个月,并返回新的选中日期。 @discardableResult mutating func movePage(_ offset: Int) -> Date { let candidate: Date switch mode { case .week: candidate = calendar.date(byAdding: .day, value: offset * 7, to: selectedDate) ?? selectedDate case .month: let current = calendar.dateComponents([.year, .month, .day], from: selectedDate) let monthStart = calendar.date(from: DateComponents(year: current.year, month: current.month, day: 1)) ?? selectedDate let targetMonth = calendar.date(byAdding: .month, value: offset, to: monthStart) ?? monthStart let days = calendar.range(of: .day, in: .month, for: targetMonth)?.count ?? 1 candidate = calendar.date(byAdding: .day, value: min(current.day ?? 1, days) - 1, to: targetMonth) ?? targetMonth } selectedDate = min(calendar.startOfDay(for: candidate), maximumDate) return selectedDate } /// 判断指定方向是否存在不晚于服务端今日的周或月页面。 func canMovePage(_ offset: Int) -> Bool { guard offset != 0 else { return false } if offset < 0 { return true } switch mode { case .week: guard let candidate = calendar.date(byAdding: .day, value: offset * 7, to: selectedDate) else { return false } let weekday = calendar.component(.weekday, from: candidate) let daysFromMonday = (weekday + 5) % 7 let targetMonday = calendar.date(byAdding: .day, value: -daysFromMonday, to: candidate) ?? candidate return calendar.startOfDay(for: targetMonday) <= maximumDate case .month: let current = calendar.dateComponents([.year, .month], from: selectedDate) guard let monthStart = calendar.date(from: current), let targetMonth = calendar.date(byAdding: .month, value: offset, to: monthStart) else { return false } let maximumMonth = calendar.date( from: calendar.dateComponents([.year, .month], from: maximumDate) ) ?? maximumDate return targetMonth <= maximumMonth } } /// 当前周的周一至周日。 var weekDates: [Date] { let weekday = calendar.component(.weekday, from: selectedDate) let daysFromMonday = (weekday + 5) % 7 let monday = calendar.date(byAdding: .day, value: -daysFromMonday, to: selectedDate) ?? selectedDate return (0 ..< 7).compactMap { calendar.date(byAdding: .day, value: $0, to: monday) } } /// 当前月份固定六行、周一开周的 42 个日期。 var monthDates: [Date] { let parts = calendar.dateComponents([.year, .month], from: selectedDate) let first = calendar.date(from: parts) ?? selectedDate let weekday = calendar.component(.weekday, from: first) let leading = (weekday + 5) % 7 let start = calendar.date(byAdding: .day, value: -leading, to: first) ?? first return (0 ..< 42).compactMap { calendar.date(byAdding: .day, value: $0, to: start) } } /// 当前选中日期所在周在固定六行月历中的零基行号。 var selectedWeekRowIndex: Int { let index = monthDates.firstIndex { calendar.isDate($0, inSameDayAs: selectedDate) } ?? 0 return index / 7 } /// 当前选中月份标题。 var monthTitle: String { let parts = calendar.dateComponents([.year, .month], from: selectedDate) return "\(parts.year ?? 0)年\(parts.month ?? 0)月" } }