同步订单详情与扫码核销流程
This commit is contained in:
@ -20,13 +20,21 @@ struct OrdersView: View {
|
||||
@State private var viewModel = OrdersViewModel()
|
||||
@State private var manualOrderNumber = ""
|
||||
@State private var showDateFilterSheet = false
|
||||
@State private var showScanner = false
|
||||
@State private var showVerifyConfirmation = false
|
||||
@State private var pendingVerifyOrder: WriteOffOrderItem?
|
||||
@State private var pendingVerifyOrderNumber: String?
|
||||
@State private var matchedScannedOrderNumber: String?
|
||||
@State private var scanHintMessage: String?
|
||||
|
||||
private var currentScenicId: Int? {
|
||||
accountContext.currentScenic?.id
|
||||
}
|
||||
|
||||
private var currentStoreId: Int? {
|
||||
accountContext.currentStore?.id
|
||||
}
|
||||
|
||||
private var currentRoleId: Int? {
|
||||
permissionContext.currentRole?.id
|
||||
}
|
||||
@ -47,28 +55,36 @@ struct OrdersView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: AppMetrics.Spacing.small) {
|
||||
entrySection
|
||||
ScrollViewReader { scrollProxy in
|
||||
ScrollView {
|
||||
VStack(spacing: AppMetrics.Spacing.small) {
|
||||
entrySection
|
||||
|
||||
if currentScenicId == nil {
|
||||
ContentUnavailableView(
|
||||
"缺少经营上下文",
|
||||
systemImage: "mountain.2",
|
||||
description: Text("请先在首页选择景区后查看订单。")
|
||||
)
|
||||
.frame(minHeight: 360)
|
||||
} else if viewModel.selectedEntry == .storeOrders {
|
||||
storeOrdersSection
|
||||
} else {
|
||||
writeOffSection
|
||||
if currentScenicId == nil {
|
||||
ContentUnavailableView(
|
||||
"缺少经营上下文",
|
||||
systemImage: "mountain.2",
|
||||
description: Text("请先在首页选择景区后查看订单。")
|
||||
)
|
||||
.frame(minHeight: 360)
|
||||
} else if viewModel.selectedEntry == .storeOrders {
|
||||
storeOrdersSection
|
||||
} else {
|
||||
writeOffSection
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: contentMaxWidth)
|
||||
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
|
||||
.padding(.top, AppMetrics.Spacing.medium)
|
||||
.padding(.bottom, AppMetrics.Spacing.pageVertical)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.onChange(of: matchedScannedOrderNumber) { _, orderNumber in
|
||||
guard let orderNumber else { return }
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
scrollProxy.scrollTo(orderNumber, anchor: .center)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: contentMaxWidth)
|
||||
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
|
||||
.padding(.top, AppMetrics.Spacing.medium)
|
||||
.padding(.bottom, AppMetrics.Spacing.pageVertical)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
|
||||
.navigationTitle("订单")
|
||||
@ -92,10 +108,24 @@ struct OrdersView: View {
|
||||
Task { await verify(orderNumber: orderNumber) }
|
||||
}
|
||||
Button("取消", role: .cancel) {
|
||||
pendingVerifyOrder = nil
|
||||
pendingVerifyOrderNumber = nil
|
||||
}
|
||||
} message: {
|
||||
Text(pendingVerifyOrderNumber.map { "订单号:\($0)" } ?? "")
|
||||
Text(verifyDialogMessage)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showScanner) {
|
||||
OrderScannerPage(
|
||||
onClose: { showScanner = false },
|
||||
onSuccess: { rawCode in
|
||||
showScanner = false
|
||||
handleScanResult(rawCode)
|
||||
},
|
||||
onFailure: { error in
|
||||
showScanner = false
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showDateFilterSheet) {
|
||||
OrdersDateFilterSheet(
|
||||
@ -220,7 +250,7 @@ struct OrdersView: View {
|
||||
private var writeOffActionSection: some View {
|
||||
VStack(spacing: AppMetrics.Spacing.small) {
|
||||
Button {
|
||||
router.navigate(to: .placeholder(title: "扫码核销"))
|
||||
showScanner = true
|
||||
} label: {
|
||||
Label("扫码核销", systemImage: "qrcode.viewfinder")
|
||||
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
|
||||
@ -232,6 +262,13 @@ struct OrdersView: View {
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("orders.scan")
|
||||
|
||||
if let scanHintMessage {
|
||||
Label(scanHintMessage, systemImage: "info.circle")
|
||||
.font(.system(size: AppMetrics.FontSize.footnote))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
HStack(spacing: AppMetrics.Spacing.xSmall) {
|
||||
TextField("手动输入订单号", text: $manualOrderNumber)
|
||||
.textInputAutocapitalization(.never)
|
||||
@ -377,7 +414,7 @@ struct OrdersView: View {
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 8))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
router.navigate(to: .placeholder(title: "订单详情"))
|
||||
router.navigate(to: .orders(.storeDetail(item)))
|
||||
}
|
||||
.accessibilityIdentifier("orders.store.card.\(item.orderNumber)")
|
||||
}
|
||||
@ -406,7 +443,7 @@ struct OrdersView: View {
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
Spacer()
|
||||
Button("详情") {
|
||||
router.navigate(to: .placeholder(title: "核销订单详情"))
|
||||
router.navigate(to: .orders(.writeOffDetail(item)))
|
||||
}
|
||||
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
@ -417,6 +454,7 @@ struct OrdersView: View {
|
||||
.accessibilityIdentifier("orders.writeoff.detail.\(item.orderNumber)")
|
||||
|
||||
Button(verifyButtonTitle(for: item)) {
|
||||
pendingVerifyOrder = item
|
||||
pendingVerifyOrderNumber = item.orderNumber
|
||||
showVerifyConfirmation = true
|
||||
}
|
||||
@ -432,7 +470,12 @@ struct OrdersView: View {
|
||||
}
|
||||
.padding(AppMetrics.Spacing.medium)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 8))
|
||||
.background(cardBackgroundColor(for: item), in: RoundedRectangle(cornerRadius: 8))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(matchedScannedOrderNumber == item.orderNumber ? AppDesign.primary : Color.clear, lineWidth: 2)
|
||||
}
|
||||
.id(item.orderNumber)
|
||||
}
|
||||
|
||||
private func orderInfoRow(_ title: String, _ value: String, valueColor: Color = .black) -> some View {
|
||||
@ -472,6 +515,7 @@ struct OrdersView: View {
|
||||
try await viewModel.reload(
|
||||
api: ordersAPI,
|
||||
scenicId: currentScenicId,
|
||||
storeId: currentStoreId,
|
||||
roleId: currentRoleId,
|
||||
showLoading: showLoading
|
||||
)
|
||||
@ -490,7 +534,7 @@ struct OrdersView: View {
|
||||
|
||||
private func loadMoreWriteOffOrders() async {
|
||||
do {
|
||||
try await viewModel.loadMoreWriteOffOrders(api: ordersAPI, scenicId: currentScenicId)
|
||||
try await viewModel.loadMoreWriteOffOrders(api: ordersAPI, scenicId: currentScenicId, storeId: currentStoreId)
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
@ -502,27 +546,73 @@ struct OrdersView: View {
|
||||
toastCenter.show("请输入订单号")
|
||||
return
|
||||
}
|
||||
pendingVerifyOrderNumber = orderNumber
|
||||
if let matched = viewModel.writeOffOrder(matching: orderNumber) {
|
||||
pendingVerifyOrder = matched
|
||||
pendingVerifyOrderNumber = matched.orderNumber
|
||||
matchedScannedOrderNumber = matched.orderNumber
|
||||
scanHintMessage = "已定位到列表订单:\(matched.orderNumber)"
|
||||
} else {
|
||||
pendingVerifyOrder = nil
|
||||
pendingVerifyOrderNumber = orderNumber
|
||||
matchedScannedOrderNumber = nil
|
||||
scanHintMessage = "未在当前列表定位到订单,将按输入订单号发起核销。"
|
||||
}
|
||||
showVerifyConfirmation = true
|
||||
}
|
||||
|
||||
private func verify(orderNumber: String) async {
|
||||
guard let scenicId = currentScenicId else { return }
|
||||
do {
|
||||
try await viewModel.verify(api: ordersAPI, scenicId: scenicId, orderNumber: orderNumber)
|
||||
try await viewModel.verify(api: ordersAPI, scenicId: scenicId, storeId: currentStoreId, orderNumber: orderNumber)
|
||||
manualOrderNumber = ""
|
||||
pendingVerifyOrder = nil
|
||||
pendingVerifyOrderNumber = nil
|
||||
matchedScannedOrderNumber = nil
|
||||
scanHintMessage = nil
|
||||
toastCenter.show("核销成功")
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理扫码返回的原始内容,并进入二次确认核销流程。
|
||||
private func handleScanResult(_ rawCode: String) {
|
||||
guard let result = viewModel.matchedWriteOffOrder(for: rawCode) else {
|
||||
toastCenter.show("扫码成功,但未识别到有效订单号,请手动输入。")
|
||||
return
|
||||
}
|
||||
|
||||
manualOrderNumber = result.orderNumber
|
||||
pendingVerifyOrder = result.matched
|
||||
pendingVerifyOrderNumber = result.orderNumber
|
||||
|
||||
if let matched = result.matched {
|
||||
matchedScannedOrderNumber = matched.orderNumber
|
||||
scanHintMessage = "已定位到列表订单:\(matched.orderNumber)"
|
||||
} else {
|
||||
matchedScannedOrderNumber = nil
|
||||
scanHintMessage = "未在当前列表定位到订单,已填入订单号,可继续核销。"
|
||||
}
|
||||
showVerifyConfirmation = true
|
||||
}
|
||||
|
||||
private func canVerify(_ item: WriteOffOrderItem) -> Bool {
|
||||
let status = displayVerificationStatus(for: item)
|
||||
return !status.contains("已核销") && !status.contains("已退款") && !status.contains("已完成")
|
||||
}
|
||||
|
||||
private var verifyDialogMessage: String {
|
||||
let orderNumber = pendingVerifyOrderNumber ?? "-"
|
||||
if let order = pendingVerifyOrder ?? viewModel.writeOffOrder(matching: orderNumber) {
|
||||
return """
|
||||
订单号:\(order.orderNumber)
|
||||
项目:\(order.projectName.isEmpty ? "--" : order.projectName)
|
||||
金额:¥\(order.orderAmount)
|
||||
"""
|
||||
}
|
||||
return "订单号:\(orderNumber)"
|
||||
}
|
||||
|
||||
private func displayVerificationStatus(for item: WriteOffOrderItem) -> String {
|
||||
let text = item.orderVerificationStatus.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? "待核销" : text
|
||||
@ -536,6 +626,10 @@ struct OrdersView: View {
|
||||
canVerify(item) ? AppDesign.primary : Color(hex: 0x22C55E)
|
||||
}
|
||||
|
||||
private func cardBackgroundColor(for item: WriteOffOrderItem) -> Color {
|
||||
matchedScannedOrderNumber == item.orderNumber ? Color(hex: 0xEEF6FF) : .white
|
||||
}
|
||||
|
||||
private func statusColor(for status: Int) -> Color {
|
||||
switch status {
|
||||
case 18:
|
||||
|
||||
Reference in New Issue
Block a user