Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
10 KiB
Swift
276 lines
10 KiB
Swift
//
|
||
// RealNameAuthViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import PhotosUI
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 实名认证页面,展示审核状态并允许提交基础实名资料。
|
||
final class RealNameAuthViewController: UIViewController {
|
||
|
||
private let viewModel = RealNameAuthViewModel()
|
||
|
||
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 statusLabel = UILabel()
|
||
private let longValidSwitch = UISwitch()
|
||
private let startDatePicker = UIDatePicker()
|
||
private let endDatePicker = UIDatePicker()
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "实名认证"
|
||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||
setupForm()
|
||
viewModel.onChange = { [weak self] in self?.applyViewModel() }
|
||
Task { await loadInfo() }
|
||
}
|
||
|
||
/// 初始化Form相关 UI 或状态。
|
||
private func setupForm() {
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = 16
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(16)
|
||
make.width.equalTo(scrollView).offset(-32)
|
||
}
|
||
|
||
[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)
|
||
|
||
let submitButton = makePrimaryButton(title: "下一步")
|
||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||
|
||
statusLabel.numberOfLines = 0
|
||
statusLabel.font = .systemFont(ofSize: 13)
|
||
statusLabel.textColor = AppDesignUIKit.textSecondary
|
||
|
||
contentStack.addArrangedSubview(makeSectionTitle("审核状态"))
|
||
contentStack.addArrangedSubview(statusLabel)
|
||
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
|
||
longValidSwitch.addTarget(self, action: #selector(longValidChanged), for: .valueChanged)
|
||
contentStack.addArrangedSubview(longValidRow)
|
||
contentStack.addArrangedSubview(startDatePicker)
|
||
contentStack.addArrangedSubview(endDatePicker)
|
||
|
||
let smsRow = UIStackView(arrangedSubviews: [smsCodeField, sendCodeButton])
|
||
smsRow.axis = .horizontal
|
||
smsRow.spacing = 8
|
||
smsCodeField.snp.makeConstraints { make in make.height.equalTo(44) }
|
||
contentStack.addArrangedSubview(smsRow)
|
||
contentStack.addArrangedSubview(submitButton)
|
||
}
|
||
|
||
/// 创建SectionTitle实例。
|
||
private func makeSectionTitle(_ text: String) -> UILabel {
|
||
let label = UILabel()
|
||
label.text = text
|
||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||
return label
|
||
}
|
||
|
||
/// labeled字段相关逻辑。
|
||
private func labeledField(_ title: String, _ field: UITextField) -> UIStackView {
|
||
let label = UILabel()
|
||
label.text = title
|
||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||
let stack = UIStackView(arrangedSubviews: [label, field])
|
||
stack.axis = .vertical
|
||
stack.spacing = 8
|
||
field.snp.makeConstraints { make in make.height.equalTo(44) }
|
||
return stack
|
||
}
|
||
|
||
/// apply视图模型相关逻辑。
|
||
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
|
||
|
||
if let data = viewModel.pendingFrontImageData, let image = UIImage(data: data) {
|
||
frontImageView.image = image
|
||
} else if !viewModel.frontUrl.isEmpty {
|
||
frontImageView.loadRemoteImage(urlString: viewModel.frontUrl, contentMode: .scaleAspectFit)
|
||
}
|
||
|
||
if let data = viewModel.pendingBackImageData, let image = UIImage(data: data) {
|
||
backImageView.image = image
|
||
} else if !viewModel.backUrl.isEmpty {
|
||
backImageView.image = nil
|
||
backImageView.loadRemoteImage(urlString: viewModel.backUrl, contentMode: .scaleAspectFit)
|
||
}
|
||
|
||
if let info = viewModel.info {
|
||
statusLabel.text = """
|
||
审核状态:\(viewModel.auditStatusText(info.auditStatus))
|
||
审核人:\(info.auditor?.name ?? "--")
|
||
审核时间:\(info.auditAt ?? "--")
|
||
审核备注:\(info.rejectReason ?? "--")
|
||
"""
|
||
} else {
|
||
statusLabel.text = "尚未提交实名认证"
|
||
}
|
||
|
||
if let message = viewModel.statusMessage {
|
||
showToast(message)
|
||
}
|
||
}
|
||
|
||
/// 加载Info数据。
|
||
private func loadInfo() async {
|
||
do {
|
||
try await appServices.globalLoading.withLoading(message: "加载中...") {
|
||
try await self.viewModel.load(api: self.appServices.profileAPI)
|
||
}
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// longValid变更相关逻辑。
|
||
@objc private func longValidChanged() {
|
||
viewModel.isLongValid = longValidSwitch.isOn
|
||
}
|
||
|
||
/// 点击send验证码的处理逻辑。
|
||
@objc private func sendCodeTapped() {
|
||
Task {
|
||
do {
|
||
try await viewModel.sendCode(api: appServices.profileAPI)
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 提交Tapped。
|
||
@objc private func submitTapped() {
|
||
viewModel.realName = realNameField.text ?? ""
|
||
viewModel.idCardNo = idCardField.text ?? ""
|
||
viewModel.smsCode = smsCodeField.text ?? ""
|
||
viewModel.startDate = startDatePicker.date
|
||
viewModel.endDate = endDatePicker.date
|
||
|
||
guard let scenicId = appServices.accountContext.currentScenic?.id else {
|
||
showToast("请先选择景区")
|
||
return
|
||
}
|
||
Task {
|
||
do {
|
||
try await appServices.globalLoading.withLoading(message: "提交中...") {
|
||
try await self.viewModel.submit(
|
||
api: self.appServices.profileAPI,
|
||
uploader: self.appServices.ossUploadService,
|
||
scenicId: scenicId
|
||
)
|
||
}
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var pendingSide: RealNameImageSide = .front
|
||
|
||
/// pickFront相关逻辑。
|
||
@objc private func pickFront() {
|
||
pendingSide = .front
|
||
presentImagePicker()
|
||
}
|
||
|
||
/// pickBack相关逻辑。
|
||
@objc private func pickBack() {
|
||
pendingSide = .back
|
||
presentImagePicker()
|
||
}
|
||
|
||
/// 弹出ImagePicker页面。
|
||
private func presentImagePicker() {
|
||
var config = PHPickerConfiguration()
|
||
config.filter = .images
|
||
config.selectionLimit = 1
|
||
let picker = PHPickerViewController(configuration: config)
|
||
picker.delegate = self
|
||
present(picker, animated: true)
|
||
}
|
||
}
|
||
|
||
extension RealNameAuthViewController: PHPickerViewControllerDelegate {
|
||
/// picker 业务逻辑。
|
||
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 }
|
||
DispatchQueue.main.async {
|
||
do {
|
||
try self.viewModel.prepareIdentityImage(data: data, side: self.pendingSide)
|
||
} catch {
|
||
self.showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private extension UILabel {
|
||
/// 初始化实例。
|
||
convenience init(text: String) {
|
||
self.init()
|
||
self.text = text
|
||
self.font = .systemFont(ofSize: 16, weight: .semibold)
|
||
}
|
||
}
|