256 lines
9.7 KiB
Swift
256 lines
9.7 KiB
Swift
//
|
||
// RealNameAuthViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import PhotosUI
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 实名认证页面,展示表单并允许提交基础实名资料。
|
||
final class RealNameAuthViewController: BaseViewController {
|
||
|
||
private let viewModel = RealNameAuthViewModel()
|
||
private let profileAPI = NetworkServices.shared.profileAPI
|
||
private let ossUploadService = NetworkServices.shared.ossUploadService
|
||
|
||
private let scrollView = UIScrollView()
|
||
private let contentStack = UIStackView()
|
||
private let realNameField = UITextField()
|
||
private let idCardField = UITextField()
|
||
private let smsCodeField = UITextField()
|
||
private let frontImageView = UIImageView()
|
||
private let backImageView = UIImageView()
|
||
private let longValidSwitch = UISwitch()
|
||
private let startDatePicker = UIDatePicker()
|
||
private let endDatePicker = UIDatePicker()
|
||
private let submitButton = AppButton(title: "提交审核")
|
||
|
||
override func setupNavigationBar() {
|
||
title = "实名认证"
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = 16
|
||
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
view.addSubview(submitButton)
|
||
|
||
[realNameField, idCardField, smsCodeField].forEach {
|
||
$0.borderStyle = .roundedRect
|
||
$0.font = .systemFont(ofSize: 16)
|
||
}
|
||
realNameField.placeholder = "请输入姓名"
|
||
idCardField.placeholder = "请输入身份证号码"
|
||
smsCodeField.placeholder = "请输入短信验证码"
|
||
smsCodeField.keyboardType = .numberPad
|
||
|
||
[frontImageView, backImageView].forEach {
|
||
$0.backgroundColor = UIColor(hex: 0xF8FAFC)
|
||
$0.contentMode = .scaleAspectFit
|
||
$0.layer.cornerRadius = 8
|
||
$0.clipsToBounds = true
|
||
$0.isUserInteractionEnabled = true
|
||
$0.snp.makeConstraints { make in make.height.equalTo(160) }
|
||
}
|
||
frontImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickFront)))
|
||
backImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(pickBack)))
|
||
|
||
startDatePicker.datePickerMode = .date
|
||
endDatePicker.datePickerMode = .date
|
||
if #available(iOS 17.0, *) {
|
||
startDatePicker.preferredDatePickerStyle = .compact
|
||
endDatePicker.preferredDatePickerStyle = .compact
|
||
}
|
||
|
||
let sendCodeButton = UIButton(type: .system)
|
||
sendCodeButton.setTitle("获取验证码", for: .normal)
|
||
sendCodeButton.addTarget(self, action: #selector(sendCodeTapped), for: .touchUpInside)
|
||
|
||
contentStack.addArrangedSubview(makeSectionTitle("身份证国徽面"))
|
||
contentStack.addArrangedSubview(backImageView)
|
||
contentStack.addArrangedSubview(makeSectionTitle("身份证人像面"))
|
||
contentStack.addArrangedSubview(frontImageView)
|
||
contentStack.addArrangedSubview(labeledField("姓名", realNameField))
|
||
contentStack.addArrangedSubview(labeledField("身份证号码", idCardField))
|
||
|
||
let longValidRow = UIStackView(arrangedSubviews: [UILabel(text: "长期有效"), longValidSwitch])
|
||
longValidRow.axis = .horizontal
|
||
contentStack.addArrangedSubview(longValidRow)
|
||
contentStack.addArrangedSubview(labeledField("起始日期", startDatePicker))
|
||
contentStack.addArrangedSubview(labeledField("结束日期", endDatePicker))
|
||
|
||
let smsRow = UIStackView(arrangedSubviews: [smsCodeField, sendCodeButton])
|
||
smsRow.axis = .horizontal
|
||
smsRow.spacing = 8
|
||
smsCodeField.snp.makeConstraints { make in make.width.equalTo(200) }
|
||
contentStack.addArrangedSubview(labeledField("短信验证码", smsRow))
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
scrollView.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview()
|
||
make.bottom.equalTo(submitButton.snp.top).offset(-12)
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(16)
|
||
make.width.equalTo(scrollView).offset(-32)
|
||
}
|
||
submitButton.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(16)
|
||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(14)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in self?.applyViewModel() }
|
||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||
longValidSwitch.addTarget(self, action: #selector(longValidChanged), for: .valueChanged)
|
||
realNameField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||
idCardField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||
smsCodeField.addTarget(self, action: #selector(fieldChanged), for: .editingChanged)
|
||
startDatePicker.addTarget(self, action: #selector(dateChanged), for: .valueChanged)
|
||
endDatePicker.addTarget(self, action: #selector(dateChanged), for: .valueChanged)
|
||
Task { await loadInfo() }
|
||
}
|
||
|
||
private func applyViewModel() {
|
||
realNameField.text = viewModel.realName
|
||
idCardField.text = viewModel.idCardNo
|
||
smsCodeField.text = viewModel.smsCode
|
||
longValidSwitch.isOn = viewModel.isLongValid
|
||
startDatePicker.date = viewModel.startDate
|
||
endDatePicker.date = viewModel.endDate
|
||
endDatePicker.isEnabled = !viewModel.isLongValid
|
||
submitButton.isEnabled = !viewModel.submitting
|
||
|
||
if let data = viewModel.pendingFrontImageData, let image = UIImage(data: data) {
|
||
frontImageView.image = image
|
||
} else {
|
||
frontImageView.loadRemoteImage(urlString: viewModel.frontUrl)
|
||
}
|
||
if let data = viewModel.pendingBackImageData, let image = UIImage(data: data) {
|
||
backImageView.image = image
|
||
} else {
|
||
backImageView.loadRemoteImage(urlString: viewModel.backUrl)
|
||
}
|
||
}
|
||
|
||
@objc private func fieldChanged() {
|
||
viewModel.updateRealName(realNameField.text ?? "")
|
||
viewModel.updateIdCardNo(idCardField.text ?? "")
|
||
viewModel.updateSmsCode(smsCodeField.text ?? "")
|
||
}
|
||
|
||
@objc private func dateChanged() {
|
||
viewModel.updateStartDate(startDatePicker.date)
|
||
viewModel.updateEndDate(endDatePicker.date)
|
||
}
|
||
|
||
@objc private func longValidChanged() {
|
||
viewModel.updateLongValid(longValidSwitch.isOn)
|
||
}
|
||
|
||
@objc private func sendCodeTapped() {
|
||
Task {
|
||
showLoading()
|
||
defer { hideLoading() }
|
||
do {
|
||
try await viewModel.sendCode(api: profileAPI)
|
||
showToast(viewModel.statusMessage ?? "验证码已发送")
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc private func submitTapped() {
|
||
Task { await submit() }
|
||
}
|
||
|
||
@objc private func pickFront() { pickImage(side: .front) }
|
||
@objc private func pickBack() { pickImage(side: .back) }
|
||
|
||
private var pickingSide: RealNameImageSide = .front
|
||
|
||
private func pickImage(side: RealNameImageSide) {
|
||
pickingSide = side
|
||
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||
configuration.filter = .images
|
||
configuration.selectionLimit = 1
|
||
let picker = PHPickerViewController(configuration: configuration)
|
||
picker.delegate = self
|
||
present(picker, animated: true)
|
||
}
|
||
|
||
private func loadInfo() async {
|
||
showLoading()
|
||
defer { hideLoading() }
|
||
do {
|
||
try await viewModel.load(api: profileAPI)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
private func submit() async {
|
||
let scenicId = AppStore.shared.currentScenicId
|
||
guard scenicId > 0 else {
|
||
showToast("当前景区信息缺失")
|
||
return
|
||
}
|
||
showLoading()
|
||
defer { hideLoading() }
|
||
do {
|
||
try await viewModel.submit(api: profileAPI, uploader: ossUploadService, scenicId: scenicId)
|
||
showToast("提交成功")
|
||
navigationController?.popViewController(animated: true)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
private func makeSectionTitle(_ text: String) -> UILabel {
|
||
let label = UILabel()
|
||
label.text = text
|
||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||
return label
|
||
}
|
||
|
||
private func labeledField(_ title: String, _ field: UIView) -> UIStackView {
|
||
let titleLabel = UILabel(text: title)
|
||
titleLabel.font = .systemFont(ofSize: 14)
|
||
let stack = UIStackView(arrangedSubviews: [titleLabel, field])
|
||
stack.axis = .vertical
|
||
stack.spacing = 8
|
||
return stack
|
||
}
|
||
}
|
||
|
||
extension RealNameAuthViewController: PHPickerViewControllerDelegate {
|
||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
picker.dismiss(animated: true)
|
||
guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { return }
|
||
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
|
||
guard let self, let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else { return }
|
||
Task { @MainActor in
|
||
do {
|
||
try self.viewModel.prepareIdentityImage(data: data, side: self.pickingSide)
|
||
} catch {
|
||
self.showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private extension UILabel {
|
||
convenience init(text: String) {
|
||
self.init()
|
||
self.text = text
|
||
}
|
||
}
|