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

重构排队设置与变更日志交互,补充 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

@ -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
}
}