Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,261 @@
//
// 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()
override func viewDidLoad() {
super.viewDidLoad()
title = "实名认证"
view.backgroundColor = AppDesignUIKit.pageBackground
setupForm()
viewModel.onChange = { [weak self] in self?.applyViewModel() }
Task { await loadInfo() }
}
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)
}
private func makeSectionTitle(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 17, weight: .semibold)
return label
}
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
}
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)
}
}
private func loadInfo() async {
do {
try await appServices.globalLoading.withLoading(message: "加载中...") {
try await self.viewModel.load(api: self.appServices.profileAPI)
}
} catch {
showToast(error.localizedDescription)
}
}
@objc private func longValidChanged() {
viewModel.isLongValid = longValidSwitch.isOn
}
@objc private func sendCodeTapped() {
Task {
do {
try await viewModel.sendCode(api: appServices.profileAPI)
} catch {
showToast(error.localizedDescription)
}
}
}
@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
@objc private func pickFront() {
pendingSide = .front
presentImagePicker()
}
@objc private func pickBack() {
pendingSide = .back
presentImagePicker()
}
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 {
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)
}
}