Add location report map page and history list aligned with Android.
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>
This commit is contained in:
@ -0,0 +1,184 @@
|
||||
//
|
||||
// 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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user