Centralize dependency wiring with Session/Context/Network/UI/Runtime bundles, unify leaf ViewControllers on appServices, and add a test initializer with AppServicesTests. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
5.6 KiB
Swift
147 lines
5.6 KiB
Swift
//
|
||
// OperatingAreaViewController.swift
|
||
// suixinkan_ios
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 运营区域页:汇总信息、围栏地图与区域列表。
|
||
final class OperatingAreaViewController: UIViewController {
|
||
private let viewModel = OperatingAreaViewModel()
|
||
private let mapView = OperatingAreaMapView()
|
||
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||
private let summaryLabel = UILabel()
|
||
private lazy var blockReasonView = AppContentUnavailableView(title: "无法展示地图", systemImage: "mappin.slash")
|
||
|
||
private var dataSource: UITableViewDiffableDataSource<Int, String>!
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "运营区域"
|
||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||
setupLayout()
|
||
wireViewModel()
|
||
Task { await reload(showLoading: true) }
|
||
}
|
||
|
||
private func setupLayout() {
|
||
summaryLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||
summaryLabel.textColor = AppDesign.textSecondary
|
||
summaryLabel.numberOfLines = 0
|
||
|
||
mapView.snp.makeConstraints { make in
|
||
make.height.equalTo(320)
|
||
}
|
||
|
||
blockReasonView.isHidden = true
|
||
|
||
tableView.register(TitleSubtitleTableViewCell.self, forCellReuseIdentifier: TitleSubtitleTableViewCell.reuseIdentifier)
|
||
tableView.refreshControl = UIRefreshControl()
|
||
tableView.refreshControl?.addTarget(self, action: #selector(onPullRefresh), for: .valueChanged)
|
||
|
||
dataSource = UITableViewDiffableDataSource<Int, String>(tableView: tableView) { [weak self] tableView, indexPath, itemID in
|
||
guard let self else { return UITableViewCell() }
|
||
let cell = tableView.dequeueReusableCell(
|
||
withIdentifier: TitleSubtitleTableViewCell.reuseIdentifier,
|
||
for: indexPath
|
||
) as! TitleSubtitleTableViewCell
|
||
guard let item = self.viewModel.items.first(where: { String($0.id) == itemID }) else {
|
||
return cell
|
||
}
|
||
let ringCount = self.viewModel.fenceRings.filter { $0.itemId == item.id }.count
|
||
cell.configure(
|
||
title: item.name.isEmpty ? "未命名区域" : item.name,
|
||
subtitle: [item.typeText, item.statusText].filter { !$0.isEmpty }.joined(separator: " · "),
|
||
detail: "围栏 \(ringCount) 个"
|
||
)
|
||
return cell
|
||
}
|
||
|
||
let headerStack = UIStackView(arrangedSubviews: [summaryLabel, blockReasonView, mapView])
|
||
headerStack.axis = .vertical
|
||
headerStack.spacing = AppMetrics.Spacing.medium
|
||
|
||
let headerContainer = UIView()
|
||
headerContainer.addSubview(headerStack)
|
||
headerStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
|
||
}
|
||
headerContainer.layoutIfNeeded()
|
||
let height = headerStack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
|
||
headerContainer.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: height + 24)
|
||
tableView.tableHeaderView = headerContainer
|
||
|
||
view.addSubview(tableView)
|
||
tableView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
private func wireViewModel() {
|
||
viewModel.onChange = { [weak self] in
|
||
self?.render()
|
||
}
|
||
}
|
||
|
||
private func render() {
|
||
summaryLabel.text = "\(viewModel.title) · \(scopeText()) · 区域 \(viewModel.summary.areaCount) · 围栏 \(viewModel.summary.fenceCount)"
|
||
|
||
if let reason = viewModel.blockReason {
|
||
blockReasonView.isHidden = false
|
||
blockReasonView.update(title: reason.message, systemImage: "mappin.slash")
|
||
mapView.isHidden = true
|
||
} else {
|
||
blockReasonView.isHidden = true
|
||
mapView.isHidden = false
|
||
mapView.rings = viewModel.fenceRings
|
||
}
|
||
applyTableSnapshot()
|
||
}
|
||
|
||
/// 通过 Diffable snapshot 刷新区域列表。
|
||
private func applyTableSnapshot(animated: Bool = true) {
|
||
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
|
||
snapshot.appendSections([0])
|
||
snapshot.appendItems(viewModel.items.map { String($0.id) }, toSection: 0)
|
||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||
}
|
||
|
||
private func scopeText() -> String {
|
||
switch viewModel.mode {
|
||
case .storeAdmin:
|
||
return appServices.accountContext.currentStore?.name ?? "当前店铺"
|
||
case .scenicAdmin:
|
||
return appServices.accountContext.currentScenic?.name ?? "当前景区"
|
||
case nil:
|
||
return appServices.accountContext.currentScenic?.name
|
||
?? appServices.accountContext.currentStore?.name
|
||
?? "未选择业务范围"
|
||
}
|
||
}
|
||
|
||
@objc private func onPullRefresh() {
|
||
Task {
|
||
await reload(showLoading: false)
|
||
tableView.refreshControl?.endRefreshing()
|
||
}
|
||
}
|
||
|
||
private func reload(showLoading: Bool) async {
|
||
if showLoading { appServices.globalLoading.show() }
|
||
defer { if showLoading { appServices.globalLoading.hide() } }
|
||
|
||
await viewModel.reload(
|
||
api: appServices.operatingAreaAPI,
|
||
accountContext: appServices.accountContext,
|
||
permissionContext: appServices.permissionContext
|
||
)
|
||
if let message = viewModel.errorMessage {
|
||
appServices.toastCenter.show(message)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension OperatingAreaViewModel: ViewModelBindable {}
|