// // 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 } }