// // ProfileViewController.swift // suixinkan // import SnapKit import UIKit /// 我的 Tab 根页面,展示个人信息、账号状态与子流程入口。 final class ProfileViewController: BaseViewController { private let viewModel = ProfileViewModel() private let profileAPI = NetworkServices.shared.profileAPI private let scrollView = UIScrollView() private let contentStack = UIStackView() private let refreshControl = UIRefreshControl() private let avatarImageView = UIImageView() private let nicknameField = UITextField() private let uidLabel = UILabel() private let editButton = AppButton(title: "编辑", style: .primary) private let infoCard = UIStackView() private let nameRow = ProfileInfoRowView(title: "姓名") private let accountRow = ProfileInfoRowView(title: "当前账号") private let phoneRow = ProfileInfoRowView(title: "手机号") private let withdrawalRow = ProfileInfoRowView(title: "提现设置") private let passwordRow = ProfileInfoRowView(title: "修改密码") private let realNameRow = ProfileInfoRowView(title: "认证状态") private let statusRow = ProfileInfoRowView(title: "账号状态") private let scenicRow = ProfileInfoRowView(title: "当前景区") private let settingRow = ProfileInfoRowView(title: "设置中心") private let logoutButton = UIButton(type: .system) private var hasLoadedProfileOnce = false override func setupNavigationBar() { title = "我的" } override func setupUI() { view.backgroundColor = UIColor(hex: 0xF7FAFF) scrollView.alwaysBounceVertical = true scrollView.refreshControl = refreshControl view.addSubview(scrollView) scrollView.addSubview(contentStack) contentStack.axis = .vertical contentStack.spacing = 24 contentStack.layoutMargins = UIEdgeInsets(top: 16, left: 15, bottom: 24, right: 15) contentStack.isLayoutMarginsRelativeArrangement = true setupHeader() setupInfoCard() setupLogoutButton() contentStack.addArrangedSubview(headerContainer) contentStack.addArrangedSubview(infoCardWrapper) contentStack.addArrangedSubview(logoutButton) } override func setupConstraints() { scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() } contentStack.snp.makeConstraints { make in make.edges.equalToSuperview() make.width.equalTo(scrollView.snp.width) } logoutButton.snp.makeConstraints { make in make.height.equalTo(46) } } override func bindActions() { viewModel.onStateChange = { [weak self] in Task { @MainActor in self?.applyViewModel() } } refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged) editButton.addTarget(self, action: #selector(editTapped), for: .touchUpInside) logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside) accountRow.addTarget(self, action: #selector(accountSwitchTapped), for: .touchUpInside) passwordRow.addTarget(self, action: #selector(changePasswordTapped), for: .touchUpInside) withdrawalRow.addTarget(self, action: #selector(withdrawalTapped), for: .touchUpInside) realNameRow.addTarget(self, action: #selector(realNameTapped), for: .touchUpInside) settingRow.addTarget(self, action: #selector(settingTapped), for: .touchUpInside) NotificationCenter.default.addObserver( self, selector: #selector(handleAccountDidSwitch), name: NotificationName.accountDidSwitch, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(handleProfileDidUpdate), name: NotificationName.userProfileDidUpdate, object: nil ) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let showLoading = !hasLoadedProfileOnce Task { await reloadProfile(showGlobalLoading: showLoading) if showLoading { hasLoadedProfileOnce = true } } } private let headerContainer = UIView() private let infoCardWrapper = UIView() private func setupHeader() { avatarImageView.layer.cornerRadius = 43 avatarImageView.clipsToBounds = true avatarImageView.contentMode = .scaleAspectFill nicknameField.font = .systemFont(ofSize: 18, weight: .medium) nicknameField.textColor = AppColor.text333 nicknameField.borderStyle = .none nicknameField.isEnabled = false uidLabel.font = .systemFont(ofSize: 14, weight: .semibold) uidLabel.textColor = UIColor(hex: 0x9CA3AF) editButton.layer.cornerRadius = 18 editButton.snp.updateConstraints { make in make.height.equalTo(36) } editButton.titleLabel?.font = .systemFont(ofSize: 12) headerContainer.addSubview(avatarImageView) headerContainer.addSubview(nicknameField) headerContainer.addSubview(uidLabel) headerContainer.addSubview(editButton) avatarImageView.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview() make.width.height.equalTo(86) } nicknameField.snp.makeConstraints { make in make.leading.equalTo(avatarImageView.snp.trailing).offset(16) make.trailing.lessThanOrEqualTo(editButton.snp.leading).offset(-8) make.top.equalTo(avatarImageView).offset(12) } uidLabel.snp.makeConstraints { make in make.leading.equalTo(nicknameField) make.top.equalTo(nicknameField.snp.bottom).offset(8) } editButton.snp.makeConstraints { make in make.trailing.equalToSuperview() make.centerY.equalTo(avatarImageView) make.width.equalTo(80) make.height.equalTo(36) } headerContainer.snp.makeConstraints { make in make.height.greaterThanOrEqualTo(86) } } private func setupInfoCard() { infoCard.axis = .vertical infoCard.backgroundColor = .white infoCard.layer.cornerRadius = 8 infoCard.clipsToBounds = true infoCard.isLayoutMarginsRelativeArrangement = true infoCard.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) infoCardWrapper.addSubview(infoCard) infoCard.snp.makeConstraints { make in make.edges.equalToSuperview() } [ nameRow, accountRow, phoneRow, withdrawalRow, passwordRow, realNameRow, statusRow, scenicRow, settingRow ].forEach { infoCard.addArrangedSubview($0) } nameRow.isUserInteractionEnabled = false phoneRow.isUserInteractionEnabled = false statusRow.isUserInteractionEnabled = false scenicRow.isUserInteractionEnabled = false settingRow.showsDivider = false } private func setupLogoutButton() { logoutButton.setTitle("退出登录", for: .normal) logoutButton.setTitleColor(UIColor(hex: 0xEF4444), for: .normal) logoutButton.titleLabel?.font = .systemFont(ofSize: 14) logoutButton.backgroundColor = .white logoutButton.layer.cornerRadius = 8 } private func applyViewModel() { nicknameField.text = viewModel.displayNickname uidLabel.text = "UID:\(viewModel.displayUID)" avatarImageView.loadRemoteImage(urlString: viewModel.displayAvatarURL) nameRow.configure(value: viewModel.displayRealName) accountRow.configureAccountInfo( storeName: viewModel.accountDisplayName, accountTypeLabel: viewModel.accountTypeLabel, isStoreAccount: viewModel.isStoreAccount ) phoneRow.configure(value: viewModel.displayPhone) withdrawalRow.isHidden = !viewModel.showPhotographerFields realNameRow.isHidden = !viewModel.showPhotographerFields if viewModel.showPhotographerFields { configureWithdrawalRow() configureRealNameRow() } passwordRow.configure(value: "点击修改密码", valueColor: UIColor(hex: 0x9CA3AF), accessory: .chevron) statusRow.configure( value: viewModel.accountStatusText, badge: viewModel.accountStatusText == "--" ? nil : .accountActive ) scenicRow.configure( value: viewModel.currentScenicName, badge: .scenic ) settingRow.configure(value: nil, accessory: .chevron) } private func configureWithdrawalRow() { let info = viewModel.bankCardInfo let text = viewModel.bankCardDisplayText var valueColor = UIColor(hex: 0x9CA3AF) var badge: ProfileStatusBadgeView.Style? if let info { switch info.auditStatus { case 2: valueColor = AppColor.text333 case 3: valueColor = UIColor(hex: 0xEF4444) badge = .bankRejected default: valueColor = UIColor(hex: 0xFF7B00) badge = .bankPending } } withdrawalRow.configure( value: text, valueColor: valueColor, accessory: .chevron, badge: badge ) _ = info } private func configureRealNameRow() { if let info = viewModel.realNameInfo { let style: ProfileStatusBadgeView.Style switch info.auditStatus { case 2: style = .auditApproved case 3: style = .auditRejected default: style = .auditPending } realNameRow.configure( value: viewModel.realNameStatusText, accessory: .chevron, badge: style ) } else { realNameRow.configure( value: "点击去实名认证", valueColor: UIColor(hex: 0x9CA3AF), accessory: .chevron ) } } @objc private func refreshPulled() { Task { await reloadProfile(showGlobalLoading: false) refreshControl.endRefreshing() } } @objc private func handleAccountDidSwitch() { Task { await reloadProfile(showGlobalLoading: true) } } @objc private func handleProfileDidUpdate() { viewModel.applyLocalProfileUpdate() applyViewModel() } @objc private func editTapped() { let editVC = ProfileEditViewController( nickname: viewModel.displayNickname, avatarURL: viewModel.displayAvatarURL ) navigationController?.pushViewController(editVC, animated: true) } @objc private func accountSwitchTapped() { navigationController?.pushViewController(AccountSwitchViewController(), animated: true) } @objc private func changePasswordTapped() { let alert = UIAlertController(title: "修改密码", message: "请输入新密码(至少 6 位)", preferredStyle: .alert) alert.addTextField { field in field.isSecureTextEntry = true field.placeholder = "新密码" } alert.addAction(UIAlertAction(title: "取消", style: .cancel)) alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in guard let password = alert.textFields?.first?.text else { return } Task { await self?.updatePassword(password) } }) present(alert, animated: true) } @objc private func withdrawalTapped() { if let bankCard = viewModel.bankCardInfo, bankCard.auditStatus != 0 { navigationController?.pushViewController( WithdrawalSettingsAuditViewController(bankCard: bankCard), animated: true ) return } if viewModel.realNameInfo?.verified != true { showToast("请实名认证成功后再试") return } navigationController?.pushViewController(WithdrawalSettingsViewController(), animated: true) } @objc private func realNameTapped() { if viewModel.realNameInfo == nil { navigationController?.pushViewController(RealNameAuthViewController(), animated: true) } else if let info = viewModel.realNameInfo { navigationController?.pushViewController( RealNameAuthAuditViewController(info: info), animated: true ) } } @objc private func settingTapped() { navigationController?.pushViewController(SettingViewController(), animated: true) } @objc private func logoutTapped() { let alert = UIAlertController(title: "确定退出登录?", message: "退出后将需要重新登录", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "取消", style: .cancel)) alert.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in NotificationCenter.default.post(name: NotificationName.userDidLogout, object: nil) }) present(alert, animated: true) } private func reloadProfile(showGlobalLoading: Bool) async { let shouldShowOverlay = showGlobalLoading && !refreshControl.isRefreshing if shouldShowOverlay { showLoading() } defer { if shouldShowOverlay { hideLoading() } } do { try await viewModel.reload(api: profileAPI) applyViewModel() } catch { showToast(error.localizedDescription) } } private func updatePassword(_ password: String) async { showLoading() defer { hideLoading() } do { try await viewModel.updatePassword(password, api: profileAPI) showToast("密码修改成功") } catch { showToast(error.localizedDescription) } } }