重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
569 lines
23 KiB
Swift
569 lines
23 KiB
Swift
//
|
|
// ScenicQueueViews.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
/// 排队统计 Banner。
|
|
final class ScenicQueueStatsBannerView: UIView {
|
|
private let peopleValueLabel = UILabel()
|
|
private let avgValueLabel = UILabel()
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
setupUI()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 更新统计。
|
|
func configure(summary: ScenicQueueSummary) {
|
|
peopleValueLabel.text = "\(summary.peopleInQueue)"
|
|
avgValueLabel.text = "\(summary.avgWaitMinutes)"
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundColor = ScenicQueueTokens.bannerBlue
|
|
|
|
let left = metricColumn(title: "在排人数", valueLabel: peopleValueLabel, unit: "人")
|
|
let right = metricColumn(title: "平均等待", valueLabel: avgValueLabel, unit: "分钟")
|
|
let divider = UIView()
|
|
divider.backgroundColor = ScenicQueueTokens.bannerDivider
|
|
|
|
addSubview(left)
|
|
addSubview(divider)
|
|
addSubview(right)
|
|
|
|
left.snp.makeConstraints { make in
|
|
make.leading.top.bottom.equalToSuperview()
|
|
make.trailing.equalTo(divider.snp.leading)
|
|
make.width.equalTo(right)
|
|
}
|
|
divider.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.centerY.equalToSuperview()
|
|
make.width.equalTo(0.8)
|
|
make.height.equalTo(42)
|
|
}
|
|
right.snp.makeConstraints { make in
|
|
make.leading.equalTo(divider.snp.trailing)
|
|
make.trailing.top.bottom.equalToSuperview()
|
|
}
|
|
}
|
|
|
|
private func metricColumn(title: String, valueLabel: UILabel, unit: String) -> UIView {
|
|
let container = UIView()
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = title
|
|
titleLabel.textColor = ScenicQueueTokens.bannerMutedLabel
|
|
titleLabel.font = ScenicQueueTokens.bannerLabelFont
|
|
titleLabel.textAlignment = .center
|
|
|
|
valueLabel.textColor = .white
|
|
valueLabel.font = ScenicQueueTokens.bannerValueFont
|
|
valueLabel.textAlignment = .center
|
|
|
|
let unitLabel = UILabel()
|
|
unitLabel.text = unit
|
|
unitLabel.textColor = .white
|
|
unitLabel.font = ScenicQueueTokens.bannerUnitFont
|
|
|
|
let valueStack = UIStackView(arrangedSubviews: [valueLabel, unitLabel])
|
|
valueStack.axis = .horizontal
|
|
valueStack.alignment = .lastBaseline
|
|
valueStack.spacing = 2
|
|
|
|
let stack = UIStackView(arrangedSubviews: [titleLabel, valueStack])
|
|
stack.axis = .vertical
|
|
stack.alignment = .center
|
|
stack.spacing = 6
|
|
container.addSubview(stack)
|
|
stack.snp.makeConstraints { make in
|
|
make.center.equalToSuperview()
|
|
}
|
|
return container
|
|
}
|
|
}
|
|
|
|
/// 排队列表标签。
|
|
final class ScenicQueueTagLabel: UILabel {
|
|
init() {
|
|
super.init(frame: .zero)
|
|
font = ScenicQueueTokens.statusChipFont
|
|
textAlignment = .center
|
|
layer.cornerRadius = ScenicQueueTokens.radiusChip
|
|
clipsToBounds = true
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 设置标签文案与颜色。
|
|
func configure(text: String, foreground: UIColor, background: UIColor) {
|
|
self.text = text
|
|
textColor = foreground
|
|
backgroundColor = background
|
|
}
|
|
}
|
|
|
|
/// 排队操作按钮。
|
|
final class ScenicQueueActionButton: UIButton {
|
|
init(title: String, background: UIColor, foreground: UIColor) {
|
|
super.init(frame: .zero)
|
|
setTitle(title, for: .normal)
|
|
titleLabel?.font = ScenicQueueTokens.buttonPrimaryFont
|
|
setTitleColor(foreground, for: .normal)
|
|
backgroundColor = background
|
|
layer.cornerRadius = ScenicQueueTokens.radiusPrimaryRowButton
|
|
clipsToBounds = true
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
/// 当前排队 cell 交互代理。
|
|
protocol ScenicQueueTicketCellDelegate: AnyObject {
|
|
func scenicQueueTicketCellDidTapDial(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
func scenicQueueTicketCellDidTapSkip(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
func scenicQueueTicketCellDidTapPrimary(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
func scenicQueueTicketCellDidTapRecall(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
func scenicQueueTicketCellDidTapComplete(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
func scenicQueueTicketCellDidTapMark(_ cell: ScenicQueueTicketCell, ticket: ScenicQueueTicket)
|
|
}
|
|
|
|
/// 当前排队列表 cell。
|
|
final class ScenicQueueTicketCell: UITableViewCell {
|
|
static let reuseIdentifier = "ScenicQueueTicketCell"
|
|
|
|
weak var delegate: ScenicQueueTicketCellDelegate?
|
|
|
|
private let cardView = UIView()
|
|
private let queueNoLabel = UILabel()
|
|
private let markButton = UIButton(type: .system)
|
|
private let tagStack = UIStackView()
|
|
private let metaStack = UIStackView()
|
|
private let actionsStack = UIStackView()
|
|
private let recallButton = ScenicQueueActionButton(title: "重新叫号", background: ScenicQueueTokens.buttonCompleteBackground, foreground: ScenicQueueTokens.bannerBlue)
|
|
private let completeButton = ScenicQueueActionButton(title: "完成", background: ScenicQueueTokens.buttonCompleteBackground, foreground: ScenicQueueTokens.bannerBlue)
|
|
private var ticket: ScenicQueueTicket?
|
|
private var showStartShootingButton = true
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
setupUI()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 配置当前排队 cell。
|
|
func configure(ticket: ScenicQueueTicket, shootingQueueNo: String?, remainSeconds: Int, showStartShootingButton: Bool) {
|
|
self.ticket = ticket
|
|
self.showStartShootingButton = showStartShootingButton
|
|
queueNoLabel.text = ticket.queueNo
|
|
markButton.isHidden = ticket.markAsPhotog == 1
|
|
rebuildTags(ticket)
|
|
rebuildMeta(ticket)
|
|
rebuildActions(ticket: ticket, shootingQueueNo: shootingQueueNo, remainSeconds: remainSeconds)
|
|
}
|
|
|
|
private func setupUI() {
|
|
selectionStyle = .none
|
|
backgroundColor = .clear
|
|
contentView.backgroundColor = .clear
|
|
|
|
cardView.backgroundColor = .white
|
|
cardView.layer.cornerRadius = ScenicQueueTokens.radiusCard
|
|
cardView.layer.borderWidth = 1
|
|
cardView.layer.borderColor = ScenicQueueTokens.cardOutline.cgColor
|
|
cardView.clipsToBounds = true
|
|
contentView.addSubview(cardView)
|
|
|
|
queueNoLabel.font = ScenicQueueTokens.ticketIdFont
|
|
queueNoLabel.textColor = AppColor.textPrimary
|
|
|
|
markButton.setTitle("标记", for: .normal)
|
|
markButton.titleLabel?.font = ScenicQueueTokens.rowLabelFont
|
|
markButton.setTitleColor(ScenicQueueTokens.bannerBlue, for: .normal)
|
|
markButton.addTarget(self, action: #selector(markTapped), for: .touchUpInside)
|
|
|
|
let titleRow = UIStackView(arrangedSubviews: [queueNoLabel, markButton, UIView(), tagStack])
|
|
titleRow.axis = .horizontal
|
|
titleRow.alignment = .center
|
|
titleRow.spacing = 4
|
|
tagStack.axis = .horizontal
|
|
tagStack.spacing = 6
|
|
tagStack.alignment = .center
|
|
|
|
metaStack.axis = .vertical
|
|
metaStack.spacing = 10
|
|
|
|
actionsStack.axis = .vertical
|
|
actionsStack.spacing = 10
|
|
|
|
let stack = UIStackView(arrangedSubviews: [titleRow, metaStack, actionsStack])
|
|
stack.axis = .vertical
|
|
stack.spacing = 12
|
|
cardView.addSubview(stack)
|
|
|
|
cardView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 5, left: 12, bottom: 5, right: 12))
|
|
}
|
|
stack.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 16, bottom: 14, right: 16))
|
|
}
|
|
}
|
|
|
|
private func rebuildTags(_ ticket: ScenicQueueTicket) {
|
|
tagStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
if ticket.isMissRequeue {
|
|
tagStack.addArrangedSubview(tag(text: ticket.missRequeueText, fg: ScenicQueueTokens.tagMissRequeueForeground, bg: ScenicQueueTokens.tagMissRequeueBackground))
|
|
}
|
|
let statusColors: (UIColor, UIColor) = ticket.status == .waiting
|
|
? (ScenicQueueTokens.tagWaitingForeground, ScenicQueueTokens.tagWaitingBackground)
|
|
: (ScenicQueueTokens.tagCalledForeground, ScenicQueueTokens.tagCalledBackground)
|
|
tagStack.addArrangedSubview(tag(text: ticket.statusText, fg: statusColors.0, bg: statusColors.1))
|
|
}
|
|
|
|
private func rebuildMeta(_ ticket: ScenicQueueTicket) {
|
|
metaStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
let identityRow = UIStackView()
|
|
identityRow.axis = .horizontal
|
|
identityRow.spacing = 6
|
|
if ticket.shouldShowIdentityTag {
|
|
let colors = identityColors(ticket.identityTag)
|
|
identityRow.addArrangedSubview(tag(text: ticket.identityTag, fg: colors.fg, bg: colors.bg))
|
|
}
|
|
if !ticket.queueBanLabel.isEmpty {
|
|
identityRow.addArrangedSubview(tag(text: ticket.queueBanLabel, fg: ScenicQueueTokens.tagBanForeground, bg: ScenicQueueTokens.tagBanBackground))
|
|
}
|
|
if !identityRow.arrangedSubviews.isEmpty {
|
|
metaStack.addArrangedSubview(identityRow)
|
|
}
|
|
metaStack.addArrangedSubview(kvRow(label: "取号时间", value: ticket.queueTimeDisplay))
|
|
metaStack.addArrangedSubview(kvRow(label: "手机号", value: ticket.phoneMasked, tap: #selector(dialTapped)))
|
|
metaStack.addArrangedSubview(kvRow(label: "等待", value: "\(ticket.waitMinutes)分钟", accent: true))
|
|
metaStack.addArrangedSubview(kvRow(label: "前方", value: "\(ticket.peopleAhead)人", accent: true))
|
|
metaStack.addArrangedSubview(kvRow(label: "排号次数", value: "\(ticket.queueCountToday)次", accent: true))
|
|
}
|
|
|
|
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)
|
|
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
|
|
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)
|
|
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 state.completeVisible {
|
|
completeButton.addTarget(self, action: #selector(completeTapped), for: .touchUpInside)
|
|
completeButton.snp.makeConstraints { make in make.height.equalTo(40) }
|
|
actionsStack.addArrangedSubview(completeButton)
|
|
}
|
|
}
|
|
|
|
private func tag(text: String, fg: UIColor, bg: UIColor) -> UILabel {
|
|
let label = PaddingLabel(insets: UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8))
|
|
label.font = ScenicQueueTokens.statusChipFont
|
|
label.textColor = fg
|
|
label.backgroundColor = bg
|
|
label.text = text
|
|
label.layer.cornerRadius = ScenicQueueTokens.radiusChip
|
|
label.clipsToBounds = true
|
|
return label
|
|
}
|
|
|
|
private func identityColors(_ label: String) -> (fg: UIColor, bg: UIColor) {
|
|
switch label.trimmingCharacters(in: .whitespacesAndNewlines) {
|
|
case "打野":
|
|
return (UIColor(hex: 0xF5222D), UIColor(hex: 0xFFF1F0))
|
|
case "摄影师":
|
|
return (UIColor(hex: 0x1890FF), UIColor(hex: 0xE6F7FF))
|
|
default:
|
|
return (UIColor(hex: 0x4B5563), UIColor(hex: 0xF3F4F6))
|
|
}
|
|
}
|
|
|
|
private func kvRow(label: String, value: String, accent: Bool = false, tap: Selector? = nil) -> UIView {
|
|
let row = UIView()
|
|
let left = UILabel()
|
|
left.text = label
|
|
left.font = ScenicQueueTokens.metaLabelFont
|
|
left.textColor = AppColor.textTertiary
|
|
let right = PaddingLabel(insets: accent ? UIEdgeInsets(top: 1, left: 6, bottom: 1, right: 6) : .zero)
|
|
right.text = value
|
|
right.font = accent ? ScenicQueueTokens.metaAccentFont : ScenicQueueTokens.rowLabelFont
|
|
right.textColor = accent ? ScenicQueueTokens.bannerBlue : AppColor.textPrimary
|
|
right.backgroundColor = accent ? AppColor.primaryLight : .clear
|
|
right.layer.cornerRadius = 2
|
|
right.clipsToBounds = true
|
|
row.addSubview(left)
|
|
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()
|
|
}
|
|
valueContainer.snp.makeConstraints { make in
|
|
make.trailing.centerY.equalToSuperview()
|
|
make.leading.greaterThanOrEqualTo(left.snp.trailing).offset(12)
|
|
}
|
|
return row
|
|
}
|
|
|
|
@objc private func dialTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapDial(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func skipTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapSkip(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func primaryTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapPrimary(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func recallTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapRecall(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func completeTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapComplete(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func markTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueTicketCellDidTapMark(self, ticket: ticket)
|
|
}
|
|
}
|
|
|
|
/// 过号列表 cell 代理。
|
|
protocol ScenicQueueSkippedCellDelegate: AnyObject {
|
|
func scenicQueueSkippedCellDidTapDial(_ cell: ScenicQueueSkippedCell, ticket: ScenicQueueSkippedTicket)
|
|
func scenicQueueSkippedCellDidTapRequeue(_ cell: ScenicQueueSkippedCell, ticket: ScenicQueueSkippedTicket)
|
|
}
|
|
|
|
/// 过号列表 cell。
|
|
final class ScenicQueueSkippedCell: UITableViewCell {
|
|
static let reuseIdentifier = "ScenicQueueSkippedCell"
|
|
|
|
weak var delegate: ScenicQueueSkippedCellDelegate?
|
|
private let cardView = UIView()
|
|
private let queueNoLabel = UILabel()
|
|
private let tagStack = UIStackView()
|
|
private let metaStack = UIStackView()
|
|
private let requeueButton = ScenicQueueActionButton(title: "重新排队", background: ScenicQueueTokens.bannerBlue, foreground: .white)
|
|
private var ticket: ScenicQueueSkippedTicket?
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
setupUI()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 配置过号 cell。
|
|
func configure(ticket: ScenicQueueSkippedTicket) {
|
|
self.ticket = ticket
|
|
queueNoLabel.text = ticket.queueNo
|
|
rebuildTags(ticket)
|
|
rebuildMeta(ticket)
|
|
}
|
|
|
|
private func setupUI() {
|
|
selectionStyle = .none
|
|
backgroundColor = .clear
|
|
cardView.backgroundColor = .white
|
|
cardView.layer.cornerRadius = ScenicQueueTokens.radiusCard
|
|
cardView.layer.borderWidth = 1
|
|
cardView.layer.borderColor = ScenicQueueTokens.cardOutline.cgColor
|
|
contentView.addSubview(cardView)
|
|
|
|
queueNoLabel.font = ScenicQueueTokens.ticketIdFont
|
|
queueNoLabel.textColor = AppColor.textPrimary
|
|
tagStack.axis = .horizontal
|
|
tagStack.spacing = 6
|
|
let titleRow = UIStackView(arrangedSubviews: [queueNoLabel, UIView(), tagStack])
|
|
titleRow.axis = .horizontal
|
|
titleRow.alignment = .center
|
|
|
|
metaStack.axis = .vertical
|
|
metaStack.spacing = 10
|
|
requeueButton.addTarget(self, action: #selector(requeueTapped), for: .touchUpInside)
|
|
|
|
let stack = UIStackView(arrangedSubviews: [titleRow, metaStack, requeueButton])
|
|
stack.axis = .vertical
|
|
stack.spacing = 12
|
|
cardView.addSubview(stack)
|
|
|
|
cardView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 5, left: 12, bottom: 5, right: 12))
|
|
}
|
|
stack.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 16, bottom: 14, right: 16))
|
|
}
|
|
requeueButton.snp.makeConstraints { make in
|
|
make.height.equalTo(46)
|
|
}
|
|
}
|
|
|
|
private func rebuildTags(_ ticket: ScenicQueueSkippedTicket) {
|
|
tagStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
if !ticket.queueBanLabel.isEmpty {
|
|
tagStack.addArrangedSubview(tag(text: ticket.queueBanLabel, fg: ScenicQueueTokens.tagBanForeground, bg: ScenicQueueTokens.tagBanBackground))
|
|
}
|
|
if ticket.isMissRequeue {
|
|
tagStack.addArrangedSubview(tag(text: ticket.missRequeueText, fg: ScenicQueueTokens.tagMissRequeueForeground, bg: ScenicQueueTokens.tagMissRequeueBackground))
|
|
}
|
|
tagStack.addArrangedSubview(tag(text: ticket.statusText, fg: ScenicQueueTokens.tagSkippedForeground, bg: ScenicQueueTokens.tagSkippedBackground))
|
|
}
|
|
|
|
private func rebuildMeta(_ ticket: ScenicQueueSkippedTicket) {
|
|
metaStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
metaStack.addArrangedSubview(kvRow(label: "手机号", value: ticket.phoneMasked, tap: #selector(dialTapped)))
|
|
if !ticket.skippedTimeDisplay.isEmpty {
|
|
metaStack.addArrangedSubview(kvRow(label: "过号时间", value: ticket.skippedTimeDisplay))
|
|
}
|
|
}
|
|
|
|
private func tag(text: String, fg: UIColor, bg: UIColor) -> UILabel {
|
|
let label = PaddingLabel(insets: UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8))
|
|
label.text = text
|
|
label.font = ScenicQueueTokens.statusChipFont
|
|
label.textColor = fg
|
|
label.backgroundColor = bg
|
|
label.layer.cornerRadius = ScenicQueueTokens.radiusChip
|
|
label.clipsToBounds = true
|
|
return label
|
|
}
|
|
|
|
private func kvRow(label: String, value: String, tap: Selector? = nil) -> UIView {
|
|
let row = UIView()
|
|
let left = UILabel()
|
|
left.text = label
|
|
left.font = ScenicQueueTokens.metaLabelFont
|
|
left.textColor = AppColor.textTertiary
|
|
let right = UILabel()
|
|
right.text = value
|
|
right.font = ScenicQueueTokens.phoneMaskedFont
|
|
right.textColor = AppColor.textPrimary
|
|
row.addSubview(left)
|
|
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()
|
|
}
|
|
valueContainer.snp.makeConstraints { make in
|
|
make.trailing.centerY.equalToSuperview()
|
|
make.leading.greaterThanOrEqualTo(left.snp.trailing).offset(12)
|
|
}
|
|
return row
|
|
}
|
|
|
|
@objc private func dialTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueSkippedCellDidTapDial(self, ticket: ticket)
|
|
}
|
|
|
|
@objc private func requeueTapped() {
|
|
guard let ticket else { return }
|
|
delegate?.scenicQueueSkippedCellDidTapRequeue(self, ticket: ticket)
|
|
}
|
|
}
|
|
|
|
/// 带内边距的 UILabel。
|
|
final class PaddingLabel: UILabel {
|
|
private let insets: UIEdgeInsets
|
|
|
|
init(insets: UIEdgeInsets) {
|
|
self.insets = insets
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func drawText(in rect: CGRect) {
|
|
super.drawText(in: rect.inset(by: insets))
|
|
}
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
let size = super.intrinsicContentSize
|
|
return CGSize(width: size.width + insets.left + insets.right, height: size.height + insets.top + insets.bottom)
|
|
}
|
|
}
|