269 lines
8.4 KiB
Swift
269 lines
8.4 KiB
Swift
//
|
|
// DepositOrderListViewModel.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// 店铺管理员押金单列表 ViewModel,对齐 Android `DepositOrderListViewModel`。
|
|
final class DepositOrderListViewModel {
|
|
|
|
private let appStore: AppStore
|
|
|
|
/// 注入账号上下文,使订单查询与测试使用各自独立的会话。
|
|
init(appStore: AppStore = .shared) {
|
|
self.appStore = appStore
|
|
}
|
|
|
|
|
|
private(set) var searchText = ""
|
|
private(set) var filterLabel = "全部"
|
|
private(set) var tabIndex = 0
|
|
private(set) var startTime: Date?
|
|
private(set) var endTime: Date?
|
|
private(set) var orderList: [StoreOrderItem] = []
|
|
private(set) var isRefreshing = false
|
|
private(set) var canLoadMore = false
|
|
private(set) var isLoadingMore = false
|
|
private(set) var writeOffResponse: WriteOffOrderResponse?
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var onShowMessage: ((String) -> Void)?
|
|
|
|
private var listRequestID = UUID()
|
|
private var currentPage = 1
|
|
private var totalCount = 0
|
|
private var searchDebounceTask: Task<Void, Never>?
|
|
|
|
private let apiDateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.calendar = Calendar(identifier: .gregorian)
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
formatter.dateFormat = "yyyy-MM-dd"
|
|
return formatter
|
|
}()
|
|
|
|
/// 清除通知进入前的查询条件,由页面随后发起第一页刷新。
|
|
func resetFiltersForOrderNotification() {
|
|
searchDebounceTask?.cancel()
|
|
searchDebounceTask = nil
|
|
searchText = ""
|
|
filterLabel = "全部"
|
|
tabIndex = 0
|
|
startTime = nil
|
|
endTime = nil
|
|
notifyStateChange()
|
|
}
|
|
|
|
func refreshOrderList(api: OrderAPI) async {
|
|
currentPage = 1
|
|
isRefreshing = true
|
|
isLoadingMore = false
|
|
notifyStateChange()
|
|
await loadOrderList(refresh: true, api: api)
|
|
}
|
|
|
|
func loadMore(api: OrderAPI) async {
|
|
guard !isLoadingMore, !isRefreshing, orderList.count < totalCount else { return }
|
|
isLoadingMore = true
|
|
notifyStateChange()
|
|
await loadOrderList(refresh: false, api: api)
|
|
}
|
|
|
|
func shouldLoadMore(at index: Int) -> Bool {
|
|
guard canLoadMore, !isRefreshing, !isLoadingMore else { return false }
|
|
return index >= max(0, orderList.count - 3)
|
|
}
|
|
|
|
func onSearchTextChange(_ text: String, api: OrderAPI) {
|
|
searchText = text
|
|
notifyStateChange()
|
|
searchDebounceTask?.cancel()
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.isEmpty {
|
|
Task { await refreshOrderList(api: api) }
|
|
return
|
|
}
|
|
if Self.isValidPhone(trimmed) {
|
|
searchDebounceTask = Task {
|
|
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
guard !Task.isCancelled else { return }
|
|
await refreshOrderList(api: api)
|
|
}
|
|
}
|
|
}
|
|
|
|
func onFilterChange(tab: Int) {
|
|
tabIndex = tab
|
|
filterLabel = StoreOrderFilter.from(tabIndex: tab).label
|
|
notifyStateChange()
|
|
}
|
|
|
|
func onDateRangeSelected(start: Date?, end: Date?) {
|
|
startTime = start
|
|
endTime = end
|
|
notifyStateChange()
|
|
}
|
|
|
|
func dismissWriteOffDialog() {
|
|
writeOffResponse = nil
|
|
notifyStateChange()
|
|
}
|
|
|
|
func processScanResult(_ scanResult: String, api: OrderAPI) async {
|
|
guard let parsed = QRCodeScanResult.parse(scanResult) else {
|
|
onShowMessage?("请扫描正确的二维码")
|
|
return
|
|
}
|
|
guard parsed.method == "order_verify" else {
|
|
onShowMessage?("请扫描订单核销码")
|
|
return
|
|
}
|
|
guard parsed.userId > 0, !parsed.orderNumber.isEmpty, !parsed.time.isEmpty else {
|
|
onShowMessage?("核销码内容不完整")
|
|
return
|
|
}
|
|
let storeId = appStore.session.currentStoreId
|
|
guard storeId > 0 else {
|
|
onShowMessage?("店铺信息异常,请先选择店铺")
|
|
return
|
|
}
|
|
await executeStoreOrderVerify(
|
|
StoreOrderVerifyRequest(
|
|
userId: parsed.userId,
|
|
orderNumber: parsed.orderNumber,
|
|
time: parsed.time,
|
|
storeId: storeId
|
|
),
|
|
api: api
|
|
)
|
|
}
|
|
|
|
func requestRefund(
|
|
orderNumber: String,
|
|
refundType: Int,
|
|
refundAmount: String,
|
|
refundReason: String,
|
|
api: OrderAPI
|
|
) async {
|
|
let storeId = appStore.session.currentStoreId
|
|
guard storeId > 0 else {
|
|
onShowMessage?("店铺信息异常")
|
|
return
|
|
}
|
|
do {
|
|
try await api.storeOrderRefund(
|
|
StoreOrderRefundRequest(
|
|
storeId: storeId,
|
|
orderNumber: orderNumber,
|
|
refundType: refundType,
|
|
refundAmount: refundAmount,
|
|
refundReason: refundReason
|
|
)
|
|
)
|
|
onShowMessage?("退款成功")
|
|
await refreshOrderList(api: api)
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
func canOpenDetail(for item: StoreOrderItem) -> Bool {
|
|
item.orderType == StoreOrderType.multiTravelShoot
|
|
}
|
|
|
|
static func isValidPhone(_ text: String) -> Bool {
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard trimmed.count == 11, trimmed.first == "1" else { return false }
|
|
guard let second = trimmed.dropFirst().first, ("3" ... "9").contains(second) else { return false }
|
|
return trimmed.allSatisfy(\.isNumber)
|
|
}
|
|
|
|
static func apiOrderStatus(forTabIndex tabIndex: Int) -> Int? {
|
|
OrderListQueryBuilder.storeOrderStatusQuery(tabIndex: tabIndex)
|
|
}
|
|
|
|
private func loadOrderList(refresh: Bool, api: OrderAPI) async {
|
|
let requestID = UUID()
|
|
listRequestID = requestID
|
|
let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone)
|
|
|
|
let storeId = appStore.session.currentStoreId
|
|
if storeId <= 0 {
|
|
finishLoading()
|
|
orderList = []
|
|
canLoadMore = false
|
|
onShowMessage?("店铺信息异常,请先选择店铺")
|
|
return
|
|
}
|
|
|
|
let orderStatus: Int?
|
|
let start: String?
|
|
let end: String?
|
|
let userPhone: String?
|
|
|
|
if isPhoneSearch {
|
|
orderStatus = 0
|
|
start = nil
|
|
end = nil
|
|
userPhone = phone
|
|
} else {
|
|
orderStatus = Self.apiOrderStatus(forTabIndex: tabIndex)
|
|
start = startTime.map { apiDateFormatter.string(from: $0) }
|
|
end = endTime.map { apiDateFormatter.string(from: $0) }
|
|
userPhone = nil
|
|
}
|
|
|
|
do {
|
|
let response = try await api.storeOrderList(
|
|
storeId: storeId,
|
|
page: currentPage,
|
|
userPhone: userPhone,
|
|
orderStatus: orderStatus,
|
|
startTime: start,
|
|
endTime: end
|
|
)
|
|
guard listRequestID == requestID else { return }
|
|
totalCount = response.total
|
|
if refresh || currentPage == 1 {
|
|
orderList = response.list
|
|
currentPage = 2
|
|
} else {
|
|
orderList.append(contentsOf: response.list)
|
|
currentPage += 1
|
|
}
|
|
canLoadMore = orderList.count < totalCount
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
guard listRequestID == requestID else { return }
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
finishLoading()
|
|
}
|
|
|
|
private func executeStoreOrderVerify(_ request: StoreOrderVerifyRequest, api: OrderAPI) async {
|
|
do {
|
|
let response = try await api.storeOrderVerify(request)
|
|
writeOffResponse = response
|
|
if response.orderVerify {
|
|
await refreshOrderList(api: api)
|
|
}
|
|
notifyStateChange()
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
private func finishLoading() {
|
|
isRefreshing = false
|
|
isLoadingMore = false
|
|
notifyStateChange()
|
|
}
|
|
|
|
private func notifyStateChange() {
|
|
onStateChange?()
|
|
}
|
|
}
|