Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user