Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
48
suixinkan_ios/Features/Invite/API/InviteAPI.swift
Normal file
48
suixinkan_ios/Features/Invite/API/InviteAPI.swift
Normal file
@ -0,0 +1,48 @@
|
||||
//
|
||||
// InviteAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 邀请服务协议,抽象邀请信息和邀请记录接口。
|
||||
@MainActor
|
||||
protocol InviteServing {
|
||||
/// 获取邀请二维码、邀请码和规则信息。
|
||||
func inviteInfo() async throws -> InviteInfoResponse
|
||||
|
||||
/// 获取邀请用户列表。
|
||||
func inviteUserList(page: Int, pageSize: Int) async throws -> [InviteUserItem]
|
||||
}
|
||||
|
||||
/// 邀请 API,封装摄影师邀请相关接口。
|
||||
@MainActor
|
||||
final class InviteAPI: InviteServing {
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化邀请 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 获取邀请二维码、邀请码和规则信息。
|
||||
func inviteInfo() async throws -> InviteInfoResponse {
|
||||
try await client.send(APIRequest(method: .get, path: "/api/yf-handset-app/photog/invite-info"))
|
||||
}
|
||||
|
||||
/// 获取邀请用户列表。
|
||||
func inviteUserList(page: Int = 1, pageSize: Int = 20) async throws -> [InviteUserItem] {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/invite/user-list",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))")
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
30
suixinkan_ios/Features/Invite/Invite.md
Normal file
30
suixinkan_ios/Features/Invite/Invite.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Invite 模块业务逻辑
|
||||
|
||||
## 模块职责
|
||||
|
||||
Invite 模块负责首页 `registration_invitation`、`photographer_invite` 和 `invite_record` 入口。
|
||||
|
||||
邀请页展示邀请码、邀请链接、二维码和规则;邀请记录页展示邀请用户和奖励记录。模块不保存业务状态到全局对象。
|
||||
|
||||
## 邀请页
|
||||
|
||||
`PhotographerInviteViewModel` 通过 `InviteAPI.inviteInfo()` 获取邀请信息,并使用 CoreImage 在本地生成二维码图片。二维码图片只用于当前页面展示,不落盘。
|
||||
|
||||
复制邀请链接时只由 View 层写入剪贴板,ViewModel 不关心系统剪贴板状态。
|
||||
|
||||
## 邀请记录
|
||||
|
||||
`InviteRecordViewModel` 提供“邀请记录 / 奖励记录”分段:
|
||||
- 邀请记录来自 `InviteAPI.inviteUserList`。
|
||||
- 奖励记录复用 `WalletAPI.walletEarningDetail`。
|
||||
- 顶部奖励汇总复用 `WalletAPI.walletSummary(type: 2)`。
|
||||
|
||||
分页状态、筛选分段和展示行只存在 ViewModel 内存中。
|
||||
|
||||
## 接口边界
|
||||
|
||||
`InviteAPI` 只封装邀请信息和邀请用户列表。钱包收益和提现入口继续由 Wallet 模块负责,Invite 模块只调用其公开服务协议,不持有钱包业务状态。
|
||||
|
||||
## 缓存边界
|
||||
|
||||
邀请码、邀请记录、奖励记录和二维码都不落盘。后续如果需要分享图片,应在分享流程中临时生成。
|
||||
156
suixinkan_ios/Features/Invite/Models/InviteModels.swift
Normal file
156
suixinkan_ios/Features/Invite/Models/InviteModels.swift
Normal file
@ -0,0 +1,156 @@
|
||||
//
|
||||
// InviteModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 邀请信息响应实体,表示邀请码、邀请链接和规则文案。
|
||||
struct InviteInfoResponse: Decodable, Equatable {
|
||||
let enableInvite: Bool
|
||||
let inviteCode: String
|
||||
let inviteUrl: String
|
||||
let description: [String]
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case enableInvite = "enable_invite"
|
||||
case inviteCode = "invite_code"
|
||||
case inviteUrl = "invite_url"
|
||||
case description
|
||||
}
|
||||
|
||||
/// 宽松解码邀请信息。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
enableInvite = try container.decodeInviteLossyBool(forKey: .enableInvite) ?? true
|
||||
inviteCode = try container.decodeInviteLossyString(forKey: .inviteCode)
|
||||
inviteUrl = try container.decodeInviteLossyString(forKey: .inviteUrl)
|
||||
description = (try? container.decodeIfPresent([String].self, forKey: .description)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请用户实体,表示邀请记录里的摄影师。
|
||||
struct InviteUserItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let realName: String
|
||||
let phone: String
|
||||
let avatar: String
|
||||
let createdAt: String
|
||||
let inviteLevel: Int
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case realName = "real_name"
|
||||
case phone
|
||||
case avatar
|
||||
case createdAt = "created_at"
|
||||
case inviteLevel = "invite_level"
|
||||
}
|
||||
|
||||
/// 宽松解码邀请用户字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeInviteLossyInt(forKey: .id) ?? 0
|
||||
realName = try container.decodeInviteLossyString(forKey: .realName)
|
||||
phone = try container.decodeInviteLossyString(forKey: .phone)
|
||||
avatar = try container.decodeInviteLossyString(forKey: .avatar)
|
||||
createdAt = try container.decodeInviteLossyString(forKey: .createdAt)
|
||||
inviteLevel = try container.decodeInviteLossyInt(forKey: .inviteLevel) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请记录页面展示行实体,统一邀请用户和奖励明细的展示字段。
|
||||
struct InviteDisplayRow: Identifiable, Equatable {
|
||||
let id: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let amount: String
|
||||
let time: String
|
||||
let phone: String
|
||||
let avatar: String
|
||||
let inviteLevel: Int
|
||||
|
||||
/// 邀请层级文案。
|
||||
var levelText: String {
|
||||
inviteLevel == 1 ? "一级" : "二级"
|
||||
}
|
||||
|
||||
/// 头像占位文案。
|
||||
var avatarPlaceholder: String {
|
||||
if let first = title.first {
|
||||
return String(first)
|
||||
}
|
||||
if let first = phone.first {
|
||||
return String(first)
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请记录分段类型。
|
||||
enum InviteRecordTab: CaseIterable, Equatable {
|
||||
case invite
|
||||
case reward
|
||||
|
||||
/// 分段标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .invite: return "邀请记录"
|
||||
case .reward: return "奖励记录"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeInviteLossyString(forKey key: Key) throws -> String {
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value ? "1" : "0"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 将 String、Double 和 Int 宽松解码为 Int。
|
||||
func decodeInviteLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Int(text) ?? Double(text).map(Int.init)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 String、数字和 Bool 宽松解码为 Bool。
|
||||
func decodeInviteLossyBool(forKey key: Key) throws -> Bool? {
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value != 0
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if ["1", "true", "yes"].contains(text) { return true }
|
||||
if ["0", "false", "no"].contains(text) { return false }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
//
|
||||
// InviteViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 摄影师邀请页。
|
||||
final class PhotographerInviteViewController: UIViewController {
|
||||
private let services = AppServices.shared
|
||||
private let viewModel = PhotographerInviteViewModel()
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let codeLabel = UILabel()
|
||||
private let urlLabel = UILabel()
|
||||
private let qrImageView = UIImageView()
|
||||
private let rulesLabel = UILabel()
|
||||
private let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "邀请摄影师"
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||||
setupUI()
|
||||
viewModel.onChange = { [weak self] in self?.render() }
|
||||
Task { await viewModel.reload(api: services.inviteAPI) }
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
navigationItem.rightBarButtonItems = [
|
||||
UIBarButtonItem(title: "复制码", style: .plain, target: self, action: #selector(copyCode)),
|
||||
UIBarButtonItem(title: "复制链接", style: .plain, target: self, action: #selector(copyURL))
|
||||
]
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 12
|
||||
scrollView.addSubview(contentStack)
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(activityIndicator)
|
||||
|
||||
scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.width.equalTo(scrollView.snp.width).offset(-32)
|
||||
}
|
||||
activityIndicator.snp.makeConstraints { $0.center.equalToSuperview() }
|
||||
|
||||
codeLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
||||
urlLabel.font = .systemFont(ofSize: 14)
|
||||
urlLabel.numberOfLines = 0
|
||||
urlLabel.textColor = AppDesign.textSecondary
|
||||
qrImageView.contentMode = .scaleAspectFit
|
||||
qrImageView.snp.makeConstraints { $0.height.equalTo(220) }
|
||||
rulesLabel.numberOfLines = 0
|
||||
rulesLabel.font = .systemFont(ofSize: 14)
|
||||
rulesLabel.textColor = AppDesign.textSecondary
|
||||
|
||||
contentStack.addArrangedSubview(codeLabel)
|
||||
contentStack.addArrangedSubview(urlLabel)
|
||||
contentStack.addArrangedSubview(qrImageView)
|
||||
contentStack.addArrangedSubview(rulesLabel)
|
||||
}
|
||||
|
||||
private func render() {
|
||||
activityIndicator.isHidden = !viewModel.loading
|
||||
if viewModel.loading { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() }
|
||||
codeLabel.text = viewModel.inviteCode.isEmpty ? "邀请码加载中..." : "邀请码:\(viewModel.inviteCode)"
|
||||
urlLabel.text = viewModel.inviteUrl
|
||||
qrImageView.image = viewModel.qrImage
|
||||
rulesLabel.text = viewModel.rules.joined(separator: "\n")
|
||||
if let error = viewModel.errorMessage {
|
||||
rulesLabel.text = (rulesLabel.text ?? "") + "\n\n" + error
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func copyCode() { viewModel.copyInviteCode() }
|
||||
@objc private func copyURL() { viewModel.copyInviteUrl() }
|
||||
}
|
||||
|
||||
extension PhotographerInviteViewModel: ViewModelBindable {}
|
||||
|
||||
/// 邀请记录页。
|
||||
final class InviteRecordViewController: ModuleTableViewController {
|
||||
private let viewModel = InviteRecordViewModel()
|
||||
private let summaryLabel = UILabel()
|
||||
|
||||
override func viewDidLoad() {
|
||||
title = "邀请记录"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "奖励明细",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(toggleTab)
|
||||
)
|
||||
super.viewDidLoad()
|
||||
setupHeader()
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.updateSummary()
|
||||
self?.reloadTable()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupHeader() {
|
||||
summaryLabel.font = .systemFont(ofSize: 14)
|
||||
summaryLabel.textColor = AppDesign.textSecondary
|
||||
summaryLabel.numberOfLines = 0
|
||||
summaryLabel.textAlignment = .center
|
||||
summaryLabel.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 56)
|
||||
tableView.tableHeaderView = summaryLabel
|
||||
}
|
||||
|
||||
override func tableRowCount() -> Int {
|
||||
viewModel.displayRows.count
|
||||
}
|
||||
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let row = viewModel.displayRows[indexPath.row]
|
||||
cell.configure(
|
||||
title: row.title,
|
||||
subtitle: row.subtitle,
|
||||
detail: row.amount.isEmpty ? row.time : "\(row.amount) · \(row.time)"
|
||||
)
|
||||
}
|
||||
|
||||
override func reloadContent() async {
|
||||
await viewModel.reload(
|
||||
inviteAPI: services.inviteAPI,
|
||||
walletAPI: services.walletAPI,
|
||||
refresh: true
|
||||
)
|
||||
updateSummary()
|
||||
}
|
||||
|
||||
private func updateSummary() {
|
||||
summaryLabel.text = "累计奖励 \(viewModel.totalRewardText) · 可提现 \(viewModel.withdrawableText)"
|
||||
navigationItem.rightBarButtonItem?.title = viewModel.tab == .invite ? "奖励明细" : "邀请用户"
|
||||
}
|
||||
|
||||
@objc private func toggleTab() {
|
||||
Task {
|
||||
let next: InviteRecordTab = viewModel.tab == .invite ? .reward : .invite
|
||||
await viewModel.selectTab(next, inviteAPI: services.inviteAPI, walletAPI: services.walletAPI)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
suixinkan_ios/Features/Invite/ViewModels/InviteViewModels.swift
Normal file
203
suixinkan_ios/Features/Invite/ViewModels/InviteViewModels.swift
Normal file
@ -0,0 +1,203 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user