234 lines
7.6 KiB
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]
|
|
)
|
|
}
|
|
}
|