完善景区排队设置页与全局按钮配置迁移。

重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 16:21:53 +08:00
parent efb3925257
commit 6336feff27
49 changed files with 1996 additions and 590 deletions

View File

@ -14,4 +14,12 @@ extension UIButton {
updatedConfiguration.contentInsets = contentInsets
configuration = updatedConfiguration
}
/// configuration
func setConfigurationImagePlacement(_ imagePlacement: NSDirectionalRectEdge, padding: CGFloat) {
var updatedConfiguration = configuration ?? .plain()
updatedConfiguration.imagePlacement = imagePlacement
updatedConfiguration.imagePadding = padding
configuration = updatedConfiguration
}
}

View File

@ -20,9 +20,9 @@ final class ScenicSelectionViewController: BaseViewController, UITableViewDataSo
private let tableView = UITableView(frame: .zero, style: .plain)
private let emptyLabel = UILabel()
init(viewModel: ScenicSelectionViewModel = ScenicSelectionViewModel(), homeAPI: HomeAPI = NetworkServices.shared.homeAPI) {
init(viewModel: ScenicSelectionViewModel = ScenicSelectionViewModel(), homeAPI: HomeAPI? = nil) {
self.viewModel = viewModel
self.homeAPI = homeAPI
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -58,8 +58,7 @@ final class HomeWorkStatusCardView: UIView {
let reminderIconConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
reminderButton.setImage(UIImage(systemName: "bell.fill", withConfiguration: reminderIconConfig), for: .normal)
reminderButton.semanticContentAttribute = .forceLeftToRight
reminderButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 6)
reminderButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: -6)
reminderButton.setConfigurationImagePlacement(.leading, padding: 6)
reminderButton.titleLabel?.adjustsFontSizeToFitWidth = true
reminderButton.titleLabel?.minimumScaleFactor = 0.8
reminderButton.addTarget(self, action: #selector(reminderTapped), for: .touchUpInside)

View File

@ -38,11 +38,11 @@ final class InviteRecordViewController: BaseViewController {
///
init(
inviteAPI: InviteServing = NetworkServices.shared.inviteAPI,
walletAPI: WalletServing = NetworkServices.shared.walletAPI
inviteAPI: InviteServing? = nil,
walletAPI: WalletServing? = nil
) {
self.inviteAPI = inviteAPI
self.walletAPI = walletAPI
self.inviteAPI = inviteAPI ?? NetworkServices.shared.inviteAPI
self.walletAPI = walletAPI ?? NetworkServices.shared.walletAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -19,8 +19,8 @@ final class PhotographerInviteViewController: BaseViewController {
private let ruleStack = UIStackView()
///
init(inviteAPI: InviteServing = NetworkServices.shared.inviteAPI) {
self.inviteAPI = inviteAPI
init(inviteAPI: InviteServing? = nil) {
self.inviteAPI = inviteAPI ?? NetworkServices.shared.inviteAPI
super.init(nibName: nil, bundle: nil)
}
@ -234,7 +234,7 @@ final class PhotographerInviteViewController: BaseViewController {
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
button.setImage(UIImage(named: "ic_invite_record")?.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = AppColor.primary
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -6, bottom: 0, right: 6)
button.setConfigurationImagePlacement(.leading, padding: 6)
button.addTarget(self, action: #selector(inviteRecordTapped), for: .touchUpInside)
container.addSubview(button)
button.snp.makeConstraints { make in
@ -360,8 +360,7 @@ private final class InviteIconButton: UIButton {
setImage(UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate), for: .normal)
tintColor = .white
titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
imageEdgeInsets = UIEdgeInsets(top: 0, left: -3, bottom: 0, right: 3)
titleEdgeInsets = UIEdgeInsets(top: 0, left: 3, bottom: 0, right: -3)
setConfigurationImagePlacement(.leading, padding: 6)
}
@available(*, unavailable)

View File

@ -207,7 +207,7 @@ final class LiveAlbumListViewController: BaseViewController, UITableViewDelegate
button.titleLabel?.font = .systemFont(ofSize: 13)
button.setImage(UIImage(systemName: "calendar"), for: .normal)
button.tintColor = AppColor.textPrimary
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
button.setConfigurationImagePlacement(.leading, padding: 4)
setDateButtonTitle(button, text: title)
}

View File

@ -75,7 +75,7 @@ final class LiveActionButton: UIButton {
layer.cornerRadius = 8
clipsToBounds = true
tintColor = .white
imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
setConfigurationImagePlacement(.leading, padding: 4)
updateAppearance()
}

View File

@ -20,10 +20,10 @@ final class LocationReportHistoryViewController: BaseViewController, UITableView
init(
viewModel: LocationReportHistoryViewModel = LocationReportHistoryViewModel(),
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
homeAPI: HomeAPI? = nil
) {
self.viewModel = viewModel
self.homeAPI = homeAPI
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -24,10 +24,10 @@ final class LocationReportViewController: BaseViewController {
init(
viewModel: LocationReportViewModel = LocationReportViewModel(),
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
homeAPI: HomeAPI? = nil
) {
self.viewModel = viewModel
self.homeAPI = homeAPI
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -30,10 +30,10 @@ final class MaterialDetailViewController: BaseViewController {
///
init(
materialId: Int,
api: any MaterialManagementServing = NetworkServices.shared.materialManagementAPI
api: (any MaterialManagementServing)? = nil
) {
viewModel = MaterialDetailViewModel(materialId: materialId)
self.api = api
self.api = api ?? NetworkServices.shared.materialManagementAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -49,12 +49,12 @@ final class MaterialFormViewController: BaseViewController {
///
init(
mode: MaterialFormViewModel.Mode,
api: any MaterialManagementServing = NetworkServices.shared.materialManagementAPI,
uploader: any MaterialOSSUploading = NetworkServices.shared.ossUploadService
api: (any MaterialManagementServing)? = nil,
uploader: (any MaterialOSSUploading)? = nil
) {
viewModel = MaterialFormViewModel(mode: mode)
self.api = api
self.uploader = uploader
self.api = api ?? NetworkServices.shared.materialManagementAPI
self.uploader = uploader ?? NetworkServices.shared.ossUploadService
super.init(nibName: nil, bundle: nil)
}

View File

@ -33,10 +33,10 @@ final class MaterialListViewController: BaseViewController {
///
init(
viewModel: MaterialListViewModel = MaterialListViewModel(),
api: any MaterialManagementServing = NetworkServices.shared.materialManagementAPI
api: (any MaterialManagementServing)? = nil
) {
self.viewModel = viewModel
self.api = api
self.api = api ?? NetworkServices.shared.materialManagementAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -25,10 +25,10 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
///
init(
viewModel: MessageCenterViewModel = MessageCenterViewModel(),
api: any MessageCenterServing = NetworkServices.shared.messageCenterAPI
api: (any MessageCenterServing)? = nil
) {
self.viewModel = viewModel
self.api = api
self.api = api ?? NetworkServices.shared.messageCenterAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -24,9 +24,9 @@ final class MessageDetailViewController: BaseViewController {
private let deleteButton = UIButton(type: .system)
///
init(message: MessageItem, api: any MessageCenterServing = NetworkServices.shared.messageCenterAPI) {
init(message: MessageItem, api: (any MessageCenterServing)? = nil) {
viewModel = MessageDetailViewModel(message: message)
self.api = api
self.api = api ?? NetworkServices.shared.messageCenterAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -14,9 +14,9 @@ final class DepositOrderDetailViewController: BaseViewController, UITableViewDat
private var detail: StoreOrderDetailData?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
init(orderNumber: String, orderAPI: OrderAPI = NetworkServices.shared.orderAPI) {
init(orderNumber: String, orderAPI: OrderAPI? = nil) {
self.orderNumber = orderNumber
self.orderAPI = orderAPI
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -14,9 +14,9 @@ final class HistoricalShootingInfoViewController: BaseViewController, UITableVie
private var response: MultiTravelShootHistoryResponse?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
init(orderNumber: String, orderAPI: OrderAPI = NetworkServices.shared.orderAPI) {
init(orderNumber: String, orderAPI: OrderAPI? = nil) {
self.orderNumber = orderNumber
self.orderAPI = orderAPI
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -53,10 +53,10 @@ final class ProfileSpaceSettingsViewController: BaseViewController {
init(
viewModel: ProfileSpaceSettingsViewModel = ProfileSpaceSettingsViewModel(),
profileAPI: ProfileAPI = NetworkServices.shared.profileAPI
profileAPI: ProfileAPI? = nil
) {
self.viewModel = viewModel
self.profileAPI = profileAPI
self.profileAPI = profileAPI ?? NetworkServices.shared.profileAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -26,10 +26,10 @@ final class SampleDetailViewController: SampleBaseViewController {
///
init(
sampleId: Int,
api: any SampleManagementServing = NetworkServices.shared.sampleManagementAPI
api: (any SampleManagementServing)? = nil
) {
viewModel = SampleDetailViewModel(sampleId: sampleId)
self.api = api
self.api = api ?? NetworkServices.shared.sampleManagementAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -32,10 +32,10 @@ final class SampleListViewController: SampleBaseViewController {
///
init(
viewModel: SampleListViewModel = SampleListViewModel(),
api: any SampleManagementServing = NetworkServices.shared.sampleManagementAPI
api: (any SampleManagementServing)? = nil
) {
self.viewModel = viewModel
self.api = api
self.api = api ?? NetworkServices.shared.sampleManagementAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -51,12 +51,12 @@ final class UploadSampleViewController: SampleBaseViewController {
///
init(
viewModel: UploadSampleViewModel = UploadSampleViewModel(),
api: any SampleManagementServing = NetworkServices.shared.sampleManagementAPI,
uploader: any SampleOSSUploading = NetworkServices.shared.ossUploadService
api: (any SampleManagementServing)? = nil,
uploader: (any SampleOSSUploading)? = nil
) {
self.viewModel = viewModel
self.api = api
self.uploader = uploader
self.api = api ?? NetworkServices.shared.sampleManagementAPI
self.uploader = uploader ?? NetworkServices.shared.ossUploadService
super.init(nibName: nil, bundle: nil)
}

View File

@ -41,14 +41,14 @@ final class ScenicApplicationViewController: BaseViewController {
init(
viewModel: ScenicApplicationViewModel = ScenicApplicationViewModel(),
homeAPI: HomeAPI = NetworkServices.shared.homeAPI,
profileAPI: ProfileAPI = NetworkServices.shared.profileAPI,
ossUploadService: OSSUploadService = NetworkServices.shared.ossUploadService
homeAPI: HomeAPI? = nil,
profileAPI: ProfileAPI? = nil,
ossUploadService: OSSUploadService? = nil
) {
self.viewModel = viewModel
self.homeAPI = homeAPI
self.profileAPI = profileAPI
self.ossUploadService = ossUploadService
self.homeAPI = homeAPI ?? NetworkServices.shared.homeAPI
self.profileAPI = profileAPI ?? NetworkServices.shared.profileAPI
self.ossUploadService = ossUploadService ?? NetworkServices.shared.ossUploadService
super.init(nibName: nil, bundle: nil)
}

View File

@ -6,12 +6,15 @@
import SnapKit
import UIKit
///
/// Android
final class ScenicQueueSettingChangeLogViewController: BaseViewController {
private let viewModel = ScenicQueueSettingChangeLogViewModel()
private let api: ScenicQueueAPIProtocol
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, ScenicQueueSettingChangeLogItem>!
@MainActor
init(api: ScenicQueueAPIProtocol? = nil) {
@ -20,133 +23,142 @@ final class ScenicQueueSettingChangeLogViewController: BaseViewController {
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.refresh(api: api) }
}
override func setupNavigationBar() {
title = "配置日志"
}
override func setupNavigationBar() { title = "配置日志" }
override func setupUI() {
view.backgroundColor = ScenicQueueTokens.pageBackground
tableView.backgroundColor = .clear
view.backgroundColor = .white
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 120
tableView.estimatedRowHeight = 132
tableView.delegate = self
tableView.dataSource = self
tableView.register(ScenicQueueLogCell.self, forCellReuseIdentifier: ScenicQueueLogCell.reuseIdentifier)
tableView.register(ScenicQueueLogTimelineCell.self, forCellReuseIdentifier: ScenicQueueLogTimelineCell.reuseIdentifier)
tableView.refreshControl = UIRefreshControl()
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
dataSource = UITableViewDiffableDataSource<Int, ScenicQueueSettingChangeLogItem>(tableView: tableView) { [weak self] tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueLogTimelineCell.reuseIdentifier, for: indexPath) as! ScenicQueueLogTimelineCell
cell.configure(item: item, showDot: self?.shouldShowDot(at: indexPath.row) ?? false)
return cell
}
emptyLabel.text = "暂无配置变更记录"
emptyLabel.textColor = AppColor.textTertiary
emptyLabel.textColor = AppColor.textSecondary
emptyLabel.font = ScenicQueueTokens.emptyStateFont
emptyLabel.textAlignment = .center
loadingIndicator.color = ScenicQueueTokens.bannerBlue
footerLabel.textAlignment = .center
footerLabel.textColor = AppColor.textSecondary
footerLabel.font = .systemFont(ofSize: 14)
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.equalToSuperview()
}
tableView.snp.makeConstraints { $0.edges.equalTo(view.safeAreaLayoutGuide) }
emptyLabel.snp.makeConstraints { $0.center.equalToSuperview() }
loadingIndicator.snp.makeConstraints { $0.center.equalToSuperview() }
}
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) }
}
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() {
tableView.refreshControl?.endRefreshing()
tableView.reloadData()
emptyLabel.isHidden = !viewModel.items.isEmpty
viewModel.initialLoading && viewModel.items.isEmpty ? loadingIndicator.startAnimating() : loadingIndicator.stopAnimating()
emptyLabel.isHidden = viewModel.initialLoading || viewModel.isRefreshing || !viewModel.items.isEmpty
var snapshot = NSDiffableDataSourceSnapshot<Int, ScenicQueueSettingChangeLogItem>()
snapshot.appendSections([0])
snapshot.appendItems(viewModel.items)
dataSource.apply(snapshot, animatingDifferences: true)
if viewModel.items.isEmpty {
tableView.tableFooterView = UIView()
} else {
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: api) }
private func shouldShowDot(at index: Int) -> Bool {
let estimatedRowHeight: CGFloat = 132
let itemsPerThird = max(1, Int(((view.bounds.height / 3) / estimatedRowHeight).rounded()))
let stride = max(3, min(12, itemsPerThird))
return index % stride == 0
}
@objc private func refreshPulled() { Task { await viewModel.refresh(api: api) } }
}
extension ScenicQueueSettingChangeLogViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueLogCell.reuseIdentifier, for: indexPath) as! ScenicQueueLogCell
cell.configure(item: viewModel.items[indexPath.row])
return cell
}
extension ScenicQueueSettingChangeLogViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 120 else { return }
guard !viewModel.items.isEmpty,
scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 180 else { return }
Task { await viewModel.loadMore(api: api) }
}
}
/// cell
final class ScenicQueueLogCell: UITableViewCell {
static let reuseIdentifier = "ScenicQueueLogCell"
///
final class ScenicQueueLogTimelineCell: UITableViewCell {
static let reuseIdentifier = "ScenicQueueLogTimelineCell"
private let rail = UIView()
private let dot = UIView()
private let card = UIView()
private let titleLabel = UILabel()
private let detailLabel = UILabel()
private let timeLabel = UILabel()
private let metaLabel = UILabel()
private let summaryLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
selectionStyle = .none
backgroundColor = .white
rail.backgroundColor = ScenicQueueTokens.timelineLine
dot.backgroundColor = ScenicQueueTokens.timelineDot
dot.layer.cornerRadius = 4
card.backgroundColor = ScenicQueueTokens.logCardBackground
card.layer.cornerRadius = 12
metaLabel.font = .systemFont(ofSize: 12)
metaLabel.textColor = AppColor.textSecondary
metaLabel.numberOfLines = 3
summaryLabel.font = .systemFont(ofSize: 15, weight: .semibold)
summaryLabel.textColor = AppColor.textPrimary
summaryLabel.numberOfLines = 0
let stack = UIStackView(arrangedSubviews: [metaLabel, summaryLabel])
stack.axis = .vertical
stack.spacing = 8
contentView.addSubview(rail)
contentView.addSubview(dot)
contentView.addSubview(card)
card.addSubview(stack)
rail.snp.makeConstraints { make in
make.centerX.equalTo(contentView.snp.leading).offset(UIScreen.main.bounds.width * 0.10)
make.top.bottom.equalToSuperview()
make.width.equalTo(1.5)
}
dot.snp.makeConstraints { make in make.centerX.equalTo(rail); make.top.equalToSuperview().offset(18); make.size.equalTo(8) }
card.snp.makeConstraints { make in
make.leading.equalTo(rail.snp.trailing).offset(20)
make.trailing.equalToSuperview().offset(-16)
make.top.equalToSuperview().offset(6)
make.bottom.equalToSuperview().offset(-6)
}
stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(15) }
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(item: ScenicQueueSettingChangeLogItem) {
titleLabel.text = item.operatorName.isEmpty ? "配置变更" : item.operatorName
detailLabel.text = item.displayText.isEmpty ? item.summary : item.displayText
timeLabel.text = item.createdAt
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .clear
card.backgroundColor = .white
card.layer.cornerRadius = ScenicQueueTokens.radiusCard
card.layer.borderWidth = 1
card.layer.borderColor = ScenicQueueTokens.cardOutline.cgColor
contentView.addSubview(card)
titleLabel.font = ScenicQueueTokens.settingsSectionTitleFont
titleLabel.textColor = AppColor.textPrimary
detailLabel.font = ScenicQueueTokens.settingsRowTitleFont
detailLabel.textColor = AppColor.textSecondary
detailLabel.numberOfLines = 0
timeLabel.font = ScenicQueueTokens.settingsHintFont
timeLabel.textColor = AppColor.textTertiary
let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel, timeLabel])
stack.axis = .vertical
stack.spacing = 8
card.addSubview(stack)
card.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
}
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(14)
}
///
func configure(item: ScenicQueueSettingChangeLogItem, showDot: Bool) {
metaLabel.text = item.metadataLine
summaryLabel.text = item.cardSummary
dot.isHidden = !showDot
}
}

View File

@ -15,12 +15,13 @@ final class ScenicQueueViewController: BaseViewController {
private let punchSpotLabel = UILabel()
private let statsBanner = ScenicQueueStatsBannerView()
private let contentContainer = UIView()
private let segmentedControl = UISegmentedControl(items: ["当前排队", "过号列表"])
private let tabStrip = ScenicQueueTabStripView(titles: ["当前排队", "过号列表"])
private let tableView = UITableView(frame: .zero, style: .plain)
private let bottomBar = UIView()
private let prepareCallButton = UIButton(type: .system)
private let quickCallButton = UIButton(type: .system)
private let emptyLabel = UILabel()
private var dataSource: UITableViewDiffableDataSource<Int, ScenicQueueListItem>!
private var selectedTab = 0
@ -64,8 +65,8 @@ final class ScenicQueueViewController: BaseViewController {
view.backgroundColor = ScenicQueueTokens.pageBackground
punchSpotRow.backgroundColor = .white
let locationIcon = UIImageView(image: UIImage(systemName: "mappin.and.ellipse"))
locationIcon.tintColor = ScenicQueueTokens.bannerBlue
let locationIcon = UIImageView(image: UIImage(named: "queue_location"))
locationIcon.contentMode = .scaleAspectFit
punchSpotLabel.font = .systemFont(ofSize: 16)
punchSpotLabel.textColor = UIColor.black.withAlphaComponent(0.7)
punchSpotLabel.lineBreakMode = .byTruncatingTail
@ -75,29 +76,21 @@ final class ScenicQueueViewController: BaseViewController {
contentContainer.backgroundColor = .white
contentContainer.layer.cornerRadius = ScenicQueueTokens.radiusContentSheetTop
contentContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
contentContainer.clipsToBounds = true
segmentedControl.selectedSegmentIndex = 0
segmentedControl.selectedSegmentTintColor = .white
segmentedControl.setTitleTextAttributes([
.font: ScenicQueueTokens.tabFont,
.foregroundColor: AppColor.textSecondary,
], for: .normal)
segmentedControl.setTitleTextAttributes([
.font: ScenicQueueTokens.tabSelectedFont,
.foregroundColor: ScenicQueueTokens.bannerBlue,
], for: .selected)
contentContainer.layer.shadowColor = UIColor.black.cgColor
contentContainer.layer.shadowOpacity = 0.08
contentContainer.layer.shadowRadius = 4
contentContainer.layer.shadowOffset = CGSize(width: 0, height: 2)
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 260
tableView.delegate = self
tableView.dataSource = self
tableView.register(ScenicQueueTicketCell.self, forCellReuseIdentifier: ScenicQueueTicketCell.reuseIdentifier)
tableView.register(ScenicQueueSkippedCell.self, forCellReuseIdentifier: ScenicQueueSkippedCell.reuseIdentifier)
tableView.refreshControl = UIRefreshControl()
tableView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
configureDataSource()
emptyLabel.textColor = AppColor.textTertiary
emptyLabel.font = ScenicQueueTokens.emptyStateFont
@ -114,7 +107,7 @@ final class ScenicQueueViewController: BaseViewController {
view.addSubview(punchSpotRow)
view.addSubview(statsBanner)
view.addSubview(contentContainer)
contentContainer.addSubview(segmentedControl)
contentContainer.addSubview(tabStrip)
contentContainer.addSubview(tableView)
contentContainer.addSubview(emptyLabel)
view.addSubview(bottomBar)
@ -149,12 +142,13 @@ final class ScenicQueueViewController: BaseViewController {
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(bottomBar.snp.top).offset(-8)
}
segmentedControl.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(12)
make.height.equalTo(40)
tabStrip.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(12)
make.height.equalTo(48)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(segmentedControl.snp.bottom).offset(8)
make.top.equalTo(tabStrip.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
emptyLabel.snp.makeConstraints { make in
@ -177,7 +171,7 @@ final class ScenicQueueViewController: BaseViewController {
}
override func bindActions() {
segmentedControl.addTarget(self, action: #selector(tabChanged), for: .valueChanged)
tabStrip.onSelect = { [weak self] index in self?.selectTab(index) }
prepareCallButton.addTarget(self, action: #selector(prepareCallTapped), for: .touchUpInside)
quickCallButton.addTarget(self, action: #selector(quickCallTapped), for: .touchUpInside)
viewModel.onStateChange = { [weak self] in
@ -202,7 +196,7 @@ final class ScenicQueueViewController: BaseViewController {
punchSpotLabel.text = viewModel.selectedPunchSpotLine
statsBanner.configure(summary: viewModel.summary)
tableView.refreshControl?.endRefreshing()
tableView.reloadData()
applySnapshot(animated: true)
let isEmpty = selectedTab == 0 ? viewModel.currentQueue.isEmpty : viewModel.skippedQueue.isEmpty
emptyLabel.text = selectedTab == 0 ? "暂无排队" : "暂无过号记录"
emptyLabel.isHidden = !isEmpty
@ -212,6 +206,41 @@ final class ScenicQueueViewController: BaseViewController {
bottomBar.isHidden = !viewModel.queueGatePassed || (!viewModel.prepareCallButtonEnabled && !viewModel.quickCallButtonEnabled)
}
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource<Int, ScenicQueueListItem>(tableView: tableView) { [weak self] tableView, indexPath, item in
guard let self else { return UITableViewCell() }
switch item {
case .current(let ticket):
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueTicketCell.reuseIdentifier, for: indexPath) as! ScenicQueueTicketCell
cell.delegate = self
cell.configure(
ticket: ticket,
shootingQueueNo: self.viewModel.shootingTimedQueueNo,
remainSeconds: self.viewModel.shootingRemainSeconds,
showStartShootingButton: self.viewModel.showStartShootingButton
)
return cell
case .skipped(let ticket):
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueSkippedCell.reuseIdentifier, for: indexPath) as! ScenicQueueSkippedCell
cell.delegate = self
cell.configure(ticket: ticket)
return cell
}
}
}
private func applySnapshot(animated: Bool) {
let previousItems = Set(dataSource.snapshot().itemIdentifiers)
var snapshot = NSDiffableDataSourceSnapshot<Int, ScenicQueueListItem>()
snapshot.appendSections([0])
let items: [ScenicQueueListItem] = selectedTab == 0
? viewModel.currentQueue.map(ScenicQueueListItem.current)
: viewModel.skippedQueue.map(ScenicQueueListItem.skipped)
snapshot.appendItems(items)
snapshot.reconfigureItems(items.filter(previousItems.contains))
dataSource.apply(snapshot, animatingDifferences: animated)
}
private func configureBottomButton(_ button: UIButton, title: String, subtitle: String, image: UIImage?, color: UIColor) {
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = color
@ -232,42 +261,63 @@ final class ScenicQueueViewController: BaseViewController {
private func presentParametersRequiredDialog() {
guard presentedViewController == nil else { return }
let alert = UIAlertController(title: "排队参数未配置", message: "请先选择打卡点并保存排队设置", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "暂不进入", style: .cancel) { [weak self] _ in
self?.viewModel.exitQueueModule()
})
alert.addAction(UIAlertAction(title: "去设置", style: .default) { [weak self] _ in
self?.navigationController?.pushViewController(ScenicQueueSettingsViewController(), animated: true)
})
present(alert, animated: true)
let dialog = ScenicQueueConfirmationDialogViewController(
configuration: ScenicQueueDialogConfiguration(
title: "请先设置排队参数",
message: "请在排队设置中选择并保存打卡点后,再使用排队管理。",
emphasizedText: nil,
dangerText: nil,
cancelTitle: "暂不使用",
confirmTitle: "去设置",
buttonRadius: 22,
dismissOnBackdrop: false
),
onCancel: { [weak self] in self?.viewModel.exitQueueModule() },
onConfirm: { [weak self] in self?.navigationController?.pushViewController(ScenicQueueSettingsViewController(), animated: true) }
)
present(dialog, animated: true)
}
private func presentConfirm(title: String, message: String?, confirm: @escaping () -> Void) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确认", style: .default) { _ in confirm() })
present(alert, animated: true)
private func presentConfirm(
title: String,
emphasizedText: String,
dangerText: String? = nil,
confirmTitle: String = "确认",
cancel: @escaping () -> Void,
confirm: @escaping () -> Void
) {
let dialog = ScenicQueueConfirmationDialogViewController(
configuration: ScenicQueueDialogConfiguration(
title: title,
message: nil,
emphasizedText: emphasizedText,
dangerText: dangerText,
cancelTitle: "取消",
confirmTitle: confirmTitle,
buttonRadius: title == "确认完成拍摄" ? 22 : 8,
dismissOnBackdrop: true
),
onCancel: cancel,
onConfirm: confirm
)
present(dialog, animated: true)
}
private func openDial(_ digits: String) {
guard !digits.isEmpty else { return }
let alert = UIAlertController(title: "拨打电话", message: digits, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "拨打", style: .default) { _ in
if let url = URL(string: "tel:\(digits)") {
UIApplication.shared.open(url)
}
})
present(alert, animated: true)
present(ScenicQueueDialSheetViewController(digits: digits), animated: true)
}
@objc private func settingsTapped() {
viewModel.openSettings()
}
@objc private func tabChanged() {
selectedTab = segmentedControl.selectedSegmentIndex
tableView.reloadData()
private func selectTab(_ index: Int) {
guard index != selectedTab else { return }
selectedTab = index
UIView.transition(with: tableView, duration: 0.22, options: .transitionCrossDissolve) {
self.applySnapshot(animated: false)
}
applyState()
}
@ -298,30 +348,7 @@ final class ScenicQueueViewController: BaseViewController {
}
}
extension ScenicQueueViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
selectedTab == 0 ? viewModel.currentQueue.count : viewModel.skippedQueue.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if selectedTab == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueTicketCell.reuseIdentifier, for: indexPath) as! ScenicQueueTicketCell
let ticket = viewModel.currentQueue[indexPath.row]
cell.delegate = self
cell.configure(
ticket: ticket,
shootingQueueNo: viewModel.shootingTimedQueueNo,
remainSeconds: viewModel.shootingRemainSeconds,
showStartShootingButton: viewModel.showStartShootingButton
)
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: ScenicQueueSkippedCell.reuseIdentifier, for: indexPath) as! ScenicQueueSkippedCell
cell.delegate = self
cell.configure(ticket: viewModel.skippedQueue[indexPath.row])
return cell
}
extension ScenicQueueViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 120 else { return }
Task {
@ -341,7 +368,9 @@ extension ScenicQueueViewController: ScenicQueueTicketCellDelegate {
func scenicQueueTicketCellDidTapSkip(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket) {
viewModel.requestSkipTicket(recordId: ticket.recordId, queueNo: ticket.queueNo)
presentConfirm(title: "确认过号", message: ticket.queueNo) { [weak self] in
presentConfirm(title: "确认过号", emphasizedText: ticket.queueNo, dangerText: "确认该用户未到场并过号", cancel: { [weak self] in
self?.viewModel.dismissSkipDialog()
}) { [weak self] in
guard let self else { return }
Task { await self.viewModel.confirmSkipTicket(api: self.api) }
}
@ -350,7 +379,9 @@ extension ScenicQueueViewController: ScenicQueueTicketCellDelegate {
func scenicQueueTicketCellDidTapPrimary(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket) {
if ticket.status == .waiting {
viewModel.requestCallConfirm(recordId: ticket.recordId, queueNo: ticket.queueNo)
presentConfirm(title: "确认叫号", message: ticket.queueNo) { [weak self] in
presentConfirm(title: "确认叫号", emphasizedText: "确认叫号【\(ticket.queueNo)", cancel: { [weak self] in
self?.viewModel.dismissCallConfirmDialog()
}) { [weak self] in
guard let self else { return }
Task { await self.viewModel.confirmCallTicket(api: self.api) }
}
@ -361,7 +392,9 @@ extension ScenicQueueViewController: ScenicQueueTicketCellDelegate {
func scenicQueueTicketCellDidTapRecall(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket) {
viewModel.requestCallConfirm(recordId: ticket.recordId, queueNo: ticket.queueNo, isRecall: true)
presentConfirm(title: "重新叫号", message: ticket.queueNo) { [weak self] in
presentConfirm(title: "重新叫号", emphasizedText: "确认叫号【\(ticket.queueNo)", cancel: { [weak self] in
self?.viewModel.dismissCallConfirmDialog()
}) { [weak self] in
guard let self else { return }
Task { await self.viewModel.confirmCallTicket(api: self.api) }
}
@ -369,7 +402,9 @@ extension ScenicQueueViewController: ScenicQueueTicketCellDelegate {
func scenicQueueTicketCellDidTapComplete(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket) {
viewModel.requestFinishConfirm(recordId: ticket.recordId, queueNo: ticket.queueNo)
presentConfirm(title: "确认完成拍摄", message: ticket.queueNo) { [weak self] in
presentConfirm(title: "确认完成拍摄", emphasizedText: ticket.queueNo, cancel: { [weak self] in
self?.viewModel.dismissFinishDialog()
}) { [weak self] in
guard let self else { return }
Task { await self.viewModel.confirmFinishTicket(api: self.api) }
}
@ -377,25 +412,16 @@ extension ScenicQueueViewController: ScenicQueueTicketCellDelegate {
func scenicQueueTicketCellDidTapMark(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket) {
viewModel.onTicketMarkClick(recordId: ticket.recordId, queueNo: ticket.queueNo, uid: ticket.uid)
let alert = UIAlertController(title: "标记用户", message: "\(ticket.queueNo)\n\(ticket.phoneMasked)", preferredStyle: .alert)
alert.addTextField { field in
field.placeholder = "限制排队天数"
field.keyboardType = .numberPad
field.text = "0"
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
self?.viewModel.dismissMarkDialog()
})
alert.addAction(UIAlertAction(title: "标记打野", style: .default) { [weak self, weak alert] _ in
guard let self else { return }
let days = Int(alert?.textFields?.first?.text ?? "") ?? 0
Task { await self.viewModel.confirmUserMark(api: self.api, markAsFreelancePhotog: true, queueBanDays: days) }
})
alert.addAction(UIAlertAction(title: "取消标记", style: .destructive) { [weak self] _ in
guard let self else { return }
Task { await self.viewModel.confirmUserMark(api: self.api, markAsFreelancePhotog: false, queueBanDays: 0) }
})
present(alert, animated: true)
guard let pending = viewModel.pendingMark else { return }
let dialog = ScenicQueueMarkDialogViewController(
pending: pending,
onCancel: { [weak self] in self?.viewModel.dismissMarkDialog() },
onConfirm: { [weak self] marked, days in
guard let self else { return }
Task { await self.viewModel.confirmUserMark(api: self.api, markAsFreelancePhotog: marked, queueBanDays: days) }
}
)
present(dialog, animated: true)
}
}
@ -406,7 +432,9 @@ extension ScenicQueueViewController: ScenicQueueSkippedCellDelegate {
func scenicQueueSkippedCellDidTapRequeue(_ cell: ScenicQueueSkippedCell, ticket: ScenicQueueSkippedTicket) {
viewModel.requestRequeueConfirm(recordId: ticket.recordId, queueNo: ticket.queueNo)
presentConfirm(title: "确认重新排队", message: ticket.queueNo) { [weak self] in
presentConfirm(title: "确认重新排队", emphasizedText: ticket.queueNo, dangerText: "确认后将插入当前排队队列", confirmTitle: "确定", cancel: { [weak self] in
self?.viewModel.dismissRequeueDialog()
}) { [weak self] in
guard let self else { return }
Task { await self.viewModel.confirmRequeueTicket(api: self.api) }
}

View File

@ -0,0 +1,396 @@
//
// ScenicQueueOverlayViews.swift
// suixinkan
//
import SnapKit
import UIKit
/// Android
final class ScenicQueueTabStripView: UIView {
private let stackView = UIStackView()
private let indicator = UIView()
private let divider = UIView()
private var buttons: [UIButton] = []
///
var onSelect: ((Int) -> Void)?
///
private(set) var selectedIndex = 0
init(titles: [String]) {
super.init(frame: .zero)
backgroundColor = .white
stackView.axis = .horizontal
stackView.distribution = .fillEqually
titles.enumerated().forEach { index, title in
let button = UIButton(type: .system)
button.tag = index
button.setTitle(title, for: .normal)
button.titleLabel?.font = ScenicQueueTokens.tabFont
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
buttons.append(button)
stackView.addArrangedSubview(button)
}
indicator.backgroundColor = ScenicQueueTokens.bannerBlue
divider.backgroundColor = ScenicQueueTokens.cardOutline
addSubview(stackView)
addSubview(divider)
addSubview(indicator)
stackView.snp.makeConstraints { $0.edges.equalToSuperview() }
divider.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(1 / UIScreen.main.scale)
}
setSelectedIndex(0, animated: false)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
updateIndicatorFrame()
}
/// 3pt
func setSelectedIndex(_ index: Int, animated: Bool) {
guard buttons.indices.contains(index) else { return }
selectedIndex = index
buttons.enumerated().forEach { item in
item.element.setTitleColor(item.offset == index ? ScenicQueueTokens.bannerBlue : AppColor.textSecondary, for: .normal)
item.element.titleLabel?.font = item.offset == index ? ScenicQueueTokens.tabSelectedFont : ScenicQueueTokens.tabFont
}
let changes = { self.updateIndicatorFrame() }
animated ? UIView.animate(withDuration: 0.22, animations: changes) : changes()
}
private func updateIndicatorFrame() {
guard !buttons.isEmpty, bounds.width > 0 else { return }
let width = bounds.width / CGFloat(buttons.count)
indicator.frame = CGRect(x: width * CGFloat(selectedIndex), y: bounds.height - 3, width: width, height: 3)
}
@objc private func buttonTapped(_ sender: UIButton) {
setSelectedIndex(sender.tag, animated: true)
onSelect?(sender.tag)
}
}
///
struct ScenicQueueDialogConfiguration {
let title: String
let message: String?
let emphasizedText: String?
let dangerText: String?
let cancelTitle: String
let confirmTitle: String
let buttonRadius: CGFloat
let dismissOnBackdrop: Bool
}
/// Android
final class ScenicQueueConfirmationDialogViewController: UIViewController, UIGestureRecognizerDelegate {
private let configuration: ScenicQueueDialogConfiguration
private let onCancel: () -> Void
private let onConfirm: () -> Void
init(configuration: ScenicQueueDialogConfiguration, onCancel: @escaping () -> Void, onConfirm: @escaping () -> Void) {
self.configuration = configuration
self.onCancel = onCancel
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.42)
if configuration.dismissOnBackdrop {
let backdropTap = UITapGestureRecognizer(target: self, action: #selector(cancelTapped))
backdropTap.delegate = self
view.addGestureRecognizer(backdropTap)
}
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = ScenicQueueTokens.radiusDialog
card.layer.shadowColor = UIColor.black.cgColor
card.layer.shadowOpacity = 0.18
card.layer.shadowRadius = 8
let titleLabel = UILabel()
titleLabel.text = configuration.title
titleLabel.font = ScenicQueueTokens.dialogTitleFont
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
let contentStack = UIStackView(arrangedSubviews: [titleLabel])
contentStack.axis = .vertical
contentStack.spacing = 10
if let message = configuration.message {
let label = makeLabel(message, font: .systemFont(ofSize: 15), color: AppColor.textSecondary)
contentStack.addArrangedSubview(label)
}
if let danger = configuration.dangerText {
let label = makeLabel(danger, font: .systemFont(ofSize: 13), color: ScenicQueueTokens.dialogDangerLine)
contentStack.addArrangedSubview(label)
}
if let emphasized = configuration.emphasizedText {
let label = makeLabel(emphasized, font: ScenicQueueTokens.dialogTicketFont, color: AppColor.textPrimary)
contentStack.addArrangedSubview(label)
contentStack.setCustomSpacing(20, after: label)
}
let cancel = makeButton(configuration.cancelTitle, background: ScenicQueueTokens.dialogCancelBackground, foreground: ScenicQueueTokens.bannerBlue)
cancel.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let confirm = makeButton(configuration.confirmTitle, background: ScenicQueueTokens.bannerBlue, foreground: .white)
confirm.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
cancel.layer.cornerRadius = configuration.buttonRadius
confirm.layer.cornerRadius = configuration.buttonRadius
let buttons = UIStackView(arrangedSubviews: [cancel, confirm])
buttons.axis = .horizontal
buttons.distribution = .fillEqually
buttons.spacing = 12
contentStack.addArrangedSubview(buttons)
view.addSubview(card)
card.addSubview(contentStack)
card.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(32)
}
contentStack.snp.makeConstraints { $0.edges.equalToSuperview().inset(22) }
buttons.snp.makeConstraints { $0.height.equalTo(44) }
}
private func makeLabel(_ text: String, font: UIFont, color: UIColor) -> UILabel {
let label = UILabel()
label.text = text
label.font = font
label.textColor = color
label.textAlignment = .center
label.numberOfLines = 0
return label
}
private func makeButton(_ title: String, background: UIColor, foreground: UIColor) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(foreground, for: .normal)
button.titleLabel?.font = ScenicQueueTokens.dialogButtonFont
button.backgroundColor = background
button.clipsToBounds = true
return button
}
@objc private func cancelTapped() {
dismiss(animated: true, completion: onCancel)
}
@objc private func confirmTapped() {
dismiss(animated: true, completion: onConfirm)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
touch.view === view
}
}
/// Android
final class ScenicQueueDialSheetViewController: UIViewController, UIGestureRecognizerDelegate {
private let digits: String
init(digits: String) {
self.digits = digits
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.36)
let backdropTap = UITapGestureRecognizer(target: self, action: #selector(cancelTapped))
backdropTap.delegate = self
view.addGestureRecognizer(backdropTap)
let panel = UIView()
panel.backgroundColor = .white
panel.layer.cornerRadius = 18
panel.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
let title = UILabel()
title.text = "拨打电话"
title.font = ScenicQueueTokens.dialogTitleFont
title.textAlignment = .center
let number = UILabel()
number.text = digits
number.font = .systemFont(ofSize: 18, weight: .medium)
number.textColor = AppColor.textPrimary
number.textAlignment = .center
let cancel = pillButton("取消", background: ScenicQueueTokens.dialogCancelBackground, foreground: ScenicQueueTokens.bannerBlue)
cancel.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let dial = pillButton("拨打", background: ScenicQueueTokens.bannerBlue, foreground: .white)
dial.addTarget(self, action: #selector(dialTapped), for: .touchUpInside)
let buttons = UIStackView(arrangedSubviews: [cancel, dial])
buttons.axis = .horizontal
buttons.distribution = .fillEqually
buttons.spacing = 12
view.addSubview(panel)
[title, number, buttons].forEach(panel.addSubview)
panel.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() }
title.snp.makeConstraints { make in make.top.equalToSuperview().offset(18); make.centerX.equalToSuperview() }
number.snp.makeConstraints { make in make.top.equalTo(title.snp.bottom).offset(18); make.leading.trailing.equalToSuperview().inset(16) }
buttons.snp.makeConstraints { make in
make.top.equalTo(number.snp.bottom).offset(18)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(44)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
}
}
private func pillButton(_ title: String, background: UIColor, foreground: UIColor) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(foreground, for: .normal)
button.titleLabel?.font = ScenicQueueTokens.dialogButtonFont
button.backgroundColor = background
button.layer.cornerRadius = 22
return button
}
@objc private func cancelTapped() { dismiss(animated: true) }
@objc private func dialTapped() {
dismiss(animated: true) {
guard let url = URL(string: "tel:\(self.digits)") else { return }
UIApplication.shared.open(url)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
touch.view === view
}
}
/// Android
final class ScenicQueueMarkDialogViewController: UIViewController {
private let pending: ScenicQueuePendingMark
private let onCancel: () -> Void
private let onConfirm: (Bool, Int) -> Void
private let markSwitch = UISwitch()
private let daysLabel = UILabel()
private var days = 0 { didSet { daysLabel.text = "\(days)" } }
init(pending: ScenicQueuePendingMark, onCancel: @escaping () -> Void, onConfirm: @escaping (Bool, Int) -> Void) {
self.pending = pending
self.onCancel = onCancel
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.42)
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = ScenicQueueTokens.markDialogRadius
let title = UILabel()
title.text = "标记用户"
title.font = ScenicQueueTokens.dialogTitleFont
title.textAlignment = .center
let queue = UILabel()
queue.text = pending.queueNo
queue.font = .systemFont(ofSize: 24, weight: .bold)
queue.textColor = ScenicQueueTokens.bannerBlue
let phone = UILabel()
phone.text = pending.phoneMasked
phone.font = ScenicQueueTokens.phoneMaskedFont
phone.textColor = AppColor.textSecondary
let identityTitle = UILabel()
identityTitle.text = "标记为打野摄影师"
identityTitle.font = ScenicQueueTokens.settingsRowTitleFont
markSwitch.isOn = pending.markAsFreelancePhotog == 1
markSwitch.onTintColor = ScenicQueueTokens.bannerBlue
let identityRow = UIStackView(arrangedSubviews: [identityTitle, UIView(), markSwitch])
identityRow.axis = .horizontal
identityRow.alignment = .center
let hint = UILabel()
hint.text = "标记后,该用户扫码时将受排队限制。"
hint.font = ScenicQueueTokens.settingsHintFont
hint.textColor = AppColor.textTertiary
hint.numberOfLines = 0
let daysTitle = UILabel()
daysTitle.text = "限制排队天数"
daysTitle.font = ScenicQueueTokens.settingsRowTitleFont
let minus = stepButton("", action: #selector(minusTapped))
let plus = stepButton("+", action: #selector(plusTapped))
daysLabel.text = "0"
daysLabel.font = .systemFont(ofSize: 16, weight: .medium)
daysLabel.textAlignment = .center
let stepper = UIStackView(arrangedSubviews: [minus, daysLabel, plus])
stepper.axis = .horizontal
stepper.alignment = .center
stepper.distribution = .fillEqually
stepper.backgroundColor = ScenicQueueTokens.markDialogStepperBackground
stepper.layer.cornerRadius = 10
let cancel = actionButton("取消", background: ScenicQueueTokens.markDialogCancelBackground, foreground: ScenicQueueTokens.bannerBlue)
cancel.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let confirm = actionButton("完成", background: ScenicQueueTokens.bannerBlue, foreground: .white)
confirm.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
let buttons = UIStackView(arrangedSubviews: [cancel, confirm])
buttons.axis = .horizontal
buttons.distribution = .fillEqually
buttons.spacing = 12
let stack = UIStackView(arrangedSubviews: [title, queue, phone, identityRow, hint, daysTitle, stepper, buttons])
stack.axis = .vertical
stack.spacing = 12
view.addSubview(card)
card.addSubview(stack)
card.snp.makeConstraints { make in make.center.equalToSuperview(); make.width.equalToSuperview().multipliedBy(0.88) }
stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(22) }
stepper.snp.makeConstraints { $0.height.equalTo(44) }
buttons.snp.makeConstraints { $0.height.equalTo(44) }
}
private func stepButton(_ title: String, action: Selector) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(ScenicQueueTokens.bannerBlue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 22, weight: .medium)
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
private func actionButton(_ title: String, background: UIColor, foreground: UIColor) -> UIButton {
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
button.setTitleColor(foreground, for: .normal)
button.titleLabel?.font = ScenicQueueTokens.dialogButtonFont
button.backgroundColor = background
button.layer.cornerRadius = ScenicQueueTokens.markDialogButtonRadius
return button
}
@objc private func minusTapped() { days = max(0, days - 1) }
@objc private func plusTapped() { days = min(999, days + 1) }
@objc private func cancelTapped() { dismiss(animated: true, completion: onCancel) }
@objc private func confirmTapped() {
let marked = markSwitch.isOn
dismiss(animated: true) { self.onConfirm(marked, marked ? self.days : 0) }
}
}

View File

@ -261,33 +261,38 @@ final class ScenicQueueTicketCell: UITableViewCell {
private func rebuildActions(ticket: ScenicQueueTicket, shootingQueueNo: String?, remainSeconds: Int) {
actionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
let state = ScenicQueueTicketActionState.make(
ticket: ticket,
shootingQueueNo: shootingQueueNo,
remainSeconds: remainSeconds,
showStartShootingButton: showStartShootingButton
)
let skip = ScenicQueueActionButton(title: "过号", background: ScenicQueueTokens.buttonSkipBackground, foreground: ScenicQueueTokens.buttonSkipForeground)
skip.addTarget(self, action: #selector(skipTapped), for: .touchUpInside)
let primaryTitle: String
if shootingQueueNo == ticket.queueNo {
primaryTitle = "剩余 \(max(0, remainSeconds))"
} else {
primaryTitle = ticket.status == .called ? "开始拍摄" : "叫号"
}
let primary = ScenicQueueActionButton(title: primaryTitle, background: ScenicQueueTokens.bannerBlue, foreground: .white)
primary.isEnabled = shootingQueueNo != ticket.queueNo
primary.alpha = primary.isEnabled ? 1 : 0.65
primary.addTarget(self, action: #selector(primaryTapped), for: .touchUpInside)
let row = UIStackView(arrangedSubviews: [skip, primary])
skip.isEnabled = state.skipEnabled
skip.alpha = state.skipEnabled ? 1 : 0.45
let row = UIStackView(arrangedSubviews: [skip])
row.axis = .horizontal
row.spacing = 10
row.distribution = .fillProportionally
skip.snp.makeConstraints { make in
make.height.equalTo(46)
make.width.equalTo(primary).multipliedBy(0.56)
if state.primaryVisible {
let primary = ScenicQueueActionButton(title: state.primaryTitle, background: ScenicQueueTokens.bannerBlue, foreground: .white)
primary.isEnabled = state.primaryEnabled
primary.alpha = state.primaryEnabled ? 1 : 0.65
primary.addTarget(self, action: #selector(primaryTapped), for: .touchUpInside)
row.addArrangedSubview(primary)
skip.snp.makeConstraints { $0.width.equalTo(primary).multipliedBy(0.56) }
}
row.snp.makeConstraints { $0.height.equalTo(46) }
actionsStack.addArrangedSubview(row)
if ticket.status == .called, shootingQueueNo != ticket.queueNo {
recallButton.removeTarget(nil, action: nil, for: .allEvents)
completeButton.removeTarget(nil, action: nil, for: .allEvents)
if state.recallVisible {
recallButton.addTarget(self, action: #selector(recallTapped), for: .touchUpInside)
recallButton.snp.makeConstraints { make in make.height.equalTo(40) }
actionsStack.addArrangedSubview(recallButton)
}
if ticket.showCompleteAction {
if state.completeVisible {
completeButton.addTarget(self, action: #selector(completeTapped), for: .touchUpInside)
completeButton.snp.makeConstraints { make in make.height.equalTo(40) }
actionsStack.addArrangedSubview(completeButton)
@ -329,17 +334,27 @@ final class ScenicQueueTicketCell: UITableViewCell {
right.backgroundColor = accent ? AppColor.primaryLight : .clear
right.layer.cornerRadius = 2
right.clipsToBounds = true
if let tap {
right.isUserInteractionEnabled = true
right.addGestureRecognizer(UITapGestureRecognizer(target: self, action: tap))
}
row.addSubview(left)
row.addSubview(right)
let valueContainer = UIStackView(arrangedSubviews: [right])
valueContainer.axis = .horizontal
valueContainer.alignment = .center
valueContainer.spacing = 8
if let tap {
let dial = UIButton(type: .system)
dial.backgroundColor = ScenicQueueTokens.bannerBlue
dial.tintColor = .white
dial.setImage(UIImage(systemName: "phone.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
dial.layer.cornerRadius = 16
dial.addTarget(self, action: tap, for: .touchUpInside)
valueContainer.addArrangedSubview(dial)
dial.snp.makeConstraints { $0.size.equalTo(32) }
}
row.addSubview(valueContainer)
left.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
make.top.bottom.equalToSuperview()
}
right.snp.makeConstraints { make in
valueContainer.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.leading.greaterThanOrEqualTo(left.snp.trailing).offset(12)
}
@ -490,17 +505,27 @@ final class ScenicQueueSkippedCell: UITableViewCell {
right.text = value
right.font = ScenicQueueTokens.phoneMaskedFont
right.textColor = AppColor.textPrimary
if let tap {
right.isUserInteractionEnabled = true
right.addGestureRecognizer(UITapGestureRecognizer(target: self, action: tap))
}
row.addSubview(left)
row.addSubview(right)
let valueContainer = UIStackView(arrangedSubviews: [right])
valueContainer.axis = .horizontal
valueContainer.alignment = .center
valueContainer.spacing = 8
if let tap {
let dial = UIButton(type: .system)
dial.backgroundColor = ScenicQueueTokens.bannerBlue
dial.tintColor = .white
dial.setImage(UIImage(systemName: "phone.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
dial.layer.cornerRadius = 16
dial.addTarget(self, action: tap, for: .touchUpInside)
valueContainer.addArrangedSubview(dial)
dial.snp.makeConstraints { $0.size.equalTo(32) }
}
row.addSubview(valueContainer)
left.snp.makeConstraints { make in
make.leading.centerY.equalToSuperview()
make.top.bottom.equalToSuperview()
}
right.snp.makeConstraints { make in
valueContainer.snp.makeConstraints { make in
make.trailing.centerY.equalToSuperview()
make.leading.greaterThanOrEqualTo(left.snp.trailing).offset(12)
}

View File

@ -19,10 +19,10 @@ final class SettingViewController: BaseViewController {
init(
viewModel: SettingViewModel = SettingViewModel(),
settingAPI: SettingAPI = NetworkServices.shared.settingAPI
settingAPI: SettingAPI? = nil
) {
self.viewModel = viewModel
self.settingAPI = settingAPI
self.settingAPI = settingAPI ?? NetworkServices.shared.settingAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -28,9 +28,9 @@ final class TravelAlbumDetailViewController: BaseViewController {
private let uploadButton = UIButton(type: .system)
private var isDeleteSelectedButtonVisible = false
init(albumId: Int, api: any TravelAlbumServing = NetworkServices.shared.travelAlbumAPI) {
init(albumId: Int, api: (any TravelAlbumServing)? = nil) {
viewModel = TravelAlbumDetailViewModel(albumId: albumId)
self.api = api
self.api = api ?? NetworkServices.shared.travelAlbumAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -18,8 +18,8 @@ final class TravelAlbumEntryViewController: BaseViewController {
private let emptyView = TravelAlbumEmptyView()
private var dataSource: UITableViewDiffableDataSource<Int, TravelAlbum>!
init(api: any TravelAlbumServing = NetworkServices.shared.travelAlbumAPI) {
self.api = api
init(api: (any TravelAlbumServing)? = nil) {
self.api = api ?? NetworkServices.shared.travelAlbumAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -36,10 +36,10 @@ final class PointsRedemptionViewController: BaseViewController, UITableViewDeleg
///
init(
viewModel: PointsRedemptionViewModel = PointsRedemptionViewModel(),
walletAPI: any WalletPageServing = NetworkServices.shared.walletAPI
walletAPI: (any WalletPageServing)? = nil
) {
self.viewModel = viewModel
self.walletAPI = walletAPI
self.walletAPI = walletAPI ?? NetworkServices.shared.walletAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -41,12 +41,12 @@ final class WalletViewController: BaseViewController, UITableViewDelegate {
///
init(
viewModel: WalletViewModel = WalletViewModel(),
walletAPI: any WalletPageServing = NetworkServices.shared.walletAPI,
profileAPI: any WalletProfileServing = NetworkServices.shared.profileAPI
walletAPI: (any WalletPageServing)? = nil,
profileAPI: (any WalletProfileServing)? = nil
) {
self.viewModel = viewModel
self.walletAPI = walletAPI
self.profileAPI = profileAPI
self.walletAPI = walletAPI ?? NetworkServices.shared.walletAPI
self.profileAPI = profileAPI ?? NetworkServices.shared.profileAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -34,10 +34,10 @@ final class WithdrawViewController: BaseViewController {
///
init(
viewModel: WithdrawViewModel = WithdrawViewModel(),
walletAPI: any WalletPageServing = NetworkServices.shared.walletAPI
walletAPI: (any WalletPageServing)? = nil
) {
self.viewModel = viewModel
self.walletAPI = walletAPI
self.walletAPI = walletAPI ?? NetworkServices.shared.walletAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -15,10 +15,10 @@ final class WildPhotographerReportHomeViewController: BaseViewController {
init(
viewModel: WildPhotographerReportHomeViewModel = WildPhotographerReportHomeViewModel(),
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI
api: (any WildPhotographerReportServing)? = nil
) {
self.viewModel = viewModel
self.api = api
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -18,10 +18,10 @@ final class WildPhotographerReportListViewController: BaseViewController {
init(
homeViewModel: WildPhotographerReportHomeViewModel,
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI
api: (any WildPhotographerReportServing)? = nil
) {
self.viewModel = WildPhotographerReportListViewModel(homeViewModel: homeViewModel)
self.api = api
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
super.init(nibName: nil, bundle: nil)
}
@ -560,11 +560,11 @@ final class WildPhotographerReportDetailViewController: BaseViewController {
init(
record: WildReportRecord,
homeViewModel: WildPhotographerReportHomeViewModel,
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI
api: (any WildPhotographerReportServing)? = nil
) {
self.viewModel = WildPhotographerReportDetailViewModel(record: record, homeViewModel: homeViewModel)
self.homeViewModel = homeViewModel
self.api = api
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
super.init(nibName: nil, bundle: nil)
}

View File

@ -16,6 +16,8 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let descriptionView = UITextView()
private let descriptionPlaceholderLabel = UILabel()
private let descriptionCountLabel = UILabel()
private let contactField = UITextField()
private let submitButton = UIButton(type: .system)
private var pickerTarget: WildReportPickerTarget = .image
@ -25,12 +27,12 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
init(
homeViewModel: WildPhotographerReportHomeViewModel,
locationProvider: any LocationProviding = LocationProvider.shared,
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
uploader: any WildReportAttachmentUploading = NetworkServices.shared.ossUploadService
api: (any WildPhotographerReportServing)? = nil,
uploader: (any WildReportAttachmentUploading)? = nil
) {
self.homeViewModel = homeViewModel
self.api = api
self.uploader = uploader
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
self.uploader = uploader ?? NetworkServices.shared.ossUploadService
self.viewModel = WildPhotographerReportSubmitViewModel(
homeViewModel: homeViewModel,
locationProvider: locationProvider
@ -64,6 +66,14 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
descriptionView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 34, right: 8)
descriptionView.delegate = self
descriptionPlaceholderLabel.text = "请描述摄影师的位置、特征、行为..."
descriptionPlaceholderLabel.font = .systemFont(ofSize: 15)
descriptionPlaceholderLabel.textColor = UIColor(hex: 0x9CA3AF)
descriptionCountLabel.font = .systemFont(ofSize: 14)
descriptionCountLabel.textColor = UIColor(hex: 0x6B7280)
descriptionCountLabel.textAlignment = .right
contactField.placeholder = "请输入摄影师微信号或手机号(选填)"
contactField.font = .app(.body)
contactField.textColor = AppColor.textPrimary
@ -271,34 +281,28 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
private func makeDescriptionCard() -> UIView {
let card = sectionCard(index: 3, title: "举报说明")
descriptionView.text = viewModel.description
updateDescriptionInputState()
let inputContainer = UIView()
inputContainer.backgroundColor = UIColor(hex: 0xFBFCFE)
inputContainer.layer.cornerRadius = 12
inputContainer.layer.borderWidth = 1
inputContainer.layer.borderColor = UIColor(hex: 0xCDD5E1).cgColor
let placeholder = UILabel()
placeholder.text = "请描述摄影师的位置、特征、行为..."
placeholder.font = .systemFont(ofSize: 15)
placeholder.textColor = UIColor(hex: 0x9CA3AF)
placeholder.isHidden = !viewModel.description.isEmpty
let count = WildReportUI.label("\(viewModel.description.count)/500", font: .systemFont(ofSize: 14), color: UIColor(hex: 0x6B7280))
count.textAlignment = .right
inputContainer.addSubview(descriptionView)
inputContainer.addSubview(placeholder)
inputContainer.addSubview(count)
inputContainer.addSubview(descriptionPlaceholderLabel)
inputContainer.addSubview(descriptionCountLabel)
descriptionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(132)
}
placeholder.snp.makeConstraints { make in
make.top.equalToSuperview().offset(18)
make.leading.equalToSuperview().offset(14)
let textOriginX = descriptionView.textContainerInset.left
+ descriptionView.textContainer.lineFragmentPadding
descriptionPlaceholderLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(descriptionView.textContainerInset.top)
make.leading.equalToSuperview().offset(textOriginX)
make.trailing.lessThanOrEqualToSuperview().inset(14)
}
count.snp.makeConstraints { make in
descriptionCountLabel.snp.makeConstraints { make in
make.trailing.bottom.equalToSuperview().inset(12)
}
card.stack.addArrangedSubview(inputContainer)
@ -376,6 +380,11 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
return card
}
private func updateDescriptionInputState() {
descriptionPlaceholderLabel.isHidden = !viewModel.description.isEmpty
descriptionCountLabel.text = "\(viewModel.description.count)/500"
}
private func makePaymentCard() -> UIView {
let card = sectionCard(index: 6, title: "线下微信、支付宝截图", optional: true)
card.stack.addArrangedSubview(makeEvidenceUploadBlock(
@ -561,7 +570,12 @@ final class WildPhotographerReportSubmitViewController: BaseViewController {
)
}
}
@objc private func contactChanged() { viewModel.updateContact(contactField.text ?? "") }
@objc private func contactChanged() {
viewModel.updateContact(contactField.text ?? "")
guard contactField.markedTextRange == nil,
contactField.text != viewModel.contact else { return }
contactField.text = viewModel.contact
}
@objc private func dismissKeyboard() { view.endEditing(true) }
@MainActor
@ -654,6 +668,10 @@ extension WildPhotographerReportSubmitViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
viewModel.updateDescription(textView.text)
if textView.markedTextRange == nil, textView.text != viewModel.description {
textView.text = viewModel.description
}
updateDescriptionInputState()
}
func textViewDidEndEditing(_ textView: UITextView) {

View File

@ -32,13 +32,13 @@ final class WildReportRiskMapViewController: BaseViewController {
private var isShowingRiskMapLoading = false
init(
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
api: (any WildPhotographerReportServing)? = nil,
locationProvider: any LocationProviding = LocationProvider.shared,
scenicIdProvider: @escaping () -> Int = { AppStore.shared.session.currentScenicId },
scenicNameProvider: @escaping () -> String = { AppStore.shared.session.currentScenicName }
) {
self.viewModel = WildReportRiskMapViewModel()
self.api = api
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
self.locationProvider = locationProvider
self.scenicIdProvider = scenicIdProvider
self.scenicNameProvider = scenicNameProvider
@ -675,7 +675,7 @@ final class WildReportRiskMapViewController: BaseViewController {
button.tintColor = AppColor.primary
button.setTitleColor(AppColor.primary, for: .normal)
button.semanticContentAttribute = .forceRightToLeft
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: -4)
button.setConfigurationImagePlacement(.trailing, padding: 4)
if let phone {
button.addAction(UIAction { [weak self] _ in
self?.callPhone(phone)

View File

@ -28,13 +28,13 @@ final class WildReportSupplementEvidenceViewController: BaseViewController {
init(
record: WildReportRecord,
homeViewModel: WildPhotographerReportHomeViewModel,
api: any WildPhotographerReportServing = NetworkServices.shared.wildPhotographerReportAPI,
uploader: any WildReportAttachmentUploading = NetworkServices.shared.ossUploadService,
api: (any WildPhotographerReportServing)? = nil,
uploader: (any WildReportAttachmentUploading)? = nil,
onSupplementSuccess: (() -> Void)? = nil
) {
self.viewModel = WildReportSupplementEvidenceViewModel(record: record, homeViewModel: homeViewModel)
self.api = api
self.uploader = uploader
self.api = api ?? NetworkServices.shared.wildPhotographerReportAPI
self.uploader = uploader ?? NetworkServices.shared.ossUploadService
self.onSupplementSuccess = onSupplementSuccess
super.init(nibName: nil, bundle: nil)
}