Files
suixinkan_uikit/suixinkan/UI/ScenicQueue/Views/ScenicQueueOverlayViews.swift
汉秋 6336feff27 完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 16:21:53 +08:00

397 lines
17 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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) }
}
}