移除订单 Tab 内门店与核销订单详情页及跳转入口。

列表卡片仅展示摘要,保留订单来源绑定与押金订单详情等其它子流程。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 13:36:27 +08:00
parent 1970572edd
commit c4de3d17d0
9 changed files with 11 additions and 820 deletions

View File

@ -1,429 +0,0 @@
//
// 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<Bool> {
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 "未知"
}
}
}

View File

@ -12,7 +12,6 @@ struct OrdersView: View {
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var permissionContext: PermissionContext
@EnvironmentObject private var appRouter: AppRouter
@EnvironmentObject private var router: RouterPath
@Environment(\.ordersAPI) private var ordersAPI
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
@EnvironmentObject private var toastCenter: ToastCenter
@ -489,10 +488,6 @@ struct OrdersView: View {
.padding(AppMetrics.Spacing.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
.contentShape(Rectangle())
.onTapGesture {
router.navigate(to: .orders(.storeDetail(item)))
}
.accessibilityIdentifier("orders.store.card.\(item.orderNumber)")
}
@ -519,17 +514,6 @@ struct OrdersView: View {
.font(.system(size: AppMetrics.FontSize.title3, weight: .bold))
.foregroundStyle(AppDesign.primary)
Spacer()
Button("详情") {
router.navigate(to: .orders(.writeOffDetail(item)))
}
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
.foregroundStyle(AppDesign.primary)
.padding(.horizontal, AppMetrics.Spacing.small)
.frame(height: 32)
.background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: 8))
.buttonStyle(.plain)
.accessibilityIdentifier("orders.writeoff.detail.\(item.orderNumber)")
Button(verifyButtonTitle(for: item)) {
pendingVerifyOrder = item
pendingVerifyOrderNumber = item.orderNumber
@ -825,7 +809,6 @@ private struct OrdersDateFilterSheet: View {
.environmentObject(AccountContext())
.environmentObject(PermissionContext())
.environmentObject(AppRouter())
.environmentObject(RouterPath())
.environment(\.ordersAPI, OrdersAPI(client: APIClient()))
.environmentObject(ToastCenter())
}