Files
suixinkan_ios_new/suixinkan/Features/Orders/Views/OrderDetailViews.swift
汉秋 47848b88f2 基于 Lottie 在关键流程中新增全局 Loading 遮罩
引入带引用计数的 GlobalLoadingCenter,通过 RootView 及主要登录/订单/个人中心页面接入,并添加 Loading 动画资源与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 11:25:56 +08:00

308 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrderDetailViews.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import SwiftUI
///
struct StoreOrderDetailView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(OrdersAPI.self) private var ordersAPI
@Environment(RouterPath.self) private var router
@Environment(ToastCenter.self) private var toastCenter
@Environment(\.globalLoading) private var globalLoading
let item: OrderEntity
@State private var viewModel: OrderDetailViewModel
/// 使 ViewModel
init(item: OrderEntity) {
self.item = item
_viewModel = State(initialValue: 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("后续功能") {
placeholderButton("历史拍摄", title: "历史拍摄")
placeholderButton("任务上传", title: "任务上传")
placeholderButton("视频预告", title: "视频预告")
placeholderButton("退款", title: "订单退款")
}
}
.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 ?? "")
}
}
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<Bool> {
Binding(
get: { viewModel.errorMessage != nil },
set: { visible in
if !visible { viewModel.clearError() }
}
)
}
///
private func placeholderButton(_ label: String, title: String) -> some View {
Button {
router.navigate(to: .placeholder(title: title))
} label: {
HStack {
Text(label)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(AppDesign.textSecondary)
}
}
.foregroundStyle(AppDesign.textPrimary)
}
/// 0
private func emptyToZero(_ value: String) -> String {
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "0" : value
}
}
///
struct WriteOffOrderDetailView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(OrdersAPI.self) private var ordersAPI
@Environment(ToastCenter.self) private var 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 "未知"
}
}
}