Files
suixinkan_uikit/suixinkan/UI/ScenicApplication/ScenicApplicationViewController.swift
汉秋 6336feff27 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:21:53 +08:00

339 lines
13 KiB
Swift
Raw Permalink 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.

//
// ScenicApplicationViewController.swift
// suixinkan
//
import PhotosUI
import SnapKit
import UIKit
/// Android `ScenicApplicationScreen`
final class ScenicApplicationViewController: BaseViewController {
private let viewModel: ScenicApplicationViewModel
private let homeAPI: HomeAPI
private let profileAPI: ProfileAPI
private let ossUploadService: OSSUploadService
private let scrollView = UIScrollView()
private let outerStack = UIStackView()
private let paddedStack = UIStackView()
private let auditCard = ScenicApplicationAuditStatusView()
private let warmReminder = ScenicApplicationWarmReminderView()
private let formCard = UIView()
private let formStack = UIStackView()
private let scenicNameField = ScenicApplicationFieldStyle.makeTextField(placeholder: "请输入景区名称")
private let scenicNameSection = ScenicApplicationFormFieldView(title: "景区名称")
private let imageSection = ScenicApplicationFormFieldView(title: "景区图片 (已添加 0/20)")
private let imageUploadView = ScenicApplicationImageUploadView()
private let provinceSelector = ScenicApplicationLocationSelector()
private let citySelector = ScenicApplicationLocationSelector()
private let cooperationStack = UIStackView()
private var cooperationOptions: [ScenicApplicationCooperationOptionView] = []
private let remarksView = ScenicApplicationPlaceholderTextView(placeholder: "请输入备注信息")
private let remarksSection = ScenicApplicationFormFieldView(title: "备注信息 (0/50)")
private let bottomBar = UIView()
private let agreementView = ScenicApplicationAgreementView()
private let submitButton = ScenicApplicationSubmitButton()
private let uploadProgressView = ScenicApplicationUploadProgressView()
init(
viewModel: ScenicApplicationViewModel = ScenicApplicationViewModel(),
homeAPI: HomeAPI? = nil,
profileAPI: ProfileAPI? = nil,
ossUploadService: OSSUploadService? = nil
) {
self.viewModel = viewModel
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
self.profileAPI = profileAPI ?? NetworkServices.shared.profileAPI
self.ossUploadService = ossUploadService ?? NetworkServices.shared.ossUploadService
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "景区申请"
}
override func setupUI() {
outerStack.axis = .vertical
outerStack.spacing = 0
paddedStack.axis = .vertical
paddedStack.spacing = 12
formCard.backgroundColor = .white
formCard.layer.cornerRadius = 8
formStack.axis = .vertical
formStack.spacing = 20
formCard.addSubview(formStack)
formStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
scenicNameSection.setContent(scenicNameField)
imageSection.setContent(imageUploadView)
let locationRow = UIStackView(arrangedSubviews: [provinceSelector, citySelector])
locationRow.axis = .horizontal
locationRow.spacing = 12
locationRow.distribution = .fillEqually
let locationSection = ScenicApplicationFormFieldView(title: "景区位置")
locationSection.setContent(locationRow)
cooperationStack.axis = .vertical
cooperationStack.spacing = 8
ScenicCooperationType.allCases.forEach { type in
let option = ScenicApplicationCooperationOptionView(title: type.displayName)
option.tag = type.rawValue
option.addTarget(self, action: #selector(cooperationTypeTapped(_:)), for: .touchUpInside)
cooperationOptions.append(option)
cooperationStack.addArrangedSubview(option)
}
let cooperationSection = ScenicApplicationFormFieldView(title: "合作类型")
cooperationSection.setContent(cooperationStack)
remarksView.text = ""
remarksSection.setContent(remarksView)
formStack.addArrangedSubview(scenicNameSection)
formStack.addArrangedSubview(imageSection)
formStack.addArrangedSubview(locationSection)
formStack.addArrangedSubview(cooperationSection)
formStack.addArrangedSubview(remarksSection)
paddedStack.addArrangedSubview(warmReminder)
paddedStack.addArrangedSubview(formCard)
outerStack.addArrangedSubview(auditCard)
outerStack.addArrangedSubview(paddedStack)
auditCard.isHidden = true
scrollView.addSubview(outerStack)
view.addSubview(scrollView)
view.addSubview(bottomBar)
view.addSubview(uploadProgressView)
bottomBar.backgroundColor = .white
let bottomStack = UIStackView(arrangedSubviews: [agreementView, submitButton])
bottomStack.axis = .vertical
bottomStack.spacing = 4
bottomBar.addSubview(bottomStack)
bottomStack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 16, bottom: 18, right: 16))
}
uploadProgressView.isHidden = true
uploadProgressView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.bottom.equalTo(bottomBar.snp.top)
}
outerStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(scrollView)
}
paddedStack.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
}
paddedStack.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 16, right: 0)
paddedStack.isLayoutMarginsRelativeArrangement = true
bottomBar.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide)
}
}
override func bindActions() {
viewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
viewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
viewModel.onSubmitSuccess = { [weak self] in
Task { @MainActor in self?.navigationController?.popViewController(animated: true) }
}
scenicNameField.addTarget(self, action: #selector(scenicNameChanged), for: .editingChanged)
remarksView.onTextChange = { [weak self] text in
self?.viewModel.onRemarksChange(text)
}
provinceSelector.addTarget(self, action: #selector(provinceTapped), for: .touchUpInside)
citySelector.addTarget(self, action: #selector(cityTapped), for: .touchUpInside)
agreementView.onToggle = { [weak self] in self?.viewModel.toggleAgreement() }
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
imageUploadView.onAdd = { [weak self] in self?.pickImages() }
imageUploadView.onDelete = { [weak self] index in self?.viewModel.deleteImage(at: index) }
imageUploadView.onRetry = { [weak self] index in
guard let self else { return }
self.viewModel.retryUpload(at: index)
Task { await self.startUploadIfNeeded() }
}
Task { await loadInitial() }
applyViewModel()
}
private func loadInitial() async {
showLoading()
defer { hideLoading() }
await viewModel.loadInitial(profileAPI: profileAPI, homeAPI: homeAPI)
}
private func applyViewModel() {
if viewModel.shouldShowAuditCard, let pending = viewModel.pendingApplication {
auditCard.apply(pending: pending)
auditCard.isHidden = false
} else {
auditCard.isHidden = true
}
let editable = viewModel.isEditable
let readOnly = viewModel.isReadOnly
scenicNameField.text = viewModel.scenicName
scenicNameField.isEnabled = editable
scenicNameField.isUserInteractionEnabled = editable && !readOnly
imageSection.updateTitle("景区图片 (已添加 \(viewModel.selectedImages.count)/20)")
imageUploadView.apply(images: viewModel.selectedImages, isReadOnly: readOnly)
provinceSelector.setValue(viewModel.selectedProvince, placeholder: "省份")
citySelector.setValue(viewModel.selectedCity, placeholder: "城市")
provinceSelector.isEnabled = editable
citySelector.isEnabled = editable
cooperationOptions.forEach { option in
let type = ScenicCooperationType(rawValue: option.tag) ?? .photographer
option.isSelected = type == viewModel.selectedCooperationType
option.isEnabled = editable
}
if remarksView.text != viewModel.remarks {
remarksView.text = viewModel.remarks
}
remarksView.isEditable = editable && !readOnly
remarksSection.updateTitle("备注信息 (\(viewModel.remarks.count)/50)")
agreementView.isHidden = !editable
agreementView.setChecked(viewModel.isAgreed)
submitButton.apply(
canSubmit: viewModel.canSubmit,
isSubmitting: viewModel.isSubmitting,
isReadOnly: readOnly
)
uploadProgressView.apply(state: viewModel.uploadDialogState)
}
@objc private func scenicNameChanged() {
viewModel.onScenicNameChange(scenicNameField.text ?? "")
}
@objc private func cooperationTypeTapped(_ sender: ScenicApplicationCooperationOptionView) {
guard viewModel.isEditable else { return }
guard let type = ScenicCooperationType(rawValue: sender.tag) else { return }
viewModel.onCooperationTypeSelected(type)
}
@objc private func provinceTapped() {
guard viewModel.isEditable else { return }
presentAreaPicker(title: "选择省份", names: viewModel.provinceList.map(\.name)) { [weak self] name in
self?.viewModel.onProvinceSelected(name)
}
}
@objc private func cityTapped() {
guard viewModel.isEditable else { return }
guard !viewModel.selectedProvince.isEmpty else {
showToast("请先选择省份")
return
}
presentAreaPicker(title: "选择城市", names: viewModel.cityList.map(\.name)) { [weak self] name in
self?.viewModel.onCitySelected(name)
}
}
private func presentAreaPicker(title: String, names: [String], onSelect: @escaping (String) -> Void) {
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
names.forEach { name in
alert.addAction(UIAlertAction(title: name, style: .default) { _ in onSelect(name) })
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
@objc private func submitTapped() {
Task {
showLoading()
defer { hideLoading() }
await viewModel.submit(homeAPI: homeAPI)
}
}
private func pickImages() {
guard viewModel.isEditable, !viewModel.isReadOnly else { return }
let remaining = 20 - viewModel.selectedImages.count
guard remaining > 0 else {
showToast("最多只能选择20张图片")
return
}
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.filter = .images
configuration.selectionLimit = remaining
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}
private func startUploadIfNeeded() async {
let scenicId = AppStore.shared.session.currentScenicId
await viewModel.processPendingUploads(uploader: ossUploadService, scenicId: scenicId)
}
}
extension ScenicApplicationViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard !results.isEmpty else { return }
var loadedItems = [ScenicApplicationImageItem?](repeating: nil, count: results.count)
let group = DispatchGroup()
for (index, result) in results.enumerated() {
let provider = result.itemProvider
guard provider.canLoadObject(ofClass: UIImage.self) else { continue }
group.enter()
provider.loadObject(ofClass: UIImage.self) { object, _ in
defer { group.leave() }
guard let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
let fileName = "scenic_apply_\(Int(Date().timeIntervalSince1970))_\(index).jpg"
loadedItems[index] = ScenicApplicationImageItem.fromLocal(data: data, fileName: fileName)
}
}
group.notify(queue: .main) { [weak self] in
guard let self else { return }
let items = loadedItems.compactMap { $0 }
guard !items.isEmpty else { return }
self.viewModel.addLocalImages(items)
Task { await self.startUploadIfNeeded() }
}
}
}