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

@ -19,6 +19,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
private let localListLabel = UILabel()
private let submitButton = UIButton(type: .system)
///
init(initialOrderNumber: String) {
self.viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: initialOrderNumber)
super.init(nibName: nil, bundle: nil)
@ -27,6 +28,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "任务上传"
@ -36,12 +38,14 @@ final class MultiTravelTaskUploadViewController: UIViewController {
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
}
/// ViewModel
private func bindViewModel() {
viewModel.onChange = { [weak self] in
self?.applyViewModel()
}
}
/// Form UI
private func setupForm() {
orderField.borderStyle = .roundedRect
orderField.placeholder = "关联订单号"
@ -90,6 +94,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
applyViewModel()
}
/// labeled
private func labeledRow(_ title: String, _ content: UIView) -> UIStackView {
let label = UILabel()
label.text = title
@ -100,6 +105,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
return row
}
/// apply
private func applyViewModel() {
orderField.text = viewModel.orderNumber
spotButton.setTitle(viewModel.isLoadingSpots ? "加载中..." : viewModel.selectedSpotName, for: .normal)
@ -123,14 +129,17 @@ final class MultiTravelTaskUploadViewController: UIViewController {
submitButton.configuration?.title = viewModel.isSubmitting ? "保存中..." : "保存任务素材"
}
/// order
@objc private func orderChanged() {
viewModel.orderNumber = orderField.text ?? ""
}
/// Spots
@objc private func refreshSpots() {
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
}
/// select
@objc private func selectSpot() {
guard !viewModel.spots.isEmpty else { return }
let sheet = UIAlertController(title: "选择打卡点", message: nil, preferredStyle: .actionSheet)
@ -144,6 +153,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
present(sheet, animated: true)
}
/// pickFiles
@objc private func pickLocalFiles() {
var config = PHPickerConfiguration()
config.selectionLimit = 9
@ -153,6 +163,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
present(picker, animated: true)
}
/// Tapped
@objc private func submitTapped() {
Task {
let success = await viewModel.submit(
@ -172,6 +183,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
}
extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
/// picker
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard !results.isEmpty else { return }
@ -199,19 +211,41 @@ extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
}
}
// MARK: - Historical Shooting Diffable
private typealias HistoricalShootingSection = Int
private typealias HistoricalShootingRow = String
/// section
private enum HistoricalShootingSectionID {
static let summary = 0
static let spots = 1
}
///
private enum HistoricalShootingRowID {
static let project = "historicalShooting:project"
static let projectType = "historicalShooting:projectType"
static let empty = "historicalShooting:empty"
static func spot(_ id: String) -> String { "historicalShooting:spot:\(id)" }
}
///
final class HistoricalShootingInfoViewController: UIViewController {
final class HistoricalShootingInfoViewController: UIViewController, UITableViewDelegate {
private let orderNumber: String
private let viewModel = HistoricalShootingInfoViewModel()
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.dataSource = self
table.delegate = self
return table
}()
/// Diffable
private var tableDataSource: UITableViewDiffableDataSource<HistoricalShootingSection, HistoricalShootingRow>!
///
init(orderNumber: String) {
self.orderNumber = orderNumber
super.init(nibName: nil, bundle: nil)
@ -220,49 +254,89 @@ final class HistoricalShootingInfoViewController: UIViewController {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "历史拍摄"
configureTableDataSource()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
viewModel.onChange = { [weak self] in self?.applyTableSnapshot(reconfigure: true) }
Task { await loadData() }
}
/// Diffable
private func configureTableDataSource() {
tableDataSource = UITableViewDiffableDataSource<HistoricalShootingSection, HistoricalShootingRow>(
tableView: tableView
) { [weak self] (_: UITableView, _: IndexPath, row: HistoricalShootingRow) -> UITableViewCell? in
guard let self else { return UITableViewCell() }
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
cell.selectionStyle = .none
switch row {
case HistoricalShootingRowID.project:
cell.textLabel?.text = "项目"
cell.detailTextLabel?.text = self.viewModel.projectName
case HistoricalShootingRowID.projectType:
cell.textLabel?.text = "项目类型"
cell.detailTextLabel?.text = self.viewModel.projectTypeName
case HistoricalShootingRowID.empty:
cell.textLabel?.text = self.viewModel.errorMessage ?? "暂无历史拍摄"
default:
if row.hasPrefix("historicalShooting:spot:") {
let id = row.replacingOccurrences(of: "historicalShooting:spot:", with: "")
if let spot = self.viewModel.spots.first(where: { $0.id == id }) {
cell.textLabel?.text = spot.scenicSpotName
cell.detailTextLabel?.text = "\(spot.files.count) 个文件 · \(spot.photographerDisplayName)"
}
}
}
return cell
}
applyTableSnapshot(animated: false)
}
/// Diffable snapshot
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<HistoricalShootingSection, HistoricalShootingRow> {
var snapshot = NSDiffableDataSourceSnapshot<HistoricalShootingSection, HistoricalShootingRow>()
snapshot.appendSections([HistoricalShootingSectionID.summary, HistoricalShootingSectionID.spots])
snapshot.appendItems([HistoricalShootingRowID.project, HistoricalShootingRowID.projectType], toSection: HistoricalShootingSectionID.summary)
if viewModel.spots.isEmpty {
snapshot.appendItems([HistoricalShootingRowID.empty], toSection: HistoricalShootingSectionID.spots)
} else {
snapshot.appendItems(viewModel.spots.map { HistoricalShootingRowID.spot($0.id) }, toSection: HistoricalShootingSectionID.spots)
}
return snapshot
}
/// snapshot
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
var snapshot = buildTableSnapshot()
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
snapshot.reconfigureItems(snapshot.itemIdentifiers)
}
tableDataSource.apply(snapshot, animatingDifferences: animated)
}
/// Data
private func loadData() async {
await appServices.globalLoading.withLoading {
await viewModel.load(api: appServices.ordersAPI, orderNumber: orderNumber)
}
applyTableSnapshot()
if let message = viewModel.errorMessage { showToast(message) }
}
}
extension HistoricalShootingInfoViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int { 2 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 2 : max(viewModel.spots.count, 1)
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 1 ? "拍摄点位" : nil
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
cell.selectionStyle = .none
if indexPath.section == 0 {
cell.textLabel?.text = indexPath.row == 0 ? "项目" : "项目类型"
cell.detailTextLabel?.text = indexPath.row == 0 ? viewModel.projectName : viewModel.projectTypeName
return cell
}
guard !viewModel.spots.isEmpty else {
cell.textLabel?.text = viewModel.errorMessage ?? "暂无历史拍摄"
return cell
}
let spot = viewModel.spots[indexPath.row]
cell.textLabel?.text = spot.scenicSpotName
cell.detailTextLabel?.text = "\(spot.files.count) 个文件 · \(spot.photographerDisplayName)"
return cell
guard let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
return sectionID == HistoricalShootingSectionID.spots ? "拍摄点位" : nil
}
}
/// section
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}