Files
suixinkan_ios_uikit/suixinkan_ios/Features/Profile/ViewControllers/ProfileViewController.swift
2026-06-26 14:33:31 +08:00

279 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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