// // 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 = { UICollectionViewDiffableDataSource(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() 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) { 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) { 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 } } }