Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -80,7 +80,7 @@ enum StatisticsPeriod: String, CaseIterable, Identifiable {
|
||||
}
|
||||
|
||||
/// 数据统计汇总实体,表示订单金额、数量、客单价、实收和退款。
|
||||
struct StatisticsSummaryResponse: Decodable, Equatable {
|
||||
struct StatisticsSummaryResponse: Decodable, Equatable, Hashable {
|
||||
let orderAmountSum: String
|
||||
let orderCount: Int
|
||||
let orderPriceAverage: String
|
||||
@ -92,6 +92,7 @@ struct StatisticsSummaryResponse: Decodable, Equatable {
|
||||
var receivedAmountValue: Double { Self.parseAmount(receivedAmountSum) }
|
||||
var refundAmountValue: Double { Self.parseAmount(refundAmountSum) }
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderAmountSum = "order_amount_sum"
|
||||
case orderCount = "order_count"
|
||||
@ -125,6 +126,7 @@ struct StatisticsSummaryResponse: Decodable, Equatable {
|
||||
refundAmountSum = try container.decodeLossyString(forKey: .refundAmountSum)
|
||||
}
|
||||
|
||||
/// 解析Amount数据。
|
||||
private static func parseAmount(_ text: String) -> Double {
|
||||
let normalized = text.filter { "0123456789.-".contains($0) }
|
||||
return Double(normalized) ?? 0
|
||||
@ -132,7 +134,7 @@ struct StatisticsSummaryResponse: Decodable, Equatable {
|
||||
}
|
||||
|
||||
/// 每日统计实体,表示某天订单数、客单价、退款和实收。
|
||||
struct StatisticsDailyItem: Decodable, Identifiable, Equatable {
|
||||
struct StatisticsDailyItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
var id: String { date }
|
||||
|
||||
let date: String
|
||||
@ -145,6 +147,7 @@ struct StatisticsDailyItem: Decodable, Identifiable, Equatable {
|
||||
var refundValue: Double { Self.parseAmount(refund) }
|
||||
var receivedValue: Double { Self.parseAmount(received) }
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case date
|
||||
case orderCount = "order_count"
|
||||
@ -163,6 +166,7 @@ struct StatisticsDailyItem: Decodable, Identifiable, Equatable {
|
||||
received = try container.decodeLossyString(forKey: .received)
|
||||
}
|
||||
|
||||
/// 解析Amount数据。
|
||||
private static func parseAmount(_ text: String) -> Double {
|
||||
let normalized = text.filter { "0123456789.-".contains($0) }
|
||||
return Double(normalized) ?? 0
|
||||
|
||||
@ -8,7 +8,7 @@ Statistics 模块负责登录后的数据 Tab,展示当前景区下的订单
|
||||
|
||||
## 核心对象
|
||||
|
||||
- `StatisticsView`:数据 Tab 根视图,读取当前景区、当前角色和统计 API。
|
||||
- `StatisticsViewController`:数据 Tab 根页面,使用 `UICollectionView` + Diffable Data Source 展示多 section 看板。
|
||||
- `StatisticsViewModel`:管理时间段、汇总数据、每日明细、分页状态和加载状态。
|
||||
- `StatisticsAPI`:封装摄影师和景区管理员两套统计接口。
|
||||
- `StatisticsPeriod`:表示今日、昨日、7日、本月四个快捷时间段。
|
||||
@ -24,3 +24,9 @@ Statistics 模块负责登录后的数据 Tab,展示当前景区下的订单
|
||||
## 分页规则
|
||||
|
||||
每日明细第一页随刷新或时间段切换加载。列表滚动到底部后,如果当前数量小于 total,则继续加载下一页。加载更多失败时保留已有数据和当前页状态。
|
||||
|
||||
## 列表实现
|
||||
|
||||
- Section:`empty`(无景区)、`period`(周期切换)、`summary`(汇总卡)、`daily`(每日明细,带 section 头「每日明细」)
|
||||
- 使用 `UICollectionViewDiffableDataSource` + `NSDiffableDataSourceSnapshot` 驱动刷新,分页追加时保留插入动画
|
||||
- 布局复用 `CollectionDiffableLayout` 全宽 section;最后一项日明细 `willDisplay` 时触发 `loadMore`
|
||||
|
||||
@ -6,39 +6,191 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - Diffable 标识
|
||||
|
||||
/// 统计页 section 标识,区分空状态、周期切换、汇总与每日明细。
|
||||
private enum StatisticsSection: Hashable {
|
||||
case empty
|
||||
case period
|
||||
case summary
|
||||
case daily
|
||||
}
|
||||
|
||||
/// 统计页 item 标识,携带展示数据以保证 diff 后 Cell 内容同步更新。
|
||||
private enum StatisticsItem: Hashable {
|
||||
case emptyContext
|
||||
case period(StatisticsPeriod)
|
||||
case summary(StatisticsSummaryResponse)
|
||||
case dailyEmpty
|
||||
case daily(StatisticsDailyItem)
|
||||
}
|
||||
|
||||
/// 数据 Tab 根页面,展示订单统计汇总和每日明细。
|
||||
final class StatisticsViewController: UIViewController {
|
||||
|
||||
private let viewModel = StatisticsViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(StatisticsSummaryCell.self, forCellReuseIdentifier: StatisticsSummaryCell.reuseID)
|
||||
table.register(StatisticsDailyCell.self, forCellReuseIdentifier: StatisticsDailyCell.reuseID)
|
||||
table.register(StatisticsPeriodCell.self, forCellReuseIdentifier: StatisticsPeriodCell.reuseID)
|
||||
return table
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = makeCollectionLayout()
|
||||
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collection.backgroundColor = AppDesignUIKit.pageBackground
|
||||
collection.delegate = self
|
||||
collection.register(StatisticsPeriodCell.self, forCellWithReuseIdentifier: StatisticsPeriodCell.reuseID)
|
||||
collection.register(StatisticsSummaryCell.self, forCellWithReuseIdentifier: StatisticsSummaryCell.reuseID)
|
||||
collection.register(StatisticsDailyCell.self, forCellWithReuseIdentifier: StatisticsDailyCell.reuseID)
|
||||
collection.register(StatisticsEmptyCell.self, forCellWithReuseIdentifier: StatisticsEmptyCell.reuseID)
|
||||
collection.register(
|
||||
CollectionSectionHeaderView.self,
|
||||
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
|
||||
withReuseIdentifier: CollectionSectionHeaderView.reuseID
|
||||
)
|
||||
return collection
|
||||
}()
|
||||
|
||||
private lazy var refreshControl = UIRefreshControl()
|
||||
|
||||
/// Diffable 数据源,驱动多 section 列表与分页插入动画。
|
||||
private var dataSource: UICollectionViewDiffableDataSource<StatisticsSection, StatisticsItem>!
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "数据"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
configureDataSource()
|
||||
view.addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||||
tableView.refreshControl = refreshControl
|
||||
collectionView.refreshControl = refreshControl
|
||||
|
||||
viewModel.onChange = { [weak self] in self?.tableView.reloadData() }
|
||||
viewModel.onChange = { [weak self] in self?.applySnapshot() }
|
||||
appServices.accountContext.onChange = { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||||
appServices.permissionContext.onChange = { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||||
applySnapshot(animated: false)
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
/// 构建 Compositional Layout,按 section 类型分配全宽卡片布局。
|
||||
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
|
||||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
|
||||
guard let self else { return CollectionDiffableLayout.fullWidthSection() }
|
||||
let snapshot = self.dataSource?.snapshot()
|
||||
let section = snapshot?.sectionIdentifiers[safe: sectionIndex]
|
||||
switch section {
|
||||
case .empty:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 360)
|
||||
case .period:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 56)
|
||||
case .summary:
|
||||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 280)
|
||||
case .daily:
|
||||
return CollectionDiffableLayout.addHeader(
|
||||
to: CollectionDiffableLayout.fullWidthSection(estimatedHeight: 52),
|
||||
height: 36
|
||||
)
|
||||
case .none:
|
||||
return CollectionDiffableLayout.fullWidthSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册 Diffable 数据源与 Cell 配置闭包。
|
||||
private func configureDataSource() {
|
||||
dataSource = UICollectionViewDiffableDataSource<StatisticsSection, StatisticsItem>(
|
||||
collectionView: collectionView
|
||||
) { [weak self] collectionView, indexPath, item in
|
||||
guard let self else { return UICollectionViewCell() }
|
||||
switch item {
|
||||
case .emptyContext:
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsEmptyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsEmptyCell
|
||||
let empty = self.makeEmptyStateView(
|
||||
title: "缺少经营上下文",
|
||||
message: "请先在首页选择景区后查看数据看板。",
|
||||
systemImage: "chart.bar.doc.horizontal"
|
||||
)
|
||||
cell.setHostedView(empty, height: 360)
|
||||
return cell
|
||||
case .period(let selectedPeriod):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsPeriodCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsPeriodCell
|
||||
cell.configure(selectedPeriod: selectedPeriod) { [weak self] period in
|
||||
self?.selectPeriod(period)
|
||||
}
|
||||
return cell
|
||||
case .summary(let summary):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsSummaryCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsSummaryCell
|
||||
cell.configure(
|
||||
periodText: self.viewModel.selectedPeriod.selectedTimeText,
|
||||
summary: summary,
|
||||
amountText: self.amountText
|
||||
)
|
||||
return cell
|
||||
case .dailyEmpty:
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsEmptyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsEmptyCell
|
||||
cell.configurePlainText("暂无数据", height: 52)
|
||||
return cell
|
||||
case .daily(let dailyItem):
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: StatisticsDailyCell.reuseID,
|
||||
for: indexPath
|
||||
) as! StatisticsDailyCell
|
||||
cell.configure(item: dailyItem, amountText: self.amountText)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
|
||||
guard kind == UICollectionView.elementKindSectionHeader,
|
||||
let section = self.dataSource.snapshot().sectionIdentifiers[safe: indexPath.section],
|
||||
section == .daily,
|
||||
let header = collectionView.dequeueReusableSupplementaryView(
|
||||
ofKind: kind,
|
||||
withReuseIdentifier: CollectionSectionHeaderView.reuseID,
|
||||
for: indexPath
|
||||
) as? CollectionSectionHeaderView
|
||||
else { return nil }
|
||||
header.configure(title: "每日明细")
|
||||
return header
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 ViewModel 状态构建 snapshot 并应用 diff 更新。
|
||||
private func applySnapshot(animated: Bool = true) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<StatisticsSection, StatisticsItem>()
|
||||
|
||||
if currentScenicId == nil {
|
||||
snapshot.appendSections([.empty])
|
||||
snapshot.appendItems([.emptyContext], toSection: .empty)
|
||||
} else {
|
||||
snapshot.appendSections([.period, .summary, .daily])
|
||||
snapshot.appendItems([.period(viewModel.selectedPeriod)], toSection: .period)
|
||||
snapshot.appendItems([.summary(viewModel.summary)], toSection: .summary)
|
||||
|
||||
if viewModel.loading, viewModel.dailyItems.isEmpty {
|
||||
// 首次加载中不展示占位行,避免闪烁。
|
||||
} else if viewModel.dailyItems.isEmpty {
|
||||
snapshot.appendItems([.dailyEmpty], toSection: .daily)
|
||||
} else {
|
||||
let items = viewModel.dailyItems.map { StatisticsItem.daily($0) }
|
||||
snapshot.appendItems(items, toSection: .daily)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
/// 下拉刷新触发。
|
||||
@objc private func refreshPulled() {
|
||||
Task {
|
||||
await reload(showLoading: false)
|
||||
@ -49,9 +201,10 @@ final class StatisticsViewController: UIViewController {
|
||||
private var currentScenicId: Int? { appServices.accountContext.currentScenic?.id }
|
||||
private var currentRoleId: Int? { appServices.permissionContext.currentRole?.id }
|
||||
|
||||
/// 重新加载汇总与第一页日数据。
|
||||
private func reload(showLoading: Bool) async {
|
||||
guard currentScenicId != nil else {
|
||||
tableView.reloadData()
|
||||
applySnapshot()
|
||||
return
|
||||
}
|
||||
do {
|
||||
@ -68,6 +221,7 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换统计周期并重新加载数据。
|
||||
private func selectPeriod(_ period: StatisticsPeriod) {
|
||||
Task {
|
||||
do {
|
||||
@ -85,6 +239,7 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载每日明细下一页。
|
||||
private func loadMore() async {
|
||||
do {
|
||||
try await viewModel.loadMore(
|
||||
@ -97,85 +252,37 @@ final class StatisticsViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化金额展示文本。
|
||||
private func amountText(_ value: Double) -> String {
|
||||
"¥\(String(format: "%.2f", value))"
|
||||
}
|
||||
}
|
||||
|
||||
extension StatisticsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
currentScenicId == nil ? 1 : 3
|
||||
}
|
||||
// MARK: - UICollectionViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if currentScenicId == nil { return 1 }
|
||||
switch section {
|
||||
case 0: return 1
|
||||
case 1: return 1
|
||||
default:
|
||||
if viewModel.loading && viewModel.dailyItems.isEmpty { return 0 }
|
||||
return max(viewModel.dailyItems.count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if currentScenicId == nil {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "缺少经营上下文", message: "请先在首页选择景区后查看数据看板。", systemImage: "chart.bar.doc.horizontal")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(360) }
|
||||
return cell
|
||||
}
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsPeriodCell.reuseID, for: indexPath) as! StatisticsPeriodCell
|
||||
cell.configure(selectedPeriod: viewModel.selectedPeriod) { [weak self] period in
|
||||
self?.selectPeriod(period)
|
||||
}
|
||||
return cell
|
||||
case 1:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsSummaryCell.reuseID, for: indexPath) as! StatisticsSummaryCell
|
||||
cell.configure(
|
||||
periodText: viewModel.selectedPeriod.selectedTimeText,
|
||||
summary: viewModel.summary,
|
||||
amountText: amountText
|
||||
)
|
||||
return cell
|
||||
default:
|
||||
if viewModel.dailyItems.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = "暂无数据"
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: StatisticsDailyCell.reuseID, for: indexPath) as! StatisticsDailyCell
|
||||
cell.configure(item: viewModel.dailyItems[indexPath.row], amountText: amountText)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 2 ? "每日明细" : nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 2, indexPath.row == viewModel.dailyItems.count - 1 else { return }
|
||||
extension StatisticsViewController: UICollectionViewDelegate {
|
||||
/// 最后一项日明细即将展示时触发分页加载。
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .daily(let dailyItem) = item,
|
||||
dailyItem.date == viewModel.dailyItems.last?.date
|
||||
else { return }
|
||||
Task { await loadMore() }
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsPeriodCell: UITableViewCell {
|
||||
// MARK: - Cells
|
||||
|
||||
/// 统计周期切换 Cell,横向展示快捷时间段按钮。
|
||||
private final class StatisticsPeriodCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsPeriodCell"
|
||||
private var onSelect: ((StatisticsPeriod) -> Void)?
|
||||
private let stack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 8
|
||||
stack.distribution = .fillEqually
|
||||
@ -186,6 +293,7 @@ private final class StatisticsPeriodCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(selectedPeriod: StatisticsPeriod, onSelect: @escaping (StatisticsPeriod) -> Void) {
|
||||
self.onSelect = onSelect
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
@ -202,19 +310,22 @@ private final class StatisticsPeriodCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击周期按钮后回调选中项。
|
||||
@objc private func periodTapped(_ sender: UIButton) {
|
||||
let period = StatisticsPeriod.allCases[sender.tag]
|
||||
onSelect?(period)
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsSummaryCell: UITableViewCell {
|
||||
/// 统计汇总 Cell,展示订单金额、数量、客单价等指标卡片。
|
||||
private final class StatisticsSummaryCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsSummaryCell"
|
||||
private let stack = UIStackView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
contentView.addSubview(stack)
|
||||
@ -224,6 +335,7 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(periodText: String, summary: StatisticsSummaryResponse, amountText: (Double) -> String) {
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
let dateLabel = UILabel()
|
||||
@ -249,6 +361,7 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
stack.addArrangedSubview(row2)
|
||||
}
|
||||
|
||||
/// 创建统计摘要卡片视图。
|
||||
private func summaryCard(_ title: String, _ value: String, _ color: UIColor) -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = color.withAlphaComponent(0.08)
|
||||
@ -270,21 +383,94 @@ private final class StatisticsSummaryCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatisticsDailyCell: UITableViewCell {
|
||||
/// 每日统计明细 Cell,展示日期、订单数与实收金额。
|
||||
private final class StatisticsDailyCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsDailyCell"
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .secondarySystemGroupedBackground
|
||||
contentView.backgroundColor = .secondarySystemGroupedBackground
|
||||
contentView.layer.cornerRadius = 8
|
||||
contentView.clipsToBounds = true
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
detailLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
detailLabel.textColor = AppDesignUIKit.textSecondary
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(detailLabel)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.top.equalToSuperview().inset(12)
|
||||
}
|
||||
detailLabel.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(item: StatisticsDailyItem, amountText: (Double) -> String) {
|
||||
textLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||||
detailTextLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
textLabel?.text = item.date
|
||||
detailTextLabel?.text = "\(item.orderCount)单 · 实收 \(amountText(item.receivedValue))"
|
||||
titleLabel.text = item.date
|
||||
detailLabel.text = "\(item.orderCount)单 · 实收 \(amountText(item.receivedValue))"
|
||||
}
|
||||
}
|
||||
|
||||
/// 空状态 Cell,用于缺少景区上下文或日明细为空时的占位展示。
|
||||
private final class StatisticsEmptyCell: UICollectionViewCell {
|
||||
static let reuseID = "StatisticsEmptyCell"
|
||||
|
||||
private var heightConstraint: Constraint?
|
||||
|
||||
/// 初始化实例。
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 嵌入外部构建的空状态视图并固定高度。
|
||||
func setHostedView(_ view: UIView, height: CGFloat) {
|
||||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
heightConstraint?.deactivate()
|
||||
contentView.addSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
heightConstraint = make.height.equalTo(height).constraint
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置纯文案占位,用于日明细为空场景。
|
||||
func configurePlainText(_ title: String, height: CGFloat) {
|
||||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
heightConstraint?.deactivate()
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
label.textColor = AppDesignUIKit.textSecondary
|
||||
label.textAlignment = .center
|
||||
contentView.addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
heightConstraint = make.height.equalTo(height).constraint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全下标,避免 section 越界。
|
||||
private extension Array {
|
||||
subscript(safe index: Int) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user