// // 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.session.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 } }