Files
2026-06-26 14:33:31 +08:00

264 lines
8.4 KiB
Swift
Raw Permalink 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 UIKit
@MainActor
/// ViewModel
final class ScenicSettlementViewModel {
var onChange: (() -> Void)?
var existingScenics: [BusinessScope] = [] { didSet { onChange?() } }
var options: [ScenicSettlementOption] = [] { didSet { onChange?() } }
var amountText = "" { didSet { onChange?() } }
var remarkText = "" { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var loadFailed = false { didSet { onChange?() } }
var loadFailureReason: String? { didSet { onChange?() } }
var message: String? { didSet { onChange?() } }
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: UIColor {
switch self {
case .empty, .notPositive:
return AppDesign.warning
case .invalidFormat:
return UIColor(hex: 0xDC2626)
case .valid:
return AppDesign.success
}
}
}
@MainActor
/// ViewModel
final class ScenicSettlementReviewViewModel {
var onChange: (() -> Void)?
var scenicApplications: [ScenicApplicationPendingResponse] = [] { didSet { onChange?() } }
var roleApplications: [RoleApplyPendingResponse] = [] { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var message: String? { didSet { onChange?() } }
var loadFailedAll = false { didSet { onChange?() } }
var scenicLoadFailed = false { didSet { onChange?() } }
var roleLoadFailed = false { didSet { onChange?() } }
///
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) -> UIColor {
switch status {
case 2: return AppDesign.success
case 3: return UIColor(hex: 0xDC2626)
case 1: return AppDesign.warning
default: return AppDesign.textSecondary
}
}
}