// // OrderDetailViews.swift // suixinkan // // Created by Codex on 2026/6/22. // import SwiftUI /// 门店订单详情页,展示订单接口补全后的支付、客户、项目和拍摄点信息。 struct StoreOrderDetailView: View { @EnvironmentObject private var accountContext: AccountContext @Environment(\.ordersAPI) private var ordersAPI @Environment(\.cooperationOrderAPI) private var cooperationOrderAPI @EnvironmentObject private var router: RouterPath @EnvironmentObject private var toastCenter: ToastCenter @Environment(\.globalLoading) private var globalLoading let item: OrderEntity @StateObject private var viewModel: OrderDetailViewModel @StateObject private var refundViewModel = OrderRefundViewModel() @State private var showRefundSheet = false /// 使用列表订单初始化详情页,并创建详情 ViewModel。 init(item: OrderEntity) { self.item = item _viewModel = StateObject(wrappedValue: OrderDetailViewModel(item: item)) } var body: some View { List { if let contextMessage = viewModel.contextMessage { Section { Label(contextMessage, systemImage: "info.circle") .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(AppDesign.textSecondary) } } Section("订单信息") { OrderDetailRow(title: "订单号", value: viewModel.display.orderNumber) Button("复制订单号") { UIPasteboard.general.string = viewModel.display.orderNumber toastCenter.show("订单号已复制") } .font(.system(size: AppMetrics.FontSize.footnote, weight: .semibold)) .foregroundStyle(AppDesign.primary) OrderDetailRow(title: "订单状态", value: viewModel.display.orderStatusName) OrderDetailRow(title: "订单类型", value: viewModel.display.orderTypeLabel) OrderDetailRow(title: "创建时间", value: viewModel.display.createdAt) OrderDetailRow(title: "付款时间", value: displayPayTime) OrderDetailRow(title: "完成时间", value: viewModel.display.completeTime) } Section("支付信息") { OrderDetailRow(title: "付款金额", value: "¥\(emptyToZero(viewModel.display.actualPayAmount))") OrderDetailRow(title: "退款金额", value: "¥\(emptyToZero(viewModel.display.actualRefundAmount))") OrderDetailRow(title: "付款方式", value: viewModel.display.payTypeName) OrderDetailRow(title: "用户 UID", value: "\(viewModel.display.userId)") } Section("客户与项目") { OrderDetailRow(title: "手机号", value: viewModel.display.phone) OrderDetailRow(title: "关联项目", value: viewModel.display.projectName) OrderDetailRow(title: "项目 ID", value: "\(viewModel.display.projectId)") } if let projectInfo = viewModel.projectInfo { Section("项目配置") { OrderDetailRow(title: "打卡点数", value: "\(projectInfo.settleSpotNum)") OrderDetailRow(title: "单点底片", value: "\(projectInfo.singleSpotMaterialNum)") OrderDetailRow(title: "单点精修", value: "\(projectInfo.singleSpotPhotoNum)") OrderDetailRow(title: "单点视频", value: "\(projectInfo.singleSpotVideoNum)") } } if let photoTravel = item.photoTravel { Section("旅拍信息") { OrderDetailRow(title: "核销时间", value: photoTravel.checkInTime) OrderDetailRow(title: "修图张数", value: "\(photoTravel.orderPhotoNum + photoTravel.retouchGiftPhotoNum) 张") OrderDetailRow(title: "视频剪辑", value: "\(photoTravel.orderVideoNum + photoTravel.retouchGiftVideoNum) 条") } } if !viewModel.shootingList.isEmpty { Section("拍摄点") { ForEach(viewModel.shootingList) { shooting in ShootingPointRow(item: shooting) } } } if !viewModel.display.remark.isEmpty { Section("备注") { Text(viewModel.display.remark) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.textPrimary) } } Section("后续功能") { Button { router.navigate(to: .orders(.historicalShooting(orderNumber: viewModel.display.orderNumber))) } label: { navigationRow("历史拍摄") } if item.orderType == 19 { Button { router.navigate(to: .orders(.multiTravelTaskUpload(orderNumber: viewModel.display.orderNumber))) } label: { navigationRow("任务上传") } } Button { router.navigate(to: .orders(.orderTrailer(orderNumber: viewModel.display.orderNumber, title: "视频预告"))) } label: { navigationRow("视频预告") } Button { router.navigate(to: .orders(.orderTrailer(orderNumber: viewModel.display.orderNumber, title: "尾片上传"))) } label: { navigationRow("尾片上传") } if refundViewModel.canRefund(item) { Button { refundViewModel.begin(item: item) showRefundSheet = true } label: { navigationRow("退款") } } } } .listStyle(.insetGrouped) .scrollContentBackground(.hidden) .background(Color(hex: 0xF5F5F5).ignoresSafeArea()) .navigationTitle("订单详情") .navigationBarTitleDisplayMode(.inline) .task { await globalLoading.withLoading(message: "加载详情中...") { await viewModel.load(api: ordersAPI, fallbackStoreId: accountContext.currentStore?.id) } } .alert("提示", isPresented: errorBinding) { Button("知道了", role: .cancel) {} } message: { Text(viewModel.errorMessage ?? "") } .sheet(isPresented: $showRefundSheet) { OrderRefundSheet( item: item, viewModel: refundViewModel, onCancel: { showRefundSheet = false }, onSubmit: { Task { await submitRefund() } } ) .presentationDetents([.medium, .large]) } } private var displayPayTime: String { let payTime = viewModel.display.payTime if payTime.isEmpty || payTime == "0" || payTime.hasPrefix("1970") { return item.depositPayTime } return payTime } private var errorBinding: Binding { Binding( get: { viewModel.errorMessage != nil }, set: { visible in if !visible { viewModel.clearError() } } ) } /// 构建可跳转业务入口行。 private func navigationRow(_ label: String) -> some View { HStack { Text(label) Spacer() Image(systemName: "chevron.right") .font(.system(size: AppMetrics.FontSize.caption, weight: .semibold)) .foregroundStyle(AppDesign.textSecondary) } } /// 提交普通订单退款并刷新详情。 private func submitRefund() async { let success = await refundViewModel.submit(api: ordersAPI, item: item) guard success else { return } if let referralOrderId = item.referralOrder?.id, referralOrderId > 0 { Task { try? await cooperationOrderAPI.saleUserUpdateReferralOrderStatus(id: referralOrderId, status: 3) } } showRefundSheet = false toastCenter.show("退款申请已提交") await viewModel.load(api: ordersAPI, fallbackStoreId: accountContext.currentStore?.id, forceReload: true) } /// 将空金额转换为 0,避免详情页出现孤立货币符号。 private func emptyToZero(_ value: String) -> String { value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "0" : value } } /// 普通订单退款表单,负责收集退款模式、金额和原因。 private struct OrderRefundSheet: View { let item: OrderEntity @ObservedObject var viewModel: OrderRefundViewModel let onCancel: () -> Void let onSubmit: () -> Void @FocusState private var focusedField: RefundInputField? var body: some View { NavigationStack { Form { Section("订单") { OrderDetailRow(title: "订单号", value: item.orderNumber) OrderDetailRow(title: "可退金额", value: "¥\(viewModel.availableAmountText(for: item))") } Section("退款方式") { Picker("退款方式", selection: $viewModel.mode) { ForEach(OrderRefundMode.allCases) { mode in Text(mode.title).tag(mode) } } .pickerStyle(.segmented) if viewModel.mode == .partial { TextField("请输入退款金额", text: $viewModel.amount) .keyboardType(.decimalPad) .focused($focusedField, equals: .amount) } } Section("退款原因") { TextEditor(text: $viewModel.reason) .frame(minHeight: 96) .focused($focusedField, equals: .reason) } if let errorMessage = viewModel.errorMessage { Section { Text(errorMessage) .font(.system(size: AppMetrics.FontSize.footnote)) .foregroundStyle(Color(hex: 0xEF4444)) } } } .navigationTitle("订单退款") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("取消", action: onCancel) .disabled(viewModel.submitting) } ToolbarItem(placement: .confirmationAction) { Button(viewModel.submitting ? "提交中..." : "提交", action: onSubmit) .disabled(viewModel.submitting) } ToolbarItemGroup(placement: .keyboard) { Spacer() Button("完成") { focusedField = nil } } } } } } /// 退款表单当前聚焦字段。 private enum RefundInputField: Hashable { case amount case reason } /// 核销订单详情页,展示核销列表项信息并支持再次发起核销。 struct WriteOffOrderDetailView: View { @EnvironmentObject private var accountContext: AccountContext @Environment(\.ordersAPI) private var ordersAPI @EnvironmentObject private var toastCenter: ToastCenter let item: WriteOffOrderItem @State private var verifying = false @State private var showVerifyConfirmation = false var body: some View { List { Section("核销订单") { OrderDetailRow(title: "订单号", value: item.orderNumber) Button("复制订单号") { UIPasteboard.general.string = item.orderNumber toastCenter.show("订单号已复制") } .font(.system(size: AppMetrics.FontSize.footnote, weight: .semibold)) .foregroundStyle(AppDesign.primary) OrderDetailRow(title: "项目名称", value: item.projectName) OrderDetailRow(title: "核销状态", value: displayVerificationStatus) OrderDetailRow(title: "订单状态", value: item.orderStatusName) OrderDetailRow(title: "订单金额", value: "¥\(item.orderAmount)") OrderDetailRow(title: "手机号", value: item.userPhone) OrderDetailRow(title: "支付时间", value: item.payTime) OrderDetailRow(title: "核销时间", value: item.orderVerificationTime) } Section { Button(verifying ? "核销中..." : "确认核销") { showVerifyConfirmation = true } .frame(maxWidth: .infinity, alignment: .center) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(.white) .listRowBackground(canVerify ? AppDesign.primary : Color(hex: 0x9CA3AF)) .disabled(!canVerify || verifying) } } .listStyle(.insetGrouped) .scrollContentBackground(.hidden) .background(Color(hex: 0xF5F5F5).ignoresSafeArea()) .navigationTitle("核销订单详情") .navigationBarTitleDisplayMode(.inline) .confirmationDialog("确认核销该订单?", isPresented: $showVerifyConfirmation, titleVisibility: .visible) { Button("确认核销") { Task { await verify() } } Button("取消", role: .cancel) {} } message: { Text(""" 订单号:\(item.orderNumber) 项目:\(item.projectName.isEmpty ? "--" : item.projectName) 金额:¥\(item.orderAmount) """) } } private var displayVerificationStatus: String { let text = item.orderVerificationStatus.trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? "待核销" : text } private var canVerify: Bool { !displayVerificationStatus.contains("已核销") && !displayVerificationStatus.contains("已退款") && !displayVerificationStatus.contains("已完成") } /// 调用核销接口并提示结果。 private func verify() async { guard !verifying else { return } guard accountContext.currentScenic?.id != nil else { toastCenter.show("缺少景区上下文") return } verifying = true defer { verifying = false } do { try await ordersAPI.writeOff(orderNumber: item.orderNumber) toastCenter.show("核销成功") } catch { toastCenter.show(error.localizedDescription) } } } /// 订单详情键值行,统一二级页面的字段排版。 private struct OrderDetailRow: View { let title: String let value: String var body: some View { HStack(alignment: .top, spacing: AppMetrics.Spacing.small) { Text(title) .font(.system(size: AppMetrics.FontSize.subheadline)) .foregroundStyle(AppDesign.textSecondary) .frame(width: 76, alignment: .leading) Spacer(minLength: AppMetrics.Spacing.small) Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "--" : value) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium)) .foregroundStyle(AppDesign.textPrimary) .multilineTextAlignment(.trailing) } } } /// 拍摄点展示行,表示单个打卡点的拍摄人员、状态和评分。 private struct ShootingPointRow: View { let item: StoreOrderShootingListItem var body: some View { VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) { HStack { Text(title) .font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold)) .foregroundStyle(AppDesign.textPrimary) Spacer() Text(statusText) .font(.system(size: AppMetrics.FontSize.caption, weight: .medium)) .foregroundStyle(item.status == 2 ? Color(hex: 0x22C55E) : AppDesign.primary) } OrderDetailRow(title: "摄影师", value: item.photogName) OrderDetailRow(title: "评分", value: String(format: "%.1f", max(item.startAvg, item.start))) } .padding(.vertical, AppMetrics.Spacing.xxSmall) } private var title: String { let value = item.scenicSpotName.isEmpty ? item.staffName : item.scenicSpotName return value.isEmpty ? "未命名打卡点" : value } private var statusText: String { switch item.status { case 1: return "待拍摄" case 2: return "已拍摄" default: return "未知" } } }