535 lines
23 KiB
Swift
535 lines
23 KiB
Swift
//
|
||
// 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 = OrderLocationProvider()
|
||
|
||
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(images: 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,
|
||
onPersonSelected: { [weak self] person in
|
||
guard let self else { return }
|
||
Task {
|
||
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)
|
||
},
|
||
onPreviewImages: { [weak self] urls, index in
|
||
self?.presentImagePreview(images: urls, startIndex: index)
|
||
}
|
||
)
|
||
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)
|
||
}
|
||
|
||
private func presentImagePreview(images: [String], startIndex: Int) {
|
||
guard !images.isEmpty else { return }
|
||
present(ImagePreviewViewController(imageURLs: images, startIndex: startIndex), animated: true)
|
||
}
|
||
}
|