Files
suixinkan_ios_uikit/suixinkan_ios/Features/Orders/ViewControllers/OrderTailUploadViewControllers.swift
2026-06-26 14:33:31 +08:00

269 lines
10 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() }
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
}
}