Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,263 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user