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