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>
817 lines
33 KiB
Swift
817 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 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.appRouter.onChange = { [weak self] in
|
||
guard let self else { return }
|
||
if self.viewModel.selectedEntry != self.appServices.appRouter.selectedOrdersEntry {
|
||
self.viewModel.selectedEntry = self.appServices.appRouter.selectedOrdersEntry
|
||
Task { await self.reload(showLoading: true) }
|
||
}
|
||
Task { await self.consumePendingScanCodeIfNeeded() }
|
||
}
|
||
appServices.accountContext.onChange = { [weak self] in
|
||
Task { await self?.reload(showLoading: true) }
|
||
}
|
||
|
||
viewModel.selectedEntry = appServices.appRouter.selectedOrdersEntry
|
||
Task {
|
||
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
|
||
appServices.appRouter.selectedOrdersEntry = 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 = appServices.appRouter.consumePendingOrderScanCode() else { return }
|
||
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 }
|
||
HomeMenuRouting.pushOrders(.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 }
|
||
HomeMenuRouting.pushOrders(.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
|
||
}
|
||
}
|
||
}
|