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>
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:
|
||
HomeMenuRouting.pushOrders(.historicalShooting(orderNumber: orderNumber), from: self)
|
||
case 1 where item.orderType == 19:
|
||
HomeMenuRouting.pushOrders(.multiTravelTaskUpload(orderNumber: orderNumber), from: self)
|
||
case 2:
|
||
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "视频预告"), from: self)
|
||
case 3:
|
||
HomeMenuRouting.pushOrders(.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
|
||
}
|
||
}
|
||
}
|