Files
suixinkan_uikit/suixinkan/UI/Orders/OrdersViewController.swift
汉秋 cfcd3eee6f Integrate Amap SDK to replace CoreLocation and MapKit for all location features.
Unify GPS, reverse geocoding, and map display behind LocationProvider with privacy-gated bootstrap after login, aligned with Android.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:49:22 +08:00

525 lines
22 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrdersViewController.swift
// suixinkan
//
import CoreLocation
import SnapKit
import UIKit
/// Tab //
final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
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 weak var orderSourcePicker: OrderSourcePickerViewController?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider: any LocationProviding = LocationProvider.shared
override func setupNavigationBar() {
title = "订单"
}
override func setupUI() {
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() {
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()
self?.orderSourcePicker?.reloadFromViewModel()
}
}
orderListViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
orderListViewModel.onNavigateBindAcquirer = { [weak self] saleUserId in
Task { @MainActor in self?.presentBindAcquirer(saleUserId: saleUserId) }
}
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,
isLoading: depositViewModel.isRefreshing && depositViewModel.orderList.isEmpty,
isLoadingMore: depositViewModel.isLoadingMore
)
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,
isLoading: orderListViewModel.isRefreshing && orderListViewModel.orderList.isEmpty,
isLoadingMore: orderListViewModel.isLoadingMore
)
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 presentBindAcquirer(saleUserId: Int) {
dismiss(animated: true) { [weak self] in
let controller = BindAcquirerViewController(saleUserId: saleUserId)
self?.navigationController?.pushViewController(controller, 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),
sourceSelection: source
)
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)
}
actions.onOrderSourceImageTap = { [weak self] images, index in
self?.presentImagePreview(imageURLs: images, startIndex: index)
}
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.onCallPhone = {
guard let url = URL(string: "tel://\(item.phone)") else { return }
UIApplication.shared.open(url)
}
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)
await MainActor.run { [weak self] in
guard let self else { return }
let controller = OrderSourcePickerViewController(
orderNumber: order.orderNumber,
viewModel: self.orderListViewModel,
referralOrder: order.referralOrder,
loadLeads: { [weak self] person in
guard let self else { return }
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
},
onLeadBound: { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }
self.dismiss(animated: true) {
self.orderSourcePicker = nil
}
Task {
await self.orderListViewModel.bindOrderSource(
orderNumber: orderNumber,
selection: selection,
api: self.orderAPI
)
}
},
onCallPhone: { [weak self] phone in
self?.callPhone(phone)
}
)
self.orderSourcePicker = controller
let nav = UINavigationController(rootViewController: controller)
nav.setNavigationBarHidden(true, animated: false)
if let sheet = nav.sheetPresentationController {
sheet.prefersGrabberVisible = true
if #available(iOS 16.0, *) {
sheet.detents = [
.custom(identifier: .init("orderSource")) { context in
context.maximumDetentValue * 0.82
},
.large(),
]
sheet.selectedDetentIdentifier = .init("orderSource")
} else {
sheet.detents = [.large()]
}
}
self.present(nav, animated: true)
}
}
}
private func callPhone(_ phone: String) {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: "tel://\(trimmed)") else { return }
UIApplication.shared.open(url)
}
}