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