Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
7.1 KiB
Swift
208 lines
7.1 KiB
Swift
//
|
||
// InviteViewModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/24.
|
||
//
|
||
|
||
import CoreImage.CIFilterBuiltins
|
||
import Foundation
|
||
import UIKit
|
||
|
||
/// 邀请页 ViewModel,负责加载邀请信息、生成二维码和复制内容。
|
||
@MainActor
|
||
final class PhotographerInviteViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var inviteCode = "" { didSet { onChange?() } }
|
||
private(set) var inviteUrl = "" { didSet { onChange?() } }
|
||
private(set) var rules: [String] = [] { didSet { onChange?() } }
|
||
private(set) var qrImage: UIImage? { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
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 {
|
||
var onChange: (() -> Void)?
|
||
var tab: InviteRecordTab = .invite { didSet { onChange?() } }
|
||
private(set) var totalRewardText = "¥ 0.00" { didSet { onChange?() } }
|
||
private(set) var withdrawableText = "¥ 0.00" { didSet { onChange?() } }
|
||
private(set) var displayRows: [InviteDisplayRow] = [] { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var loadingMore = false { didSet { onChange?() } }
|
||
private(set) var hasMore = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private var inviteRows: [InviteDisplayRow] = []
|
||
private var rewardRows: [InviteDisplayRow] = []
|
||
private var invitePage = 1
|
||
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)
|
||
}
|
||
|
||
/// 加载InviteRows数据。
|
||
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
|
||
}
|
||
|
||
/// 加载RewardRows数据。
|
||
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
|
||
}()
|
||
}
|