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:
@ -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) { }
|
||||
}
|
||||
|
||||
/// table行Count相关逻辑。
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
/// table行Count相关逻辑。
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user