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>
388 lines
16 KiB
Swift
388 lines
16 KiB
Swift
//
|
||
// OrderDetailViewControllers.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
// MARK: - Store Order Diffable 标识
|
||
|
||
private typealias StoreOrderDetailSection = Int
|
||
private typealias StoreOrderDetailRow = String
|
||
|
||
/// 门店订单详情 section 索引。
|
||
private enum StoreOrderDetailSectionID {
|
||
static let context = 0
|
||
static let orderInfo = 1
|
||
static let payment = 2
|
||
static let customer = 3
|
||
static let shooting = 4
|
||
static let actions = 5
|
||
}
|
||
|
||
/// 门店订单详情行标识。
|
||
private enum StoreOrderDetailRowID {
|
||
static let context = "storeDetail:context"
|
||
static func orderInfo(_ index: Int) -> String { "storeDetail:orderInfo:\(index)" }
|
||
static func payment(_ index: Int) -> String { "storeDetail:payment:\(index)" }
|
||
static func customer(_ index: Int) -> String { "storeDetail:customer:\(index)" }
|
||
static func shooting(_ id: String) -> String { "storeDetail:shooting:\(id)" }
|
||
static func action(_ index: Int) -> String { "storeDetail:action:\(index)" }
|
||
}
|
||
|
||
// MARK: - Write-Off Diffable 标识
|
||
|
||
/// 核销详情行标识。
|
||
private enum WriteOffDetailRowID {
|
||
static let orderNumber = "writeOff:orderNumber"
|
||
static let project = "writeOff:project"
|
||
static let phone = "writeOff:phone"
|
||
static let amount = "writeOff:amount"
|
||
static let status = "writeOff:status"
|
||
static let verificationTime = "writeOff:verificationTime"
|
||
}
|
||
|
||
/// 门店订单详情页,展示订单接口补全后的支付、客户、项目和拍摄点信息。
|
||
final class StoreOrderDetailViewController: UIViewController, UITableViewDelegate {
|
||
|
||
private let item: OrderEntity
|
||
private let viewModel: OrderDetailViewModel
|
||
private let refundViewModel = OrderRefundViewModel()
|
||
|
||
private lazy var tableView: UITableView = {
|
||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||
table.delegate = self
|
||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||
return table
|
||
}()
|
||
|
||
/// Diffable 数据源,驱动多 section 订单详情刷新。
|
||
private var tableDataSource: UITableViewDiffableDataSource<StoreOrderDetailSection, StoreOrderDetailRow>!
|
||
|
||
/// 初始化实例。
|
||
init(item: OrderEntity) {
|
||
self.item = item
|
||
self.viewModel = OrderDetailViewModel(item: item)
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "订单详情"
|
||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||
configureTableDataSource()
|
||
view.addSubview(tableView)
|
||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||
|
||
viewModel.onChange = { [weak self] in self?.applyTableSnapshot(reconfigure: true) }
|
||
Task { await loadDetail() }
|
||
}
|
||
|
||
/// 配置 Diffable 数据源。
|
||
private func configureTableDataSource() {
|
||
tableDataSource = UITableViewDiffableDataSource<StoreOrderDetailSection, StoreOrderDetailRow>(
|
||
tableView: tableView
|
||
) { [weak self] (_: UITableView, indexPath: IndexPath, row: StoreOrderDetailRow) -> UITableViewCell? in
|
||
guard let self else { return UITableViewCell() }
|
||
return self.configureDetailCell(row: row)
|
||
}
|
||
applyTableSnapshot(animated: false)
|
||
}
|
||
|
||
/// 构建 Diffable snapshot。
|
||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<StoreOrderDetailSection, StoreOrderDetailRow> {
|
||
var snapshot = NSDiffableDataSourceSnapshot<StoreOrderDetailSection, StoreOrderDetailRow>()
|
||
|
||
if viewModel.contextMessage != nil {
|
||
snapshot.appendSections([StoreOrderDetailSectionID.context])
|
||
snapshot.appendItems([StoreOrderDetailRowID.context], toSection: StoreOrderDetailSectionID.context)
|
||
}
|
||
|
||
snapshot.appendSections([StoreOrderDetailSectionID.orderInfo])
|
||
snapshot.appendItems((0..<7).map { StoreOrderDetailRowID.orderInfo($0) }, toSection: StoreOrderDetailSectionID.orderInfo)
|
||
|
||
snapshot.appendSections([StoreOrderDetailSectionID.payment])
|
||
snapshot.appendItems((0..<4).map { StoreOrderDetailRowID.payment($0) }, toSection: StoreOrderDetailSectionID.payment)
|
||
|
||
snapshot.appendSections([StoreOrderDetailSectionID.customer])
|
||
snapshot.appendItems((0..<4).map { StoreOrderDetailRowID.customer($0) }, toSection: StoreOrderDetailSectionID.customer)
|
||
|
||
if !viewModel.shootingList.isEmpty {
|
||
snapshot.appendSections([StoreOrderDetailSectionID.shooting])
|
||
let shootingRows = viewModel.shootingList.enumerated().map { index, shooting in
|
||
StoreOrderDetailRowID.shooting("\(shooting.id)-\(index)")
|
||
}
|
||
snapshot.appendItems(shootingRows, toSection: StoreOrderDetailSectionID.shooting)
|
||
}
|
||
|
||
snapshot.appendSections([StoreOrderDetailSectionID.actions])
|
||
snapshot.appendItems((0..<5).map { StoreOrderDetailRowID.action($0) }, toSection: StoreOrderDetailSectionID.actions)
|
||
|
||
return snapshot
|
||
}
|
||
|
||
/// 应用 snapshot 刷新列表。
|
||
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
|
||
var snapshot = buildTableSnapshot()
|
||
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
|
||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||
}
|
||
tableDataSource.apply(snapshot, animatingDifferences: animated)
|
||
}
|
||
|
||
/// 配置详情 Cell 内容。
|
||
private func configureDetailCell(row: StoreOrderDetailRow) -> UITableViewCell {
|
||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||
cell.selectionStyle = .none
|
||
cell.textLabel?.numberOfLines = 1
|
||
cell.textLabel?.textColor = nil
|
||
cell.accessoryType = .none
|
||
cell.isUserInteractionEnabled = true
|
||
let display = viewModel.display
|
||
|
||
if row == StoreOrderDetailRowID.context {
|
||
cell.textLabel?.text = viewModel.contextMessage
|
||
cell.textLabel?.numberOfLines = 0
|
||
return cell
|
||
}
|
||
|
||
if row.hasPrefix("storeDetail:orderInfo:") {
|
||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||
let rows = ["订单号", "状态", "类型", "创建时间", "付款时间", "完成时间", "复制订单号"]
|
||
cell.textLabel?.text = rows[index]
|
||
switch index {
|
||
case 0: cell.detailTextLabel?.text = display.orderNumber
|
||
case 1: cell.detailTextLabel?.text = display.orderStatusName
|
||
case 2: cell.detailTextLabel?.text = display.orderTypeLabel
|
||
case 3: cell.detailTextLabel?.text = display.createdAt
|
||
case 4: cell.detailTextLabel?.text = display.payTime
|
||
case 5: cell.detailTextLabel?.text = display.completeTime
|
||
default:
|
||
cell.textLabel?.textColor = AppDesignUIKit.primary
|
||
cell.selectionStyle = .default
|
||
}
|
||
return cell
|
||
}
|
||
|
||
if row.hasPrefix("storeDetail:payment:") {
|
||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||
let rows = ["付款金额", "退款金额", "付款方式", "用户 UID"]
|
||
cell.textLabel?.text = rows[index]
|
||
switch index {
|
||
case 0: cell.detailTextLabel?.text = "¥\(emptyToZero(display.actualPayAmount))"
|
||
case 1: cell.detailTextLabel?.text = "¥\(emptyToZero(display.actualRefundAmount))"
|
||
case 2: cell.detailTextLabel?.text = display.payTypeName
|
||
default: cell.detailTextLabel?.text = "\(display.userId)"
|
||
}
|
||
return cell
|
||
}
|
||
|
||
if row.hasPrefix("storeDetail:customer:") {
|
||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||
let rows = ["手机号", "关联项目", "项目 ID", "备注"]
|
||
cell.textLabel?.text = rows[index]
|
||
switch index {
|
||
case 0: cell.detailTextLabel?.text = display.phone
|
||
case 1: cell.detailTextLabel?.text = display.projectName
|
||
case 2: cell.detailTextLabel?.text = "\(display.projectId)"
|
||
default: cell.detailTextLabel?.text = display.remark
|
||
}
|
||
return cell
|
||
}
|
||
|
||
if row.hasPrefix("storeDetail:shooting:") {
|
||
let suffix = row.replacingOccurrences(of: "storeDetail:shooting:", with: "")
|
||
let indexPart = suffix.split(separator: "-").last.flatMap { Int($0) } ?? 0
|
||
if indexPart < viewModel.shootingList.count {
|
||
let shooting = viewModel.shootingList[indexPart]
|
||
cell.textLabel?.text = shooting.scenicSpotName
|
||
cell.detailTextLabel?.text = shooting.staffName.isEmpty ? "状态 \(shooting.status)" : shooting.staffName
|
||
}
|
||
return cell
|
||
}
|
||
|
||
if row.hasPrefix("storeDetail:action:") {
|
||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||
let actions = ["历史拍摄", "任务上传", "视频预告", "尾片上传", "退款"]
|
||
cell.textLabel?.text = actions[index]
|
||
cell.accessoryType = .disclosureIndicator
|
||
cell.selectionStyle = .default
|
||
if index == 4, !refundViewModel.canRefund(item) {
|
||
cell.isUserInteractionEnabled = false
|
||
cell.textLabel?.textColor = AppDesignUIKit.textSecondary
|
||
}
|
||
return cell
|
||
}
|
||
|
||
return cell
|
||
}
|
||
|
||
/// 加载Detail数据。
|
||
private func loadDetail() async {
|
||
await appServices.globalLoading.withLoading(message: "加载详情中...") {
|
||
await viewModel.load(api: appServices.ordersAPI, fallbackStoreId: appServices.accountContext.currentStore?.id)
|
||
}
|
||
applyTableSnapshot(reconfigure: true)
|
||
if let error = viewModel.errorMessage {
|
||
showToast(error)
|
||
}
|
||
}
|
||
|
||
/// 复制订单Number。
|
||
private func copyOrderNumber() {
|
||
UIPasteboard.general.string = viewModel.display.orderNumber
|
||
showToast("订单号已复制")
|
||
}
|
||
|
||
/// 弹出Refund页面。
|
||
private func presentRefund() {
|
||
refundViewModel.begin(item: item)
|
||
let alert = UIAlertController(title: "订单退款", message: "可退 ¥\(refundViewModel.availableAmountText(for: item))", preferredStyle: .alert)
|
||
alert.addTextField { $0.placeholder = "退款原因" }
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
alert.addAction(UIAlertAction(title: "提交", style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
self.refundViewModel.reason = alert.textFields?.first?.text ?? ""
|
||
Task {
|
||
let success = await self.refundViewModel.submit(api: self.appServices.ordersAPI, item: self.item)
|
||
if success {
|
||
self.showToast("退款申请已提交")
|
||
await self.viewModel.load(api: self.appServices.ordersAPI, fallbackStoreId: self.appServices.accountContext.currentStore?.id, forceReload: true)
|
||
} else if let message = self.refundViewModel.errorMessage {
|
||
self.showToast(message)
|
||
}
|
||
}
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
/// UITableView 代理:section 标题。
|
||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||
guard let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
|
||
switch sectionID {
|
||
case StoreOrderDetailSectionID.orderInfo: return "订单信息"
|
||
case StoreOrderDetailSectionID.payment: return "支付信息"
|
||
case StoreOrderDetailSectionID.customer: return "客户与项目"
|
||
case StoreOrderDetailSectionID.shooting: return "拍摄点"
|
||
case StoreOrderDetailSectionID.actions: return "后续功能"
|
||
default: return nil
|
||
}
|
||
}
|
||
|
||
/// UITableView 代理:处理行选中。
|
||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
tableView.deselectRow(at: indexPath, animated: true)
|
||
guard let row = tableDataSource.itemIdentifier(for: indexPath) else { return }
|
||
|
||
if row == StoreOrderDetailRowID.orderInfo(6) {
|
||
copyOrderNumber()
|
||
return
|
||
}
|
||
|
||
guard row.hasPrefix("storeDetail:action:") else { return }
|
||
let index = Int(row.split(separator: ":").last ?? "") ?? -1
|
||
let orderNumber = viewModel.display.orderNumber
|
||
switch index {
|
||
case 0:
|
||
appServices.appNavigator.push(.orders(.historicalShooting(orderNumber: orderNumber)), from: self)
|
||
case 1 where item.orderType == 19:
|
||
appServices.appNavigator.push(.orders(.multiTravelTaskUpload(orderNumber: orderNumber)), from: self)
|
||
case 2:
|
||
appServices.appNavigator.push(.orders(.orderTrailer(orderNumber: orderNumber, title: "视频预告")), from: self)
|
||
case 3:
|
||
appServices.appNavigator.push(.orders(.orderTrailer(orderNumber: orderNumber, title: "尾片上传")), from: self)
|
||
case 4:
|
||
presentRefund()
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
private var displayPayTime: String {
|
||
let payTime = viewModel.display.payTime
|
||
if payTime.isEmpty || payTime == "0" || payTime.hasPrefix("1970") { return item.depositPayTime }
|
||
return payTime
|
||
}
|
||
|
||
/// empty至Zero相关逻辑。
|
||
private func emptyToZero(_ value: String) -> String {
|
||
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "0" : value
|
||
}
|
||
}
|
||
|
||
/// 安全下标,避免 section 越界。
|
||
private extension Array {
|
||
subscript(safe index: Int) -> Element? {
|
||
indices.contains(index) ? self[index] : nil
|
||
}
|
||
}
|
||
|
||
/// 核销订单详情页,展示核销列表项摘要信息。
|
||
final class WriteOffOrderDetailViewController: SimpleTableDiffableViewController {
|
||
|
||
private let item: WriteOffOrderItem
|
||
|
||
/// 初始化实例。
|
||
init(item: WriteOffOrderItem) {
|
||
self.item = item
|
||
super.init(style: .insetGrouped)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) { fatalError() }
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
title = "核销详情"
|
||
tableView.backgroundColor = AppDesignUIKit.pageBackground
|
||
}
|
||
|
||
/// 构建 Diffable snapshot。
|
||
override func buildSnapshot() -> NSDiffableDataSourceSnapshot<SimpleTableSection, SimpleTableRow> {
|
||
var snapshot = NSDiffableDataSourceSnapshot<SimpleTableSection, SimpleTableRow>()
|
||
snapshot.appendSections([0])
|
||
snapshot.appendItems([
|
||
WriteOffDetailRowID.orderNumber,
|
||
WriteOffDetailRowID.project,
|
||
WriteOffDetailRowID.phone,
|
||
WriteOffDetailRowID.amount,
|
||
WriteOffDetailRowID.status,
|
||
WriteOffDetailRowID.verificationTime
|
||
], toSection: 0)
|
||
return snapshot
|
||
}
|
||
|
||
/// 配置 Cell 展示内容。
|
||
override func configureCell(_ cell: UITableViewCell, row: SimpleTableRow, at indexPath: IndexPath) {
|
||
cell.selectionStyle = .none
|
||
switch row {
|
||
case WriteOffDetailRowID.orderNumber:
|
||
cell.textLabel?.text = "订单号"
|
||
cell.detailTextLabel?.text = item.orderNumber
|
||
case WriteOffDetailRowID.project:
|
||
cell.textLabel?.text = "项目"
|
||
cell.detailTextLabel?.text = item.projectName
|
||
case WriteOffDetailRowID.phone:
|
||
cell.textLabel?.text = "手机号"
|
||
cell.detailTextLabel?.text = item.userPhone
|
||
case WriteOffDetailRowID.amount:
|
||
cell.textLabel?.text = "金额"
|
||
cell.detailTextLabel?.text = "¥\(item.orderAmount)"
|
||
case WriteOffDetailRowID.status:
|
||
cell.textLabel?.text = "状态"
|
||
cell.detailTextLabel?.text = item.orderStatusName
|
||
case WriteOffDetailRowID.verificationTime:
|
||
cell.textLabel?.text = "核销时间"
|
||
cell.detailTextLabel?.text = item.orderVerificationTime
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
}
|