Files
suixinkan_ios_new/suixinkan/Features/Invite/ViewModels/InviteViewModels.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

203 lines
6.8 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.

//
// InviteViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import CoreImage.CIFilterBuiltins
import Foundation
import Combine
import UIKit
/// ViewModel
@MainActor
final class PhotographerInviteViewModel: ObservableObject {
@Published private(set) var loading = false
@Published private(set) var inviteCode = ""
@Published private(set) var inviteUrl = ""
@Published private(set) var rules: [String] = []
@Published private(set) var qrImage: UIImage?
@Published var errorMessage: String?
private let qrContext = CIContext()
private let qrFilter = CIFilter.qrCodeGenerator()
///
func reload(api: any InviteServing) async {
loading = true
errorMessage = nil
defer { loading = false }
do {
let response = try await api.inviteInfo()
inviteCode = response.inviteCode
inviteUrl = response.inviteUrl
rules = response.description
qrImage = makeQrImage(from: response.inviteUrl)
} catch {
clear()
errorMessage = error.localizedDescription
}
}
///
func copyInviteCode() {
UIPasteboard.general.string = inviteCode
}
///
func copyInviteUrl() {
UIPasteboard.general.string = inviteUrl
}
///
func makeQrImage(from text: String) -> UIImage? {
guard !text.isEmpty else { return nil }
qrFilter.setValue(Data(text.utf8), forKey: "inputMessage")
qrFilter.setValue("M", forKey: "inputCorrectionLevel")
guard let output = qrFilter.outputImage else { return nil }
let scaled = output.transformed(by: CGAffineTransform(scaleX: 12, y: 12))
guard let cgImage = qrContext.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
private func clear() {
inviteCode = ""
inviteUrl = ""
rules = []
qrImage = nil
}
}
/// ViewModel
@MainActor
final class InviteRecordViewModel: ObservableObject {
@Published var tab: InviteRecordTab = .invite
@Published private(set) var totalRewardText = "¥ 0.00"
@Published private(set) var withdrawableText = "¥ 0.00"
@Published private(set) var displayRows: [InviteDisplayRow] = []
@Published private(set) var loading = false
@Published private(set) var loadingMore = false
@Published private(set) var hasMore = false
@Published var errorMessage: String?
@Published private var inviteRows: [InviteDisplayRow] = []
@Published private var rewardRows: [InviteDisplayRow] = []
@Published private var invitePage = 1
@Published private var rewardPage = 1
private let invitePageSize = 20
private let rewardPageSize = 10
///
func reload(inviteAPI: any InviteServing, walletAPI: any WalletServing, refresh: Bool) async {
if refresh {
loading = true
} else {
guard hasMore else { return }
loadingMore = true
}
errorMessage = nil
defer {
loading = false
loadingMore = false
}
do {
async let summary = walletAPI.walletSummary(type: 2)
if tab == .invite {
try await loadInviteRows(api: inviteAPI, refresh: refresh)
} else {
try await loadRewardRows(api: walletAPI, refresh: refresh)
}
let summaryValue = try await summary
totalRewardText = "¥ \(summaryValue.amountTotal)"
withdrawableText = "¥ \(summaryValue.amountWithdrawable)"
displayRows = tab == .invite ? inviteRows : rewardRows
} catch {
if refresh {
clear()
}
errorMessage = error.localizedDescription
}
}
///
func selectTab(_ newTab: InviteRecordTab, inviteAPI: any InviteServing, walletAPI: any WalletServing) async {
tab = newTab
await reload(inviteAPI: inviteAPI, walletAPI: walletAPI, refresh: true)
}
private func loadInviteRows(api: any InviteServing, refresh: Bool) async throws {
let users = try await api.inviteUserList(page: refresh ? 1 : invitePage, pageSize: invitePageSize)
let mapped = users.map {
InviteDisplayRow(
id: "invite_\($0.id)",
title: $0.realName.isEmpty ? "**\($0.phone.suffix(4))" : $0.realName,
subtitle: "",
amount: "",
time: $0.createdAt,
phone: $0.phone,
avatar: $0.avatar,
inviteLevel: $0.inviteLevel
)
}
if refresh {
inviteRows = mapped
invitePage = 2
} else {
inviteRows.append(contentsOf: mapped)
invitePage += 1
}
hasMore = users.count >= invitePageSize
}
private func loadRewardRows(api: any WalletServing, refresh: Bool) async throws {
let response = try await api.walletEarningDetail(
startDate: "2025-01-01",
endDate: Self.dayFormatter.string(from: Date()),
page: refresh ? 1 : rewardPage,
pageSize: rewardPageSize
)
let mapped = response.list.flatMap { group in
group.items.map {
InviteDisplayRow(
id: "reward_\($0.id)",
title: $0.typeLabel.isEmpty ? "奖励记录" : $0.typeLabel,
subtitle: $0.withdrawLabel ?? "订单尾号:\($0.orderNumberSuffix.isEmpty ? "--" : $0.orderNumberSuffix)",
amount: $0.amount.isEmpty ? "--" : $0.amount,
time: $0.createdAt.isEmpty ? group.date : $0.createdAt,
phone: "",
avatar: "",
inviteLevel: 0
)
}
}
if refresh {
rewardRows = mapped
rewardPage = 2
} else {
rewardRows.append(contentsOf: mapped)
rewardPage += 1
}
hasMore = rewardRows.count < response.total && !mapped.isEmpty
}
private func clear() {
totalRewardText = "¥ 0.00"
withdrawableText = "¥ 0.00"
displayRows = []
inviteRows = []
rewardRows = []
invitePage = 1
rewardPage = 1
hasMore = false
}
private static let dayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}