完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user