Merge branch 'dev_1_2_1' into xh_test
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// SessionExpiredDialogViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 登录凭证失效时展示的强制确认弹窗,用户确认后由调用方执行退出登录。
|
||||
@MainActor
|
||||
final class SessionExpiredDialogViewController: UIViewController {
|
||||
|
||||
private let onConfirm: () -> Void
|
||||
private let confirmButton = AppButton(title: "确认并退出登录")
|
||||
private var didConfirm = false
|
||||
|
||||
/// 创建不可通过点击遮罩关闭的登录失效弹窗。
|
||||
init(onConfirm: @escaping () -> Void) {
|
||||
self.onConfirm = onConfirm
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .overFullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
isModalInPresentation = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = UIColor.black.withAlphaComponent(0.32)
|
||||
view.accessibilityViewIsModal = true
|
||||
view.accessibilityIdentifier = "sessionExpired.dialog"
|
||||
|
||||
let cardView = UIView()
|
||||
cardView.backgroundColor = AppColor.cardBackground
|
||||
cardView.layer.cornerRadius = AppRadius.lg
|
||||
cardView.clipsToBounds = true
|
||||
|
||||
let warningImageView = UIImageView(
|
||||
image: UIImage(systemName: "exclamationmark.circle.fill")
|
||||
)
|
||||
warningImageView.tintColor = UIColor(hex: 0xF05A5A)
|
||||
warningImageView.contentMode = .scaleAspectFit
|
||||
warningImageView.isAccessibilityElement = true
|
||||
warningImageView.accessibilityLabel = "登录状态异常"
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "登录提醒"
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.text = "当前用户已在其他设备登录,\n请重新登录"
|
||||
messageLabel.textColor = UIColor(hex: 0x4B5563)
|
||||
messageLabel.font = .systemFont(ofSize: 14)
|
||||
messageLabel.textAlignment = .center
|
||||
messageLabel.numberOfLines = 0
|
||||
|
||||
confirmButton.accessibilityIdentifier = "sessionExpired.confirmButton"
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
|
||||
let contentStack = UIStackView(arrangedSubviews: [
|
||||
warningImageView,
|
||||
titleLabel,
|
||||
messageLabel,
|
||||
confirmButton,
|
||||
])
|
||||
contentStack.axis = .vertical
|
||||
contentStack.alignment = .fill
|
||||
contentStack.spacing = 16
|
||||
contentStack.setCustomSpacing(4, after: titleLabel)
|
||||
contentStack.setCustomSpacing(20, after: messageLabel)
|
||||
|
||||
view.addSubview(cardView)
|
||||
cardView.addSubview(contentStack)
|
||||
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(32)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(24)
|
||||
}
|
||||
warningImageView.snp.makeConstraints { make in
|
||||
make.height.equalTo(64)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
guard !didConfirm else { return }
|
||||
didConfirm = true
|
||||
confirmButton.isEnabled = false
|
||||
onConfirm()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//
|
||||
// CommissionRateLogViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 获客员分成比例修改记录页,以时间轴形式展示历史变更。
|
||||
final class CommissionRateLogViewController: BaseViewController {
|
||||
private let viewModel: CommissionRateLogViewModel
|
||||
private let orderAPI: OrderAPI
|
||||
private let navigationTitle: String
|
||||
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let emptyLabel = UILabel()
|
||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||
private let footerLabel = UILabel()
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, CommissionRateLogEntity>!
|
||||
|
||||
init(saleUserId: Int, acquirerName: String, orderAPI: OrderAPI? = nil) {
|
||||
viewModel = CommissionRateLogViewModel(saleUserId: saleUserId)
|
||||
let trimmedName = acquirerName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
navigationTitle = trimmedName.isEmpty ? "修改记录" : "给\(trimmedName)的分成记录"
|
||||
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
Task { await viewModel.refresh(api: orderAPI) }
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = navigationTitle
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
|
||||
tableView.backgroundColor = AppColor.pageBackground
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 132
|
||||
tableView.delegate = self
|
||||
tableView.contentInset.top = AppSpacing.sm
|
||||
tableView.register(
|
||||
CommissionRateLogTimelineCell.self,
|
||||
forCellReuseIdentifier: CommissionRateLogTimelineCell.reuseIdentifier
|
||||
)
|
||||
tableView.refreshControl = UIRefreshControl()
|
||||
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
|
||||
dataSource = UITableViewDiffableDataSource<Int, CommissionRateLogEntity>(
|
||||
tableView: tableView
|
||||
) { tableView, indexPath, item in
|
||||
guard let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: CommissionRateLogTimelineCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? CommissionRateLogTimelineCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.configure(with: item, isLatest: indexPath.item == 0)
|
||||
return cell
|
||||
}
|
||||
|
||||
emptyLabel.text = "暂无修改记录"
|
||||
emptyLabel.font = .systemFont(ofSize: 14)
|
||||
emptyLabel.textColor = AppColor.textTertiary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.isHidden = true
|
||||
|
||||
loadingIndicator.color = AppColor.primary
|
||||
|
||||
footerLabel.font = .systemFont(ofSize: 13)
|
||||
footerLabel.textColor = AppColor.textTertiary
|
||||
footerLabel.textAlignment = .center
|
||||
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyLabel)
|
||||
view.addSubview(loadingIndicator)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
emptyLabel.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
}
|
||||
loadingIndicator.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyState() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
private func applyState() {
|
||||
if !viewModel.isRefreshing {
|
||||
tableView.refreshControl?.endRefreshing()
|
||||
}
|
||||
if viewModel.initialLoading && viewModel.items.isEmpty {
|
||||
loadingIndicator.startAnimating()
|
||||
} else {
|
||||
loadingIndicator.stopAnimating()
|
||||
}
|
||||
emptyLabel.isHidden = viewModel.initialLoading || viewModel.isRefreshing || !viewModel.items.isEmpty
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, CommissionRateLogEntity>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(viewModel.items)
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
|
||||
guard !viewModel.items.isEmpty else {
|
||||
tableView.tableFooterView = UIView()
|
||||
return
|
||||
}
|
||||
footerLabel.text = viewModel.isLoadingMore ? "加载中…" : (viewModel.canLoadMore ? "" : "没有更多")
|
||||
footerLabel.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 52)
|
||||
tableView.tableFooterView = footerLabel
|
||||
}
|
||||
|
||||
@objc private func refreshPulled() {
|
||||
Task { await viewModel.refresh(api: orderAPI) }
|
||||
}
|
||||
}
|
||||
|
||||
extension CommissionRateLogViewController: UITableViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard !viewModel.items.isEmpty,
|
||||
scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 180 else {
|
||||
return
|
||||
}
|
||||
Task { await viewModel.loadMore(api: orderAPI) }
|
||||
}
|
||||
}
|
||||
|
||||
/// 分成比例修改记录的时间轴 Cell。
|
||||
final class CommissionRateLogTimelineCell: UITableViewCell {
|
||||
static let reuseIdentifier = "CommissionRateLogTimelineCell"
|
||||
|
||||
private let railView = UIView()
|
||||
private let dotView = UIView()
|
||||
private let cardView = UIView()
|
||||
private let metadataLabel = UILabel()
|
||||
private let latestBadgeLabel = UILabel()
|
||||
private let beforeRateLabel = UILabel()
|
||||
private let arrowImageView = UIImageView(image: UIImage(systemName: "arrow.right"))
|
||||
private let afterRateLabel = UILabel()
|
||||
private let remarkLabel = UILabel()
|
||||
private let contentStack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = AppColor.pageBackground
|
||||
contentView.backgroundColor = AppColor.pageBackground
|
||||
|
||||
railView.backgroundColor = AppColor.primary.withAlphaComponent(0.16)
|
||||
dotView.backgroundColor = AppColor.primary
|
||||
dotView.layer.cornerRadius = 4
|
||||
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = AppRadius.md
|
||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||
cardView.layer.shadowOpacity = 0.04
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||
cardView.layer.shadowRadius = 2
|
||||
|
||||
metadataLabel.font = .systemFont(ofSize: 12)
|
||||
metadataLabel.textColor = AppColor.textSecondary
|
||||
metadataLabel.numberOfLines = 2
|
||||
|
||||
latestBadgeLabel.text = "最新"
|
||||
latestBadgeLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
||||
latestBadgeLabel.textColor = AppColor.primary
|
||||
latestBadgeLabel.textAlignment = .center
|
||||
latestBadgeLabel.backgroundColor = AppColor.primary.withAlphaComponent(0.10)
|
||||
latestBadgeLabel.layer.cornerRadius = 9
|
||||
latestBadgeLabel.clipsToBounds = true
|
||||
latestBadgeLabel.isHidden = true
|
||||
latestBadgeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||
latestBadgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
latestBadgeLabel.snp.makeConstraints { make in
|
||||
make.width.equalTo(38)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
let headerStack = UIStackView(arrangedSubviews: [metadataLabel, latestBadgeLabel])
|
||||
headerStack.axis = .horizontal
|
||||
headerStack.alignment = .top
|
||||
headerStack.spacing = AppSpacing.sm
|
||||
|
||||
[beforeRateLabel, afterRateLabel].forEach { label in
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = AppColor.textPrimary
|
||||
label.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
}
|
||||
afterRateLabel.textColor = AppColor.primary
|
||||
arrowImageView.tintColor = AppColor.textTertiary
|
||||
arrowImageView.contentMode = .scaleAspectFit
|
||||
|
||||
let rateStack = UIStackView(arrangedSubviews: [beforeRateLabel, arrowImageView, afterRateLabel, UIView()])
|
||||
rateStack.axis = .horizontal
|
||||
rateStack.alignment = .center
|
||||
rateStack.spacing = AppSpacing.sm
|
||||
arrowImageView.snp.makeConstraints { make in
|
||||
make.size.equalTo(16)
|
||||
}
|
||||
|
||||
remarkLabel.font = .systemFont(ofSize: 13)
|
||||
remarkLabel.textColor = AppColor.textSecondary
|
||||
remarkLabel.numberOfLines = 0
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = AppSpacing.sm
|
||||
contentStack.addArrangedSubview(headerStack)
|
||||
contentStack.addArrangedSubview(rateStack)
|
||||
contentStack.addArrangedSubview(remarkLabel)
|
||||
|
||||
contentView.addSubview(railView)
|
||||
contentView.addSubview(dotView)
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(contentStack)
|
||||
|
||||
railView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(24)
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.width.equalTo(1.5)
|
||||
}
|
||||
dotView.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(railView)
|
||||
make.top.equalToSuperview().offset(20)
|
||||
make.size.equalTo(8)
|
||||
}
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(railView.snp.trailing).offset(16)
|
||||
make.trailing.equalToSuperview().inset(14)
|
||||
make.top.equalToSuperview().offset(4)
|
||||
make.bottom.equalToSuperview().inset(8)
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(15)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 绑定比例变化、时间及可选备注。
|
||||
func configure(with item: CommissionRateLogEntity, isLatest: Bool) {
|
||||
metadataLabel.text = item.metadataLine
|
||||
latestBadgeLabel.isHidden = !isLatest
|
||||
beforeRateLabel.text = item.displayBeforeRate
|
||||
afterRateLabel.text = item.displayAfterRate
|
||||
if let remark = item.displayRemark {
|
||||
remarkLabel.text = "备注:\(remark)"
|
||||
remarkLabel.isHidden = false
|
||||
} else {
|
||||
remarkLabel.text = nil
|
||||
remarkLabel.isHidden = true
|
||||
}
|
||||
accessibilityLabel = [
|
||||
isLatest ? "最新记录" : nil,
|
||||
item.metadataLine,
|
||||
"\(item.displayBeforeRate)修改为\(item.displayAfterRate)",
|
||||
item.displayRemark.map { "备注\($0)" },
|
||||
].compactMap { $0 }.joined(separator: ",")
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,13 @@ final class CooperationAcquirerViewController: BaseViewController, UITableViewDa
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF6F7FA)
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 142
|
||||
tableView.contentInset.top = AppSpacing.sm
|
||||
tableView.verticalScrollIndicatorInsets.top = AppSpacing.sm
|
||||
tableView.estimatedRowHeight = 224
|
||||
tableView.contentInset = UIEdgeInsets(top: AppSpacing.sm, left: 0, bottom: AppSpacing.md, right: 0)
|
||||
tableView.verticalScrollIndicatorInsets = UIEdgeInsets(top: AppSpacing.sm, left: 0, bottom: AppSpacing.md, right: 0)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(CooperationAcquirerCell.self, forCellReuseIdentifier: CooperationAcquirerCell.reuseIdentifier)
|
||||
@@ -245,6 +246,17 @@ final class CooperationAcquirerViewController: BaseViewController, UITableViewDa
|
||||
cell.onEditCommission = { [weak self] in
|
||||
self?.presentCommissionDialog(for: acquirer)
|
||||
}
|
||||
cell.onViewCommissionLogs = { [weak self] in
|
||||
guard acquirer.saleUserId > 0 else {
|
||||
self?.showToast("获客员信息无效")
|
||||
return
|
||||
}
|
||||
let controller = CommissionRateLogViewController(
|
||||
saleUserId: acquirer.saleUserId,
|
||||
acquirerName: acquirer.displayName
|
||||
)
|
||||
self?.navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
cell.onCall = {
|
||||
let phone = acquirer.displayPhone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !phone.isEmpty, let url = URL(string: "tel://\(phone)") else { return }
|
||||
|
||||
@@ -445,22 +445,25 @@ final class CooperationAcquirerCell: UITableViewCell {
|
||||
|
||||
var onEditRemark: (() -> Void)?
|
||||
var onEditCommission: (() -> Void)?
|
||||
var onViewCommissionLogs: (() -> Void)?
|
||||
var onCall: (() -> Void)?
|
||||
|
||||
private let cardView = UIView()
|
||||
private let avatarContainer = UIView()
|
||||
private let avatarIconView = UIImageView(image: UIImage(systemName: "person.fill"))
|
||||
private let nameTitleLabel = UILabel()
|
||||
private let nameValueLabel = UILabel()
|
||||
private let editRemarkButton = UIButton(type: .system)
|
||||
private let phoneTitleLabel = UILabel()
|
||||
private let phoneValueLabel = UILabel()
|
||||
private let callButton = UIButton(type: .system)
|
||||
private let bindTimeTitleLabel = UILabel()
|
||||
private let bindTimeValueLabel = UILabel()
|
||||
private let commissionContainer = UIView()
|
||||
private let commissionTitleLabel = UILabel()
|
||||
private let commissionValueLabel = UILabel()
|
||||
private let bindTimeIconView = UIImageView(image: UIImage(systemName: "calendar"))
|
||||
private let bindTimeValueLabel = UILabel()
|
||||
private let dividerView = UIView()
|
||||
private let editRemarkButton = UIButton(type: .system)
|
||||
private let editCommissionButton = UIButton(type: .system)
|
||||
private let commissionLogButton = UIButton(type: .system)
|
||||
private let actionStack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
@@ -469,135 +472,180 @@ final class CooperationAcquirerCell: UITableViewCell {
|
||||
contentView.backgroundColor = .clear
|
||||
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = AppRadius.md
|
||||
cardView.layer.cornerRadius = AppRadius.xl
|
||||
cardView.layer.borderWidth = 0.5
|
||||
cardView.layer.borderColor = AppColor.cardOutline.cgColor
|
||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||
cardView.layer.shadowOpacity = 0.04
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||
cardView.layer.shadowRadius = 2
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 3)
|
||||
cardView.layer.shadowRadius = 8
|
||||
cardView.accessibilityIdentifier = "cooperation_acquirer_card"
|
||||
|
||||
avatarContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.08)
|
||||
avatarContainer.layer.cornerRadius = AppRadius.sm
|
||||
avatarContainer.layer.cornerRadius = 24
|
||||
avatarContainer.clipsToBounds = true
|
||||
|
||||
avatarIconView.tintColor = AppColor.primary
|
||||
avatarIconView.contentMode = .scaleAspectFit
|
||||
|
||||
configureTitleLabel(nameTitleLabel, text: "获客员名称")
|
||||
configureTitleLabel(phoneTitleLabel, text: "手机号")
|
||||
configureTitleLabel(bindTimeTitleLabel, text: "绑定时间")
|
||||
configureTitleLabel(commissionTitleLabel, text: "分成比例")
|
||||
|
||||
nameValueLabel.font = .systemFont(ofSize: 15, weight: .bold)
|
||||
nameValueLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
nameValueLabel.textColor = AppColor.textPrimary
|
||||
nameValueLabel.numberOfLines = 1
|
||||
nameValueLabel.lineBreakMode = .byTruncatingTail
|
||||
nameValueLabel.accessibilityIdentifier = "cooperation_acquirer_name"
|
||||
|
||||
[phoneValueLabel, bindTimeValueLabel, commissionValueLabel].forEach { label in
|
||||
label.font = .systemFont(ofSize: 13)
|
||||
label.textColor = AppColor.textSecondary
|
||||
label.numberOfLines = 1
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
}
|
||||
commissionValueLabel.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
|
||||
editRemarkButton.setTitle("修改备注", for: .normal)
|
||||
editRemarkButton.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
editRemarkButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
editRemarkButton.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 16, leading: 14, bottom: 16, trailing: 14)
|
||||
)
|
||||
editRemarkButton.addTarget(self, action: #selector(editRemarkTapped), for: .touchUpInside)
|
||||
phoneValueLabel.font = .systemFont(ofSize: 14)
|
||||
phoneValueLabel.textColor = AppColor.textTertiary
|
||||
phoneValueLabel.numberOfLines = 1
|
||||
phoneValueLabel.lineBreakMode = .byTruncatingTail
|
||||
phoneValueLabel.accessibilityIdentifier = "cooperation_acquirer_phone"
|
||||
|
||||
callButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
||||
callButton.tintColor = AppColor.primary
|
||||
callButton.backgroundColor = AppColor.primaryLight
|
||||
callButton.layer.cornerRadius = 15
|
||||
callButton.layer.cornerRadius = AppSpacing.minTouchTarget / 2
|
||||
callButton.layer.borderWidth = 0.5
|
||||
callButton.layer.borderColor = AppColor.primary.withAlphaComponent(0.12).cgColor
|
||||
callButton.imageView?.contentMode = .scaleAspectFit
|
||||
callButton.accessibilityLabel = "拨打获客员电话"
|
||||
callButton.accessibilityIdentifier = "cooperation_acquirer_call"
|
||||
callButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
|
||||
|
||||
editCommissionButton.setTitle("修改比例", for: .normal)
|
||||
editCommissionButton.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
editCommissionButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
editCommissionButton.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)
|
||||
commissionContainer.backgroundColor = AppColor.primary.withAlphaComponent(0.06)
|
||||
commissionContainer.layer.cornerRadius = AppRadius.lg
|
||||
|
||||
commissionTitleLabel.text = "分成比例"
|
||||
commissionTitleLabel.font = .systemFont(ofSize: 12)
|
||||
commissionTitleLabel.textColor = AppColor.textTertiary
|
||||
|
||||
commissionValueLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||
commissionValueLabel.textColor = AppColor.primary
|
||||
commissionValueLabel.numberOfLines = 1
|
||||
commissionValueLabel.adjustsFontSizeToFitWidth = true
|
||||
commissionValueLabel.minimumScaleFactor = 0.75
|
||||
commissionValueLabel.accessibilityIdentifier = "cooperation_acquirer_commission_rate"
|
||||
|
||||
bindTimeIconView.tintColor = AppColor.textTertiary
|
||||
bindTimeIconView.contentMode = .scaleAspectFit
|
||||
|
||||
bindTimeValueLabel.font = .systemFont(ofSize: 13)
|
||||
bindTimeValueLabel.textColor = AppColor.textTertiary
|
||||
bindTimeValueLabel.numberOfLines = 2
|
||||
bindTimeValueLabel.lineBreakMode = .byTruncatingTail
|
||||
bindTimeValueLabel.accessibilityIdentifier = "cooperation_acquirer_bind_time"
|
||||
|
||||
dividerView.backgroundColor = AppColor.cardOutline
|
||||
|
||||
configureActionButton(
|
||||
editRemarkButton,
|
||||
title: "修改备注",
|
||||
isPrimary: false,
|
||||
accessibilityIdentifier: "cooperation_acquirer_edit_remark"
|
||||
)
|
||||
editRemarkButton.addTarget(self, action: #selector(editRemarkTapped), for: .touchUpInside)
|
||||
|
||||
configureActionButton(
|
||||
commissionLogButton,
|
||||
title: "修改记录",
|
||||
isPrimary: false,
|
||||
accessibilityIdentifier: "cooperation_acquirer_commission_logs"
|
||||
)
|
||||
commissionLogButton.accessibilityLabel = "查看分成比例修改记录"
|
||||
commissionLogButton.addTarget(self, action: #selector(commissionLogTapped), for: .touchUpInside)
|
||||
|
||||
configureActionButton(
|
||||
editCommissionButton,
|
||||
title: "修改比例",
|
||||
isPrimary: true,
|
||||
accessibilityIdentifier: "cooperation_acquirer_edit_commission"
|
||||
)
|
||||
editCommissionButton.addTarget(self, action: #selector(editCommissionTapped), for: .touchUpInside)
|
||||
|
||||
actionStack.axis = .horizontal
|
||||
actionStack.alignment = .fill
|
||||
actionStack.distribution = .fillEqually
|
||||
actionStack.spacing = AppSpacing.xs
|
||||
actionStack.addArrangedSubview(editRemarkButton)
|
||||
actionStack.addArrangedSubview(commissionLogButton)
|
||||
actionStack.addArrangedSubview(editCommissionButton)
|
||||
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(avatarContainer)
|
||||
avatarContainer.addSubview(avatarIconView)
|
||||
cardView.addSubview(nameTitleLabel)
|
||||
cardView.addSubview(nameValueLabel)
|
||||
cardView.addSubview(editRemarkButton)
|
||||
cardView.addSubview(phoneTitleLabel)
|
||||
cardView.addSubview(phoneValueLabel)
|
||||
cardView.addSubview(callButton)
|
||||
cardView.addSubview(bindTimeTitleLabel)
|
||||
cardView.addSubview(commissionContainer)
|
||||
commissionContainer.addSubview(commissionTitleLabel)
|
||||
commissionContainer.addSubview(commissionValueLabel)
|
||||
cardView.addSubview(bindTimeIconView)
|
||||
cardView.addSubview(bindTimeValueLabel)
|
||||
cardView.addSubview(commissionTitleLabel)
|
||||
cardView.addSubview(commissionValueLabel)
|
||||
cardView.addSubview(editCommissionButton)
|
||||
cardView.addSubview(dividerView)
|
||||
cardView.addSubview(actionStack)
|
||||
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 14, bottom: AppSpacing.sm, right: 14))
|
||||
make.edges.equalToSuperview().inset(
|
||||
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md)
|
||||
)
|
||||
}
|
||||
avatarContainer.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(14)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(44)
|
||||
make.top.leading.equalToSuperview().inset(AppSpacing.md)
|
||||
make.size.equalTo(48)
|
||||
}
|
||||
avatarIconView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.size.equalTo(22)
|
||||
}
|
||||
nameTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(AppSpacing.md)
|
||||
make.leading.equalTo(avatarContainer.snp.trailing).offset(AppSpacing.sm)
|
||||
make.size.equalTo(24)
|
||||
}
|
||||
nameValueLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(nameTitleLabel)
|
||||
make.leading.equalTo(nameTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.lessThanOrEqualTo(editRemarkButton.snp.leading).offset(-AppSpacing.xs)
|
||||
}
|
||||
editRemarkButton.snp.makeConstraints { make in
|
||||
make.top.trailing.equalToSuperview()
|
||||
}
|
||||
phoneTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(nameTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.equalTo(nameTitleLabel)
|
||||
make.top.equalTo(avatarContainer).offset(2)
|
||||
make.leading.equalTo(avatarContainer.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.sm)
|
||||
}
|
||||
phoneValueLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(phoneTitleLabel)
|
||||
make.leading.equalTo(phoneTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.xs)
|
||||
make.top.equalTo(nameValueLabel.snp.bottom).offset(AppSpacing.xxs)
|
||||
make.leading.equalTo(nameValueLabel)
|
||||
make.trailing.lessThanOrEqualTo(callButton.snp.leading).offset(-AppSpacing.sm)
|
||||
}
|
||||
callButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(14)
|
||||
make.centerY.equalTo(phoneTitleLabel)
|
||||
make.size.equalTo(30)
|
||||
make.top.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.size.equalTo(AppSpacing.minTouchTarget)
|
||||
}
|
||||
bindTimeTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(phoneTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.equalTo(nameTitleLabel)
|
||||
}
|
||||
bindTimeValueLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(bindTimeTitleLabel)
|
||||
make.leading.equalTo(bindTimeTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(14)
|
||||
commissionContainer.snp.makeConstraints { make in
|
||||
make.top.equalTo(avatarContainer.snp.bottom).offset(14)
|
||||
make.leading.equalToSuperview().inset(AppSpacing.md)
|
||||
make.width.equalTo(120)
|
||||
make.height.equalTo(64)
|
||||
}
|
||||
commissionTitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(bindTimeTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||
make.leading.equalTo(nameTitleLabel)
|
||||
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||
make.top.equalToSuperview().offset(AppSpacing.xs)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
commissionValueLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(commissionTitleLabel)
|
||||
make.leading.equalTo(commissionTitleLabel.snp.trailing).offset(AppSpacing.sm)
|
||||
make.trailing.lessThanOrEqualTo(editCommissionButton.snp.leading).offset(-AppSpacing.xs)
|
||||
make.top.equalTo(commissionTitleLabel.snp.bottom).offset(2)
|
||||
make.centerX.equalToSuperview()
|
||||
make.leading.greaterThanOrEqualToSuperview().offset(AppSpacing.xs)
|
||||
make.trailing.lessThanOrEqualToSuperview().inset(AppSpacing.xs)
|
||||
}
|
||||
editCommissionButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(commissionTitleLabel)
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.xs)
|
||||
bindTimeIconView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(commissionContainer.snp.trailing).offset(AppSpacing.md)
|
||||
make.centerY.equalTo(commissionContainer)
|
||||
make.size.equalTo(18)
|
||||
}
|
||||
bindTimeValueLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(bindTimeIconView.snp.trailing).offset(AppSpacing.xs)
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.centerY.equalTo(bindTimeIconView)
|
||||
}
|
||||
dividerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(commissionContainer.snp.bottom).offset(14)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
actionStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(dividerView.snp.bottom).offset(AppSpacing.sm)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||
make.height.equalTo(AppSpacing.minTouchTarget)
|
||||
make.bottom.equalToSuperview().inset(14)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,26 +658,52 @@ final class CooperationAcquirerCell: UITableViewCell {
|
||||
super.prepareForReuse()
|
||||
onEditRemark = nil
|
||||
onEditCommission = nil
|
||||
onViewCommissionLogs = nil
|
||||
onCall = nil
|
||||
}
|
||||
|
||||
func configure(with acquirer: CooperativeSalerEntity) {
|
||||
nameValueLabel.text = acquirer.displayName.isEmpty ? "—" : acquirer.displayName
|
||||
phoneValueLabel.text = acquirer.displayPhone.isEmpty ? "—" : CooperationOrderPhoneMask.mask(acquirer.displayPhone)
|
||||
bindTimeValueLabel.text = acquirer.displayBindTime.isEmpty ? "—" : acquirer.displayBindTime
|
||||
let bindTime = acquirer.displayBindTime.isEmpty ? "—" : Self.minutePrecisionTime(acquirer.displayBindTime)
|
||||
bindTimeValueLabel.text = "绑定于 \(bindTime)"
|
||||
commissionValueLabel.text = acquirer.displayCommissionRate.isEmpty ? "—" : acquirer.displayCommissionRate
|
||||
}
|
||||
|
||||
private func configureTitleLabel(_ label: UILabel, text: String) {
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 13)
|
||||
label.textColor = AppColor.textTertiary
|
||||
label.setContentHuggingPriority(.required, for: .horizontal)
|
||||
label.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
private func configureActionButton(
|
||||
_ button: UIButton,
|
||||
title: String,
|
||||
isPrimary: Bool,
|
||||
accessibilityIdentifier: String
|
||||
) {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.title = title
|
||||
configuration.baseForegroundColor = isPrimary ? AppColor.primary : UIColor(hex: 0x5F6673)
|
||||
configuration.background.backgroundColor = isPrimary ? AppColor.primaryLight : .white
|
||||
configuration.background.strokeColor = isPrimary
|
||||
? AppColor.primary.withAlphaComponent(0.35)
|
||||
: UIColor(hex: 0xD9DEE7)
|
||||
configuration.background.strokeWidth = 1
|
||||
configuration.background.cornerRadius = AppRadius.md
|
||||
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6)
|
||||
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
||||
var updated = attributes
|
||||
updated.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
return updated
|
||||
}
|
||||
button.configuration = configuration
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
|
||||
private static func minutePrecisionTime(_ value: String) -> String {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard trimmed.count == 19, trimmed.dropFirst(16).first == ":" else { return trimmed }
|
||||
return String(trimmed.prefix(16))
|
||||
}
|
||||
|
||||
@objc private func editRemarkTapped() { onEditRemark?() }
|
||||
@objc private func editCommissionTapped() { onEditCommission?() }
|
||||
@objc private func commissionLogTapped() { onViewCommissionLogs?() }
|
||||
@objc private func callTapped() { onCall?() }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import CoreLocation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
/// 地图缩放/定位控件。
|
||||
final class LocationReportMapControlStack: UIView {
|
||||
|
||||
@@ -295,7 +299,112 @@ final class LocationReportBottomActionView: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地图容器,封装高德 `MAMapView`。
|
||||
/// 位置上报地图容器,模拟器使用 MapKit。
|
||||
#if targetEnvironment(simulator)
|
||||
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
let mapView: MKMapView
|
||||
private var markerAnnotation: MKPointAnnotation?
|
||||
private var shouldCenterOnNextUserLocation = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
mapView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 进入页面后,在首次拿到用户位置时将地图居中。
|
||||
func enableInitialUserLocationCentering() {
|
||||
shouldCenterOnNextUserLocation = true
|
||||
centerOnUserLocationIfNeeded()
|
||||
}
|
||||
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = validUserCoordinate else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||
let region = MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.012, longitudeDelta: 0.012)
|
||||
)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
}
|
||||
|
||||
func zoomIn() {
|
||||
updateZoom(scale: 0.5)
|
||||
}
|
||||
|
||||
func zoomOut() {
|
||||
updateZoom(scale: 2)
|
||||
}
|
||||
|
||||
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||
if let markerAnnotation {
|
||||
mapView.removeAnnotation(markerAnnotation)
|
||||
self.markerAnnotation = nil
|
||||
}
|
||||
guard let latitude, let longitude else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
markerAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||
centerOnUserLocationIfNeeded()
|
||||
}
|
||||
|
||||
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
let point = gesture.location(in: mapView)
|
||||
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||
}
|
||||
|
||||
private var validUserCoordinate: CLLocationCoordinate2D? {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||
CLLocationCoordinate2DIsValid(coordinate),
|
||||
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||
return nil
|
||||
}
|
||||
return coordinate
|
||||
}
|
||||
|
||||
private func centerOnUserLocationIfNeeded() {
|
||||
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
||||
setCenter(coordinate, animated: false)
|
||||
shouldCenterOnNextUserLocation = false
|
||||
}
|
||||
|
||||
private func updateZoom(scale: Double) {
|
||||
let current = mapView.region
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||
)
|
||||
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// 位置上报地图容器,真机封装高德 `MAMapView`。
|
||||
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
@@ -381,3 +490,4 @@ final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||
shouldCenterOnNextUserLocation = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
private let qrContainerView = UIView()
|
||||
private let qrImageView = UIImageView()
|
||||
private let qrPlaceholderView = UIView()
|
||||
private let qrLoadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||
private let qrErrorLabel = UILabel()
|
||||
private let refreshButton = UIButton(type: .system)
|
||||
private let setAmountButton = UIButton(type: .system)
|
||||
@@ -127,6 +128,10 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
|
||||
qrPlaceholderView.isHidden = true
|
||||
|
||||
qrLoadingIndicator.color = AppColor.primary
|
||||
qrLoadingIndicator.hidesWhenStopped = true
|
||||
qrLoadingIndicator.accessibilityLabel = "收款码加载中"
|
||||
|
||||
qrErrorLabel.text = "未选景区或网络问题"
|
||||
qrErrorLabel.font = .systemFont(ofSize: 14)
|
||||
qrErrorLabel.textColor = AppColor.danger
|
||||
@@ -150,6 +155,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
trailing: 24
|
||||
)
|
||||
}
|
||||
actionRow.alpha = 0
|
||||
actionRow.isUserInteractionEnabled = false
|
||||
actionRow.accessibilityElementsHidden = true
|
||||
|
||||
configureLinkButton(setAmountButton, title: "设置金额")
|
||||
configureLinkButton(saveQRButton, title: "保存二维码")
|
||||
@@ -206,6 +214,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
|
||||
qrCardView.addSubview(qrContentStack)
|
||||
|
||||
qrPlaceholderView.addSubview(qrLoadingIndicator)
|
||||
qrPlaceholderView.addSubview(qrErrorLabel)
|
||||
qrPlaceholderView.addSubview(refreshButton)
|
||||
|
||||
@@ -275,6 +284,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
qrPlaceholderView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
|
||||
}
|
||||
qrLoadingIndicator.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
qrErrorLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(56)
|
||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||
@@ -370,9 +382,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
|
||||
private func loadData() async {
|
||||
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
|
||||
showLoading()
|
||||
await viewModel.loadPayCode(api: paymentAPI)
|
||||
hideLoading()
|
||||
await pageConfig
|
||||
}
|
||||
|
||||
@@ -387,17 +397,33 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
||||
applyBrandConfigIfNeeded()
|
||||
|
||||
if let image = viewModel.qrImage {
|
||||
if viewModel.isPayCodeLoading {
|
||||
qrImageView.isHidden = true
|
||||
qrContainerView.isHidden = true
|
||||
qrPlaceholderView.backgroundColor = viewModel.usesNalatiBranding
|
||||
? .white
|
||||
: AppColor.inputBackground
|
||||
qrPlaceholderView.isHidden = false
|
||||
qrErrorLabel.isHidden = true
|
||||
refreshButton.isHidden = true
|
||||
qrLoadingIndicator.startAnimating()
|
||||
setActionRowAvailable(false)
|
||||
} else if let image = viewModel.qrImage {
|
||||
qrImageView.image = image
|
||||
qrImageView.isHidden = false
|
||||
qrContainerView.isHidden = !viewModel.usesNalatiBranding
|
||||
qrPlaceholderView.isHidden = true
|
||||
actionRow.isHidden = false
|
||||
qrLoadingIndicator.stopAnimating()
|
||||
setActionRowAvailable(true)
|
||||
} else {
|
||||
qrImageView.isHidden = true
|
||||
qrContainerView.isHidden = true
|
||||
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
||||
qrPlaceholderView.isHidden = false
|
||||
actionRow.isHidden = true
|
||||
qrErrorLabel.isHidden = false
|
||||
refreshButton.isHidden = false
|
||||
qrLoadingIndicator.stopAnimating()
|
||||
setActionRowAvailable(false)
|
||||
}
|
||||
|
||||
if viewModel.showAmountDialog {
|
||||
@@ -408,6 +434,12 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func setActionRowAvailable(_ isAvailable: Bool) {
|
||||
actionRow.alpha = isAvailable ? 1 : 0
|
||||
actionRow.isUserInteractionEnabled = isAvailable
|
||||
actionRow.accessibilityElementsHidden = !isAvailable
|
||||
}
|
||||
|
||||
private func presentAmountDialogIfNeeded() {
|
||||
guard amountDialog == nil else {
|
||||
amountDialog?.apply(amount: viewModel.amount, remark: viewModel.remark)
|
||||
@@ -625,9 +657,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
||||
|
||||
@objc private func refreshTapped() {
|
||||
Task {
|
||||
showLoading()
|
||||
await viewModel.refresh(api: paymentAPI)
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ final class ProfileViewController: BaseViewController {
|
||||
title = "我的"
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
|
||||
@@ -104,12 +109,10 @@ final class ProfileViewController: BaseViewController {
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
let showLoading = !hasLoadedProfileOnce
|
||||
let showLoading = !hasLoadedProfileOnce && !viewModel.hasCachedBasicInfo
|
||||
Task {
|
||||
await reloadProfile(showGlobalLoading: showLoading)
|
||||
if showLoading {
|
||||
hasLoadedProfileOnce = true
|
||||
}
|
||||
hasLoadedProfileOnce = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Kingfisher
|
||||
import Photos
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
||||
final class PunchPointDetailViewController: BaseViewController {
|
||||
private let viewModel: PunchPointDetailViewModel
|
||||
@@ -447,7 +452,192 @@ final class PunchPointDetailViewController: BaseViewController {
|
||||
}
|
||||
|
||||
/// 高德地图容器,供打卡点详情与表单复用。
|
||||
/// 打卡点地图视图,封装高德地图 marker、缩放、定位与点击选点行为。
|
||||
/// 打卡点地图视图,模拟器使用 MapKit 提供 marker、缩放、定位与点击选点行为。
|
||||
#if targetEnvironment(simulator)
|
||||
final class PunchPointMapView: UIView, MKMapViewDelegate {
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
private let mapView: MKMapView
|
||||
private var punchPointAnnotation: MKPointAnnotation?
|
||||
private var selectedAnnotation: MKPointAnnotation?
|
||||
private var punchPointImageURL: URL?
|
||||
private var fitWithUserLocation = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
mapView.showsScale = false
|
||||
mapView.pointOfInterestFilter = .includingAll
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
mapView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新原打卡点标记,可使用首张图片作为圆角 Marker。
|
||||
func updatePunchPointMarker(
|
||||
coordinate: CLLocationCoordinate2D?,
|
||||
imageURL: String? = nil,
|
||||
fitWithUserLocation: Bool = false
|
||||
) {
|
||||
if let punchPointAnnotation {
|
||||
mapView.removeAnnotation(punchPointAnnotation)
|
||||
self.punchPointAnnotation = nil
|
||||
}
|
||||
punchPointImageURL = imageURL.flatMap(URL.init(string:))
|
||||
self.fitWithUserLocation = fitWithUserLocation
|
||||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = "punch_point_original"
|
||||
punchPointAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
if fitWithUserLocation {
|
||||
fitPunchPointAndUserLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容仅需更新普通 Marker 的调用。
|
||||
func updateMarker(coordinate: CLLocationCoordinate2D?) {
|
||||
updatePunchPointMarker(coordinate: coordinate)
|
||||
}
|
||||
|
||||
/// 更新地图点击产生的新位置 Marker。
|
||||
func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
|
||||
if let selectedAnnotation {
|
||||
mapView.removeAnnotation(selectedAnnotation)
|
||||
self.selectedAnnotation = nil
|
||||
}
|
||||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = "punch_point_selected"
|
||||
selectedAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
/// 设置地图中心点。
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
|
||||
let delta = min(max(0.012 * pow(2, 15 - zoomLevel), 0.0005), 120)
|
||||
let region = MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)
|
||||
)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
}
|
||||
|
||||
/// 放大地图。
|
||||
func zoomIn() {
|
||||
updateZoom(scale: 0.5)
|
||||
}
|
||||
|
||||
/// 缩小地图。
|
||||
func zoomOut() {
|
||||
updateZoom(scale: 2)
|
||||
}
|
||||
|
||||
/// 居中到用户当前位置。
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||
CLLocationCoordinate2DIsValid(coordinate),
|
||||
coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
/// 同时展示原打卡点、用户位置和新选位置。
|
||||
func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
|
||||
var annotations: [any MKAnnotation] = []
|
||||
if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
|
||||
if let selectedAnnotation { annotations.append(selectedAnnotation) }
|
||||
if mapView.userLocation.location != nil { annotations.append(mapView.userLocation) }
|
||||
guard !annotations.isEmpty else { return }
|
||||
if annotations.count == 1, let annotation = annotations.first {
|
||||
setCenter(annotation.coordinate, zoomLevel: 16)
|
||||
} else {
|
||||
var mapRect = MKMapRect.null
|
||||
for annotation in annotations {
|
||||
let point = MKMapPoint(annotation.coordinate)
|
||||
mapRect = mapRect.union(MKMapRect(
|
||||
origin: point,
|
||||
size: MKMapSize(width: 1, height: 1)
|
||||
))
|
||||
}
|
||||
mapView.setVisibleMapRect(mapRect, edgePadding: edgePadding, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
|
||||
if annotation is MKUserLocation { return nil }
|
||||
let isOriginal = (annotation as AnyObject) === punchPointAnnotation
|
||||
let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
|
||||
?? MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||
annotationView.annotation = annotation
|
||||
annotationView.canShowCallout = false
|
||||
annotationView.centerOffset = CGPoint(x: 0, y: -12)
|
||||
|
||||
if isOriginal, let punchPointImageURL {
|
||||
annotationView.image = UIImage(named: "punch_point_marker")
|
||||
KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
|
||||
guard case let .success(value) = result else { return }
|
||||
Task { @MainActor in
|
||||
guard annotationView.annotation === annotation else { return }
|
||||
annotationView.image = Self.roundedMarkerImage(value.image)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
annotationView.image = UIImage(named: "punch_point_marker")
|
||||
}
|
||||
return annotationView
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||
guard userLocation.location != nil else { return }
|
||||
fitPunchPointAndUserLocation()
|
||||
}
|
||||
|
||||
private func fitPunchPointAndUserLocation() {
|
||||
guard fitWithUserLocation else { return }
|
||||
fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
|
||||
}
|
||||
|
||||
private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
|
||||
let size = CGSize(width: 40, height: 40)
|
||||
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
|
||||
source.draw(in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
let point = gesture.location(in: mapView)
|
||||
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||
}
|
||||
|
||||
private func updateZoom(scale: Double) {
|
||||
let current = mapView.region
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||
)
|
||||
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// 打卡点地图视图,真机封装高德地图 marker、缩放、定位与点击选点行为。
|
||||
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
@@ -614,6 +804,7 @@ final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||
onMapTap?(poi.coordinate)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// 打卡点地图控制按钮。
|
||||
/// 地图悬浮控制按钮。
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import MAMapKit
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#else
|
||||
import MAMapKit
|
||||
#endif
|
||||
|
||||
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
||||
final class WildPhotographerReportListViewController: BaseViewController {
|
||||
private let viewModel: WildPhotographerReportListViewModel
|
||||
@@ -1797,6 +1802,49 @@ private final class WildReportDetailMediaThumbnailView: UIView {
|
||||
}
|
||||
|
||||
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
||||
#if targetEnvironment(simulator)
|
||||
private final class WildReportDetailMapPreviewView: UIView {
|
||||
private let mapView: MKMapView
|
||||
|
||||
init(scenicName: String, detailAddress: String, coordinate: CLLocationCoordinate2D?) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = AppColor.pageBackgroundSoft
|
||||
layer.cornerRadius = 10
|
||||
clipsToBounds = true
|
||||
|
||||
mapView.showsCompass = false
|
||||
mapView.showsScale = false
|
||||
mapView.isScrollEnabled = false
|
||||
mapView.isZoomEnabled = false
|
||||
mapView.isRotateEnabled = false
|
||||
mapView.isPitchEnabled = false
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
guard let coordinate else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = scenicName
|
||||
annotation.subtitle = detailAddress
|
||||
mapView.addAnnotation(annotation)
|
||||
mapView.setRegion(
|
||||
MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.006, longitudeDelta: 0.006)
|
||||
),
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
#else
|
||||
private final class WildReportDetailMapPreviewView: UIView {
|
||||
private let mapView: MAMapView
|
||||
|
||||
@@ -1834,6 +1882,7 @@ private final class WildReportDetailMapPreviewView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// 举报详情页处理进度时间线单行。
|
||||
private final class WildReportDetailTimelineRowView: UIView {
|
||||
|
||||
Reference in New Issue
Block a user