Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,8 +6,18 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - Diffable 标识
|
||||
|
||||
private typealias AccountSwitchSection = Int
|
||||
private typealias AccountSwitchItem = String
|
||||
|
||||
/// 账号切换列表空态标识。
|
||||
private enum AccountSwitchItemID {
|
||||
static let empty = "accountSwitch:empty"
|
||||
}
|
||||
|
||||
/// 账号切换页面,展示当前登录主体下可进入的景区账号和门店账号。
|
||||
final class AccountSwitchViewController: UIViewController {
|
||||
final class AccountSwitchViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let viewModel = AccountSwitchViewModel()
|
||||
|
||||
@ -15,7 +25,6 @@ final class AccountSwitchViewController: UIViewController {
|
||||
let table = UITableView(frame: .zero, style: .plain)
|
||||
table.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||||
table.separatorStyle = .none
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(AccountSwitchCell.self, forCellReuseIdentifier: AccountSwitchCell.reuseID)
|
||||
return table
|
||||
@ -27,11 +36,16 @@ final class AccountSwitchViewController: UIViewController {
|
||||
return button
|
||||
}()
|
||||
|
||||
/// Diffable 数据源,驱动账号列表与选中态刷新。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>!
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "账号切换"
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||||
|
||||
configureTableDataSource()
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(confirmButton)
|
||||
tableView.snp.makeConstraints { make in
|
||||
@ -45,18 +59,75 @@ final class AccountSwitchViewController: UIViewController {
|
||||
}
|
||||
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
self?.applyTableSnapshot(reconfigure: true)
|
||||
self?.updateConfirmButton()
|
||||
}
|
||||
Task { await loadAccounts() }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>(
|
||||
tableView: tableView
|
||||
) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: AccountSwitchItem) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
if item == AccountSwitchItemID.empty {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(
|
||||
title: "暂无可切换账号",
|
||||
message: "当前登录账号下没有其他可切换账号。",
|
||||
systemImage: "person.crop.circle.badge.exclamationmark"
|
||||
)
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) }
|
||||
return cell
|
||||
}
|
||||
guard let account = self.viewModel.accounts.first(where: { $0.id == item }) else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
|
||||
cell.configure(
|
||||
account: account,
|
||||
selected: self.viewModel.selectedAccountId == account.id,
|
||||
isCurrent: account.isCurrent || self.isCurrentAccount(account)
|
||||
)
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
/// 构建 Diffable snapshot。
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem>()
|
||||
snapshot.appendSections([0])
|
||||
if viewModel.accounts.isEmpty, !viewModel.loading {
|
||||
snapshot.appendItems([AccountSwitchItemID.empty], toSection: 0)
|
||||
} else {
|
||||
snapshot.appendItems(viewModel.accounts.map(\.id), toSection: 0)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/// 应用 snapshot 刷新列表。
|
||||
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
|
||||
var snapshot = buildTableSnapshot()
|
||||
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
|
||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||
}
|
||||
tableDataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
/// 更新ConfirmButton状态。
|
||||
private func updateConfirmButton() {
|
||||
let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching
|
||||
confirmButton.isEnabled = enabled
|
||||
confirmButton.alpha = enabled ? 1 : 0.5
|
||||
}
|
||||
|
||||
/// 加载Accounts数据。
|
||||
private func loadAccounts(force: Bool = false) async {
|
||||
do {
|
||||
try await appServices.globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") {
|
||||
@ -82,10 +153,12 @@ final class AccountSwitchViewController: UIViewController {
|
||||
return current.userId.isEmpty ? nil : current.userId
|
||||
}
|
||||
|
||||
/// 判断isCurrentAccount条件。
|
||||
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {
|
||||
viewModel.isCurrent(account, currentAccountId: currentAccountId)
|
||||
}
|
||||
|
||||
/// 点击confirm的处理逻辑。
|
||||
@objc private func confirmTapped() {
|
||||
guard let account = viewModel.selectedAccount else { return }
|
||||
if account.isCurrent || isCurrentAccount(account) {
|
||||
@ -95,6 +168,7 @@ final class AccountSwitchViewController: UIViewController {
|
||||
Task { await switchAccount(account) }
|
||||
}
|
||||
|
||||
/// switch账号相关逻辑。
|
||||
private func switchAccount(_ account: AccountSwitchAccount) async {
|
||||
do {
|
||||
let response = try await appServices.globalLoading.withLoading(message: "切换中...") {
|
||||
@ -121,53 +195,29 @@ final class AccountSwitchViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// non空相关逻辑。
|
||||
private func nonEmpty(_ value: String?) -> String? {
|
||||
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountSwitchViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.accounts.isEmpty && !viewModel.loading ? 1 : viewModel.accounts.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard !viewModel.accounts.isEmpty else {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(
|
||||
title: "暂无可切换账号",
|
||||
message: "当前登录账号下没有其他可切换账号。",
|
||||
systemImage: "person.crop.circle.badge.exclamationmark"
|
||||
)
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) }
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
|
||||
let account = viewModel.accounts[indexPath.row]
|
||||
cell.configure(
|
||||
account: account,
|
||||
selected: viewModel.selectedAccountId == account.id,
|
||||
isCurrent: account.isCurrent || isCurrentAccount(account)
|
||||
)
|
||||
return cell
|
||||
}
|
||||
|
||||
/// UITableView 代理:处理行选中。
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.row < viewModel.accounts.count else { return }
|
||||
viewModel.select(viewModel.accounts[indexPath.row])
|
||||
guard let item = tableDataSource.itemIdentifier(for: indexPath),
|
||||
item != AccountSwitchItemID.empty,
|
||||
let account = viewModel.accounts.first(where: { $0.id == item }) else { return }
|
||||
viewModel.select(account)
|
||||
}
|
||||
|
||||
/// UITableView 代理:行高。
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
viewModel.accounts.isEmpty ? 280 : 92
|
||||
guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return 92 }
|
||||
return item == AccountSwitchItemID.empty ? 280 : 92
|
||||
}
|
||||
}
|
||||
|
||||
/// AccountSwitch列表或网格 Cell,负责单项内容展示。
|
||||
private final class AccountSwitchCell: UITableViewCell {
|
||||
static let reuseID = "AccountSwitchCell"
|
||||
|
||||
@ -178,6 +228,7 @@ private final class AccountSwitchCell: UITableViewCell {
|
||||
private let tagLabel = UILabel()
|
||||
private let checkView = UIImageView()
|
||||
|
||||
/// 初始化实例。
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
@ -245,6 +296,7 @@ private final class AccountSwitchCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) {
|
||||
let title = account.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
titleLabel.text = title.isEmpty ? account.accountTypeLabel : title
|
||||
|
||||
@ -7,8 +7,29 @@ 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"
|
||||
}
|
||||
|
||||
/// 个人信息页,展示用户资料、账号状态和设置入口。
|
||||
final class ProfileViewController: UIViewController {
|
||||
final class ProfileViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let viewModel = ProfileViewModel()
|
||||
|
||||
@ -19,18 +40,22 @@ final class ProfileViewController: UIViewController {
|
||||
|
||||
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()
|
||||
|
||||
/// Diffable 数据源,驱动个人信息列表刷新。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<ProfileTableSection, ProfileTableRow>!
|
||||
|
||||
/// 视图加载完成后的 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)
|
||||
@ -49,6 +74,74 @@ final class ProfileViewController: UIViewController {
|
||||
Task { await reloadProfile(showToast: false) }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<ProfileTableSection, ProfileTableRow>(
|
||||
tableView: tableView
|
||||
) { [weak self] (_: UITableView, indexPath: IndexPath, row: ProfileTableRow) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
switch row {
|
||||
case ProfileRowID.logout:
|
||||
cell.textLabel?.text = "退出登录"
|
||||
cell.textLabel?.textColor = UIColor(hex: 0xEF4444)
|
||||
cell.textLabel?.textAlignment = .center
|
||||
cell.selectionStyle = .default
|
||||
case ProfileRowID.name:
|
||||
cell.textLabel?.text = "姓名"
|
||||
cell.detailTextLabel?.text = self.viewModel.displayRealName
|
||||
cell.selectionStyle = .none
|
||||
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
|
||||
cell.selectionStyle = .none
|
||||
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<ProfileTableSection, ProfileTableRow> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ProfileTableSection, ProfileTableRow>()
|
||||
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
|
||||
@ -101,6 +194,7 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// apply视图模型相关逻辑。
|
||||
private func applyViewModel() {
|
||||
nicknameLabel.text = viewModel.displayNickname
|
||||
uidLabel.text = "UID: \(appServices.accountContext.profile?.userId ?? "--")"
|
||||
@ -111,9 +205,10 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
editButton.setImage(UIImage(systemName: viewModel.isEditingProfile ? "checkmark" : "pencil"), for: .normal)
|
||||
editButton.isEnabled = !viewModel.isSaving
|
||||
tableView.reloadData()
|
||||
applyTableSnapshot(animated: false, reconfigure: true)
|
||||
}
|
||||
|
||||
/// 刷新Pulled展示。
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reloadProfile(showToast: true)
|
||||
@ -121,6 +216,7 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新Profile。
|
||||
private func reloadProfile(showToast: Bool) async {
|
||||
do {
|
||||
try await appServices.globalLoading.withOptionalLoading(!showToast && viewModel.userInfo == nil, message: "加载资料...") {
|
||||
@ -131,6 +227,7 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击edit的处理逻辑。
|
||||
@objc private func editTapped() {
|
||||
if viewModel.isEditingProfile {
|
||||
Task { await saveProfileEdits() }
|
||||
@ -140,6 +237,7 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 弹出NicknameEditor页面。
|
||||
private func presentNicknameEditor() {
|
||||
let alert = UIAlertController(title: "编辑昵称", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { [weak self] field in
|
||||
@ -157,6 +255,7 @@ final class ProfileViewController: UIViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// save个人中心Edits相关逻辑。
|
||||
private func saveProfileEdits() async {
|
||||
guard let scenicId = appServices.accountContext.currentScenic?.id else {
|
||||
showToast("请先选择景区")
|
||||
@ -176,6 +275,7 @@ final class ProfileViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// pick头像相关逻辑。
|
||||
@objc private func pickAvatar() {
|
||||
var config = PHPickerConfiguration()
|
||||
config.filter = .images
|
||||
@ -185,6 +285,7 @@ final class ProfileViewController: UIViewController {
|
||||
present(picker, animated: true)
|
||||
}
|
||||
|
||||
/// 点击logout的处理逻辑。
|
||||
@objc private func logoutTapped() {
|
||||
let alert = UIAlertController(title: "确认退出当前账号?", message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
@ -201,58 +302,19 @@ final class ProfileViewController: UIViewController {
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
/// UITableView 代理:处理行选中。
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if indexPath.section == 1 {
|
||||
guard let row = tableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
switch row {
|
||||
case ProfileRowID.logout:
|
||||
logoutTapped()
|
||||
return
|
||||
}
|
||||
switch indexPath.row {
|
||||
case 1:
|
||||
case ProfileRowID.account:
|
||||
HomeMenuRouting.pushProfile(.accountSwitch, from: self)
|
||||
case 3:
|
||||
case ProfileRowID.realName:
|
||||
HomeMenuRouting.pushProfile(.realNameAuth, from: self)
|
||||
case 4:
|
||||
case ProfileRowID.settings:
|
||||
HomeMenuRouting.pushProfile(.settings, from: self)
|
||||
default:
|
||||
break
|
||||
@ -261,6 +323,7 @@ extension ProfileViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
@ -25,6 +25,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
private let startDatePicker = UIDatePicker()
|
||||
private let endDatePicker = UIDatePicker()
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "实名认证"
|
||||
@ -34,6 +35,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
Task { await loadInfo() }
|
||||
}
|
||||
|
||||
/// 初始化Form相关 UI 或状态。
|
||||
private func setupForm() {
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
@ -107,6 +109,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
contentStack.addArrangedSubview(submitButton)
|
||||
}
|
||||
|
||||
/// 创建SectionTitle实例。
|
||||
private func makeSectionTitle(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
@ -114,6 +117,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
return label
|
||||
}
|
||||
|
||||
/// labeled字段相关逻辑。
|
||||
private func labeledField(_ title: String, _ field: UITextField) -> UIStackView {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
@ -125,6 +129,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
return stack
|
||||
}
|
||||
|
||||
/// apply视图模型相关逻辑。
|
||||
private func applyViewModel() {
|
||||
realNameField.text = viewModel.realName
|
||||
idCardField.text = viewModel.idCardNo
|
||||
@ -163,6 +168,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载Info数据。
|
||||
private func loadInfo() async {
|
||||
do {
|
||||
try await appServices.globalLoading.withLoading(message: "加载中...") {
|
||||
@ -173,10 +179,12 @@ final class RealNameAuthViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// longValid变更相关逻辑。
|
||||
@objc private func longValidChanged() {
|
||||
viewModel.isLongValid = longValidSwitch.isOn
|
||||
}
|
||||
|
||||
/// 点击send验证码的处理逻辑。
|
||||
@objc private func sendCodeTapped() {
|
||||
Task {
|
||||
do {
|
||||
@ -187,6 +195,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交Tapped。
|
||||
@objc private func submitTapped() {
|
||||
viewModel.realName = realNameField.text ?? ""
|
||||
viewModel.idCardNo = idCardField.text ?? ""
|
||||
@ -215,16 +224,19 @@ final class RealNameAuthViewController: UIViewController {
|
||||
|
||||
private var pendingSide: RealNameImageSide = .front
|
||||
|
||||
/// pickFront相关逻辑。
|
||||
@objc private func pickFront() {
|
||||
pendingSide = .front
|
||||
presentImagePicker()
|
||||
}
|
||||
|
||||
/// pickBack相关逻辑。
|
||||
@objc private func pickBack() {
|
||||
pendingSide = .back
|
||||
presentImagePicker()
|
||||
}
|
||||
|
||||
/// 弹出ImagePicker页面。
|
||||
private func presentImagePicker() {
|
||||
var config = PHPickerConfiguration()
|
||||
config.filter = .images
|
||||
@ -236,6 +248,7 @@ final class RealNameAuthViewController: UIViewController {
|
||||
}
|
||||
|
||||
extension RealNameAuthViewController: 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 }
|
||||
@ -253,6 +266,7 @@ extension RealNameAuthViewController: PHPickerViewControllerDelegate {
|
||||
}
|
||||
|
||||
private extension UILabel {
|
||||
/// 初始化实例。
|
||||
convenience init(text: String) {
|
||||
self.init()
|
||||
self.text = text
|
||||
|
||||
@ -7,64 +7,81 @@ import SnapKit
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
// MARK: - Diffable 标识
|
||||
|
||||
/// 设置页行标识。
|
||||
private enum SettingsRowID {
|
||||
static let about = "settings:about"
|
||||
static let version = "settings:version"
|
||||
static let download = "settings:download"
|
||||
static let userAgreement = "settings:userAgreement"
|
||||
static let privacyPolicy = "settings:privacyPolicy"
|
||||
}
|
||||
|
||||
/// 设置页行操作类型。
|
||||
private enum SettingsRowAction {
|
||||
case agreement(AgreementPage)
|
||||
case version
|
||||
case download
|
||||
}
|
||||
|
||||
/// 设置中心页面。
|
||||
final class SettingsViewController: UIViewController {
|
||||
final class SettingsViewController: SimpleTableDiffableViewController {
|
||||
|
||||
private var copiedDownloadLink = false
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
return table
|
||||
}()
|
||||
|
||||
private let rows: [(title: String, action: SettingsRowAction)] = [
|
||||
("关于我们", .agreement(.about)),
|
||||
("系统版本", .version),
|
||||
("App下载", .download),
|
||||
("用户协议", .agreement(.userAgreement)),
|
||||
("隐私政策", .agreement(.privacyPolicy))
|
||||
private let rowActions: [String: SettingsRowAction] = [
|
||||
SettingsRowID.about: .agreement(.about),
|
||||
SettingsRowID.version: .version,
|
||||
SettingsRowID.download: .download,
|
||||
SettingsRowID.userAgreement: .agreement(.userAgreement),
|
||||
SettingsRowID.privacyPolicy: .agreement(.privacyPolicy)
|
||||
]
|
||||
|
||||
private enum SettingsRowAction {
|
||||
case agreement(AgreementPage)
|
||||
case version
|
||||
case download
|
||||
}
|
||||
private let rowTitles: [String: String] = [
|
||||
SettingsRowID.about: "关于我们",
|
||||
SettingsRowID.version: "系统版本",
|
||||
SettingsRowID.download: "App下载",
|
||||
SettingsRowID.userAgreement: "用户协议",
|
||||
SettingsRowID.privacyPolicy: "隐私政策"
|
||||
]
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "设置"
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
}
|
||||
|
||||
private var downloadLink: String {
|
||||
APIEnvironment.current.baseURL.appending(path: "/h5/app/download").absoluteString
|
||||
/// 构建 Diffable snapshot。
|
||||
override func buildSnapshot() -> NSDiffableDataSourceSnapshot<SimpleTableSection, SimpleTableRow> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SimpleTableSection, SimpleTableRow>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems([
|
||||
SettingsRowID.about,
|
||||
SettingsRowID.version,
|
||||
SettingsRowID.download,
|
||||
SettingsRowID.userAgreement,
|
||||
SettingsRowID.privacyPolicy
|
||||
], toSection: 0)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
private func copyDownloadLink() {
|
||||
UIPasteboard.general.string = downloadLink
|
||||
copiedDownloadLink = true
|
||||
showToast("下载链接已复制")
|
||||
tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .none)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||||
self?.copiedDownloadLink = false
|
||||
self?.tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .none)
|
||||
}
|
||||
/// section 页脚文案。
|
||||
override func sectionFooter(for section: SimpleTableSection) -> String? {
|
||||
"Copyright © 2025 All Rights Reserved\n苏ICP备2025157647号"
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { rows.count }
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
let row = rows[indexPath.row]
|
||||
cell.textLabel?.text = row.title
|
||||
/// 配置 Cell 展示内容。
|
||||
override func configureCell(_ cell: UITableViewCell, row: SimpleTableRow, at indexPath: IndexPath) {
|
||||
cell.textLabel?.text = rowTitles[row]
|
||||
cell.textLabel?.textColor = UIColor(hex: 0x4B5563)
|
||||
switch row.action {
|
||||
cell.detailTextLabel?.text = nil
|
||||
cell.detailTextLabel?.textColor = nil
|
||||
cell.accessoryType = .none
|
||||
cell.selectionStyle = .default
|
||||
|
||||
guard let action = rowActions[row] else { return }
|
||||
switch action {
|
||||
case .version:
|
||||
cell.detailTextLabel?.text = SettingsDisplayPolicy.versionText()
|
||||
cell.selectionStyle = .none
|
||||
@ -74,12 +91,12 @@ extension SettingsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
case .agreement:
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch rows[indexPath.row].action {
|
||||
/// 处理行选中。
|
||||
override func didSelectRow(_ row: SimpleTableRow, at indexPath: IndexPath) {
|
||||
guard let action = rowActions[row] else { return }
|
||||
switch action {
|
||||
case .agreement(let page):
|
||||
HomeMenuRouting.pushProfile(.agreement(page), from: self)
|
||||
case .download:
|
||||
@ -89,8 +106,25 @@ extension SettingsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
"Copyright © 2025 All Rights Reserved\n苏ICP备2025157647号"
|
||||
private var downloadLink: String {
|
||||
APIEnvironment.current.baseURL.appending(path: "/h5/app/download").absoluteString
|
||||
}
|
||||
|
||||
/// 复制下载链接并刷新对应行展示。
|
||||
private func copyDownloadLink() {
|
||||
UIPasteboard.general.string = downloadLink
|
||||
copiedDownloadLink = true
|
||||
showToast("下载链接已复制")
|
||||
var snapshot = tableDataSource.snapshot()
|
||||
snapshot.reconfigureItems([SettingsRowID.download])
|
||||
tableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.copiedDownloadLink = false
|
||||
var snapshot = self.tableDataSource.snapshot()
|
||||
snapshot.reconfigureItems([SettingsRowID.download])
|
||||
self.tableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,6 +135,7 @@ final class AgreementViewController: UIViewController {
|
||||
private let webView = WKWebView(frame: .zero)
|
||||
private let loadingIndicator = UIActivityIndicatorView(style: .large)
|
||||
|
||||
/// 初始化实例。
|
||||
init(page: AgreementPage) {
|
||||
self.page = page
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -109,6 +144,7 @@ final class AgreementViewController: UIViewController {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = page.title
|
||||
@ -124,10 +160,12 @@ final class AgreementViewController: UIViewController {
|
||||
}
|
||||
|
||||
extension AgreementViewController: WKNavigationDelegate {
|
||||
/// web视图相关逻辑。
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
loadingIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
/// web视图相关逻辑。
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
loadingIndicator.stopAnimating()
|
||||
showToast(error.localizedDescription)
|
||||
|
||||
Reference in New Issue
Block a user