Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

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