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

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

165 lines
7.3 KiB
Swift
Raw 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.

//
// ScenicQueueSettingChangeLogViewController.swift
// suixinkan
//
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) {
self.api = api ?? NetworkServices.shared.scenicQueueAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
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 setupUI() {
view.backgroundColor = .white
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 132
tableView.delegate = self
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.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 { $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) } }
}
private func applyState() {
tableView.refreshControl?.endRefreshing()
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
}
}
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: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !viewModel.items.isEmpty,
scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height - 180 else { return }
Task { await viewModel.loadMore(api: api) }
}
}
///
final class ScenicQueueLogTimelineCell: UITableViewCell {
static let reuseIdentifier = "ScenicQueueLogTimelineCell"
private let rail = UIView()
private let dot = UIView()
private let card = UIView()
private let metaLabel = UILabel()
private let summaryLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
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") }
///
func configure(item: ScenicQueueSettingChangeLogItem, showDot: Bool) {
metaLabel.text = item.metadataLine
summaryLabel.text = item.cardSummary
dot.isHidden = !showDot
}
}