Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,268 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user