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

@ -5,6 +5,7 @@
// Created by Codex on 2026/6/26.
//
import CoreLocation
import SnapKit
import UIKit
@ -18,6 +19,7 @@ private extension PunchPointItem {
final class PunchPointListViewController: ModuleTableViewController {
private let viewModel = PunchPointListViewModel()
/// UI
override func viewDidLoad() {
title = "打卡点"
navigationItem.rightBarButtonItem = UIBarButtonItem(
@ -30,13 +32,16 @@ final class PunchPointListViewController: ModuleTableViewController {
wireViewModel(viewModel) { }
}
/// tableCount
override func tableRowCount() -> Int { viewModel.items.count }
/// Cell
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
let item = viewModel.items[indexPath.row]
cell.configure(title: item.name, subtitle: item.displayAddress, detail: item.statusLabel)
}
/// didSelectTableRow
override func didSelectTableRow(at indexPath: IndexPath) {
let item = viewModel.items[indexPath.row]
navigationController?.pushViewController(
@ -45,10 +50,12 @@ final class PunchPointListViewController: ModuleTableViewController {
)
}
/// Content
override func reloadContent() async {
await viewModel.reload(scenicId: services.currentScenicId, api: services.punchPointAPI)
}
/// Point
@objc private func createPunchPoint() {
navigationController?.pushViewController(PunchPointEditorViewController(punchPointId: nil), animated: true)
}
@ -62,6 +69,7 @@ final class PunchPointDetailViewController: ModuleTableViewController {
private let summary: PunchPointItem?
private let viewModel = PunchPointListViewModel()
///
init(punchPointId: Int, summary: PunchPointItem?) {
self.punchPointId = punchPointId
self.summary = summary
@ -73,6 +81,7 @@ final class PunchPointDetailViewController: ModuleTableViewController {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
title = summary?.name ?? "打卡点详情"
navigationItem.rightBarButtonItem = UIBarButtonItem(
@ -88,11 +97,13 @@ final class PunchPointDetailViewController: ModuleTableViewController {
}
}
/// tableCount
override func tableRowCount() -> Int {
guard viewModel.selectedDetail != nil || summary != nil else { return 0 }
return 4
}
/// Cell
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
let detail = viewModel.selectedDetail ?? summary
guard let detail else { return }
@ -104,10 +115,12 @@ final class PunchPointDetailViewController: ModuleTableViewController {
}
}
/// Content
override func reloadContent() async {
await viewModel.loadDetail(id: punchPointId, api: services.punchPointAPI)
}
/// QR
@objc private func showQR() {
guard let detail = viewModel.selectedDetail ?? summary else { return }
navigationController?.pushViewController(
@ -118,10 +131,17 @@ final class PunchPointDetailViewController: ModuleTableViewController {
}
///
final class PunchPointEditorViewController: ModuleTableViewController {
final class PunchPointEditorViewController: UIViewController {
private let viewModel = PunchPointEditorViewModel()
private let nameField = UITextField()
private let punchPointId: Int?
private let nameField = UITextField()
private let addressField = UITextField()
private let latitudeField = UITextField()
private let longitudeField = UITextField()
private let mapPicker = PunchPointMapPickerView()
private let locationProvider = ForegroundLocationProvider()
private var services: AppServices { AppServices.shared }
init(punchPointId: Int?) {
self.punchPointId = punchPointId
@ -134,48 +154,126 @@ final class PunchPointEditorViewController: ModuleTableViewController {
}
override func viewDidLoad() {
super.viewDidLoad()
title = punchPointId == nil ? "新建打卡点" : "编辑打卡点"
view.backgroundColor = UIColor(hex: 0xF5F7FA)
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "保存",
style: .done,
target: self,
action: #selector(save)
)
super.viewDidLoad()
nameField.placeholder = "打卡点名称"
nameField.borderStyle = .roundedRect
nameField.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 52)
tableView.tableHeaderView = nameField
wireViewModel(viewModel) { [weak self] in
if self?.nameField.text?.isEmpty != false {
self?.nameField.text = self?.viewModel.name
configureFields()
layoutForm()
wireViewModel()
mapPicker.onLocationPicked = { [weak self] lat, lng, address in
self?.viewModel.applyLocation(latitude: lat, longitude: lng, address: address)
self?.syncFieldsFromViewModel()
}
Task {
guard let punchPointId else { return }
if let detail = try? await services.punchPointAPI.punchPointInfo(id: punchPointId) {
viewModel.apply(detail)
syncFieldsFromViewModel()
}
}
}
override func tableRowCount() -> Int { 1 }
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
cell.configure(title: "地址", subtitle: viewModel.address)
private func configureFields() {
[nameField, addressField, latitudeField, longitudeField].forEach {
$0.borderStyle = .roundedRect
$0.font = .systemFont(ofSize: AppMetrics.FontSize.body)
}
nameField.placeholder = "打卡点名称"
addressField.placeholder = "地址"
latitudeField.placeholder = "纬度"
longitudeField.placeholder = "经度"
latitudeField.keyboardType = .decimalPad
longitudeField.keyboardType = .decimalPad
}
override func reloadContent() async {
guard let punchPointId else { return }
if let detail = try? await services.punchPointAPI.punchPointInfo(id: punchPointId) {
viewModel.apply(detail)
nameField.text = viewModel.name
private func layoutForm() {
let locateButton = UIButton(type: .system)
locateButton.setTitle("使用当前位置", for: .normal)
locateButton.addTarget(self, action: #selector(useCurrentLocation), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [
nameField,
mapPicker,
addressField,
latitudeField,
longitudeField,
locateButton
])
stack.axis = .vertical
stack.spacing = AppMetrics.Spacing.small
view.addSubview(stack)
nameField.snp.makeConstraints { make in make.height.equalTo(44) }
mapPicker.snp.makeConstraints { make in make.height.equalTo(220) }
addressField.snp.makeConstraints { make in make.height.equalTo(44) }
latitudeField.snp.makeConstraints { make in make.height.equalTo(44) }
longitudeField.snp.makeConstraints { make in make.height.equalTo(44) }
locateButton.snp.makeConstraints { make in make.height.equalTo(44) }
stack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppMetrics.Spacing.medium)
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
}
}
private func wireViewModel() {
viewModel.onChange = { [weak self] in
self?.syncFieldsFromViewModel()
}
}
private func syncFieldsFromViewModel() {
if nameField.text?.isEmpty != false { nameField.text = viewModel.name }
addressField.text = viewModel.address
latitudeField.text = viewModel.latitudeText
longitudeField.text = viewModel.longitudeText
if let lat = Double(viewModel.latitudeText), let lng = Double(viewModel.longitudeText) {
mapPicker.setCoordinate(latitude: lat, longitude: lng)
}
}
@objc private func useCurrentLocation() {
Task {
do {
let result = try await locationProvider.requestCurrentLocation()
viewModel.applyLocation(
latitude: result.latitude,
longitude: result.longitude,
address: result.address
)
syncFieldsFromViewModel()
} catch {
services.toastCenter.show("定位失败:\(error.localizedDescription)")
}
}
}
@objc private func save() {
viewModel.name = nameField.text ?? ""
viewModel.address = addressField.text ?? ""
viewModel.latitudeText = latitudeField.text ?? ""
viewModel.longitudeText = longitudeField.text ?? ""
Task {
let success = await viewModel.submit(
scenicId: services.currentScenicId,
api: services.punchPointAPI,
uploadService: services.ossUploadService
)
if success { navigationController?.popViewController(animated: true) }
if success {
services.toastCenter.show("保存成功")
navigationController?.popViewController(animated: true)
} else if let message = viewModel.errorMessage {
services.toastCenter.show(message)
}
}
}
}
@ -187,6 +285,7 @@ final class PunchPointQRViewController: UIViewController {
private let pageTitle: String
private let qrURL: String
///
init(title: String, qrURL: String) {
pageTitle = title
self.qrURL = qrURL
@ -198,6 +297,7 @@ final class PunchPointQRViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = pageTitle