Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
827 lines
33 KiB
Swift
827 lines
33 KiB
Swift
//
|
||
// OrdersViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
// MARK: - Diffable 标识(Int / String 避免 MainActor 默认隔离下 Hashable 冲突)
|
||
|
||
/// 订单列表 section 索引。
|
||
private enum OrdersSectionID {
|
||
static let header = 0
|
||
static let toolbar = 1
|
||
static let content = 2
|
||
}
|
||
|
||
private typealias OrdersSection = Int
|
||
|
||
/// 订单列表 item 字符串标识。
|
||
private enum OrdersItemID {
|
||
static let header = "orders:header"
|
||
static let filter = "orders:filter"
|
||
static let writeOffAction = "orders:writeOffAction"
|
||
static let missingContext = "orders:missingContext"
|
||
static let emptyStore = "orders:empty:store"
|
||
static let emptyWriteOff = "orders:empty:writeOff"
|
||
static func storeOrder(_ orderNumber: String) -> String { "orders:store:\(orderNumber)" }
|
||
static func writeOffOrder(_ orderNumber: String) -> String { "orders:writeoff:\(orderNumber)" }
|
||
}
|
||
|
||
private typealias OrdersItem = String
|
||
|
||
/// 订单 Tab 根页面,展示订单管理和核销订单两个子入口及对应列表。
|
||
final class OrdersViewController: UIViewController {
|
||
|
||
private let viewModel = OrdersViewModel()
|
||
private var manualOrderNumber = ""
|
||
private var scanHintMessage: String?
|
||
private var pendingScanCode: String?
|
||
|
||
private lazy var collectionView: UICollectionView = {
|
||
let layout = makeCollectionLayout()
|
||
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||
collection.backgroundColor = AppDesignUIKit.pageBackground
|
||
collection.delegate = self
|
||
collection.register(OrdersHeaderCell.self, forCellWithReuseIdentifier: OrdersHeaderCell.reuseID)
|
||
collection.register(OrdersFilterCell.self, forCellWithReuseIdentifier: OrdersFilterCell.reuseID)
|
||
collection.register(OrdersWriteOffActionCell.self, forCellWithReuseIdentifier: OrdersWriteOffActionCell.reuseID)
|
||
collection.register(OrderEntityCell.self, forCellWithReuseIdentifier: OrderEntityCell.reuseID)
|
||
collection.register(WriteOffOrderCell.self, forCellWithReuseIdentifier: WriteOffOrderCell.reuseID)
|
||
collection.register(OrdersEmptyStateCell.self, forCellWithReuseIdentifier: OrdersEmptyStateCell.reuseID)
|
||
return collection
|
||
}()
|
||
|
||
private lazy var dataSource: UICollectionViewDiffableDataSource<OrdersSection, OrdersItem> = {
|
||
UICollectionViewDiffableDataSource<OrdersSection, OrdersItem>(collectionView: collectionView) { [weak self] collectionView, indexPath, item in
|
||
self?.cell(for: collectionView, at: indexPath, item: item) ?? UICollectionViewCell()
|
||
}
|
||
}()
|
||
|
||
private lazy var refreshControl = UIRefreshControl()
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "订单"
|
||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||
|
||
view.addSubview(collectionView)
|
||
collectionView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
refreshControl.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
|
||
collectionView.refreshControl = refreshControl
|
||
|
||
viewModel.onChange = { [weak self] in
|
||
self?.applySnapshot()
|
||
}
|
||
|
||
appServices.accountContext.onChange = { [weak self] in
|
||
Task { await self?.reload(showLoading: true) }
|
||
}
|
||
|
||
Task {
|
||
await reload(showLoading: true)
|
||
await consumePendingScanCodeIfNeeded()
|
||
}
|
||
}
|
||
|
||
/// 应用来自首页、推送或全局扫码入口的订单子入口状态。
|
||
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) {
|
||
let didChangeEntry = viewModel.selectedEntry != entry
|
||
viewModel.selectedEntry = entry
|
||
if let scannedCode {
|
||
pendingScanCode = scannedCode
|
||
}
|
||
|
||
guard isViewLoaded else { return }
|
||
|
||
Task {
|
||
if didChangeEntry {
|
||
await reload(showLoading: true)
|
||
}
|
||
await consumePendingScanCodeIfNeeded()
|
||
}
|
||
}
|
||
|
||
/// 构建 Compositional Layout,按 section 类型选用全宽布局。
|
||
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
|
||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
|
||
guard let self else {
|
||
return CollectionDiffableLayout.fullWidthSection()
|
||
}
|
||
let section = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
||
switch section {
|
||
case OrdersSectionID.header:
|
||
return CollectionDiffableLayout.fullWidthSection(height: 120)
|
||
case OrdersSectionID.toolbar:
|
||
let height: CGFloat = self.viewModel.selectedEntry == .storeOrders ? 130 : 150
|
||
return CollectionDiffableLayout.fullWidthSection(height: height)
|
||
case OrdersSectionID.content:
|
||
return CollectionDiffableLayout.fullWidthSection(estimatedHeight: 88)
|
||
default:
|
||
return CollectionDiffableLayout.fullWidthSection()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 根据当前 ViewModel 与页面状态构建并应用 Diffable 快照。
|
||
private func applySnapshot(animatingDifferences: Bool = true) {
|
||
var snapshot = NSDiffableDataSourceSnapshot<OrdersSection, OrdersItem>()
|
||
snapshot.appendSections([OrdersSectionID.header])
|
||
snapshot.appendItems([OrdersItemID.header], toSection: OrdersSectionID.header)
|
||
|
||
guard currentScenicId != nil else {
|
||
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
||
return
|
||
}
|
||
|
||
snapshot.appendSections([OrdersSectionID.toolbar, OrdersSectionID.content])
|
||
|
||
if viewModel.selectedEntry == .storeOrders {
|
||
snapshot.appendItems([OrdersItemID.filter], toSection: OrdersSectionID.toolbar)
|
||
appendStoreOrderItems(to: &snapshot)
|
||
} else {
|
||
snapshot.appendItems([OrdersItemID.writeOffAction], toSection: OrdersSectionID.toolbar)
|
||
appendWriteOffOrderItems(to: &snapshot)
|
||
}
|
||
|
||
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
||
}
|
||
|
||
/// 向快照追加订单管理列表或空态 item。
|
||
private func appendStoreOrderItems(to snapshot: inout NSDiffableDataSourceSnapshot<OrdersSection, OrdersItem>) {
|
||
if viewModel.storeOrders.isEmpty {
|
||
guard !(viewModel.loading && viewModel.storeOrders.isEmpty) else { return }
|
||
snapshot.appendItems([OrdersItemID.emptyStore], toSection: OrdersSectionID.content)
|
||
return
|
||
}
|
||
snapshot.appendItems(
|
||
viewModel.storeOrders.map { OrdersItemID.storeOrder($0.orderNumber) },
|
||
toSection: OrdersSectionID.content
|
||
)
|
||
}
|
||
|
||
/// 向快照追加核销订单列表或空态 item。
|
||
private func appendWriteOffOrderItems(to snapshot: inout NSDiffableDataSourceSnapshot<OrdersSection, OrdersItem>) {
|
||
if viewModel.writeOffOrders.isEmpty {
|
||
guard !(viewModel.loading && viewModel.writeOffOrders.isEmpty) else { return }
|
||
snapshot.appendItems([OrdersItemID.emptyWriteOff], toSection: OrdersSectionID.content)
|
||
return
|
||
}
|
||
snapshot.appendItems(
|
||
viewModel.writeOffOrders.map { OrdersItemID.writeOffOrder($0.orderNumber) },
|
||
toSection: OrdersSectionID.content
|
||
)
|
||
}
|
||
|
||
/// 按 item 类型 dequeue 并配置对应 Cell。
|
||
private func cell(
|
||
for collectionView: UICollectionView,
|
||
at indexPath: IndexPath,
|
||
item: OrdersItem
|
||
) -> UICollectionViewCell {
|
||
switch item {
|
||
case OrdersItemID.header:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersHeaderCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersHeaderCell
|
||
cell.configure(
|
||
selectedEntry: viewModel.selectedEntry,
|
||
scenicName: appServices.accountContext.currentScenic?.name ?? "--",
|
||
storeTotal: viewModel.storeTotal,
|
||
writeOffTotal: viewModel.writeOffTotal,
|
||
onSelectEntry: { [weak self] entry in self?.switchEntry(entry) }
|
||
)
|
||
return cell
|
||
|
||
case OrdersItemID.filter:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersFilterCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersFilterCell
|
||
let statusTitle = OrderFilters.statusFilters.first(where: { $0.id == viewModel.selectedStatus })?.title ?? "全部"
|
||
cell.configure(
|
||
statusTitle: statusTitle,
|
||
phone: viewModel.searchPhone,
|
||
onStatus: { [weak self] in self?.presentStatusFilter() },
|
||
onDate: { [weak self] in self?.presentDateFilter() },
|
||
onPhoneChange: { [weak self] text in self?.viewModel.searchPhone = text },
|
||
onSearch: { [weak self] in Task { await self?.reload(showLoading: true) } }
|
||
)
|
||
return cell
|
||
|
||
case OrdersItemID.writeOffAction:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersWriteOffActionCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersWriteOffActionCell
|
||
cell.configure(
|
||
manualOrderNumber: manualOrderNumber,
|
||
isVerifying: viewModel.isVerifying,
|
||
hint: scanHintMessage,
|
||
onScan: { [weak self] in self?.presentScanner() },
|
||
onManualChange: { [weak self] text in self?.manualOrderNumber = text },
|
||
onVerify: { [weak self] in
|
||
guard let self, !self.manualOrderNumber.isEmpty else { return }
|
||
self.confirmVerify(orderNumber: self.manualOrderNumber)
|
||
}
|
||
)
|
||
return cell
|
||
|
||
case OrdersItemID.missingContext:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersEmptyStateCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersEmptyStateCell
|
||
cell.embed(
|
||
makeEmptyStateView(
|
||
title: "缺少经营上下文",
|
||
message: "请先在首页选择景区后查看订单。",
|
||
systemImage: "mountain.2"
|
||
),
|
||
preferredHeight: 360
|
||
)
|
||
return cell
|
||
|
||
case OrdersItemID.emptyStore:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersEmptyStateCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersEmptyStateCell
|
||
cell.embed(
|
||
makeEmptyStateView(title: "暂无订单", message: "可切换筛选条件或下拉刷新。", systemImage: "tray"),
|
||
preferredHeight: 260
|
||
)
|
||
return cell
|
||
|
||
case OrdersItemID.emptyWriteOff:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrdersEmptyStateCell.reuseID,
|
||
for: indexPath
|
||
) as! OrdersEmptyStateCell
|
||
cell.embed(
|
||
makeEmptyStateView(title: "暂无核销订单", message: "可下拉刷新或切换景区查看。", systemImage: "tray"),
|
||
preferredHeight: 260
|
||
)
|
||
return cell
|
||
|
||
default:
|
||
if item.hasPrefix("orders:store:") {
|
||
let orderNumber = String(item.dropFirst("orders:store:".count))
|
||
guard let order = viewModel.storeOrders.first(where: { $0.orderNumber == orderNumber }) else {
|
||
return UICollectionViewCell()
|
||
}
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: OrderEntityCell.reuseID,
|
||
for: indexPath
|
||
) as! OrderEntityCell
|
||
cell.configure(item: order)
|
||
return cell
|
||
}
|
||
if item.hasPrefix("orders:writeoff:") {
|
||
let orderNumber = String(item.dropFirst("orders:writeoff:".count))
|
||
guard let order = viewModel.writeOffOrders.first(where: { $0.orderNumber == orderNumber }) else {
|
||
return UICollectionViewCell()
|
||
}
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: WriteOffOrderCell.reuseID,
|
||
for: indexPath
|
||
) as! WriteOffOrderCell
|
||
cell.configure(
|
||
item: order,
|
||
isVerifying: viewModel.currentVerifyingOrderNumber == order.orderNumber
|
||
)
|
||
return cell
|
||
}
|
||
return UICollectionViewCell()
|
||
}
|
||
}
|
||
|
||
/// 下拉刷新触发重新加载。
|
||
@objc private func refreshPulled() {
|
||
Task {
|
||
await reload(showLoading: false)
|
||
refreshControl.endRefreshing()
|
||
}
|
||
}
|
||
|
||
private var currentScenicId: Int? { appServices.accountContext.currentScenic?.id }
|
||
private var currentStoreId: Int? { appServices.accountContext.currentStore?.id }
|
||
private var currentRoleId: Int? { appServices.permissionContext.currentRole?.id }
|
||
|
||
/// 重新加载当前子入口对应的订单数据。
|
||
private func reload(showLoading: Bool) async {
|
||
do {
|
||
try await appServices.globalLoading.withOptionalLoading(showLoading, message: "加载订单...") {
|
||
try await self.viewModel.reload(
|
||
api: self.appServices.ordersAPI,
|
||
scenicId: self.currentScenicId,
|
||
storeId: self.currentStoreId,
|
||
roleId: self.currentRoleId,
|
||
showLoading: false
|
||
)
|
||
}
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// 滚动到底部时加载下一页。
|
||
private func loadMore() async {
|
||
do {
|
||
if viewModel.selectedEntry == .storeOrders {
|
||
try await viewModel.loadMoreStoreOrders(
|
||
api: appServices.ordersAPI,
|
||
scenicId: currentScenicId,
|
||
roleId: currentRoleId
|
||
)
|
||
} else {
|
||
try await viewModel.loadMoreWriteOffOrders(
|
||
api: appServices.ordersAPI,
|
||
scenicId: currentScenicId,
|
||
storeId: currentStoreId
|
||
)
|
||
}
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// 切换订单管理 / 核销订单子入口。
|
||
private func switchEntry(_ entry: OrdersEntry) {
|
||
guard viewModel.selectedEntry != entry else { return }
|
||
viewModel.selectedEntry = entry
|
||
Task { await reload(showLoading: true) }
|
||
}
|
||
|
||
/// 提交订单核销请求。
|
||
private func verify(orderNumber: String) async {
|
||
guard let scenicId = currentScenicId else { return }
|
||
do {
|
||
try await appServices.globalLoading.withLoading(message: "核销中...") {
|
||
try await self.viewModel.verify(
|
||
api: self.appServices.ordersAPI,
|
||
scenicId: scenicId,
|
||
storeId: self.currentStoreId,
|
||
orderNumber: orderNumber
|
||
)
|
||
}
|
||
showToast("核销成功")
|
||
} catch {
|
||
showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// 消费路由中暂存的扫码结果(核销入口)。
|
||
private func consumePendingScanCodeIfNeeded() async {
|
||
guard viewModel.selectedEntry == .verificationOrders,
|
||
let code = pendingScanCode else { return }
|
||
pendingScanCode = nil
|
||
handleScanResult(code)
|
||
}
|
||
|
||
/// 解析扫码结果并引导核销或提示未匹配。
|
||
private func handleScanResult(_ rawCode: String) {
|
||
guard let parsed = viewModel.matchedWriteOffOrder(for: rawCode) else {
|
||
showToast("未识别到有效订单号")
|
||
return
|
||
}
|
||
if parsed.matched != nil {
|
||
confirmVerify(orderNumber: parsed.orderNumber)
|
||
} else {
|
||
scanHintMessage = "扫码成功,当前列表未找到该订单"
|
||
applySnapshot()
|
||
}
|
||
}
|
||
|
||
/// 弹出核销确认对话框。
|
||
private func confirmVerify(orderNumber: String) {
|
||
let alert = UIAlertController(title: "确认核销该订单?", message: orderNumber, preferredStyle: .alert)
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
alert.addAction(UIAlertAction(title: "确认核销", style: .default) { [weak self] _ in
|
||
Task { await self?.verify(orderNumber: orderNumber) }
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
/// 打开订单号扫码页。
|
||
private func presentScanner() {
|
||
let scanner = OrderCodeScannerViewController()
|
||
scanner.onScanResult = { [weak self, weak scanner] result in
|
||
scanner?.dismiss(animated: true)
|
||
guard let self else { return }
|
||
switch result {
|
||
case .success(let code):
|
||
self.handleScanResult(code)
|
||
case .failure(let error):
|
||
self.showToast(error.localizedDescription)
|
||
}
|
||
}
|
||
let nav = UINavigationController(rootViewController: scanner)
|
||
nav.modalPresentationStyle = .fullScreen
|
||
present(nav, animated: true)
|
||
}
|
||
|
||
/// 弹出时间筛选快捷选项。
|
||
private func presentDateFilter() {
|
||
let alert = UIAlertController(title: "时间筛选", message: "选择开始和结束日期", preferredStyle: .alert)
|
||
alert.addAction(UIAlertAction(title: "清除筛选", style: .destructive) { [weak self] _ in
|
||
self?.viewModel.filterStartDate = nil
|
||
self?.viewModel.filterEndDate = nil
|
||
Task { await self?.reload(showLoading: true) }
|
||
})
|
||
alert.addAction(UIAlertAction(title: "近7天", style: .default) { [weak self] _ in
|
||
let end = Date()
|
||
let start = Calendar.current.date(byAdding: .day, value: -6, to: end) ?? end
|
||
self?.viewModel.filterStartDate = start
|
||
self?.viewModel.filterEndDate = end
|
||
Task { await self?.reload(showLoading: true) }
|
||
})
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
/// 弹出订单状态筛选 ActionSheet。
|
||
private func presentStatusFilter() {
|
||
let sheet = UIAlertController(title: "订单状态", message: nil, preferredStyle: .actionSheet)
|
||
for filter in OrderFilters.statusFilters {
|
||
sheet.addAction(UIAlertAction(title: filter.title, style: .default) { [weak self] _ in
|
||
self?.viewModel.selectedStatus = filter.id
|
||
Task { await self?.reload(showLoading: true) }
|
||
})
|
||
}
|
||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(sheet, animated: true)
|
||
}
|
||
}
|
||
|
||
// MARK: - UICollectionViewDelegate
|
||
|
||
extension OrdersViewController: UICollectionViewDelegate {
|
||
/// 点击订单卡片进入详情。
|
||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||
collectionView.deselectItem(at: indexPath, animated: true)
|
||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||
if item.hasPrefix("orders:store:") {
|
||
let orderNumber = String(item.dropFirst("orders:store:".count))
|
||
guard let order = viewModel.storeOrders.first(where: { $0.orderNumber == orderNumber }) else { return }
|
||
appServices.appNavigator.push(.orders(.storeDetail(order)), from: self)
|
||
} else if item.hasPrefix("orders:writeoff:") {
|
||
let orderNumber = String(item.dropFirst("orders:writeoff:".count))
|
||
guard let order = viewModel.writeOffOrders.first(where: { $0.orderNumber == orderNumber }) else { return }
|
||
appServices.appNavigator.push(.orders(.writeOffDetail(order)), from: self)
|
||
}
|
||
}
|
||
|
||
/// 列表即将展示最后一项时触发分页加载。
|
||
func collectionView(
|
||
_ collectionView: UICollectionView,
|
||
willDisplay cell: UICollectionViewCell,
|
||
forItemAt indexPath: IndexPath
|
||
) {
|
||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||
let isLast: Bool
|
||
if item.hasPrefix("orders:store:") {
|
||
let orderNumber = String(item.dropFirst("orders:store:".count))
|
||
isLast = orderNumber == viewModel.storeOrders.last?.orderNumber
|
||
} else if item.hasPrefix("orders:writeoff:") {
|
||
let orderNumber = String(item.dropFirst("orders:writeoff:".count))
|
||
isLast = orderNumber == viewModel.writeOffOrders.last?.orderNumber
|
||
} else {
|
||
isLast = false
|
||
}
|
||
if isLast {
|
||
Task { await loadMore() }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Cells
|
||
|
||
/// 订单页头部 Cell,展示子入口切换与统计摘要。
|
||
private final class OrdersHeaderCell: UICollectionViewCell {
|
||
static let reuseID = "OrdersHeaderCell"
|
||
private var onSelectEntry: ((OrdersEntry) -> Void)?
|
||
|
||
private let storeButton = UIButton(type: .system)
|
||
private let verifyButton = UIButton(type: .system)
|
||
private let leftPill = UILabel()
|
||
private let rightPill = UILabel()
|
||
|
||
/// 初始化 Cell 与子视图布局。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
let card = UIView()
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 8
|
||
contentView.addSubview(card)
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||
}
|
||
|
||
let segmentBackground = UIView()
|
||
segmentBackground.backgroundColor = UIColor(hex: 0xF3F4F6)
|
||
segmentBackground.layer.cornerRadius = 8
|
||
card.addSubview(segmentBackground)
|
||
storeButton.addTarget(self, action: #selector(storeTapped), for: .touchUpInside)
|
||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||
segmentBackground.addSubview(storeButton)
|
||
segmentBackground.addSubview(verifyButton)
|
||
|
||
[leftPill, rightPill].forEach {
|
||
$0.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||
$0.numberOfLines = 2
|
||
card.addSubview($0)
|
||
}
|
||
|
||
segmentBackground.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalToSuperview().inset(12)
|
||
make.height.equalTo(40)
|
||
}
|
||
storeButton.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(2)
|
||
make.width.equalToSuperview().multipliedBy(0.5)
|
||
}
|
||
verifyButton.snp.makeConstraints { make in
|
||
make.trailing.top.bottom.equalToSuperview().inset(2)
|
||
make.width.equalToSuperview().multipliedBy(0.5)
|
||
}
|
||
leftPill.snp.makeConstraints { make in
|
||
make.leading.bottom.equalToSuperview().inset(12)
|
||
make.top.equalTo(segmentBackground.snp.bottom).offset(10)
|
||
make.width.equalToSuperview().multipliedBy(0.45)
|
||
}
|
||
rightPill.snp.makeConstraints { make in
|
||
make.trailing.bottom.equalToSuperview().inset(12)
|
||
make.top.equalTo(leftPill)
|
||
make.width.equalToSuperview().multipliedBy(0.45)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 配置头部展示内容与入口切换回调。
|
||
func configure(
|
||
selectedEntry: OrdersEntry,
|
||
scenicName: String,
|
||
storeTotal: Int,
|
||
writeOffTotal: Int,
|
||
onSelectEntry: @escaping (OrdersEntry) -> Void
|
||
) {
|
||
self.onSelectEntry = onSelectEntry
|
||
updateSegment(storeButton, title: "订单管理", selected: selectedEntry == .storeOrders)
|
||
updateSegment(verifyButton, title: "核销订单", selected: selectedEntry == .verificationOrders)
|
||
if selectedEntry == .storeOrders {
|
||
leftPill.text = "当前景区\n\(scenicName)"
|
||
rightPill.text = "订单总数\n\(storeTotal)"
|
||
} else {
|
||
leftPill.text = "核销订单\n\(writeOffTotal)"
|
||
rightPill.text = "当前景区\n\(scenicName)"
|
||
}
|
||
}
|
||
|
||
/// 更新分段按钮选中样式。
|
||
private func updateSegment(_ button: UIButton, title: String, selected: Bool) {
|
||
button.setTitle(title, for: .normal)
|
||
button.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: selected ? .semibold : .medium)
|
||
button.setTitleColor(selected ? AppDesignUIKit.primary : AppDesignUIKit.textSecondary, for: .normal)
|
||
button.backgroundColor = selected ? .white : .clear
|
||
button.layer.cornerRadius = 6
|
||
}
|
||
|
||
/// 切换到订单管理入口。
|
||
@objc private func storeTapped() { onSelectEntry?(.storeOrders) }
|
||
/// 切换到核销订单入口。
|
||
@objc private func verifyTapped() { onSelectEntry?(.verificationOrders) }
|
||
}
|
||
|
||
/// 订单筛选栏 Cell,承载状态 / 时间与手机号搜索。
|
||
private final class OrdersFilterCell: UICollectionViewCell, UITextFieldDelegate {
|
||
static let reuseID = "OrdersFilterCell"
|
||
private let phoneField = UITextField()
|
||
private var onPhoneChange: ((String) -> Void)?
|
||
private var onSearch: (() -> Void)?
|
||
|
||
/// 初始化 Cell 与子视图布局。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
let card = UIView()
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 8
|
||
contentView.addSubview(card)
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||
}
|
||
phoneField.delegate = self
|
||
phoneField.keyboardType = .phonePad
|
||
phoneField.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||
phoneField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||
phoneField.layer.cornerRadius = 6
|
||
phoneField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||
phoneField.leftViewMode = .always
|
||
card.addSubview(phoneField)
|
||
phoneField.snp.makeConstraints { make in
|
||
make.leading.bottom.equalToSuperview().inset(12)
|
||
make.height.equalTo(40)
|
||
make.trailing.equalToSuperview().inset(100)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 配置筛选栏展示内容与交互回调。
|
||
func configure(
|
||
statusTitle: String,
|
||
phone: String,
|
||
onStatus: @escaping () -> Void,
|
||
onDate: @escaping () -> Void,
|
||
onPhoneChange: @escaping (String) -> Void,
|
||
onSearch: @escaping () -> Void
|
||
) {
|
||
phoneField.text = phone
|
||
self.onPhoneChange = onPhoneChange
|
||
self.onSearch = onSearch
|
||
}
|
||
|
||
/// 手机号输入变化时同步到 ViewModel。
|
||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||
onPhoneChange?(textField.text ?? "")
|
||
}
|
||
}
|
||
|
||
/// 核销操作栏 Cell,承载扫码与手动输入核销。
|
||
private final class OrdersWriteOffActionCell: UICollectionViewCell, UITextFieldDelegate {
|
||
static let reuseID = "OrdersWriteOffActionCell"
|
||
private let manualField = UITextField()
|
||
private var onManualChange: ((String) -> Void)?
|
||
private var onVerify: (() -> Void)?
|
||
|
||
/// 初始化 Cell 与子视图布局。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
let card = UIView()
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 8
|
||
contentView.addSubview(card)
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||
}
|
||
|
||
manualField.delegate = self
|
||
manualField.placeholder = "手动输入订单号"
|
||
manualField.autocorrectionType = .no
|
||
manualField.autocapitalizationType = .none
|
||
manualField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||
manualField.layer.cornerRadius = 8
|
||
manualField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
|
||
manualField.leftViewMode = .always
|
||
card.addSubview(manualField)
|
||
manualField.snp.makeConstraints { make in
|
||
make.leading.bottom.equalToSuperview().inset(12)
|
||
make.height.equalTo(42)
|
||
make.trailing.equalToSuperview().inset(90)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 配置核销操作栏展示内容与交互回调。
|
||
func configure(
|
||
manualOrderNumber: String,
|
||
isVerifying: Bool,
|
||
hint: String?,
|
||
onScan: @escaping () -> Void,
|
||
onManualChange: @escaping (String) -> Void,
|
||
onVerify: @escaping () -> Void
|
||
) {
|
||
manualField.text = manualOrderNumber
|
||
self.onManualChange = onManualChange
|
||
self.onVerify = onVerify
|
||
}
|
||
|
||
/// 手动订单号输入变化时同步到页面状态。
|
||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||
onManualChange?(textField.text ?? "")
|
||
}
|
||
}
|
||
|
||
/// 订单管理列表 Cell,展示单笔订单摘要。
|
||
private final class OrderEntityCell: UICollectionViewCell {
|
||
static let reuseID = "OrderEntityCell"
|
||
private let titleLabel = UILabel()
|
||
private let statusLabel = UILabel()
|
||
private let amountLabel = UILabel()
|
||
private let phoneLabel = UILabel()
|
||
|
||
/// 初始化 Cell 与子视图布局。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
let card = UIView()
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 8
|
||
contentView.addSubview(card)
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||
}
|
||
|
||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
|
||
amountLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold)
|
||
amountLabel.textColor = AppDesignUIKit.primary
|
||
phoneLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||
phoneLabel.textColor = AppDesignUIKit.textSecondary
|
||
|
||
let stack = UIStackView(arrangedSubviews: [titleLabel, phoneLabel, amountLabel])
|
||
stack.axis = .vertical
|
||
stack.spacing = 4
|
||
card.addSubview(stack)
|
||
card.addSubview(statusLabel)
|
||
stack.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview().inset(12) }
|
||
statusLabel.snp.makeConstraints { make in make.trailing.top.equalToSuperview().inset(12) }
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 配置订单卡片展示内容。
|
||
func configure(item: OrderEntity) {
|
||
titleLabel.text = item.orderNumber
|
||
statusLabel.text = item.orderStatusName
|
||
amountLabel.text = "¥\(item.actualPayAmount.isEmpty ? item.orderAmount : item.actualPayAmount)"
|
||
phoneLabel.text = item.phone
|
||
}
|
||
}
|
||
|
||
/// 核销订单列表 Cell,展示单笔核销订单摘要。
|
||
private final class WriteOffOrderCell: UICollectionViewCell {
|
||
static let reuseID = "WriteOffOrderCell"
|
||
private let titleLabel = UILabel()
|
||
private let subtitleLabel = UILabel()
|
||
|
||
/// 初始化 Cell 与子视图布局。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
let card = UIView()
|
||
card.backgroundColor = .white
|
||
card.layer.cornerRadius = 8
|
||
contentView.addSubview(card)
|
||
card.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||
}
|
||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
|
||
subtitleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||
subtitleLabel.textColor = AppDesignUIKit.textSecondary
|
||
let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||
stack.axis = .vertical
|
||
stack.spacing = 4
|
||
card.addSubview(stack)
|
||
stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(12) }
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 配置核销订单卡片展示内容。
|
||
func configure(item: WriteOffOrderItem, isVerifying: Bool) {
|
||
titleLabel.text = item.orderNumber
|
||
subtitleLabel.text = isVerifying ? "核销中..." : (item.projectName.isEmpty ? item.userPhone : item.projectName)
|
||
}
|
||
}
|
||
|
||
/// 订单列表空态 Cell,复用通用空态视图。
|
||
private final class OrdersEmptyStateCell: UICollectionViewCell {
|
||
static let reuseID = "OrdersEmptyStateCell"
|
||
private var heightConstraint: Constraint?
|
||
|
||
/// 初始化 Cell。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 嵌入空态视图并设置占位高度。
|
||
func embed(_ emptyView: UIView, preferredHeight: CGFloat) {
|
||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||
contentView.addSubview(emptyView)
|
||
emptyView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
heightConstraint = make.height.equalTo(preferredHeight).constraint
|
||
}
|
||
}
|
||
}
|