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:
@ -62,6 +62,7 @@ struct OrderEntity: Decodable, Identifiable, Equatable, Hashable {
|
||||
let isNeedEdit: Bool
|
||||
let isRefined: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case photogUid = "photog_uid"
|
||||
case orderNumber = "order_number"
|
||||
@ -131,6 +132,7 @@ struct OrderPhotoTravel: Decodable, Equatable, Hashable {
|
||||
let retouchGiftPhotoNum: Int
|
||||
let retouchGiftVideoNum: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderPhotoNum = "order_photo_num"
|
||||
case orderVideoNum = "order_video_num"
|
||||
@ -163,6 +165,7 @@ struct WriteOffOrderItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
let orderStatusName: String
|
||||
let orderVerificationTime: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
case orderVerificationStatus = "order_verification_status"
|
||||
@ -192,6 +195,7 @@ struct WriteOffOrderItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
struct WriteOffRequest: Encodable {
|
||||
let orderNumber: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
}
|
||||
@ -207,6 +211,7 @@ struct DepositOrderListItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
let statusName: String
|
||||
let createdAt: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case orderNumber = "order_number"
|
||||
@ -234,6 +239,7 @@ struct DepositOrderListItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
struct DepositOrderWriteOffRequest: Encodable, Equatable {
|
||||
let orderNumber: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
}
|
||||
@ -244,6 +250,7 @@ struct DepositOrderRefundRequest: Encodable, Equatable {
|
||||
let orderNumber: String
|
||||
let refundReason: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
case refundReason = "refund_reason"
|
||||
@ -275,6 +282,7 @@ struct OrderRefundRequest: Encodable, Equatable {
|
||||
let refundAmount: String
|
||||
let refundReason: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
case refundType = "refund_type"
|
||||
@ -314,6 +322,7 @@ struct StoreOrderDetailResponse: Decodable, Equatable, Hashable {
|
||||
let remark: String
|
||||
let multiTravel: StoreOrderMultiTravelInfo?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
case orderType = "order_type"
|
||||
@ -362,6 +371,7 @@ struct StoreOrderMultiTravelInfo: Decodable, Equatable, Hashable {
|
||||
let projectInfo: StoreOrderProjectInfo?
|
||||
let shootingList: [StoreOrderShootingListItem]
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case projectInfo = "project_info"
|
||||
case shootingList = "shooting_list"
|
||||
@ -382,6 +392,7 @@ struct StoreOrderProjectInfo: Decodable, Equatable, Hashable {
|
||||
let singleSpotPhotoNum: Int
|
||||
let singleSpotVideoNum: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case settleSpotNum = "settle_spot_num"
|
||||
case singleSpotMaterialNum = "single_spot_material_num"
|
||||
@ -412,6 +423,7 @@ struct StoreOrderShootingListItem: Decodable, Identifiable, Equatable, Hashable
|
||||
let startAvg: Double
|
||||
let start: Double
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
case photogUid = "photog_uid"
|
||||
@ -447,6 +459,7 @@ struct StoreOrderShootingDetailResponse: Decodable, Equatable, Hashable {
|
||||
let materialList: [OrderMediaFile]
|
||||
let completeList: [OrderMediaFile]
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
case scenicSpotName = "scenic_spot_name"
|
||||
@ -480,6 +493,7 @@ struct StoreOrderComment: Decodable, Equatable, Hashable {
|
||||
let content: String
|
||||
let createdAt: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case starShooting = "star_shooting"
|
||||
case starRetouching = "star_retouching"
|
||||
@ -522,6 +536,7 @@ struct OrderMediaFile: Decodable, Identifiable, Equatable, Hashable {
|
||||
coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
|
||||
}
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case fileName = "file_name"
|
||||
case fileUrl = "file_url"
|
||||
@ -550,6 +565,7 @@ struct MultiTravelShootHistoryResponse: Decodable, Equatable, Hashable {
|
||||
let projectTypeName: String
|
||||
let photogSpotList: [PhotogSpotItem]
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case projectName = "project_name"
|
||||
case projectType = "project_type"
|
||||
@ -583,6 +599,7 @@ struct PhotogSpotItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
return nickname.isEmpty ? photogName : nickname
|
||||
}
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
case photogUid = "photog_uid"
|
||||
@ -609,6 +626,7 @@ struct MultiTravelVerifiedScenicSpotItem: Decodable, Identifiable, Equatable, Ha
|
||||
let id: Int
|
||||
let name: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
@ -629,6 +647,7 @@ struct MultiTravelUploadMaterialRequest: Encodable, Equatable {
|
||||
let cloudFile: [MultiTravelCloudFileItem]
|
||||
let uploadFile: [MultiTravelUploadFileItem]
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case orderNumber = "order_number"
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
@ -641,6 +660,7 @@ struct MultiTravelUploadMaterialRequest: Encodable, Equatable {
|
||||
struct MultiTravelCloudFileItem: Encodable, Equatable {
|
||||
let fileId: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case fileId = "file_id"
|
||||
}
|
||||
@ -651,6 +671,7 @@ struct MultiTravelUploadFileItem: Encodable, Equatable {
|
||||
let fileName: String
|
||||
let fileUrl: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case fileName = "file_name"
|
||||
case fileUrl = "file_url"
|
||||
|
||||
@ -6,32 +6,127 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - Deposit List Diffable 标识
|
||||
|
||||
private typealias DepositListSection = Int
|
||||
private typealias DepositListItem = String
|
||||
|
||||
/// 押金列表空态 / 加载态标识。
|
||||
private enum DepositListItemID {
|
||||
static let placeholder = "depositList:placeholder"
|
||||
}
|
||||
|
||||
// MARK: - Deposit Detail Diffable 标识
|
||||
|
||||
private typealias DepositDetailSection = Int
|
||||
private typealias DepositDetailRow = String
|
||||
|
||||
/// 押金详情行标识。
|
||||
private enum DepositDetailRowID {
|
||||
static let placeholder = "depositDetail:placeholder"
|
||||
static func field(_ index: Int) -> String { "depositDetail:field:\(index)" }
|
||||
}
|
||||
|
||||
// MARK: - Deposit Shooting Diffable 标识
|
||||
|
||||
private typealias DepositShootingSection = Int
|
||||
private typealias DepositShootingRow = String
|
||||
|
||||
/// 押金拍摄信息 section 索引。
|
||||
private enum DepositShootingSectionID {
|
||||
static let summary = 0
|
||||
static let material = 1
|
||||
static let complete = 2
|
||||
}
|
||||
|
||||
/// 押金拍摄信息行标识。
|
||||
private enum DepositShootingRowID {
|
||||
static let placeholder = "depositShooting:placeholder"
|
||||
static let scenicSpot = "depositShooting:scenicSpot"
|
||||
static let rating = "depositShooting:rating"
|
||||
static func material(_ index: Int) -> String { "depositShooting:material:\(index)" }
|
||||
static func complete(_ index: Int) -> String { "depositShooting:complete:\(index)" }
|
||||
}
|
||||
|
||||
/// 押金订单列表页,支持查询、分页、核销和退款。
|
||||
final class DepositOrderListViewController: UIViewController {
|
||||
final class DepositOrderListViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let viewModel = DepositOrderListViewModel()
|
||||
private var orderNumberField = UITextField()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(DepositOrderCell.self, forCellReuseIdentifier: DepositOrderCell.reuseID)
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Diffable 数据源,驱动押金订单列表与分页刷新。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<DepositListSection, DepositListItem>!
|
||||
|
||||
/// 视图加载完成后的 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) }
|
||||
setupHeader()
|
||||
Task { await reload(showLoading: true) }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<DepositListSection, DepositListItem>(
|
||||
tableView: tableView
|
||||
) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: DepositListItem) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
if item == DepositListItemID.placeholder {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = self.viewModel.loading ? "加载中..." : "暂无押金订单"
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
guard let orderItem = self.viewModel.orders.first(where: { $0.orderNumber == item }) else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCell.reuseID, for: indexPath) as! DepositOrderCell
|
||||
cell.configure(item: orderItem, isOperating: self.viewModel.operatingOrderNumber == orderItem.orderNumber)
|
||||
cell.onDetail = { [weak self] in
|
||||
self.flatMap { HomeMenuRouting.pushOrders(.depositDetail(orderNumber: orderItem.orderNumber), from: $0) }
|
||||
}
|
||||
cell.onWriteOff = { [weak self] in self?.writeOff(orderItem) }
|
||||
cell.onRefund = { [weak self] in self?.refund(orderItem) }
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
/// 构建 Diffable snapshot。
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<DepositListSection, DepositListItem> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<DepositListSection, DepositListItem>()
|
||||
snapshot.appendSections([0])
|
||||
if viewModel.orders.isEmpty {
|
||||
snapshot.appendItems([DepositListItemID.placeholder], toSection: 0)
|
||||
} else {
|
||||
snapshot.appendItems(viewModel.orders.map(\.orderNumber), toSection: 0)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 初始化Header相关 UI 或状态。
|
||||
private func setupHeader() {
|
||||
let header = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 120))
|
||||
orderNumberField.placeholder = "请输入押金订单号"
|
||||
@ -55,12 +150,14 @@ final class DepositOrderListViewController: UIViewController {
|
||||
tableView.tableHeaderView = header
|
||||
}
|
||||
|
||||
/// open详情相关逻辑。
|
||||
@objc private func openDetail() {
|
||||
let text = orderNumberField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !text.isEmpty else { return }
|
||||
HomeMenuRouting.pushOrders(.depositDetail(orderNumber: text), from: self)
|
||||
}
|
||||
|
||||
/// 刷新。
|
||||
private func reload(showLoading: Bool) async {
|
||||
if showLoading {
|
||||
await appServices.globalLoading.withLoading {
|
||||
@ -72,6 +169,7 @@ final class DepositOrderListViewController: UIViewController {
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
|
||||
/// writeOff相关逻辑。
|
||||
private func writeOff(_ item: DepositOrderListItem) {
|
||||
let alert = UIAlertController(title: "确认核销该押金订单?", message: item.orderNumber, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
@ -86,6 +184,7 @@ final class DepositOrderListViewController: UIViewController {
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// refund 业务逻辑。
|
||||
private func refund(_ item: DepositOrderListItem) {
|
||||
let alert = UIAlertController(title: "申请退款", message: "请填写退款原因", preferredStyle: .alert)
|
||||
alert.addTextField { $0.placeholder = "退款原因" }
|
||||
@ -101,43 +200,25 @@ final class DepositOrderListViewController: UIViewController {
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderListViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
max(viewModel.orders.count, 1)
|
||||
}
|
||||
|
||||
/// UITableView 代理:section 标题。
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
"押金订单列表 \(viewModel.orders.count)/\(viewModel.total)"
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard !viewModel.orders.isEmpty else {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = viewModel.loading ? "加载中..." : "暂无押金订单"
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCell.reuseID, for: indexPath) as! DepositOrderCell
|
||||
let item = viewModel.orders[indexPath.row]
|
||||
cell.configure(item: item, isOperating: viewModel.operatingOrderNumber == item.orderNumber)
|
||||
cell.onDetail = { [weak self] in
|
||||
self.flatMap { HomeMenuRouting.pushOrders(.depositDetail(orderNumber: item.orderNumber), from: $0) }
|
||||
}
|
||||
cell.onWriteOff = { [weak self] in self?.writeOff(item) }
|
||||
cell.onRefund = { [weak self] in self?.refund(item) }
|
||||
return cell
|
||||
}
|
||||
|
||||
/// UITableView 代理:分页加载。
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard indexPath.row == viewModel.orders.count - 1, viewModel.hasMore else { return }
|
||||
guard let item = tableDataSource.itemIdentifier(for: indexPath),
|
||||
item != DepositListItemID.placeholder,
|
||||
item == viewModel.orders.last?.orderNumber,
|
||||
viewModel.hasMore else { return }
|
||||
Task {
|
||||
await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DepositOrder列表或网格 Cell,负责单项内容展示。
|
||||
private final class DepositOrderCell: UITableViewCell {
|
||||
static let reuseID = "DepositOrderCell"
|
||||
var onDetail: (() -> Void)?
|
||||
@ -148,6 +229,7 @@ private final class DepositOrderCell: UITableViewCell {
|
||||
private let statusLabel = UILabel()
|
||||
private let amountLabel = UILabel()
|
||||
|
||||
/// 初始化实例。
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
@ -182,14 +264,18 @@ private final class DepositOrderCell: UITableViewCell {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(item: DepositOrderListItem, isOperating: Bool) {
|
||||
titleLabel.text = item.orderNumber
|
||||
statusLabel.text = isOperating ? "处理中" : item.statusName
|
||||
amountLabel.text = "¥\(item.amount.isEmpty ? "0.00" : item.amount)"
|
||||
}
|
||||
|
||||
/// 点击detail的处理逻辑。
|
||||
@objc private func detailTapped() { onDetail?() }
|
||||
/// 点击writeOff的处理逻辑。
|
||||
@objc private func writeOffTapped() { onWriteOff?() }
|
||||
/// 点击refund的处理逻辑。
|
||||
@objc private func refundTapped() { onRefund?() }
|
||||
}
|
||||
|
||||
@ -201,11 +287,14 @@ final class DepositOrderDetailViewController: UIViewController {
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Diffable 数据源,驱动详情字段展示。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<DepositDetailSection, DepositDetailRow>!
|
||||
|
||||
/// 初始化实例。
|
||||
init(orderNumber: String) {
|
||||
self.orderNumber = orderNumber
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -214,14 +303,66 @@ final class DepositOrderDetailViewController: UIViewController {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "押金订单详情"
|
||||
configureTableDataSource()
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
Task { await loadDetail() }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<DepositDetailSection, DepositDetailRow>(
|
||||
tableView: tableView
|
||||
) { [weak self] (_: UITableView, _: IndexPath, row: DepositDetailRow) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
guard let detail = self.viewModel.detail else {
|
||||
cell.textLabel?.text = self.viewModel.errorMessage ?? (self.viewModel.loading ? "加载中..." : "暂无详情")
|
||||
return cell
|
||||
}
|
||||
let rows: [(String, String)] = [
|
||||
("订单号", detail.orderNumber),
|
||||
("状态", detail.orderStatusName),
|
||||
("类型", detail.orderTypeLabel),
|
||||
("付款金额", detail.actualPayAmount),
|
||||
("手机号", detail.phone),
|
||||
("项目", detail.projectName),
|
||||
("创建时间", detail.createdAt),
|
||||
("备注", detail.remark)
|
||||
]
|
||||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||||
if index < rows.count {
|
||||
cell.textLabel?.text = rows[index].0
|
||||
cell.detailTextLabel?.text = rows[index].1
|
||||
}
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
/// 构建 Diffable snapshot。
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<DepositDetailSection, DepositDetailRow> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<DepositDetailSection, DepositDetailRow>()
|
||||
snapshot.appendSections([0])
|
||||
if viewModel.detail == nil {
|
||||
snapshot.appendItems([DepositDetailRowID.placeholder], toSection: 0)
|
||||
} else {
|
||||
snapshot.appendItems((0..<8).map { DepositDetailRowID.field($0) }, toSection: 0)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/// 应用 snapshot 刷新列表。
|
||||
private func applyTableSnapshot(animated: Bool = true) {
|
||||
tableDataSource.apply(buildTableSnapshot(), animatingDifferences: animated)
|
||||
}
|
||||
|
||||
/// 加载Detail数据。
|
||||
private func loadDetail() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(
|
||||
@ -230,41 +371,13 @@ final class DepositOrderDetailViewController: UIViewController {
|
||||
orderNumber: orderNumber
|
||||
)
|
||||
}
|
||||
tableView.reloadData()
|
||||
applyTableSnapshot()
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderDetailViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.detail == nil ? 1 : 8
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
guard let detail = viewModel.detail else {
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? (viewModel.loading ? "加载中..." : "暂无详情")
|
||||
return cell
|
||||
}
|
||||
let rows: [(String, String)] = [
|
||||
("订单号", detail.orderNumber),
|
||||
("状态", detail.orderStatusName),
|
||||
("类型", detail.orderTypeLabel),
|
||||
("付款金额", detail.actualPayAmount),
|
||||
("手机号", detail.phone),
|
||||
("项目", detail.projectName),
|
||||
("创建时间", detail.createdAt),
|
||||
("备注", detail.remark)
|
||||
]
|
||||
cell.textLabel?.text = rows[indexPath.row].0
|
||||
cell.detailTextLabel?.text = rows[indexPath.row].1
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
/// 押金拍摄信息页。
|
||||
final class DepositOrderShootingInfoViewController: UIViewController {
|
||||
final class DepositOrderShootingInfoViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let orderNumber: String
|
||||
private let scenicSpotId: Int
|
||||
@ -273,10 +386,14 @@ final class DepositOrderShootingInfoViewController: UIViewController {
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Diffable 数据源,驱动拍摄摘要与素材列表刷新。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<DepositShootingSection, DepositShootingRow>!
|
||||
|
||||
/// 初始化实例。
|
||||
init(orderNumber: String, scenicSpotId: Int, photogUid: Int) {
|
||||
self.orderNumber = orderNumber
|
||||
self.scenicSpotId = scenicSpotId
|
||||
@ -287,14 +404,75 @@ final class DepositOrderShootingInfoViewController: UIViewController {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "押金拍摄信息"
|
||||
configureTableDataSource()
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
Task { await loadDetail() }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<DepositShootingSection, DepositShootingRow>(
|
||||
tableView: tableView
|
||||
) { [weak self] (_: UITableView, indexPath: IndexPath, row: DepositShootingRow) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
guard let detail = self.viewModel.detail else {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = self.viewModel.errorMessage ?? "加载中..."
|
||||
return cell
|
||||
}
|
||||
if row == DepositShootingRowID.scenicSpot || row == DepositShootingRowID.rating {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
if row == DepositShootingRowID.scenicSpot {
|
||||
cell.textLabel?.text = "打卡点"
|
||||
cell.detailTextLabel?.text = detail.scenicSpotName
|
||||
} else {
|
||||
cell.textLabel?.text = "拍摄评分"
|
||||
cell.detailTextLabel?.text = detail.orderComment.map { "\($0.starShooting) 星" } ?? "--"
|
||||
}
|
||||
return cell
|
||||
}
|
||||
let files: [OrderMediaFile]
|
||||
if row.hasPrefix("depositShooting:material:") {
|
||||
files = detail.materialList
|
||||
} else {
|
||||
files = detail.completeList
|
||||
}
|
||||
let index = Int(row.split(separator: ":").last ?? "") ?? 0
|
||||
let media = files[index]
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
cell.textLabel?.text = media.fileName
|
||||
cell.detailTextLabel?.text = media.fileUrl
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
/// 构建 Diffable snapshot。
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<DepositShootingSection, DepositShootingRow> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<DepositShootingSection, DepositShootingRow>()
|
||||
guard let detail = viewModel.detail else {
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems([DepositShootingRowID.placeholder], toSection: 0)
|
||||
return snapshot
|
||||
}
|
||||
snapshot.appendSections([DepositShootingSectionID.summary, DepositShootingSectionID.material, DepositShootingSectionID.complete])
|
||||
snapshot.appendItems([DepositShootingRowID.scenicSpot, DepositShootingRowID.rating], toSection: DepositShootingSectionID.summary)
|
||||
snapshot.appendItems(detail.materialList.indices.map { DepositShootingRowID.material($0) }, toSection: DepositShootingSectionID.material)
|
||||
snapshot.appendItems(detail.completeList.indices.map { DepositShootingRowID.complete($0) }, toSection: DepositShootingSectionID.complete)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/// 应用 snapshot 刷新列表。
|
||||
private func applyTableSnapshot(animated: Bool = true) {
|
||||
tableDataSource.apply(buildTableSnapshot(), animatingDifferences: animated)
|
||||
}
|
||||
|
||||
/// 加载Detail数据。
|
||||
private func loadDetail() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(
|
||||
@ -305,56 +483,25 @@ final class DepositOrderShootingInfoViewController: UIViewController {
|
||||
photogUid: photogUid
|
||||
)
|
||||
}
|
||||
tableView.reloadData()
|
||||
applyTableSnapshot()
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension DepositOrderShootingInfoViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
viewModel.detail == nil ? 1 : 3
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard let detail = viewModel.detail else { return 1 }
|
||||
switch section {
|
||||
case 0: return 2
|
||||
case 1: return detail.materialList.count
|
||||
default: return detail.completeList.count
|
||||
}
|
||||
}
|
||||
|
||||
/// UITableView 代理:section 标题。
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
guard viewModel.detail != nil else { return nil }
|
||||
switch section {
|
||||
case 1: return "素材"
|
||||
case 2: return "成片"
|
||||
guard viewModel.detail != nil,
|
||||
let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
|
||||
switch sectionID {
|
||||
case DepositShootingSectionID.material: return "素材"
|
||||
case DepositShootingSectionID.complete: return "成片"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let detail = viewModel.detail else {
|
||||
let cell = UITableViewCell()
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? "加载中..."
|
||||
return cell
|
||||
}
|
||||
if indexPath.section == 0 {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
if indexPath.row == 0 {
|
||||
cell.textLabel?.text = "打卡点"
|
||||
cell.detailTextLabel?.text = detail.scenicSpotName
|
||||
} else {
|
||||
cell.textLabel?.text = "拍摄评分"
|
||||
cell.detailTextLabel?.text = detail.orderComment.map { "\($0.starShooting) 星" } ?? "--"
|
||||
}
|
||||
return cell
|
||||
}
|
||||
let files = indexPath.section == 1 ? detail.materialList : detail.completeList
|
||||
let media = files[indexPath.row]
|
||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||
cell.textLabel?.text = media.fileName
|
||||
cell.detailTextLabel?.text = media.fileUrl
|
||||
return cell
|
||||
/// 安全下标,避免 section 越界。
|
||||
private extension Array {
|
||||
subscript(safe index: Int) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ final class OrderCodeScannerViewController: UIViewController {
|
||||
|
||||
private let scannerController = OrderScannerCaptureViewController()
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
@ -49,6 +50,7 @@ final class OrderCodeScannerViewController: UIViewController {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// embed扫码相关逻辑。
|
||||
private func embedScanner() {
|
||||
scannerController.onScanResult = { [weak self] result in
|
||||
self?.onScanResult?(result)
|
||||
@ -77,6 +79,7 @@ final class OrderCodeScannerViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 展示UnavailablePlaceholder。
|
||||
private func showUnavailablePlaceholder() {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
@ -109,6 +112,7 @@ final class OrderCodeScannerViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击close的处理逻辑。
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
@ -145,6 +149,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
return button
|
||||
}()
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
@ -152,17 +157,20 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
checkPermissionAndStart()
|
||||
}
|
||||
|
||||
/// 子视图布局完成后调整依赖 frame 的 UI。
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
previewLayer?.frame = view.layer.bounds
|
||||
}
|
||||
|
||||
/// 视图即将消失,保存或清理临时状态。
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
updateTorch(enabled: false)
|
||||
stopSession()
|
||||
}
|
||||
|
||||
/// check权限并启动相关逻辑。
|
||||
private func checkPermissionAndStart() {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
@ -185,6 +193,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动Session流程。
|
||||
private func startSession() {
|
||||
hasFinishedScan = false
|
||||
guard !session.isRunning else { return }
|
||||
@ -199,11 +208,13 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止Session流程。
|
||||
private func stopSession() {
|
||||
guard session.isRunning else { return }
|
||||
session.stopRunning()
|
||||
}
|
||||
|
||||
/// 创建ScannerButton实例。
|
||||
private static func makeScannerButton(title: String) -> UIButton {
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.title = title
|
||||
@ -213,6 +224,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
return UIButton(configuration: config)
|
||||
}
|
||||
|
||||
/// 配置Session展示内容。
|
||||
private func configureSession() throws {
|
||||
if previewLayer != nil { return }
|
||||
guard let videoDevice = AVCaptureDevice.default(for: .video) else {
|
||||
@ -242,6 +254,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
torchButton.isEnabled = videoDevice.hasTorch
|
||||
}
|
||||
|
||||
/// metadataOutput相关逻辑。
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
||||
guard !hasFinishedScan else { return }
|
||||
guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
||||
@ -253,6 +266,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
onScanResult?(.success(code))
|
||||
}
|
||||
|
||||
/// 初始化Controls相关 UI 或状态。
|
||||
private func setupControls() {
|
||||
view.addSubview(scanFrameView)
|
||||
view.addSubview(torchButton)
|
||||
@ -273,10 +287,12 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
}
|
||||
}
|
||||
|
||||
/// toggleTorch相关逻辑。
|
||||
@objc private func toggleTorch() {
|
||||
updateTorch(enabled: !(videoDevice?.isTorchActive ?? false))
|
||||
}
|
||||
|
||||
/// 更新Torch状态。
|
||||
private func updateTorch(enabled: Bool) {
|
||||
guard let device = videoDevice, device.hasTorch else { return }
|
||||
do {
|
||||
@ -293,6 +309,7 @@ final class OrderScannerCaptureViewController: UIViewController, AVCaptureMetada
|
||||
}
|
||||
}
|
||||
|
||||
/// restart扫码相关逻辑。
|
||||
@objc private func restartScan() {
|
||||
hasFinishedScan = false
|
||||
updateTorch(enabled: false)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/// 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: 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
private let localListLabel = UILabel()
|
||||
private let submitButton = UIButton(type: .system)
|
||||
|
||||
/// 初始化实例。
|
||||
init(initialOrderNumber: String) {
|
||||
self.viewModel = MultiTravelTaskUploadViewModel(initialOrderNumber: initialOrderNumber)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -27,6 +28,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "任务上传"
|
||||
@ -36,12 +38,14 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||||
}
|
||||
|
||||
/// 绑定ViewModel回调或数据。
|
||||
private func bindViewModel() {
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.applyViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化Form相关 UI 或状态。
|
||||
private func setupForm() {
|
||||
orderField.borderStyle = .roundedRect
|
||||
orderField.placeholder = "关联订单号"
|
||||
@ -90,6 +94,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
/// labeled行相关逻辑。
|
||||
private func labeledRow(_ title: String, _ content: UIView) -> UIStackView {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
@ -100,6 +105,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
return row
|
||||
}
|
||||
|
||||
/// apply视图模型相关逻辑。
|
||||
private func applyViewModel() {
|
||||
orderField.text = viewModel.orderNumber
|
||||
spotButton.setTitle(viewModel.isLoadingSpots ? "加载中..." : viewModel.selectedSpotName, for: .normal)
|
||||
@ -123,14 +129,17 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
submitButton.configuration?.title = viewModel.isSubmitting ? "保存中..." : "保存任务素材"
|
||||
}
|
||||
|
||||
/// order变更相关逻辑。
|
||||
@objc private func orderChanged() {
|
||||
viewModel.orderNumber = orderField.text ?? ""
|
||||
}
|
||||
|
||||
/// 刷新Spots展示。
|
||||
@objc private func refreshSpots() {
|
||||
Task { await viewModel.loadSpots(api: appServices.ordersAPI) }
|
||||
}
|
||||
|
||||
/// select打卡点相关逻辑。
|
||||
@objc private func selectSpot() {
|
||||
guard !viewModel.spots.isEmpty else { return }
|
||||
let sheet = UIAlertController(title: "选择打卡点", message: nil, preferredStyle: .actionSheet)
|
||||
@ -144,6 +153,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
/// pick本地Files相关逻辑。
|
||||
@objc private func pickLocalFiles() {
|
||||
var config = PHPickerConfiguration()
|
||||
config.selectionLimit = 9
|
||||
@ -153,6 +163,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
present(picker, animated: true)
|
||||
}
|
||||
|
||||
/// 提交Tapped。
|
||||
@objc private func submitTapped() {
|
||||
Task {
|
||||
let success = await viewModel.submit(
|
||||
@ -172,6 +183,7 @@ final class MultiTravelTaskUploadViewController: UIViewController {
|
||||
}
|
||||
|
||||
extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
|
||||
/// picker 业务逻辑。
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
picker.dismiss(animated: true)
|
||||
guard !results.isEmpty else { return }
|
||||
@ -199,19 +211,41 @@ extension MultiTravelTaskUploadViewController: PHPickerViewControllerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Historical Shooting Diffable 标识
|
||||
|
||||
private typealias HistoricalShootingSection = Int
|
||||
private typealias HistoricalShootingRow = String
|
||||
|
||||
/// 历史拍摄 section 索引。
|
||||
private enum HistoricalShootingSectionID {
|
||||
static let summary = 0
|
||||
static let spots = 1
|
||||
}
|
||||
|
||||
/// 历史拍摄行标识。
|
||||
private enum HistoricalShootingRowID {
|
||||
static let project = "historicalShooting:project"
|
||||
static let projectType = "historicalShooting:projectType"
|
||||
static let empty = "historicalShooting:empty"
|
||||
static func spot(_ id: String) -> String { "historicalShooting:spot:\(id)" }
|
||||
}
|
||||
|
||||
/// 历史拍摄信息页。
|
||||
final class HistoricalShootingInfoViewController: UIViewController {
|
||||
final class HistoricalShootingInfoViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let orderNumber: String
|
||||
private let viewModel = HistoricalShootingInfoViewModel()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .insetGrouped)
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
return table
|
||||
}()
|
||||
|
||||
/// Diffable 数据源,驱动历史拍摄摘要与点位列表刷新。
|
||||
private var tableDataSource: UITableViewDiffableDataSource<HistoricalShootingSection, HistoricalShootingRow>!
|
||||
|
||||
/// 初始化实例。
|
||||
init(orderNumber: String) {
|
||||
self.orderNumber = orderNumber
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -220,49 +254,89 @@ final class HistoricalShootingInfoViewController: UIViewController {
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "历史拍摄"
|
||||
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 loadData() }
|
||||
}
|
||||
|
||||
/// 配置 Diffable 数据源。
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<HistoricalShootingSection, HistoricalShootingRow>(
|
||||
tableView: tableView
|
||||
) { [weak self] (_: UITableView, _: IndexPath, row: HistoricalShootingRow) -> UITableViewCell? in
|
||||
guard let self else { return UITableViewCell() }
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
switch row {
|
||||
case HistoricalShootingRowID.project:
|
||||
cell.textLabel?.text = "项目"
|
||||
cell.detailTextLabel?.text = self.viewModel.projectName
|
||||
case HistoricalShootingRowID.projectType:
|
||||
cell.textLabel?.text = "项目类型"
|
||||
cell.detailTextLabel?.text = self.viewModel.projectTypeName
|
||||
case HistoricalShootingRowID.empty:
|
||||
cell.textLabel?.text = self.viewModel.errorMessage ?? "暂无历史拍摄"
|
||||
default:
|
||||
if row.hasPrefix("historicalShooting:spot:") {
|
||||
let id = row.replacingOccurrences(of: "historicalShooting:spot:", with: "")
|
||||
if let spot = self.viewModel.spots.first(where: { $0.id == id }) {
|
||||
cell.textLabel?.text = spot.scenicSpotName
|
||||
cell.detailTextLabel?.text = "\(spot.files.count) 个文件 · \(spot.photographerDisplayName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
/// 构建 Diffable snapshot。
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<HistoricalShootingSection, HistoricalShootingRow> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<HistoricalShootingSection, HistoricalShootingRow>()
|
||||
snapshot.appendSections([HistoricalShootingSectionID.summary, HistoricalShootingSectionID.spots])
|
||||
snapshot.appendItems([HistoricalShootingRowID.project, HistoricalShootingRowID.projectType], toSection: HistoricalShootingSectionID.summary)
|
||||
if viewModel.spots.isEmpty {
|
||||
snapshot.appendItems([HistoricalShootingRowID.empty], toSection: HistoricalShootingSectionID.spots)
|
||||
} else {
|
||||
snapshot.appendItems(viewModel.spots.map { HistoricalShootingRowID.spot($0.id) }, toSection: HistoricalShootingSectionID.spots)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 加载Data数据。
|
||||
private func loadData() async {
|
||||
await appServices.globalLoading.withLoading {
|
||||
await viewModel.load(api: appServices.ordersAPI, orderNumber: orderNumber)
|
||||
}
|
||||
applyTableSnapshot()
|
||||
if let message = viewModel.errorMessage { showToast(message) }
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoricalShootingInfoViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
section == 0 ? 2 : max(viewModel.spots.count, 1)
|
||||
}
|
||||
|
||||
/// UITableView 代理:section 标题。
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 1 ? "拍摄点位" : nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
|
||||
cell.selectionStyle = .none
|
||||
if indexPath.section == 0 {
|
||||
cell.textLabel?.text = indexPath.row == 0 ? "项目" : "项目类型"
|
||||
cell.detailTextLabel?.text = indexPath.row == 0 ? viewModel.projectName : viewModel.projectTypeName
|
||||
return cell
|
||||
}
|
||||
guard !viewModel.spots.isEmpty else {
|
||||
cell.textLabel?.text = viewModel.errorMessage ?? "暂无历史拍摄"
|
||||
return cell
|
||||
}
|
||||
let spot = viewModel.spots[indexPath.row]
|
||||
cell.textLabel?.text = spot.scenicSpotName
|
||||
cell.detailTextLabel?.text = "\(spot.files.count) 个文件 · \(spot.photographerDisplayName)"
|
||||
return cell
|
||||
guard let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
|
||||
return sectionID == HistoricalShootingSectionID.spots ? "拍摄点位" : nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全下标,避免 section 越界。
|
||||
private extension Array {
|
||||
subscript(safe index: Int) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,6 +120,7 @@ final class DepositOrderListViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 重置State状态。
|
||||
private func resetState() {
|
||||
orders = []
|
||||
total = 0
|
||||
@ -170,6 +171,7 @@ final class DepositOrderDetailViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset(message: String) {
|
||||
detail = nil
|
||||
loading = false
|
||||
@ -220,6 +222,7 @@ final class DepositOrderShootingInfoViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset(message: String) {
|
||||
detail = nil
|
||||
loading = false
|
||||
@ -324,10 +327,12 @@ final class OrderRefundViewModel {
|
||||
return moneyText(value)
|
||||
}
|
||||
|
||||
/// decimal值相关逻辑。
|
||||
private static func decimalValue(_ text: String) -> Decimal {
|
||||
Decimal(string: text.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ",", with: "")) ?? 0
|
||||
}
|
||||
|
||||
/// money文本相关逻辑。
|
||||
private static func moneyText(_ value: Decimal) -> String {
|
||||
let number = NSDecimalNumber(decimal: value)
|
||||
let handler = NSDecimalNumberHandler(
|
||||
@ -379,6 +384,7 @@ final class HistoricalShootingInfoViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset(message: String) {
|
||||
projectName = ""
|
||||
projectTypeName = ""
|
||||
@ -520,6 +526,7 @@ final class MultiTravelTaskUploadViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 校验BeforeSubmit输入或状态。
|
||||
private func validateBeforeSubmit(scenicId: Int?) -> SubmitContext? {
|
||||
let normalizedOrderNumber = orderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let scenicId, scenicId > 0 else {
|
||||
@ -549,6 +556,7 @@ final class MultiTravelTaskUploadViewModel {
|
||||
return SubmitContext(scenicId: scenicId, orderNumber: normalizedOrderNumber, scenicSpotId: selectedSpotId)
|
||||
}
|
||||
|
||||
/// upload本地Files相关逻辑。
|
||||
private func uploadLocalFiles(uploadService: any OSSUploadServing, scenicId: Int) async throws -> [MultiTravelUploadFileItem] {
|
||||
var uploadFiles: [MultiTravelUploadFileItem] = []
|
||||
for index in selectedLocalFiles.indices {
|
||||
@ -583,11 +591,13 @@ final class MultiTravelTaskUploadViewModel {
|
||||
return uploadFiles
|
||||
}
|
||||
|
||||
/// 更新LocalFileProgress状态。
|
||||
private func updateLocalFileProgress(id: UUID, progress: Int) {
|
||||
guard let index = selectedLocalFiles.firstIndex(where: { $0.id == id }) else { return }
|
||||
selectedLocalFiles[index].progress = progress
|
||||
}
|
||||
|
||||
/// 重置SpotSelection状态。
|
||||
private func resetSpotSelection() {
|
||||
spots = []
|
||||
selectedSpotId = nil
|
||||
@ -595,11 +605,13 @@ final class MultiTravelTaskUploadViewModel {
|
||||
isLoadingSpots = false
|
||||
}
|
||||
|
||||
/// fileType相关逻辑。
|
||||
private static func fileType(for fileName: String) -> Int {
|
||||
let ext = URL(fileURLWithPath: fileName).pathExtension.lowercased()
|
||||
return ["mp4", "mov", "m4v", "avi"].contains(ext) ? 1 : 2
|
||||
}
|
||||
|
||||
/// Submit上下文,持有跨页面共享状态。
|
||||
private struct SubmitContext {
|
||||
let scenicId: Int
|
||||
let orderNumber: String
|
||||
|
||||
Reference in New Issue
Block a user