// // 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) } }