Extract shared LocationReportService, wire location_report menu routing, and add unit tests for reporting cooldown and history pagination. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
7.2 KiB
Swift
185 lines
7.2 KiB
Swift
//
|
||
// LocationReportHistoryViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 位置上报历史页,对齐 Android `LocationReportHistoryScreen`。
|
||
final class LocationReportHistoryViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource {
|
||
|
||
private let viewModel: LocationReportHistoryViewModel
|
||
private let homeAPI: HomeAPI
|
||
|
||
private let filterBar = LocationReportHistoryFilterBar()
|
||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||
private let emptyLabel = UILabel()
|
||
private let footerLabel = UILabel()
|
||
private let refreshControl = UIRefreshControl()
|
||
|
||
init(
|
||
viewModel: LocationReportHistoryViewModel = LocationReportHistoryViewModel(),
|
||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||
) {
|
||
self.viewModel = viewModel
|
||
self.homeAPI = homeAPI
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func setupNavigationBar() {
|
||
title = "位置上报历史"
|
||
}
|
||
|
||
override func setupUI() {
|
||
tableView.delegate = self
|
||
tableView.dataSource = self
|
||
tableView.separatorStyle = .none
|
||
tableView.backgroundColor = AppColor.pageBackground
|
||
tableView.register(LocationReportHistoryCell.self, forCellReuseIdentifier: LocationReportHistoryCell.reuseIdentifier)
|
||
tableView.refreshControl = refreshControl
|
||
|
||
emptyLabel.text = "暂无位置上报记录"
|
||
emptyLabel.textColor = AppColor.textTertiary
|
||
emptyLabel.textAlignment = .center
|
||
emptyLabel.isHidden = true
|
||
|
||
footerLabel.text = "没有更多"
|
||
footerLabel.font = .systemFont(ofSize: 13)
|
||
footerLabel.textColor = AppColor.textTertiary
|
||
footerLabel.textAlignment = .center
|
||
footerLabel.isHidden = true
|
||
|
||
view.addSubview(filterBar)
|
||
view.addSubview(tableView)
|
||
view.addSubview(emptyLabel)
|
||
view.addSubview(footerLabel)
|
||
|
||
filterBar.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||
}
|
||
tableView.snp.makeConstraints { make in
|
||
make.top.equalTo(filterBar.snp.bottom)
|
||
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
|
||
}
|
||
emptyLabel.snp.makeConstraints { make in
|
||
make.center.equalTo(tableView)
|
||
}
|
||
footerLabel.snp.makeConstraints { make in
|
||
make.centerX.equalToSuperview()
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
Task { @MainActor in self?.applyViewModel() }
|
||
}
|
||
viewModel.onShowMessage = { [weak self] message in
|
||
Task { @MainActor in self?.showToast(message) }
|
||
}
|
||
|
||
refreshControl.addTarget(self, action: #selector(refreshTapped), for: .valueChanged)
|
||
filterBar.onFilterTap = { [weak self] in self?.presentFilterPicker() }
|
||
filterBar.onDateFilterTap = { [weak self] in self?.presentDateFilter() }
|
||
filterBar.onClearDates = { [weak self] in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.clearDateRange(api: self.homeAPI) }
|
||
}
|
||
|
||
Task { await viewModel.loadReportList(api: homeAPI, refresh: true) }
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
filterBar.apply(
|
||
filterTitle: viewModel.selectedFilter.displayName,
|
||
startText: viewModel.startDateText,
|
||
endText: viewModel.endDateText,
|
||
showsDateRange: viewModel.hasDateFilter
|
||
)
|
||
tableView.reloadData()
|
||
emptyLabel.isHidden = !viewModel.reportList.isEmpty
|
||
footerLabel.isHidden = viewModel.reportList.isEmpty || viewModel.canLoadMore
|
||
if !viewModel.isRefreshing {
|
||
refreshControl.endRefreshing()
|
||
}
|
||
}
|
||
|
||
@objc private func refreshTapped() {
|
||
Task { await viewModel.loadReportList(api: homeAPI, refresh: true) }
|
||
}
|
||
|
||
private func presentFilterPicker() {
|
||
let alert = UIAlertController(title: "筛选类型", message: nil, preferredStyle: .actionSheet)
|
||
LocationReportFilterType.allCases.forEach { filter in
|
||
alert.addAction(UIAlertAction(title: filter.displayName, style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
Task { await self.viewModel.selectFilter(filter, api: self.homeAPI) }
|
||
})
|
||
}
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentDateFilter() {
|
||
let alert = UIAlertController(title: "时间筛选", message: nil, preferredStyle: .actionSheet)
|
||
alert.addAction(UIAlertAction(title: "选择开始日期", style: .default) { [weak self] _ in
|
||
self?.presentDatePicker(isStart: true)
|
||
})
|
||
alert.addAction(UIAlertAction(title: "选择结束日期", style: .default) { [weak self] _ in
|
||
self?.presentDatePicker(isStart: false)
|
||
})
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentDatePicker(isStart: Bool) {
|
||
let alert = UIAlertController(title: isStart ? "开始日期" : "结束日期", message: "\n\n\n\n\n\n\n\n", preferredStyle: .actionSheet)
|
||
let picker = UIDatePicker()
|
||
picker.datePickerMode = .date
|
||
if #available(iOS 13.4, *) {
|
||
picker.preferredDatePickerStyle = .wheels
|
||
}
|
||
picker.date = isStart ? (viewModel.startDate ?? Date()) : (viewModel.endDate ?? Date())
|
||
alert.view.addSubview(picker)
|
||
picker.snp.makeConstraints { make in
|
||
make.centerX.equalToSuperview()
|
||
make.top.equalToSuperview().offset(24)
|
||
}
|
||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
Task {
|
||
if isStart {
|
||
await self.viewModel.setDateRange(start: picker.date, end: self.viewModel.endDate, api: self.homeAPI)
|
||
} else {
|
||
await self.viewModel.setDateRange(start: self.viewModel.startDate, end: picker.date, api: self.homeAPI)
|
||
}
|
||
}
|
||
})
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
viewModel.reportList.count
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
let cell = tableView.dequeueReusableCell(
|
||
withIdentifier: LocationReportHistoryCell.reuseIdentifier,
|
||
for: indexPath
|
||
) as! LocationReportHistoryCell
|
||
cell.apply(item: viewModel.reportList[indexPath.row])
|
||
return cell
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||
Task { await viewModel.loadMoreIfNeeded(currentIndex: indexPath.row, api: homeAPI) }
|
||
}
|
||
}
|