Implement orders tab aligned with Android, including scan verify flows.
Add role-based order/deposit lists, AVFoundation QR scanning, verify dialogs, and ViewModel tests so photographers, scenic admins, and store admins match Android behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,243 @@
|
||||
//
|
||||
// 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.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.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.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?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user