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:
2026-06-26 15:16:12 +08:00
parent 9edf993432
commit d99a5b1bf8
124 changed files with 5195 additions and 1536 deletions

View File

@ -6,8 +6,45 @@
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 {
final class StoreOrderDetailViewController: UIViewController, UITableViewDelegate {
private let item: OrderEntity
private let viewModel: OrderDetailViewModel
@ -15,12 +52,15 @@ final class StoreOrderDetailViewController: UIViewController {
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.dataSource = self
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)
@ -30,31 +70,176 @@ final class StoreOrderDetailViewController: UIViewController {
@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?.tableView.reloadData() }
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)
@ -75,99 +260,34 @@ final class StoreOrderDetailViewController: UIViewController {
})
present(alert, animated: true)
}
}
extension StoreOrderDetailViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int { 6 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return viewModel.contextMessage == nil ? 0 : 1
case 1: return 7
case 2: return 4
case 3: return 4
case 4: return viewModel.shootingList.count
case 5: return 5
default: return 0
}
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 1: "订单信息"
case 2: "支付信息"
case 3: "客户与项目"
case 4 where !viewModel.shootingList.isEmpty: "拍摄点"
case 5: "后续功能"
default: nil
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
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
cell.selectionStyle = .none
let display = viewModel.display
switch indexPath.section {
case 0:
cell.textLabel?.text = viewModel.contextMessage
cell.textLabel?.numberOfLines = 0
case 1:
let rows = ["订单号", "状态", "类型", "创建时间", "付款时间", "完成时间", "复制订单号"]
cell.textLabel?.text = rows[indexPath.row]
switch indexPath.row {
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 = displayPayTime
case 5: cell.detailTextLabel?.text = display.completeTime
default:
cell.textLabel?.textColor = AppDesignUIKit.primary
cell.selectionStyle = .default
}
case 2:
let rows = ["付款金额", "退款金额", "付款方式", "用户 UID"]
cell.textLabel?.text = rows[indexPath.row]
switch indexPath.row {
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)"
}
case 3:
let rows = ["手机号", "关联项目", "项目 ID", "备注"]
cell.textLabel?.text = rows[indexPath.row]
switch indexPath.row {
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
}
case 4:
let shooting = viewModel.shootingList[indexPath.row]
cell.textLabel?.text = shooting.scenicSpotName
cell.detailTextLabel?.text = shooting.staffName.isEmpty ? "状态 \(shooting.status)" : shooting.staffName
case 5:
let actions = ["历史拍摄", "任务上传", "视频预告", "尾片上传", "退款"]
cell.textLabel?.text = actions[indexPath.row]
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
if indexPath.row == 4, !refundViewModel.canRefund(item) {
cell.isUserInteractionEnabled = false
cell.textLabel?.textColor = AppDesignUIKit.textSecondary
}
default: break
}
return cell
}
/// UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 1, indexPath.row == 6 { copyOrderNumber(); return }
guard indexPath.section == 5 else { return }
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 indexPath.row {
switch index {
case 0:
HomeMenuRouting.pushOrders(.historicalShooting(orderNumber: orderNumber), from: self)
case 1 where item.orderType == 19:
@ -178,7 +298,8 @@ extension StoreOrderDetailViewController: UITableViewDataSource, UITableViewDele
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "尾片上传"), from: self)
case 4:
presentRefund()
default: break
default:
break
}
}
@ -188,65 +309,79 @@ extension StoreOrderDetailViewController: UITableViewDataSource, UITableViewDele
return payTime
}
/// emptyZero
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: UIViewController {
final class WriteOffOrderDetailViewController: SimpleTableDiffableViewController {
private let item: WriteOffOrderItem
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .insetGrouped)
table.dataSource = self
table.backgroundColor = AppDesignUIKit.pageBackground
return table
}()
///
init(item: WriteOffOrderItem) {
self.item = item
super.init(nibName: nil, bundle: nil)
super.init(style: .insetGrouped)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "核销详情"
view.addSubview(tableView)
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
tableView.backgroundColor = AppDesignUIKit.pageBackground
}
}
extension WriteOffOrderDetailViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 6 }
/// 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
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
/// Cell
override func configureCell(_ cell: UITableViewCell, row: SimpleTableRow, at indexPath: IndexPath) {
cell.selectionStyle = .none
switch indexPath.row {
case 0:
switch row {
case WriteOffDetailRowID.orderNumber:
cell.textLabel?.text = "订单号"
cell.detailTextLabel?.text = item.orderNumber
case 1:
case WriteOffDetailRowID.project:
cell.textLabel?.text = "项目"
cell.detailTextLabel?.text = item.projectName
case 2:
case WriteOffDetailRowID.phone:
cell.textLabel?.text = "手机号"
cell.detailTextLabel?.text = item.userPhone
case 3:
case WriteOffDetailRowID.amount:
cell.textLabel?.text = "金额"
cell.detailTextLabel?.text = "¥\(item.orderAmount)"
case 4:
case WriteOffDetailRowID.status:
cell.textLabel?.text = "状态"
cell.detailTextLabel?.text = item.orderStatusName
default:
case WriteOffDetailRowID.verificationTime:
cell.textLabel?.text = "核销时间"
cell.detailTextLabel?.text = item.orderVerificationTime
default:
break
}
return cell
}
}