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:
@ -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)
|
||||
}
|
||||
|
||||
/// pick本地Files相关逻辑。
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user