Advance UIKit rewrite with AMap integration and core UI modules.

Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 15:16:12 +08:00
parent 9edf993432
commit d99a5b1bf8
124 changed files with 5195 additions and 1536 deletions

View File

@ -1,37 +1,146 @@
//
// OperatingAreaViewController.swift
// suixinkan
//
// Created by Codex on 2026/6/26.
// suixinkan_ios
//
import SnapKit
import UIKit
///
final class OperatingAreaViewController: ModuleTableViewController {
///
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>!
private var services: AppServices { AppServices.shared }
/// UI
override func viewDidLoad() {
title = "运营区域"
super.viewDidLoad()
wireViewModel(viewModel) { }
title = "运营区域"
view.backgroundColor = UIColor(hex: 0xF5F7FA)
setupLayout()
wireViewModel()
Task { await reload(showLoading: true) }
}
override func tableRowCount() -> Int {
viewModel.items.count
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()
}
}
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
let item = viewModel.items[indexPath.row]
cell.configure(title: item.name, subtitle: "\(item.typeText) · \(item.statusText)", detail: "围栏 \(viewModel.fenceRings.count)")
private func wireViewModel() {
viewModel.onChange = { [weak self] in
self?.render()
}
}
override func reloadContent() async {
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 services.accountContext.currentStore?.name ?? "当前店铺"
case .scenicAdmin:
return services.accountContext.currentScenic?.name ?? "当前景区"
case nil:
return services.accountContext.currentScenic?.name
?? services.accountContext.currentStore?.name
?? "未选择业务范围"
}
}
@objc private func onPullRefresh() {
Task {
await reload(showLoading: false)
tableView.refreshControl?.endRefreshing()
}
}
private func reload(showLoading: Bool) async {
if showLoading { services.globalLoading.show() }
defer { if showLoading { services.globalLoading.hide() } }
await viewModel.reload(
api: services.operatingAreaAPI,
accountContext: services.accountContext,
permissionContext: services.permissionContext
)
if let message = viewModel.errorMessage {
services.toastCenter.show(message)
}
}
}