// // DepositOrderViewControllers.swift // suixinkan // 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, UITableViewDelegate { private let viewModel = DepositOrderListViewModel() private var orderNumberField = UITextField() private lazy var tableView: UITableView = { let table = UITableView(frame: .zero, style: .insetGrouped) table.delegate = self table.register(DepositOrderCell.self, forCellReuseIdentifier: DepositOrderCell.reuseID) return table }() /// Diffable 数据源,驱动押金订单列表与分页刷新。 private var tableDataSource: UITableViewDiffableDataSource! /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = "押金订单" view.backgroundColor = AppDesignUIKit.pageBackground configureTableDataSource() view.addSubview(tableView) tableView.snp.makeConstraints { make in make.edges.equalToSuperview() } viewModel.onChange = { [weak self] in self?.applyTableSnapshot(reconfigure: true) } setupHeader() Task { await reload(showLoading: true) } } /// 配置 Diffable 数据源。 private func configureTableDataSource() { tableDataSource = UITableViewDiffableDataSource( 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 { $0.appServices.appNavigator.push(.orders(.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 { var snapshot = NSDiffableDataSourceSnapshot() 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 = "请输入押金订单号" orderNumberField.borderStyle = .roundedRect orderNumberField.autocorrectionType = .no orderNumberField.autocapitalizationType = .none let detailButton = makePrimaryButton(title: "查看订单详情") detailButton.addTarget(self, action: #selector(openDetail), for: .touchUpInside) header.addSubview(orderNumberField) header.addSubview(detailButton) orderNumberField.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(44) } detailButton.snp.makeConstraints { make in make.top.equalTo(orderNumberField.snp.bottom).offset(12) make.leading.trailing.equalToSuperview().inset(16) } tableView.tableHeaderView = header } /// open详情相关逻辑。 @objc private func openDetail() { let text = orderNumberField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !text.isEmpty else { return } appServices.appNavigator.push(.orders(.depositDetail(orderNumber: text)), from: self) } /// 刷新。 private func reload(showLoading: Bool) async { if showLoading { await appServices.globalLoading.withLoading { await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: true) } } else { await viewModel.reload(api: appServices.ordersAPI, scenicId: appServices.accountContext.currentScenic?.id, reset: true) } 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)) alert.addAction(UIAlertAction(title: "确认核销", style: .default) { [weak self] _ in Task { guard let self else { return } let success = await self.viewModel.writeOff(api: self.appServices.ordersAPI, scenicId: self.appServices.accountContext.currentScenic?.id, orderNumber: item.orderNumber) if success { self.showToast("核销成功") } else if let message = self.viewModel.errorMessage { self.showToast(message) } } }) present(alert, animated: true) } /// refund 业务逻辑。 private func refund(_ item: DepositOrderListItem) { let alert = UIAlertController(title: "申请退款", message: "请填写退款原因", preferredStyle: .alert) alert.addTextField { $0.placeholder = "退款原因" } alert.addAction(UIAlertAction(title: "取消", style: .cancel)) alert.addAction(UIAlertAction(title: "提交", style: .default) { [weak self] _ in Task { guard let self else { return } let reason = alert.textFields?.first?.text ?? "" let success = await self.viewModel.refund(api: self.appServices.ordersAPI, scenicId: self.appServices.accountContext.currentScenic?.id, orderNumber: item.orderNumber, reason: reason) if success { self.showToast("退款申请已提交") } else if let message = self.viewModel.errorMessage { self.showToast(message) } } }) present(alert, animated: true) } /// UITableView 代理:section 标题。 func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { "押金订单列表 \(viewModel.orders.count)/\(viewModel.total)" } /// UITableView 代理:分页加载。 func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 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)? var onWriteOff: (() -> Void)? var onRefund: (() -> Void)? private let titleLabel = UILabel() private let statusLabel = UILabel() private let amountLabel = UILabel() /// 初始化实例。 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold) statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption) amountLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .semibold) amountLabel.textColor = AppDesignUIKit.primary let detailButton = UIButton(type: .system) detailButton.setTitle("详情", for: .normal) detailButton.addTarget(self, action: #selector(detailTapped), for: .touchUpInside) let writeOffButton = UIButton(type: .system) writeOffButton.setTitle("核销", for: .normal) writeOffButton.addTarget(self, action: #selector(writeOffTapped), for: .touchUpInside) let refundButton = UIButton(type: .system) refundButton.setTitle("退款", for: .normal) refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside) let buttonRow = UIStackView(arrangedSubviews: [detailButton, writeOffButton, refundButton]) buttonRow.axis = .horizontal buttonRow.distribution = .fillEqually let stack = UIStackView(arrangedSubviews: [titleLabel, amountLabel, buttonRow]) stack.axis = .vertical stack.spacing = 8 contentView.addSubview(stack) contentView.addSubview(statusLabel) stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(12) } statusLabel.snp.makeConstraints { make in make.trailing.top.equalToSuperview().inset(12) } } @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?() } } /// 押金订单详情页。 final class DepositOrderDetailViewController: UIViewController { private let orderNumber: String private let viewModel = DepositOrderDetailViewModel() private lazy var tableView: UITableView = { let table = UITableView(frame: .zero, style: .insetGrouped) table.backgroundColor = AppDesignUIKit.pageBackground return table }() /// Diffable 数据源,驱动详情字段展示。 private var tableDataSource: UITableViewDiffableDataSource! /// 初始化实例。 init(orderNumber: String) { self.orderNumber = orderNumber super.init(nibName: nil, bundle: nil) } @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( 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 { var snapshot = NSDiffableDataSourceSnapshot() 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( api: appServices.ordersAPI, storeId: appServices.accountContext.currentStore?.id, orderNumber: orderNumber ) } applyTableSnapshot() if let message = viewModel.errorMessage { showToast(message) } } } /// 押金拍摄信息页。 final class DepositOrderShootingInfoViewController: UIViewController, UITableViewDelegate { private let orderNumber: String private let scenicSpotId: Int private let photogUid: Int private let viewModel = DepositOrderShootingInfoViewModel() private lazy var tableView: UITableView = { let table = UITableView(frame: .zero, style: .insetGrouped) table.delegate = self return table }() /// Diffable 数据源,驱动拍摄摘要与素材列表刷新。 private var tableDataSource: UITableViewDiffableDataSource! /// 初始化实例。 init(orderNumber: String, scenicSpotId: Int, photogUid: Int) { self.orderNumber = orderNumber self.scenicSpotId = scenicSpotId self.photogUid = photogUid super.init(nibName: nil, bundle: nil) } @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( 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 { var snapshot = NSDiffableDataSourceSnapshot() 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( api: appServices.ordersAPI, storeId: appServices.accountContext.currentStore?.id, orderNumber: orderNumber, scenicSpotId: scenicSpotId, photogUid: photogUid ) } applyTableSnapshot() if let message = viewModel.errorMessage { showToast(message) } } /// UITableView 代理:section 标题。 func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 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 } } } /// 安全下标,避免 section 越界。 private extension Array { subscript(safe index: Int) -> Element? { indices.contains(index) ? self[index] : nil } }