Files
suixinkan_uikit/suixinkan/UI/Common/DateRangePickerViewController.swift
汉秋 efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00

309 lines
11 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.

//
// DateRangePickerViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Sheet Statistics Android `DateRangeBottomSheet`
final class DateRangePickerViewController: UIViewController {
var onShortcutSelected: ((String) -> Void)?
var onRangeSelected: ((Date, Date) -> Void)?
private var currentMonth: Date
private var tempStart: Date?
private var tempEnd: Date?
private var selectedShortcut: String?
private let shortcutStack = UIStackView()
private let monthTitleLabel = UILabel()
private let weekdayStack = UIStackView()
private let daysCollectionView: UICollectionView
private var shortcutButtons: [StatisticsPeriodButton] = []
private let calendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "zh_CN")
calendar.firstWeekday = 2
return calendar
}()
init(rangeStart: Date?, rangeEnd: Date?) {
self.tempStart = rangeStart
self.tempEnd = rangeEnd
self.currentMonth = rangeStart ?? Date()
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 8
daysCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
let today = Calendar.current.startOfDay(for: Date())
let initCalendar = Calendar(identifier: .gregorian)
if rangeStart == nil, rangeEnd == nil {
selectedShortcut = StatisticsRangeShortcut.all.rawValue
} else if rangeStart == today, rangeEnd == today {
selectedShortcut = StatisticsRangeShortcut.today.rawValue
} else if
let start = rangeStart,
let end = rangeEnd,
start == initCalendar.date(byAdding: .day, value: -1, to: today),
end == initCalendar.date(byAdding: .day, value: -1, to: today) {
selectedShortcut = StatisticsRangeShortcut.yesterday.rawValue
} else if
let start = rangeStart,
let end = rangeEnd,
start == initCalendar.date(byAdding: .day, value: -6, to: today),
end == today {
selectedShortcut = StatisticsRangeShortcut.last7Days.rawValue
}
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
updateMonthTitle()
}
private func setupUI() {
shortcutStack.axis = .horizontal
shortcutStack.distribution = .equalSpacing
StatisticsRangeShortcut.allCases.forEach { shortcut in
let button = StatisticsPeriodButton()
button.cornerStyle = .fixed(10)
button.periodTitle = shortcut.rawValue
button.isSelected = shortcut.rawValue == selectedShortcut
button.addTarget(self, action: #selector(shortcutTapped(_:)), for: .touchUpInside)
shortcutButtons.append(button)
shortcutStack.addArrangedSubview(button)
}
let previousButton = UIButton(type: .system)
previousButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
previousButton.tintColor = UIColor(hex: 0x9CA3AF)
previousButton.addTarget(self, action: #selector(previousMonth), for: .touchUpInside)
let nextButton = UIButton(type: .system)
nextButton.setImage(UIImage(systemName: "chevron.right"), for: .normal)
nextButton.tintColor = UIColor(hex: 0x9CA3AF)
nextButton.addTarget(self, action: #selector(nextMonth), for: .touchUpInside)
monthTitleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
monthTitleLabel.textAlignment = .center
let monthRow = UIStackView(arrangedSubviews: [previousButton, monthTitleLabel, nextButton])
monthRow.axis = .horizontal
monthRow.alignment = .center
monthRow.distribution = .equalCentering
weekdayStack.axis = .horizontal
weekdayStack.distribution = .fillEqually
["", "", "", "", "", "", ""].forEach { title in
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hex: 0x9CA3AF)
label.textAlignment = .center
weekdayStack.addArrangedSubview(label)
}
daysCollectionView.backgroundColor = .clear
daysCollectionView.dataSource = self
daysCollectionView.delegate = self
daysCollectionView.register(DateCell.self, forCellWithReuseIdentifier: DateCell.reuseIdentifier)
view.addSubview(shortcutStack)
view.addSubview(monthRow)
view.addSubview(weekdayStack)
view.addSubview(daysCollectionView)
shortcutStack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(36)
}
monthRow.snp.makeConstraints { make in
make.top.equalTo(shortcutStack.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(32)
}
previousButton.snp.makeConstraints { make in
make.size.equalTo(24)
}
nextButton.snp.makeConstraints { make in
make.size.equalTo(24)
}
weekdayStack.snp.makeConstraints { make in
make.top.equalTo(monthRow.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(16)
}
daysCollectionView.snp.makeConstraints { make in
make.top.equalTo(weekdayStack.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
make.height.equalTo(240)
}
}
@objc private func shortcutTapped(_ sender: StatisticsPeriodButton) {
onShortcutSelected?(sender.periodTitle)
}
@objc private func previousMonth() {
currentMonth = calendar.date(byAdding: .month, value: -1, to: currentMonth) ?? currentMonth
updateMonthTitle()
daysCollectionView.reloadData()
}
@objc private func nextMonth() {
currentMonth = calendar.date(byAdding: .month, value: 1, to: currentMonth) ?? currentMonth
updateMonthTitle()
daysCollectionView.reloadData()
}
private func updateMonthTitle() {
let components = calendar.dateComponents([.year, .month], from: currentMonth)
monthTitleLabel.text = "\(components.year ?? 0)\(components.month ?? 0)"
}
private func monthDates() -> [Date?] {
guard
let monthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: currentMonth)),
let dayRange = calendar.range(of: .day, in: .month, for: monthStart)
else {
return []
}
let firstWeekday = calendar.component(.weekday, from: monthStart)
let leadingEmpty = (firstWeekday + 5) % 7
var dates: [Date?] = Array(repeating: nil, count: leadingEmpty)
for day in dayRange {
if let date = calendar.date(byAdding: .day, value: day - 1, to: monthStart) {
dates.append(date)
}
}
while dates.count % 7 != 0 {
dates.append(nil)
}
return dates
}
private func handleDateTap(_ date: Date) {
selectedShortcut = nil
shortcutButtons.forEach { $0.isSelected = false }
if tempStart == nil || (tempStart != nil && tempEnd != nil) {
tempStart = date
tempEnd = nil
} else if let start = tempStart {
if date < start {
tempEnd = start
tempStart = date
} else {
tempEnd = date
}
if let start = tempStart, let end = tempEnd {
onRangeSelected?(start, end)
}
}
daysCollectionView.reloadData()
}
private func isInRange(_ date: Date) -> Bool {
guard let start = tempStart else { return false }
if let end = tempEnd {
return date >= min(start, end) && date <= max(start, end)
}
return calendar.isDate(date, inSameDayAs: start)
}
private final class DateCell: UICollectionViewCell {
static let reuseIdentifier = "DateRangePickerDateCell"
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.layer.cornerRadius = 16
contentView.clipsToBounds = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(text: String?, selected: Bool, inRange: Bool) {
label.text = text
if selected {
contentView.backgroundColor = AppColor.primary
label.textColor = .white
} else if inRange {
contentView.backgroundColor = UIColor(hex: 0xEFF6FF)
label.textColor = AppColor.primary
} else {
contentView.backgroundColor = .clear
label.textColor = .black
}
}
}
}
extension DateRangePickerViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
monthDates().count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: DateCell.reuseIdentifier,
for: indexPath
) as! DateCell
let dates = monthDates()
guard indexPath.item < dates.count, let date = dates[indexPath.item] else {
cell.configure(text: nil, selected: false, inRange: false)
return cell
}
let day = calendar.component(.day, from: date)
let selected = (tempStart != nil && calendar.isDate(date, inSameDayAs: tempStart!))
|| (tempEnd != nil && calendar.isDate(date, inSameDayAs: tempEnd!))
cell.configure(text: "\(day)", selected: selected, inRange: isInRange(date))
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let dates = monthDates()
guard indexPath.item < dates.count, let date = dates[indexPath.item] else { return }
handleDateTap(date)
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
let width = collectionView.bounds.width / 7
return CGSize(width: width, height: 32)
}
}
/// Statistics
typealias StatisticsDateRangeViewController = DateRangePickerViewController