Files
suixinkan_ios_uikit/suixinkan_ios/Features/Orders/ViewControllers/OrderTailUploadViewControllers.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

343 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrderTailUploadViewControllers.swift
// suixinkan
//
import PhotosUI
import SnapKit
import UIKit
import UniformTypeIdentifiers
///
final class MultiTravelTaskUploadViewController: UIViewController {
private let viewModel: MultiTravelTaskUploadViewModel
private let orderField = UITextField()
private let spotButton = UIButton(type: .system)
private let cloudListLabel = UILabel()
private let localListLabel = UILabel()
private let submitButton = UIButton(type: .system)
///
init(initialOrderNumber: String) {
self.viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: initialOrderNumber)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "任务上传"
view.backgroundColor = UIColor(hex: 0xF5F7FA)
setupForm()
bindViewModel()
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 = "关联订单号"
orderField.autocorrectionType = .no
orderField.autocapitalizationType = .none
orderField.text = viewModel.orderNumber
orderField.addTarget(self, action: #selector(orderChanged), for: .editingChanged)
spotButton.setTitle("选择打卡点", for: .normal)
spotButton.addTarget(self, action: #selector(selectSpot), for: .touchUpInside)
cloudListLabel.numberOfLines = 0
cloudListLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
localListLabel.numberOfLines = 0
localListLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
submitButton.configuration = .filled()
submitButton.configuration?.title = "保存任务素材"
submitButton.configuration?.baseBackgroundColor = AppDesignUIKit.primary
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
let refreshButton = UIButton(type: .system)
refreshButton.setTitle("刷新打卡点", for: .normal)
refreshButton.addTarget(self, action: #selector(refreshSpots), for: .touchUpInside)
let pickLocalButton = UIButton(type: .system)
pickLocalButton.setTitle("选择图片/视频", for: .normal)
pickLocalButton.addTarget(self, action: #selector(pickLocalFiles), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [
labeledRow("订单号", orderField),
refreshButton,
spotButton,
labeledRow("云盘素材", cloudListLabel),
pickLocalButton,
labeledRow("本地素材", localListLabel),
submitButton
])
stack.axis = .vertical
stack.spacing = 12
view.addSubview(stack)
stack.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
make.leading.trailing.equalToSuperview().inset(16)
}
applyViewModel()
}
/// labeled
private func labeledRow(_ title: String, _ content: UIView) -> UIStackView {
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold)
let row = UIStackView(arrangedSubviews: [label, content])
row.axis = .vertical
row.spacing = 8
return row
}
/// apply
private func applyViewModel() {
orderField.text = viewModel.orderNumber
spotButton.setTitle(viewModel.isLoadingSpots ? "加载中..." : viewModel.selectedSpotName, for: .normal)
spotButton.isEnabled = !viewModel.isLoadingSpots
if viewModel.selectedCloudFiles.isEmpty {
cloudListLabel.text = "未选择云盘文件"
} else {
cloudListLabel.text = viewModel.selectedCloudFiles.map(\.fileName).joined(separator: "\n")
}
if viewModel.selectedLocalFiles.isEmpty {
localListLabel.text = "未选择本地文件"
} else {
localListLabel.text = viewModel.selectedLocalFiles.map { file in
"\(file.fileName) \(file.statusText)"
}.joined(separator: "\n")
}
submitButton.isEnabled = viewModel.canSubmit
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)
for spot in viewModel.spots {
let title = spot.name.isEmpty ? "打卡点 \(spot.id)" : spot.name
sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.viewModel.selectSpot(id: spot.id)
})
}
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
present(sheet, animated: true)
}
/// pickFiles
@objc private func pickLocalFiles() {
var config = PHPickerConfiguration()
config.selectionLimit = 9
config.filter = .any(of: [.images, .videos])
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
/// Tapped
@objc private func submitTapped() {
Task {
let success = await viewModel.submit(
api: appServices.ordersAPI,
uploadService: appServices.ossUploadService,
scenicId: appServices.accountContext.currentScenic?.id
)
if success {
showToast("提交成功")
navigationController?.popViewController(animated: true)
} else if let message = viewModel.errorMessage {
showToast(message)
}
applyViewModel()
}
}
}
extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
/// picker
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard !results.isEmpty else { return }
for result in results {
let provider = result.itemProvider
if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, _ in
guard let self, let url else { return }
let data = (try? Data(contentsOf: url)) ?? Data()
DispatchQueue.main.async {
self.viewModel.addLocalFile(data: data, fileName: url.lastPathComponent)
self.applyViewModel()
}
}
} else if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
DispatchQueue.main.async {
self.viewModel.addLocalFile(data: data, fileName: "upload_\(Int(Date().timeIntervalSince1970)).jpg")
self.applyViewModel()
}
}
}
}
}
}
// 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, UITableViewDelegate {
private let orderNumber: String
private let viewModel = HistoricalShootingInfoViewModel()
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.delegate = self
return table
}()
/// Diffable
private var tableDataSource: UITableViewDiffableDataSource<HistoricalShootingSection, HistoricalShootingRow>!
///
init(orderNumber: String) {
self.orderNumber = orderNumber
super.init(nibName: nil, bundle: nil)
}
@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?.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) }
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
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
}
}