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,263 @@
//
// AccountSwitchViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class AccountSwitchViewController: UIViewController {
private let viewModel = AccountSwitchViewModel()
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .plain)
table.backgroundColor = UIColor(hex: 0xF5F7FB)
table.separatorStyle = .none
table.dataSource = self
table.delegate = self
table.register(AccountSwitchCell.self, forCellReuseIdentifier: AccountSwitchCell.reuseID)
return table
}()
private lazy var confirmButton: UIButton = {
let button = makePrimaryButton(title: "确认切换")
button.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "账号切换"
view.backgroundColor = UIColor(hex: 0xF5F7FB)
view.addSubview(tableView)
view.addSubview(confirmButton)
tableView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.bottom.equalTo(confirmButton.snp.top).offset(-12)
}
confirmButton.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(14)
make.height.equalTo(50)
}
viewModel.onChange = { [weak self] in
self?.tableView.reloadData()
self?.updateConfirmButton()
}
Task { await loadAccounts() }
}
private func updateConfirmButton() {
let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching
confirmButton.isEnabled = enabled
confirmButton.alpha = enabled ? 1 : 0.5
}
private func loadAccounts(force: Bool = false) async {
do {
try await appServices.globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") {
try await self.viewModel.load(
api: self.appServices.profileAPI,
force: force,
currentAccountId: self.currentAccountId
)
}
} catch {
showToast(error.localizedDescription)
}
}
private var currentAccountId: String? {
guard let current = appServices.accountContext.profile else { return nil }
if let store = appServices.accountContext.currentStore {
return "\(V9StoreUser.accountTypeValue)_\(store.id)"
}
if let scenic = appServices.accountContext.currentScenic {
return "\(V9ScenicUser.accountTypeValue)_\(scenic.id)"
}
return current.userId.isEmpty ? nil : current.userId
}
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {
viewModel.isCurrent(account, currentAccountId: currentAccountId)
}
@objc private func confirmTapped() {
guard let account = viewModel.selectedAccount else { return }
if account.isCurrent || isCurrentAccount(account) {
navigationController?.popViewController(animated: true)
return
}
Task { await switchAccount(account) }
}
private func switchAccount(_ account: AccountSwitchAccount) async {
do {
let response = try await appServices.globalLoading.withLoading(message: "切换中...") {
try await self.viewModel.switchAccount(account, api: self.appServices.authAPI)
}
let username = nonEmpty(appServices.accountContext.profile?.phone)
?? nonEmpty(account.phone)
?? ""
try await appServices.authSessionCoordinator.completeLogin(
with: response,
username: username,
privacyAgreementAccepted: appServices.authSessionCoordinator.loginPreferences().privacyAgreementAccepted,
appSession: appServices.appSession,
accountContext: appServices.accountContext,
permissionContext: appServices.permissionContext,
profileAPI: appServices.profileAPI,
accountContextAPI: appServices.accountContextAPI
)
appServices.appRouter.reset()
showToast("账号已切换")
navigationController?.popToRootViewController(animated: true)
} catch {
showToast(error.localizedDescription)
}
}
private func nonEmpty(_ value: String?) -> String? {
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return text.isEmpty ? nil : text
}
}
extension AccountSwitchViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.accounts.isEmpty && !viewModel.loading ? 1 : viewModel.accounts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard !viewModel.accounts.isEmpty else {
let cell = UITableViewCell()
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
let empty = makeEmptyStateView(
title: "暂无可切换账号",
message: "当前登录账号下没有其他可切换账号。",
systemImage: "person.crop.circle.badge.exclamationmark"
)
cell.contentView.addSubview(empty)
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) }
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
let account = viewModel.accounts[indexPath.row]
cell.configure(
account: account,
selected: viewModel.selectedAccountId == account.id,
isCurrent: account.isCurrent || isCurrentAccount(account)
)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.row < viewModel.accounts.count else { return }
viewModel.select(viewModel.accounts[indexPath.row])
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
viewModel.accounts.isEmpty ? 280 : 92
}
}
private final class AccountSwitchCell: UITableViewCell {
static let reuseID = "AccountSwitchCell"
private let avatarView = UIImageView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let phoneLabel = UILabel()
private let tagLabel = UILabel()
private let checkView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = 12
contentView.addSubview(card)
avatarView.layer.cornerRadius = 25
avatarView.clipsToBounds = true
avatarView.contentMode = .scaleAspectFill
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
subtitleLabel.font = .systemFont(ofSize: 13)
subtitleLabel.textColor = AppDesignUIKit.textSecondary
phoneLabel.font = .systemFont(ofSize: 12)
phoneLabel.textColor = UIColor(hex: 0x9AA1AA)
tagLabel.font = .systemFont(ofSize: 11, weight: .semibold)
tagLabel.textAlignment = .center
tagLabel.layer.cornerRadius = 4
tagLabel.clipsToBounds = true
card.addSubview(avatarView)
card.addSubview(titleLabel)
card.addSubview(subtitleLabel)
card.addSubview(phoneLabel)
card.addSubview(tagLabel)
card.addSubview(checkView)
card.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
}
avatarView.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview().inset(14)
make.width.height.equalTo(50)
}
titleLabel.snp.makeConstraints { make in
make.leading.equalTo(avatarView.snp.trailing).offset(13)
make.top.equalTo(avatarView).offset(2)
make.trailing.lessThanOrEqualTo(tagLabel.snp.leading).offset(-8)
}
subtitleLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(4)
}
phoneLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel)
make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
}
tagLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(14)
make.top.equalToSuperview().offset(14)
make.height.equalTo(22)
make.width.greaterThanOrEqualTo(40)
}
checkView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(14)
make.bottom.equalToSuperview().inset(14)
make.width.height.equalTo(22)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) {
let title = account.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title.isEmpty ? account.accountTypeLabel : title
subtitleLabel.text = account.subtitle
phoneLabel.text = account.phone
tagLabel.text = account.isStoreUser ? "门店" : "景区"
tagLabel.textColor = account.isStoreUser ? UIColor(hex: 0x0F9F6E) : UIColor(hex: 0x7C3AED)
tagLabel.backgroundColor = account.isStoreUser ? UIColor(hex: 0xE8F8F1) : UIColor(hex: 0xF3ECFF)
checkView.image = UIImage(systemName: selected ? "checkmark.circle.fill" : "circle")
checkView.tintColor = selected ? AppDesignUIKit.primary : UIColor(hex: 0xB6BECA)
avatarView.loadRemoteAvatar(urlString: account.avatar)
if isCurrent {
titleLabel.text = (titleLabel.text ?? "") + " (当前)"
}
}
}

View File

@ -0,0 +1,278 @@
//
// ProfileViewController.swift
// suixinkan
//
import PhotosUI
import SnapKit
import UIKit
///
final class ProfileViewController: UIViewController {
private let viewModel = ProfileViewModel()
private let avatarImageView = UIImageView()
private let nicknameLabel = UILabel()
private let uidLabel = UILabel()
private let editButton = UIButton(type: .system)
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.dataSource = self
table.delegate = self
return table
}()
private lazy var refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
title = "个人信息"
view.backgroundColor = UIColor(hex: 0xF7FAFF)
setupHeader()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(150)
make.leading.trailing.bottom.equalToSuperview()
}
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
tableView.refreshControl = refreshControl
viewModel.onChange = { [weak self] in self?.applyViewModel() }
editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside)
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(pickAvatar))
avatarImageView.isUserInteractionEnabled = true
avatarImageView.addGestureRecognizer(avatarTap)
Task { await reloadProfile(showToast: false) }
}
private func setupHeader() {
let header = UIView()
header.backgroundColor = .white
header.layer.cornerRadius = 17
view.addSubview(header)
avatarImageView.layer.cornerRadius = 43
avatarImageView.clipsToBounds = true
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.backgroundColor = AppDesignUIKit.primarySoft
nicknameLabel.font = .systemFont(ofSize: 24, weight: .semibold)
nicknameLabel.textColor = UIColor(hex: 0x252525)
uidLabel.font = .systemFont(ofSize: 18, weight: .semibold)
uidLabel.textColor = UIColor(hex: 0x606A7A)
editButton.setImage(UIImage(systemName: "pencil"), for: .normal)
editButton.tintColor = .white
editButton.backgroundColor = AppDesignUIKit.primary
editButton.layer.cornerRadius = 22
header.addSubview(avatarImageView)
header.addSubview(nicknameLabel)
header.addSubview(uidLabel)
header.addSubview(editButton)
header.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(16)
make.leading.trailing.equalToSuperview().inset(19)
make.height.equalTo(132)
}
avatarImageView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(18)
make.centerY.equalToSuperview()
make.width.height.equalTo(86)
}
nicknameLabel.snp.makeConstraints { make in
make.leading.equalTo(avatarImageView.snp.trailing).offset(14)
make.trailing.lessThanOrEqualTo(editButton.snp.leading).offset(-8)
make.top.equalTo(avatarImageView).offset(8)
}
uidLabel.snp.makeConstraints { make in
make.leading.equalTo(nicknameLabel)
make.top.equalTo(nicknameLabel.snp.bottom).offset(8)
}
editButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(18)
make.centerY.equalToSuperview()
make.width.height.equalTo(44)
}
}
private func applyViewModel() {
nicknameLabel.text = viewModel.displayNickname
uidLabel.text = "UID: \(appServices.accountContext.profile?.userId ?? "--")"
if let data = viewModel.pendingAvatarData, let image = UIImage(data: data) {
avatarImageView.image = image
} else {
avatarImageView.loadRemoteAvatar(urlString: viewModel.displayAvatarURL)
}
editButton.setImage(UIImage(systemName: viewModel.isEditingProfile ? "checkmark" : "pencil"), for: .normal)
editButton.isEnabled = !viewModel.isSaving
tableView.reloadData()
}
@objc private func refreshPulled() {
Task {
await reloadProfile(showToast: true)
refreshControl.endRefreshing()
}
}
private func reloadProfile(showToast: Bool) async {
do {
try await appServices.globalLoading.withOptionalLoading(!showToast && viewModel.userInfo == nil, message: "加载资料...") {
try await self.viewModel.reload(api: self.appServices.profileAPI)
}
} catch {
if showToast { self.showToast(error.localizedDescription) }
}
}
@objc private func editTapped() {
if viewModel.isEditingProfile {
Task { await saveProfileEdits() }
} else {
viewModel.beginEditing()
presentNicknameEditor()
}
}
private func presentNicknameEditor() {
let alert = UIAlertController(title: "编辑昵称", message: nil, preferredStyle: .alert)
alert.addTextField { [weak self] field in
field.text = self?.viewModel.editingNickname
field.placeholder = "请输入昵称"
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
self?.viewModel.cancelEditing()
})
alert.addAction(UIAlertAction(title: "保存", style: .default) { [weak self] _ in
guard let self else { return }
self.viewModel.editingNickname = alert.textFields?.first?.text ?? ""
Task { await self.saveProfileEdits() }
})
present(alert, animated: true)
}
private func saveProfileEdits() async {
guard let scenicId = appServices.accountContext.currentScenic?.id else {
showToast("请先选择景区")
return
}
do {
try await appServices.globalLoading.withLoading(message: "保存中...") {
try await self.viewModel.saveProfile(
api: self.appServices.profileAPI,
uploader: self.appServices.ossUploadService,
scenicId: scenicId
)
}
showToast("资料已更新")
} catch {
showToast(error.localizedDescription)
}
}
@objc private func pickAvatar() {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
@objc private func logoutTapped() {
let alert = UIAlertController(title: "确认退出当前账号?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "退出登录", style: .destructive) { [weak self] _ in
guard let self else { return }
self.appServices.authSessionCoordinator.logout(
appSession: self.appServices.appSession,
accountContext: self.appServices.accountContext,
permissionContext: self.appServices.permissionContext,
scenicSpotContext: self.appServices.scenicSpotContext,
appRouter: self.appServices.appRouter,
toastCenter: self.appServices.toastCenter
)
})
present(alert, animated: true)
}
}
extension ProfileViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int { 2 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 5 : 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
if indexPath.section == 1 {
cell.textLabel?.text = "退出登录"
cell.textLabel?.textColor = UIColor(hex: 0xEF4444)
cell.textLabel?.textAlignment = .center
return cell
}
switch indexPath.row {
case 0:
cell.textLabel?.text = "姓名"
cell.detailTextLabel?.text = viewModel.displayRealName
case 1:
cell.textLabel?.text = "当前账号"
cell.detailTextLabel?.text = appServices.accountContext.profile?.displayName ?? "--"
cell.accessoryType = .disclosureIndicator
case 2:
cell.textLabel?.text = "手机号"
cell.detailTextLabel?.text = viewModel.displayPhone
case 3:
cell.textLabel?.text = "实名认证"
cell.detailTextLabel?.text = viewModel.realNameStatusText
cell.accessoryType = .disclosureIndicator
default:
cell.textLabel?.text = "设置"
cell.accessoryType = .disclosureIndicator
}
cell.selectionStyle = indexPath.row == 0 || indexPath.row == 2 ? .none : .default
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 1 {
logoutTapped()
return
}
switch indexPath.row {
case 1:
HomeMenuRouting.pushProfile(.accountSwitch, from: self)
case 3:
HomeMenuRouting.pushProfile(.realNameAuth, from: self)
case 4:
HomeMenuRouting.pushProfile(.settings, from: self)
default:
break
}
}
}
extension ProfileViewController: 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.prepareAvatarImage(data: data)
} catch {
self.showToast(error.localizedDescription)
}
}
}
}
}

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

View File

@ -0,0 +1,135 @@
//
// SettingsViewControllers.swift
// suixinkan
//
import SnapKit
import UIKit
import WebKit
///
final class SettingsViewController: UIViewController {
private var copiedDownloadLink = false
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.dataSource = self
table.delegate = self
return table
}()
private let rows: [(title: String, action: SettingsRowAction)] = [
("关于我们", .agreement(.about)),
("系统版本", .version),
("App下载", .download),
("用户协议", .agreement(.userAgreement)),
("隐私政策", .agreement(.privacyPolicy))
]
private enum SettingsRowAction {
case agreement(AgreementPage)
case version
case download
}
override func viewDidLoad() {
super.viewDidLoad()
title = "设置"
view.addSubview(tableView)
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
}
private var downloadLink: String {
APIEnvironment.current.baseURL.appending(path: "/h5/app/download").absoluteString
}
private func copyDownloadLink() {
UIPasteboard.general.string = downloadLink
copiedDownloadLink = true
showToast("下载链接已复制")
tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .none)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
self?.copiedDownloadLink = false
self?.tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .none)
}
}
}
extension SettingsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { rows.count }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
let row = rows[indexPath.row]
cell.textLabel?.text = row.title
cell.textLabel?.textColor = UIColor(hex: 0x4B5563)
switch row.action {
case .version:
cell.detailTextLabel?.text = SettingsDisplayPolicy.versionText()
cell.selectionStyle = .none
case .download:
cell.detailTextLabel?.text = copiedDownloadLink ? "已复制" : "复制链接"
cell.detailTextLabel?.textColor = AppDesignUIKit.primary
case .agreement:
cell.accessoryType = .disclosureIndicator
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch rows[indexPath.row].action {
case .agreement(let page):
HomeMenuRouting.pushProfile(.agreement(page), from: self)
case .download:
copyDownloadLink()
case .version:
break
}
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
"Copyright © 2025 All Rights Reserved\n苏ICP备2025157647号"
}
}
/// H5
final class AgreementViewController: UIViewController {
private let page: AgreementPage
private let webView = WKWebView(frame: .zero)
private let loadingIndicator = UIActivityIndicatorView(style: .large)
init(page: AgreementPage) {
self.page = page
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
title = page.title
view.backgroundColor = AppDesignUIKit.pageBackground
webView.navigationDelegate = self
view.addSubview(webView)
view.addSubview(loadingIndicator)
webView.snp.makeConstraints { make in make.edges.equalToSuperview() }
loadingIndicator.snp.makeConstraints { make in make.center.equalToSuperview() }
loadingIndicator.startAnimating()
webView.load(URLRequest(url: page.url))
}
}
extension AgreementViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
loadingIndicator.stopAnimating()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
loadingIndicator.stopAnimating()
showToast(error.localizedDescription)
}
}