实现订单 Tab 并接入扫码核销流程
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user