// // FeedbackVC.swift // QuickLocation // import UIKit import RxSwift import RxCocoa final class FeedbackVC: BaseViewController { private enum FeedbackCategory: String, CaseIterable { case feature = "功能问题" case suggestion = "优化建议" case other = "其他" } private struct FeedbackDraft { let category: FeedbackCategory let detail: String let images: [UIImage] let contact: String } private var rootView: FeedbackView! private var selectedCategory: FeedbackCategory = .feature private var selectedImages: [UIImage] = [] private var pendingDraft: FeedbackDraft? private var isSubmitting = false private let contextText: String init(contextText: String = "") { self.contextText = contextText.trimmingCharacters(in: .whitespacesAndNewlines) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { rootView = FeedbackView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() bind() } private func bind() { rootView.navView.onBackTapped = { [weak self] in guard self?.isSubmitting == false else { return } self?.navigationController?.popViewController(animated: true) } rootView.categoryButtons.forEach { button in button.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside) } rootView.detailTextView.rx.text.orEmpty .subscribe(onNext: { [weak self] text in self?.handleDetailChange(text) }) .disposed(by: disposeBag) rootView.contactTextField.rx.controlEvent(.editingDidEndOnExit) .subscribe(onNext: { [weak self] in self?.rootView.contactTextField.resignFirstResponder() }) .disposed(by: disposeBag) rootView.onAddImage = { [weak self] in self?.showAlbum() } rootView.onDeleteImage = { [weak self] index in guard let self, self.selectedImages.indices.contains(index) else { return } self.selectedImages.remove(at: index) self.rootView.updateImages(self.selectedImages) } rootView.saveBtn.rx.tap .subscribe(onNext: { [weak self] in self?.submit() }) .disposed(by: disposeBag) } @objc private func categoryTapped(_ sender: UIButton) { let categories = FeedbackCategory.allCases guard categories.indices.contains(sender.tag) else { return } let category = categories[sender.tag] selectedCategory = category rootView.updateCategory(selectedIndex: sender.tag) } private func handleDetailChange(_ text: String) { let limitedText = text.count > 200 ? String(text.prefix(200)) : text if limitedText != text { rootView.detailTextView.text = limitedText rootView.detailTextView.selectedRange = NSRange(location: limitedText.count, length: 0) } rootView.updateDetail(text: limitedText) } private func showAlbum() { let remainingCount = 3 - selectedImages.count guard remainingCount > 0, !isSubmitting else { return } let picker = PhotoPickerVC(maxCount: remainingCount) picker.onConfirm = { [weak self] images in guard let self else { return } self.selectedImages.append(contentsOf: images.prefix(remainingCount)) self.selectedImages = Array(self.selectedImages.prefix(3)) self.rootView.updateImages(self.selectedImages) } present(picker, animated: true) } private func submit() { guard !isSubmitting else { return } view.endEditing(true) let detail = (rootView.detailTextView.text ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) guard !detail.isEmpty else { DLToast.show(text: "请补充详细问题或意见") return } pendingDraft = FeedbackDraft( category: selectedCategory, detail: detail, images: selectedImages, contact: (rootView.contactTextField.text ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) ) isSubmitting = true rootView.setSubmitting(true) guard let draft = pendingDraft else { return } let uploadRequests: [Observable] do { uploadRequests = try draft.images.map { image in guard let data = compressedJPEG(from: image) else { throw feedbackError("图片处理失败,请重新选择") } return UploadService.uploadURL(data, kind: .jpeg, scene: "feedback") } } catch { finishSubmission(error: error) return } DLToast.showLoading() let imageURLs = uploadRequests.isEmpty ? Observable.just([String]()) : Observable.zip(uploadRequests) imageURLs .flatMap { [weak self] urls -> Observable in guard let self else { return .empty() } var type = draft.category.rawValue if !self.contextText.isEmpty { type += "(\(self.contextText))" } return UserService.feedback( type: type, content: draft.detail, contact: draft.contact, images: urls ) } .subscribe(onNext: { [weak self] _ in self?.showSuccess() }, onError: { [weak self] error in self?.finishSubmission(error: error) }) .disposed(by: disposeBag) } private func showSuccess() { DLToast.dismiss() let successView = FeedbackSuccessView() successView.show(in: view) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak successView] in guard let successView else { return } successView.dismiss { AppRouter.shared.popOrDismiss() } } } private func finishSubmission(error: Error) { DLToast.dismiss() pendingDraft = nil isSubmitting = false rootView.setSubmitting(false) DLToast.show(text: error.localizedDescription) } private func compressedJPEG(from image: UIImage) -> Data? { let maxEdge: CGFloat = 1280 let longest = max(image.size.width, image.size.height) let output: UIImage if longest > maxEdge, longest > 0 { let scale = maxEdge / longest let size = CGSize( width: (image.size.width * scale).rounded(), height: (image.size.height * scale).rounded() ) let format = UIGraphicsImageRendererFormat.default() format.scale = 1 format.opaque = true output = UIGraphicsImageRenderer(size: size, format: format).image { _ in image.draw(in: CGRect(origin: .zero, size: size)) } } else { output = image } return output.jpegData(compressionQuality: 0.65) ?? image.jpegData(compressionQuality: 0.8) } private func feedbackError(_ message: String) -> NSError { NSError( domain: "Feedback", code: -1, userInfo: [NSLocalizedDescriptionKey: message] ) } }