Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -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
}
}
}