同步订单详情与扫码核销流程

This commit is contained in:
2026-06-22 17:03:27 +08:00
parent 726f19c124
commit d02fd2c069
15 changed files with 1456 additions and 54 deletions

View File

@ -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: