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:
@ -6,6 +6,31 @@
|
||||
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 {
|
||||
|
||||
@ -13,36 +38,43 @@ final class OrdersViewController: UIViewController {
|
||||
private var manualOrderNumber = ""
|
||||
private var scanHintMessage: String?
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.separatorStyle = .none
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(OrderEntityCell.self, forCellReuseIdentifier: OrderEntityCell.reuseID)
|
||||
table.register(WriteOffOrderCell.self, forCellReuseIdentifier: WriteOffOrderCell.reuseID)
|
||||
table.register(OrdersHeaderCell.self, forCellReuseIdentifier: OrdersHeaderCell.reuseID)
|
||||
table.register(OrdersFilterCell.self, forCellReuseIdentifier: OrdersFilterCell.reuseID)
|
||||
table.register(OrdersWriteOffActionCell.self, forCellReuseIdentifier: OrdersWriteOffActionCell.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(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(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
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()
|
||||
self?.applySnapshot()
|
||||
}
|
||||
|
||||
appServices.appRouter.onChange = { [weak self] in
|
||||
@ -64,6 +96,202 @@ final class OrdersViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 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)
|
||||
@ -75,6 +303,7 @@ final class OrdersViewController: UIViewController {
|
||||
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: "加载订单...") {
|
||||
@ -91,6 +320,7 @@ final class OrdersViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 滚动到底部时加载下一页。
|
||||
private func loadMore() async {
|
||||
do {
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
@ -111,6 +341,7 @@ final class OrdersViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换订单管理 / 核销订单子入口。
|
||||
private func switchEntry(_ entry: OrdersEntry) {
|
||||
guard viewModel.selectedEntry != entry else { return }
|
||||
viewModel.selectedEntry = entry
|
||||
@ -118,6 +349,7 @@ final class OrdersViewController: UIViewController {
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
/// 提交订单核销请求。
|
||||
private func verify(orderNumber: String) async {
|
||||
guard let scenicId = currentScenicId else { return }
|
||||
do {
|
||||
@ -135,12 +367,14 @@ final class OrdersViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 消费路由中暂存的扫码结果(核销入口)。
|
||||
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("未识别到有效订单号")
|
||||
@ -150,10 +384,11 @@ final class OrdersViewController: UIViewController {
|
||||
confirmVerify(orderNumber: parsed.orderNumber)
|
||||
} else {
|
||||
scanHintMessage = "扫码成功,当前列表未找到该订单"
|
||||
tableView.reloadData()
|
||||
applySnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
/// 弹出核销确认对话框。
|
||||
private func confirmVerify(orderNumber: String) {
|
||||
let alert = UIAlertController(title: "确认核销该订单?", message: orderNumber, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
@ -163,6 +398,7 @@ final class OrdersViewController: UIViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 打开订单号扫码页。
|
||||
private func presentScanner() {
|
||||
let scanner = OrderCodeScannerViewController()
|
||||
scanner.onScanResult = { [weak self, weak scanner] result in
|
||||
@ -180,6 +416,7 @@ final class OrdersViewController: UIViewController {
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
/// 弹出时间筛选快捷选项。
|
||||
private func presentDateFilter() {
|
||||
let alert = UIAlertController(title: "时间筛选", message: "选择开始和结束日期", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "清除筛选", style: .destructive) { [weak self] _ in
|
||||
@ -198,6 +435,7 @@ final class OrdersViewController: UIViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 弹出订单状态筛选 ActionSheet。
|
||||
private func presentStatusFilter() {
|
||||
let sheet = UIAlertController(title: "订单状态", message: nil, preferredStyle: .actionSheet)
|
||||
for filter in OrderFilters.statusFilters {
|
||||
@ -211,143 +449,51 @@ final class OrdersViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
extension OrdersViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
guard currentScenicId != nil else { return 1 }
|
||||
return viewModel.selectedEntry == .storeOrders ? 3 : 3
|
||||
}
|
||||
// MARK: - UICollectionViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == 0 { return 1 }
|
||||
if section == 1 { return viewModel.selectedEntry == .storeOrders ? 1 : 1 }
|
||||
if currentScenicId == nil { return 1 }
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
return max(viewModel.storeOrders.count, viewModel.loading && viewModel.storeOrders.isEmpty ? 0 : 1)
|
||||
}
|
||||
return max(viewModel.writeOffOrders.count, viewModel.loading && viewModel.writeOffOrders.isEmpty ? 0 : 1)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.section == 0 {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrdersHeaderCell.reuseID, for: indexPath) as! OrdersHeaderCell
|
||||
let services = appServices
|
||||
cell.configure(
|
||||
selectedEntry: viewModel.selectedEntry,
|
||||
scenicName: services.accountContext.currentScenic?.name ?? "--",
|
||||
storeTotal: viewModel.storeTotal,
|
||||
writeOffTotal: viewModel.writeOffTotal,
|
||||
onSelectEntry: { [weak self] entry in self?.switchEntry(entry) }
|
||||
)
|
||||
return cell
|
||||
}
|
||||
|
||||
if currentScenicId == nil {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "缺少经营上下文", message: "请先在首页选择景区后查看订单。", systemImage: "mountain.2")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.height.equalTo(360)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
if indexPath.section == 1 {
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: 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
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: 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
|
||||
}
|
||||
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
if viewModel.storeOrders.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "暂无订单", message: "可切换筛选条件或下拉刷新。", systemImage: "tray")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(260) }
|
||||
return cell
|
||||
}
|
||||
let item = viewModel.storeOrders[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrderEntityCell.reuseID, for: indexPath) as! OrderEntityCell
|
||||
cell.configure(item: item)
|
||||
return cell
|
||||
}
|
||||
|
||||
if viewModel.writeOffOrders.isEmpty {
|
||||
let cell = UITableViewCell()
|
||||
cell.selectionStyle = .none
|
||||
cell.backgroundColor = .clear
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
let empty = makeEmptyStateView(title: "暂无核销订单", message: "可下拉刷新或切换景区查看。", systemImage: "tray")
|
||||
cell.contentView.addSubview(empty)
|
||||
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(260) }
|
||||
return cell
|
||||
}
|
||||
let item = viewModel.writeOffOrders[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: WriteOffOrderCell.reuseID, for: indexPath) as! WriteOffOrderCell
|
||||
cell.configure(item: item, isVerifying: viewModel.currentVerifyingOrderNumber == item.orderNumber)
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 2 else { return }
|
||||
if viewModel.selectedEntry == .storeOrders, indexPath.row < viewModel.storeOrders.count {
|
||||
HomeMenuRouting.pushOrders(.storeDetail(viewModel.storeOrders[indexPath.row]), from: self)
|
||||
} else if viewModel.selectedEntry == .verificationOrders, indexPath.row < viewModel.writeOffOrders.count {
|
||||
HomeMenuRouting.pushOrders(.writeOffDetail(viewModel.writeOffOrders[indexPath.row]), from: self)
|
||||
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 tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 2 else { return }
|
||||
/// 列表即将展示最后一项时触发分页加载。
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
willDisplay cell: UICollectionViewCell,
|
||||
forItemAt indexPath: IndexPath
|
||||
) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
let isLast: Bool
|
||||
if viewModel.selectedEntry == .storeOrders {
|
||||
isLast = indexPath.row == viewModel.storeOrders.count - 1
|
||||
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 = indexPath.row == viewModel.writeOffOrders.count - 1
|
||||
isLast = false
|
||||
}
|
||||
if isLast {
|
||||
Task { await loadMore() }
|
||||
}
|
||||
if isLast { Task { await loadMore() } }
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
if indexPath.section == 0 { return 120 }
|
||||
if indexPath.section == 1 { return viewModel.selectedEntry == .storeOrders ? 130 : 150 }
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cells
|
||||
|
||||
private final class OrdersHeaderCell: UITableViewCell {
|
||||
/// 订单页头部 Cell,展示子入口切换与统计摘要。
|
||||
private final class OrdersHeaderCell: UICollectionViewCell {
|
||||
static let reuseID = "OrdersHeaderCell"
|
||||
private var onSelectEntry: ((OrdersEntry) -> Void)?
|
||||
|
||||
@ -356,16 +502,16 @@ private final class OrdersHeaderCell: UITableViewCell {
|
||||
private let leftPill = UILabel()
|
||||
private let rightPill = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化 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: 16, bottom: 4, right: 16))
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||||
}
|
||||
|
||||
let segmentBackground = UIView()
|
||||
@ -410,7 +556,14 @@ private final class OrdersHeaderCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(selectedEntry: OrdersEntry, scenicName: String, storeTotal: Int, writeOffTotal: Int, onSelectEntry: @escaping (OrdersEntry) -> Void) {
|
||||
/// 配置头部展示内容与入口切换回调。
|
||||
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)
|
||||
@ -423,6 +576,7 @@ private final class OrdersHeaderCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新分段按钮选中样式。
|
||||
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)
|
||||
@ -431,26 +585,29 @@ private final class OrdersHeaderCell: UITableViewCell {
|
||||
button.layer.cornerRadius = 6
|
||||
}
|
||||
|
||||
/// 切换到订单管理入口。
|
||||
@objc private func storeTapped() { onSelectEntry?(.storeOrders) }
|
||||
/// 切换到核销订单入口。
|
||||
@objc private func verifyTapped() { onSelectEntry?(.verificationOrders) }
|
||||
}
|
||||
|
||||
private final class OrdersFilterCell: UITableViewCell, UITextFieldDelegate {
|
||||
/// 订单筛选栏 Cell,承载状态 / 时间与手机号搜索。
|
||||
private final class OrdersFilterCell: UICollectionViewCell, UITextFieldDelegate {
|
||||
static let reuseID = "OrdersFilterCell"
|
||||
private let phoneField = UITextField()
|
||||
private var onPhoneChange: ((String) -> Void)?
|
||||
private var onSearch: (() -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化 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: 16, bottom: 4, right: 16))
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||||
}
|
||||
phoneField.delegate = self
|
||||
phoneField.keyboardType = .phonePad
|
||||
@ -470,32 +627,44 @@ private final class OrdersFilterCell: UITableViewCell, UITextFieldDelegate {
|
||||
@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) {
|
||||
/// 配置筛选栏展示内容与交互回调。
|
||||
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 ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private final class OrdersWriteOffActionCell: UITableViewCell, UITextFieldDelegate {
|
||||
/// 核销操作栏 Cell,承载扫码与手动输入核销。
|
||||
private final class OrdersWriteOffActionCell: UICollectionViewCell, UITextFieldDelegate {
|
||||
static let reuseID = "OrdersWriteOffActionCell"
|
||||
private let manualField = UITextField()
|
||||
private var onManualChange: ((String) -> Void)?
|
||||
private var onVerify: (() -> Void)?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
/// 初始化 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: 16, bottom: 4, right: 16)) }
|
||||
card.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0))
|
||||
}
|
||||
|
||||
manualField.delegate = self
|
||||
manualField.placeholder = "手动输入订单号"
|
||||
@ -516,33 +685,45 @@ private final class OrdersWriteOffActionCell: UITableViewCell, UITextFieldDelega
|
||||
@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) {
|
||||
/// 配置核销操作栏展示内容与交互回调。
|
||||
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 ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private final class OrderEntityCell: UITableViewCell {
|
||||
/// 订单管理列表 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()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .default
|
||||
/// 初始化 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: 16, bottom: 4, right: 16)) }
|
||||
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)
|
||||
@ -563,6 +744,7 @@ private final class OrderEntityCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置订单卡片展示内容。
|
||||
func configure(item: OrderEntity) {
|
||||
titleLabel.text = item.orderNumber
|
||||
statusLabel.text = item.orderStatusName
|
||||
@ -571,19 +753,23 @@ private final class OrderEntityCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
private final class WriteOffOrderCell: UITableViewCell {
|
||||
/// 核销订单列表 Cell,展示单笔核销订单摘要。
|
||||
private final class WriteOffOrderCell: UICollectionViewCell {
|
||||
static let reuseID = "WriteOffOrderCell"
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
/// 初始化 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: 16, bottom: 4, right: 16)) }
|
||||
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
|
||||
@ -597,8 +783,34 @@ private final class WriteOffOrderCell: UITableViewCell {
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user