Implement statistics tab aligned with Android and fix summary decode.

Add role-based analyse APIs, summary/daily UI with date range sheet, and flexible parsing for string refund amounts from the backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:32:14 +08:00
parent 39a9038a3b
commit 6492f80ef0
32 changed files with 1769 additions and 10 deletions

View File

@ -0,0 +1,174 @@
//
// StatisticsDailyListView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class StatisticsDailyListView: UIView {
var onRangeTap: (() -> Void)?
var onRefresh: (() -> Void)?
var onLoadMore: (() -> Void)?
private let titleLabel = UILabel()
private let rangeButton = UIControl()
private let rangeLabel = UILabel()
private let arrowView = UIImageView()
private let divider = UIView()
private let tableView = UITableView(frame: .zero, style: .plain)
private let refreshControl = UIRefreshControl()
private let footerIndicator = UIActivityIndicatorView(style: .medium)
private var items: [StatisticsDailyItem] = []
private var showsLoadMoreFooter = false
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(rangeText: String, items: [StatisticsDailyItem], isRefreshing: Bool, canLoadMore: Bool) {
rangeLabel.text = rangeText
self.items = items
showsLoadMoreFooter = canLoadMore
tableView.reloadData()
updateFooterVisibility()
if !isRefreshing {
refreshControl.endRefreshing()
}
}
private func setupUI() {
backgroundColor = .white
layer.cornerRadius = 12
clipsToBounds = true
titleLabel.text = "时间范围"
titleLabel.font = .systemFont(ofSize: 14, weight: .bold)
titleLabel.textColor = .black
rangeButton.backgroundColor = UIColor(hex: 0xF4F4F4)
rangeButton.layer.cornerRadius = 4
rangeButton.addTarget(self, action: #selector(rangeTapped), for: .touchUpInside)
rangeLabel.font = .systemFont(ofSize: 14)
rangeLabel.textColor = UIColor(hex: 0x4B5563)
rangeLabel.text = "今日"
arrowView.image = UIImage(systemName: "chevron.down")
arrowView.tintColor = UIColor(hex: 0x4B5563)
arrowView.contentMode = .scaleAspectFit
divider.backgroundColor = UIColor(hex: 0xF4F4F4)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
tableView.register(StatisticsDailyCell.self, forCellReuseIdentifier: StatisticsDailyCell.reuseIdentifier)
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
footerIndicator.hidesWhenStopped = true
addSubview(titleLabel)
addSubview(rangeButton)
rangeButton.addSubview(rangeLabel)
rangeButton.addSubview(arrowView)
addSubview(divider)
addSubview(tableView)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().inset(16)
}
rangeButton.snp.makeConstraints { make in
make.centerY.equalTo(titleLabel)
make.trailing.equalToSuperview().inset(16)
make.height.equalTo(36)
make.width.greaterThanOrEqualTo(166)
}
rangeLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualTo(arrowView.snp.leading).offset(-8)
}
arrowView.snp.makeConstraints { make in
make.trailing.equalToSuperview().inset(12)
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
divider.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(1)
}
tableView.snp.makeConstraints { make in
make.top.equalTo(divider.snp.bottom).offset(12)
make.leading.trailing.bottom.equalToSuperview().inset(16)
}
}
@objc private func rangeTapped() {
onRangeTap?()
}
@objc private func refreshPulled() {
onRefresh?()
}
private func updateFooterVisibility() {
if showsLoadMoreFooter {
footerIndicator.startAnimating()
tableView.tableFooterView = footerContainer()
} else {
footerIndicator.stopAnimating()
tableView.tableFooterView = UIView(frame: .zero)
}
}
private func footerContainer() -> UIView {
let container = UIView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: 56))
container.addSubview(footerIndicator)
footerIndicator.center = CGPoint(x: container.bounds.midX, y: container.bounds.midY)
footerIndicator.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
return container
}
}
extension StatisticsDailyListView: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard
let cell = tableView.dequeueReusableCell(
withIdentifier: StatisticsDailyCell.reuseIdentifier,
for: indexPath
) as? StatisticsDailyCell
else {
return UITableViewCell()
}
cell.configure(with: items[indexPath.row], showsDivider: indexPath.row < items.count - 1)
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard showsLoadMoreFooter else { return }
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let frameHeight = scrollView.frame.size.height
guard contentHeight > frameHeight else { return }
if offsetY > contentHeight - frameHeight - 120 {
onLoadMore?()
}
}
}