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