jsdw_ios/QuickLocation/Section/Mine/Feedback/FeedbackVC.swift

234 lines
7.6 KiB
Swift

//
// 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<String>]
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<ResponseModel> 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]
)
}
}