// // ProfileViewController.swift // suixinkan // import PhotosUI import SnapKit import UIKit // MARK: - Diffable 标识 private typealias ProfileTableSection = Int private typealias ProfileTableRow = String /// 个人信息页 section 索引。 private enum ProfileSectionID { static let info = 0 static let logout = 1 } /// 个人信息页行标识。 private enum ProfileRowID { static let name = "profile:name" static let account = "profile:account" static let phone = "profile:phone" static let realName = "profile:realName" static let settings = "profile:settings" static let logout = "profile:logout" } /// 个人信息页 value1 样式 Cell,供 Diffable reconfigure 正确复用。 private final class ProfileValueTableViewCell: UITableViewCell { /// 初始化 value1 样式 Cell。 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .value1, reuseIdentifier: reuseIdentifier) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private enum ProfileCellReuseID { static let value = "profile.value" static let logout = "profile.logout" } /// 个人信息页,展示用户资料、账号状态和设置入口。 final class ProfileViewController: UIViewController, UITableViewDelegate { 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.delegate = self return table }() private lazy var refreshControl = UIRefreshControl() /// Diffable 数据源,驱动个人信息列表刷新。 private var tableDataSource: UITableViewDiffableDataSource! /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = "个人信息" view.backgroundColor = UIColor(hex: 0xF7FAFF) setupHeader() configureTableDataSource() 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) } } /// 配置 Diffable 数据源。 private func configureTableDataSource() { tableView.register(ProfileValueTableViewCell.self, forCellReuseIdentifier: ProfileCellReuseID.value) tableView.register(UITableViewCell.self, forCellReuseIdentifier: ProfileCellReuseID.logout) tableDataSource = UITableViewDiffableDataSource( tableView: tableView ) { [weak self] (tableView: UITableView, indexPath: IndexPath, row: ProfileTableRow) -> UITableViewCell? in guard let self else { return UITableViewCell() } switch row { case ProfileRowID.logout: let cell = tableView.dequeueReusableCell(withIdentifier: ProfileCellReuseID.logout, for: indexPath) cell.textLabel?.text = "退出登录" cell.textLabel?.textColor = UIColor(hex: 0xEF4444) cell.textLabel?.textAlignment = .center cell.selectionStyle = .default cell.accessoryType = .none return cell default: let cell = tableView.dequeueReusableCell( withIdentifier: ProfileCellReuseID.value, for: indexPath ) cell.textLabel?.textAlignment = .natural cell.textLabel?.textColor = .label cell.accessoryType = .none cell.selectionStyle = .none switch row { case ProfileRowID.name: cell.textLabel?.text = "姓名" cell.detailTextLabel?.text = self.viewModel.displayRealName case ProfileRowID.account: cell.textLabel?.text = "当前账号" cell.detailTextLabel?.text = self.appServices.accountContext.profile?.displayName ?? "--" cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default case ProfileRowID.phone: cell.textLabel?.text = "手机号" cell.detailTextLabel?.text = self.viewModel.displayPhone case ProfileRowID.realName: cell.textLabel?.text = "实名认证" cell.detailTextLabel?.text = self.viewModel.realNameStatusText cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default case ProfileRowID.settings: cell.textLabel?.text = "设置" cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default default: break } return cell } } applyTableSnapshot(animated: false) } /// 构建 Diffable snapshot。 private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([ProfileSectionID.info, ProfileSectionID.logout]) snapshot.appendItems([ ProfileRowID.name, ProfileRowID.account, ProfileRowID.phone, ProfileRowID.realName, ProfileRowID.settings ], toSection: ProfileSectionID.info) snapshot.appendItems([ProfileRowID.logout], toSection: ProfileSectionID.logout) return snapshot } /// 应用 snapshot 刷新列表,必要时重配 Cell 内容。 private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) { var snapshot = buildTableSnapshot() if reconfigure { snapshot.reconfigureItems(snapshot.itemIdentifiers) } tableDataSource.apply(snapshot, animatingDifferences: animated) } /// 初始化Header相关 UI 或状态。 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) } } /// apply视图模型相关逻辑。 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 applyTableSnapshot(animated: false, reconfigure: true) } /// 刷新Pulled展示。 @objc private func refreshPulled() { Task { await reloadProfile(showToast: true) refreshControl.endRefreshing() } } /// 刷新Profile。 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) } } } /// 点击edit的处理逻辑。 @objc private func editTapped() { if viewModel.isEditingProfile { Task { await saveProfileEdits() } } else { viewModel.beginEditing() presentNicknameEditor() } } /// 弹出NicknameEditor页面。 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) } /// save个人中心Edits相关逻辑。 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) } } /// pick头像相关逻辑。 @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) } /// 点击logout的处理逻辑。 @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, appNavigator: self.appServices.appNavigator, toastCenter: self.appServices.toastCenter ) }) present(alert, animated: true) } /// UITableView 代理:处理行选中。 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let row = tableDataSource.itemIdentifier(for: indexPath) else { return } switch row { case ProfileRowID.logout: logoutTapped() case ProfileRowID.account: appServices.appNavigator.push(.profile(.accountSwitch), from: self) case ProfileRowID.realName: appServices.appNavigator.push(.profile(.realNameAuth), from: self) case ProfileRowID.settings: appServices.appNavigator.push(.profile(.settings), from: self) default: break } } } extension ProfileViewController: 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.prepareAvatarImage(data: data) } catch { self.showToast(error.localizedDescription) } } } } }