269 lines
10 KiB
Swift
269 lines
10 KiB
Swift
//
|
||
// 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() }
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "任务上传"
|
||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||
setupForm()
|
||
bindViewModel()
|
||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||
}
|
||
|
||
private func bindViewModel() {
|
||
viewModel.onChange = { [weak self] in
|
||
self?.applyViewModel()
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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 ? "保存中..." : "保存任务素材"
|
||
}
|
||
|
||
@objc private func orderChanged() {
|
||
viewModel.orderNumber = orderField.text ?? ""
|
||
}
|
||
|
||
@objc private func refreshSpots() {
|
||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||
}
|
||
|
||
@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)
|
||
}
|
||
|
||
@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)
|
||
}
|
||
|
||
@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 {
|
||
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()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 历史拍摄信息页。
|
||
final class HistoricalShootingInfoViewController: UIViewController {
|
||
|
||
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
|
||
}()
|
||
|
||
init(orderNumber: String) {
|
||
self.orderNumber = orderNumber
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "历史拍摄"
|
||
view.addSubview(tableView)
|
||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||
Task { await loadData() }
|
||
}
|
||
|
||
private func loadData() async {
|
||
await appServices.globalLoading.withLoading {
|
||
await viewModel.load(api: appServices.ordersAPI, orderNumber: orderNumber)
|
||
}
|
||
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)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|