Files
suixinkan_ios_uikit/suixinkan_ios/Features/PunchPoint/ViewControllers/PunchPointViewControllers.swift
汉秋 d99a5b1bf8 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>
2026-06-26 15:16:12 +08:00

318 lines
11 KiB
Swift

//
// PunchPointViewControllers.swift
// suixinkan
//
// Created by Codex on 2026/6/26.
//
import CoreLocation
import SnapKit
import UIKit
private extension PunchPointItem {
var displayAddress: String {
region?.address ?? scenicSpotStr
}
}
///
final class PunchPointListViewController: ModuleTableViewController {
private let viewModel = PunchPointListViewModel()
/// UI
override func viewDidLoad() {
title = "打卡点"
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "新建",
style: .plain,
target: self,
action: #selector(createPunchPoint)
)
super.viewDidLoad()
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(
PunchPointDetailViewController(punchPointId: item.id, summary: item),
animated: true
)
}
/// 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)
}
}
extension PunchPointListViewModel: ViewModelBindable {}
///
final class PunchPointDetailViewController: ModuleTableViewController {
private let punchPointId: Int
private let summary: PunchPointItem?
private let viewModel = PunchPointListViewModel()
///
init(punchPointId: Int, summary: PunchPointItem?) {
self.punchPointId = punchPointId
self.summary = summary
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
title = summary?.name ?? "打卡点详情"
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "二维码",
style: .plain,
target: self,
action: #selector(showQR)
)
super.viewDidLoad()
viewModel.onChange = { [weak self] in
self?.title = self?.viewModel.selectedDetail?.name ?? self?.summary?.name
self?.reloadTable()
}
}
/// 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 }
switch indexPath.row {
case 0: cell.configure(title: "名称", subtitle: detail.name)
case 1: cell.configure(title: "地址", subtitle: detail.displayAddress)
case 2: cell.configure(title: "状态", subtitle: detail.statusLabel)
default: cell.configure(title: "创建时间", subtitle: detail.createdAt)
}
}
/// 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(
PunchPointQRViewController(title: detail.name, qrURL: detail.mpQrcode),
animated: true
)
}
}
///
final class PunchPointEditorViewController: UIViewController {
private let viewModel = PunchPointEditorViewModel()
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
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = punchPointId == nil ? "新建打卡点" : "编辑打卡点"
view.backgroundColor = UIColor(hex: 0xF5F7FA)
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "保存",
style: .done,
target: self,
action: #selector(save)
)
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()
}
}
}
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
}
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 {
services.toastCenter.show("保存成功")
navigationController?.popViewController(animated: true)
} else if let message = viewModel.errorMessage {
services.toastCenter.show(message)
}
}
}
}
extension PunchPointEditorViewModel: ViewModelBindable {}
///
final class PunchPointQRViewController: UIViewController {
private let pageTitle: String
private let qrURL: String
///
init(title: String, qrURL: String) {
pageTitle = title
self.qrURL = qrURL
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = pageTitle
view.backgroundColor = UIColor(hex: 0xF5F7FA)
let urlLabel = UILabel()
urlLabel.text = qrURL
urlLabel.numberOfLines = 0
urlLabel.textAlignment = .center
urlLabel.textColor = AppDesign.textSecondary
view.addSubview(urlLabel)
urlLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(24)
}
}
}