将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
7.6 KiB
Swift
244 lines
7.6 KiB
Swift
//
|
||
// DepositOrderListViewModel.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 店铺管理员押金单列表 ViewModel,对齐 Android `DepositOrderListViewModel`。
|
||
final class DepositOrderListViewModel {
|
||
|
||
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 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 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.shared.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.shared.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 phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone)
|
||
|
||
let storeId = AppStore.shared.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
|
||
)
|
||
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 {
|
||
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?()
|
||
}
|
||
}
|