Implement orders tab aligned with Android, including scan verify flows.

Add role-based order/deposit lists, AVFoundation QR scanning, verify dialogs, and ViewModel tests so photographers, scenic admins, and store admins match Android behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:59:53 +08:00
parent 6492f80ef0
commit 31d2b6094b
27 changed files with 4226 additions and 17 deletions

View File

@ -3,26 +3,476 @@
// suixinkan
//
import CoreLocation
import SnapKit
import UIKit
/// Tab
final class OrdersViewController: BaseViewController {
/// Tab //
final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
private let titleLabel = UILabel()
private let orderAPI = NetworkServices.shared.orderAPI
private let orderListViewModel = OrderListViewModel()
private let depositViewModel = DepositOrderListViewModel()
private let filterBar = OrderFilterBarView()
private let listView = OrderListView()
private var hasInitialized = false
private var isStoreMode: Bool { AppStore.shared.currentAppRole == .storeAdmin }
private var pendingOrderSourceNumber: String?
private var pendingOrderSourcePerson: OrderSourcePerson?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider = OrderLocationProvider()
override func setupNavigationBar() {
title = "订单"
}
override func setupUI() {
title = "订单"
titleLabel.text = "订单"
titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
titleLabel.textAlignment = .center
titleLabel.textColor = AppColor.text333
view.addSubview(titleLabel)
view.backgroundColor = UIColor(hex: 0xF5F5F5)
view.addSubview(filterBar)
view.addSubview(listView)
listView.tableView.dataSource = self
listView.tableView.delegate = self
listView.tableView.register(OrderCardCell.self, forCellReuseIdentifier: OrderCardCell.reuseIdentifier)
listView.tableView.register(DepositOrderCardCell.self, forCellReuseIdentifier: DepositOrderCardCell.reuseIdentifier)
}
override func setupConstraints() {
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
filterBar.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
}
listView.snp.makeConstraints { make in
make.top.equalTo(filterBar.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
orderListViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
orderListViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
depositViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
depositViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
filterBar.onSearchChanged = { [weak self] text in
guard let self else { return }
if self.isStoreMode {
self.depositViewModel.onSearchTextChange(text, api: self.orderAPI)
} else {
self.orderListViewModel.onSearchTextChange(text)
}
}
filterBar.onSearchSubmitted = { [weak self] in
guard let self else { return }
Task {
if self.isStoreMode {
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
filterBar.onFilterSelected = { [weak self] tab, rTab in
guard let self else { return }
Task {
if self.isStoreMode {
self.depositViewModel.onFilterChange(tab: tab)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onFilterChange(orderStatus: tab, isRefined: rTab)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
filterBar.onDateRangeTap = { [weak self] in
self?.presentDateRangeSheet()
}
listView.onRefresh = { [weak self] in
guard let self else { return }
Task {
if self.isStoreMode {
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
listView.onLoadMore = { [weak self] index in
guard let self else { return }
Task {
if self.isStoreMode {
guard self.depositViewModel.shouldLoadMore(at: index) else { return }
await self.depositViewModel.loadMore(api: self.orderAPI)
} else {
guard self.orderListViewModel.shouldLoadMore(at: index) else { return }
await self.orderListViewModel.loadMore(api: self.orderAPI)
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !hasInitialized else { return }
hasInitialized = true
Task { await initializeList() }
}
private func initializeList() async {
if isStoreMode {
await depositViewModel.refreshOrderList(api: orderAPI)
} else {
await orderListViewModel.refreshOrderList(api: orderAPI)
}
}
private func applyViewModel() {
if isStoreMode {
filterBar.configure(
isStoreMode: true,
filterLabel: depositViewModel.filterLabel,
dateRangeText: dateRangeText(start: depositViewModel.startTime, end: depositViewModel.endTime),
searchText: depositViewModel.searchText
)
listView.apply(
isRefreshing: depositViewModel.isRefreshing,
canLoadMore: depositViewModel.canLoadMore,
isEmpty: depositViewModel.orderList.isEmpty
)
if let response = depositViewModel.writeOffResponse {
depositViewModel.dismissWriteOffDialog()
WriteOffResultAlertController.present(from: self, response: response)
}
} else {
filterBar.configure(
isStoreMode: false,
filterLabel: orderListViewModel.filterLabel,
dateRangeText: dateRangeText(start: orderListViewModel.startTime, end: orderListViewModel.endTime),
searchText: orderListViewModel.searchText
)
listView.apply(
isRefreshing: orderListViewModel.isRefreshing,
canLoadMore: orderListViewModel.canLoadMore,
isEmpty: orderListViewModel.orderList.isEmpty
)
if let response = orderListViewModel.signInResponse {
orderListViewModel.clearSignInResponse()
OrderSignInAlertController.present(from: self, response: response)
}
if let response = orderListViewModel.writeOffResponse {
orderListViewModel.clearWriteOffResponse()
WriteOffResultAlertController.present(from: self, response: response)
}
if orderListViewModel.multiVerifyPendingData != nil,
orderListViewModel.multiVerifyScenicSpotList.isEmpty,
!isFetchingMultiVerifySpots {
Task { await self.fetchMultiVerifySpotsIfNeeded() }
}
if !orderListViewModel.multiVerifyScenicSpotList.isEmpty {
presentMultiVerifySheet()
}
}
listView.tableView.reloadData()
}
private func dateRangeText(start: Date?, end: Date?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
if let start, let end {
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
}
return "全部"
}
private func presentDateRangeSheet() {
let start = isStoreMode ? depositViewModel.startTime : orderListViewModel.startTime
let end = isStoreMode ? depositViewModel.endTime : orderListViewModel.endTime
let controller = DateRangePickerViewController(rangeStart: start, rangeEnd: end)
controller.onShortcutSelected = { [weak self, weak controller] shortcut in
guard let self else { return }
let range = self.resolveShortcutRange(shortcut)
Task {
if self.isStoreMode {
self.depositViewModel.onDateRangeSelected(start: range.start, end: range.end)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onDateRangeSelected(start: range.start, end: range.end)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
controller?.dismiss(animated: true)
}
}
controller.onRangeSelected = { [weak self, weak controller] start, end in
guard let self else { return }
Task {
if self.isStoreMode {
self.depositViewModel.onDateRangeSelected(start: start, end: end)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onDateRangeSelected(start: start, end: end)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
controller?.dismiss(animated: true)
}
}
if let sheet = controller.sheetPresentationController {
sheet.detents = [.medium(), .large()]
}
present(controller, animated: true)
}
private func resolveShortcutRange(_ shortcut: String) -> (start: Date?, end: Date?) {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
switch shortcut {
case StatisticsRangeShortcut.today.rawValue:
return (today, today)
case StatisticsRangeShortcut.yesterday.rawValue:
let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
return (yesterday, yesterday)
case StatisticsRangeShortcut.last7Days.rawValue:
let start = calendar.date(byAdding: .day, value: -6, to: today)!
return (start, today)
default:
return (nil, nil)
}
}
private func presentScanner(onResult: @escaping (String) -> Void) {
let scanner = QRCodeScannerViewController()
scanner.onScanResult = onResult
let nav = UINavigationController(rootViewController: scanner)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
}
private func fetchMultiVerifySpotsIfNeeded() async {
isFetchingMultiVerifySpots = true
defer { isFetchingMultiVerifySpots = false }
do {
let coordinate = try await locationProvider.requestCoordinate()
await orderListViewModel.fetchScenicSpotsAndShowSheet(
lat: coordinate.latitude,
lng: coordinate.longitude,
api: orderAPI
)
applyViewModel()
} catch {
showToast(error.localizedDescription)
orderListViewModel.clearMultiVerifyPending()
}
}
private func presentMultiVerifySheet() {
let spots = orderListViewModel.multiVerifyScenicSpotList
guard !spots.isEmpty else { return }
let controller = MultiVerifyScenicSpotViewController(
spots: spots,
selected: orderListViewModel.selectedMultiVerifySpot
)
controller.onConfirm = { [weak self] spot in
guard let self else { return }
self.orderListViewModel.onMultiVerifySpotSelected(spot)
Task { await self.orderListViewModel.onMultiVerifyConfirm(api: self.orderAPI) }
}
controller.onCancel = { [weak self] in
self?.orderListViewModel.clearMultiVerifySheet()
}
let nav = UINavigationController(rootViewController: controller)
if let sheet = nav.sheetPresentationController {
sheet.detents = [.medium(), .large()]
}
present(nav, animated: true)
orderListViewModel.clearMultiVerifySheet()
}
// MARK: - UITableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
isStoreMode ? depositViewModel.orderList.count : orderListViewModel.orderList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if isStoreMode {
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier, for: indexPath) as! DepositOrderCardCell
let item = depositViewModel.orderList[indexPath.row]
cell.configure(item: item, actions: makeDepositActions(for: item))
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: depositViewModel.shouldLoadMore(at: indexPath.row))
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier, for: indexPath) as! OrderCardCell
let order = orderListViewModel.orderList[indexPath.row]
let source = orderListViewModel.orderSourceSelection(for: order)
cell.configure(
order: order,
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
actions: makeOrderActions(for: order),
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
sourceText: source.map { "\($0.person.name) · \($0.lead.displayText)" }
)
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
return cell
}
private func makeOrderActions(for order: OrderEntity) -> OrderCardActions {
var actions = OrderCardActions()
actions.onRefund = { [weak self] in
guard let self else { return }
OrderRefundAlertController.present(from: self, order: order) { request in
Task { await self.orderListViewModel.refundOrder(request, api: self.orderAPI) }
}
}
actions.onVerify = { [weak self] in
self?.presentScanner { result in
Task { await self?.orderListViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
}
}
actions.onUploadTask = { [weak self] in
self?.showToast("上传任务待接入")
}
actions.onCancel = { [weak self] in
guard let self else { return }
Task { await self.orderListViewModel.cancelOrder(orderNumber: order.orderNumber, api: self.orderAPI) }
}
actions.onCallPhone = {
guard let url = URL(string: "tel://\(order.phone)") else { return }
UIApplication.shared.open(url)
}
actions.onRefinedChanged = { [weak self] value in
guard let self else { return }
Task { await self.orderListViewModel.updateRefined(orderNumber: order.orderNumber, isRefined: value, api: self.orderAPI) }
}
actions.onHistoricalPhoto = { [weak self] in
let controller = HistoricalShootingInfoViewController(orderNumber: order.orderNumber)
self?.navigationController?.pushViewController(controller, animated: true)
}
actions.onOrderSourceTap = { [weak self] in
self?.presentOrderSourcePicker(for: order)
}
if let travel = order.photoTravel, travel.canGiftRetouch {
actions.onSendGift = { [weak self] in
guard let self else { return }
self.pendingGiftOrderNumber = order.orderNumber
SendGiftAlertController.present(
from: self,
photoNum: travel.retouchGiftPhotoNum,
videoNum: travel.retouchGiftVideoNum
) { photo, video in
Task {
await self.orderListViewModel.sendGift(
orderNumber: order.orderNumber,
photoNum: photo,
videoNum: video,
api: self.orderAPI
)
}
}
}
}
return actions
}
private func makeDepositActions(for item: StoreOrderItem) -> DepositOrderCardActions {
var actions = DepositOrderCardActions()
actions.onRefund = { [weak self] in
guard let self else { return }
DepositOrderRefundAlertController.present(
from: self,
orderNumber: item.orderNumber,
amount: item.actualPayAmount,
refundType: 1
) { amount, reason in
Task {
await self.depositViewModel.requestRefund(
orderNumber: item.orderNumber,
refundType: 1,
refundAmount: amount,
refundReason: reason,
api: self.orderAPI
)
}
}
}
actions.onPartialRefund = { [weak self] in
guard let self else { return }
DepositOrderRefundAlertController.present(
from: self,
orderNumber: item.orderNumber,
amount: item.actualPayAmount,
refundType: 3
) { amount, reason in
Task {
await self.depositViewModel.requestRefund(
orderNumber: item.orderNumber,
refundType: 3,
refundAmount: amount,
refundReason: reason,
api: self.orderAPI
)
}
}
}
actions.onVerify = { [weak self] in
self?.presentScanner { result in
Task { await self?.depositViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
}
}
actions.onTap = { [weak self] in
guard let self else { return }
guard self.depositViewModel.canOpenDetail(for: item) else {
self.showToast("该订单类型暂无详情")
return
}
let controller = DepositOrderDetailViewController(orderNumber: item.orderNumber)
self.navigationController?.pushViewController(controller, animated: true)
}
return actions
}
private func presentOrderSourcePicker(for order: OrderEntity) {
pendingOrderSourceNumber = order.orderNumber
Task {
await orderListViewModel.onOrderSourceSheetOpen(api: orderAPI)
let controller = OrderSourcePickerViewController(
people: orderListViewModel.orderSourcePeople,
leads: [],
selectedPerson: nil
)
controller.onPersonSelected = { [weak self] person in
guard let self else { return }
self.pendingOrderSourcePerson = person
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
let leadsController = OrderSourcePickerViewController(
people: self.orderListViewModel.orderSourcePeople,
leads: self.orderListViewModel.orderSourceLeads[person.key] ?? [],
selectedPerson: person
)
leadsController.onLeadSelected = { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }
Task {
await self.orderListViewModel.bindOrderSource(
orderNumber: orderNumber,
selection: selection,
api: self.orderAPI
)
}
}
self.present(leadsController, animated: true)
}
}
present(controller, animated: true)
}
}
}