Files
suixinkan_ios_new/suixinkan/Features/ScenicSettlement/ViewModels/ScenicSettlementViewModels.swift
汉秋 26f4d0e671 Migrate from iOS 17 Observation to iOS 16-compatible Combine architecture.
Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

263 lines
8.2 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.

//
// ScenicSettlementViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/25.
//
import Foundation
import Combine
import SwiftUI
@MainActor
/// ViewModel
final class ScenicSettlementViewModel: ObservableObject {
@Published var existingScenics: [BusinessScope] = []
@Published var options: [ScenicSettlementOption] = []
@Published var amountText = ""
@Published var remarkText = ""
@Published var isLoading = false
@Published var isSubmitting = false
@Published var loadFailed = false
@Published var loadFailureReason: String?
@Published var message: String?
@Published private var selectedIds = Set<Int>()
///
var selectedCount: Int {
selectedIds.count
}
/// ID使
var selectedScenicIds: Set<Int> {
selectedIds
}
///
var amountValidation: ScenicSettlementAmountValidation {
let trimmed = amountText.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return .empty }
guard trimmed.range(of: #"^\d+(\.\d{1,2})?$"#, options: .regularExpression) != nil else {
return .invalidFormat
}
guard let decimal = Decimal(string: trimmed), decimal > 0 else {
return .notPositive
}
return .valid
}
///
var canSubmit: Bool {
!isSubmitting && !selectedIds.isEmpty && amountValidation == .valid
}
///
func load(api: any ScenicPermissionServing, existingScenics: [BusinessScope]) async {
isLoading = true
loadFailed = false
loadFailureReason = nil
self.existingScenics = existingScenics
defer { isLoading = false }
do {
let all = try await api.scenicListAll().list
let existingIds = Set(existingScenics.map(\.id))
options = all
.filter { !existingIds.contains($0.id) }
.map { ScenicSettlementOption(id: $0.id, name: $0.name, selected: selectedIds.contains($0.id)) }
} catch {
options = []
selectedIds.removeAll()
loadFailed = true
loadFailureReason = error.localizedDescription
message = "可申请景区加载失败,请重试"
}
}
///
func toggleScenic(id: Int) {
guard let index = options.firstIndex(where: { $0.id == id }) else { return }
options[index].selected.toggle()
if options[index].selected {
selectedIds.insert(id)
} else {
selectedIds.remove(id)
}
}
/// ID
func submit(api: any ScenicSettlementServing) async -> Bool {
guard !isSubmitting else { return false }
guard canSubmit else {
message = validationMessage
return false
}
isSubmitting = true
defer { isSubmitting = false }
let amount = normalizedAmount
let remark = remarkText.trimmingCharacters(in: .whitespacesAndNewlines)
do {
for scenicId in selectedIds.sorted() {
try await api.scenicSettlementSubmit(
ScenicSettlementSubmitRequest(
scenicId: scenicId,
applyAmount: amount,
applyRemark: remark
)
)
}
selectedIds.removeAll()
options = options.map { option in
ScenicSettlementOption(id: option.id, name: option.name, selected: false)
}
amountText = ""
remarkText = ""
message = "提交成功,等待审核"
return true
} catch {
message = error.localizedDescription
return false
}
}
private var normalizedAmount: String {
let decimal = Decimal(string: amountText.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
return String(format: "%.2f", NSDecimalNumber(decimal: decimal).doubleValue)
}
private var validationMessage: String {
if selectedIds.isEmpty { return "请先选择景区" }
switch amountValidation {
case .empty:
return "请填写结算金额"
case .invalidFormat:
return "金额格式错误,最多支持两位小数"
case .notPositive:
return "金额需大于 0"
case .valid:
return ""
}
}
}
///
enum ScenicSettlementAmountValidation: Equatable {
case empty
case invalidFormat
case notPositive
case valid
///
var badgeText: String {
switch self {
case .empty: "待填写金额"
case .invalidFormat: "金额格式错误"
case .notPositive: "金额需大于 0"
case .valid: "金额有效"
}
}
///
var badgeColor: Color {
switch self {
case .empty, .notPositive:
return AppDesign.warning
case .invalidFormat:
return Color(hex: 0xDC2626)
case .valid:
return AppDesign.success
}
}
}
@MainActor
/// ViewModel
final class ScenicSettlementReviewViewModel: ObservableObject {
@Published var scenicApplications: [ScenicApplicationPendingResponse] = []
@Published var roleApplications: [RoleApplyPendingResponse] = []
@Published var isLoading = false
@Published var message: String?
@Published var loadFailedAll = false
@Published var scenicLoadFailed = false
@Published var roleLoadFailed = false
///
var pendingCount: Int {
scenicApplications.filter { $0.status == 1 }.count + roleApplications.filter { $0.status == 1 }.count
}
///
func load(api: any ScenicPermissionServing) async {
isLoading = true
loadFailedAll = false
scenicLoadFailed = false
roleLoadFailed = false
message = nil
defer { isLoading = false }
var errors: [String] = []
var scenicLoaded = false
var roleLoaded = false
do {
scenicApplications = try await api.scenicApplicationPendingAll().items
scenicLoaded = true
} catch {
scenicApplications = []
scenicLoadFailed = true
errors.append("景区申请记录加载失败")
}
do {
roleApplications = try await api.roleApplyAll()
roleLoaded = true
} catch {
roleApplications = []
roleLoadFailed = true
errors.append("权限申请记录加载失败")
}
loadFailedAll = !scenicLoaded && !roleLoaded
if !errors.isEmpty {
message = errors.joined(separator: "")
}
}
///
func statusText(_ status: Int, fallback: String = "") -> String {
let trimmed = fallback.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { return trimmed }
switch status {
case 1: return "待审核"
case 2: return "已通过"
case 3: return "已驳回"
case 9: return "已取消"
default: return "未知"
}
}
///
func statusIcon(_ status: Int) -> String {
switch status {
case 2: return "checkmark.circle.fill"
case 3: return "xmark.octagon.fill"
case 1: return "clock.badge.exclamationmark.fill"
default: return "questionmark.circle.fill"
}
}
///
func statusColor(_ status: Int) -> Color {
switch status {
case 2: return AppDesign.success
case 3: return Color(hex: 0xDC2626)
case 1: return AppDesign.warning
default: return AppDesign.textSecondary
}
}
}