Files
suixinkan_ios_new/suixinkan/Features/Invite/Views/InviteViews.swift
汉秋 311a70d610 新增项目、排期与邀请模块,并接入首页路由
将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 15:57:15 +08:00

285 lines
12 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.

//
// InviteViews.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import SwiftUI
///
struct PhotographerInviteView: View {
@Environment(InviteAPI.self) private var inviteAPI
@Environment(ToastCenter.self) private var toastCenter
@Environment(\.globalLoading) private var globalLoading
@State private var viewModel = PhotographerInviteViewModel()
var body: some View {
ScrollView {
VStack(spacing: AppMetrics.Spacing.medium) {
inviteCard
ruleCard
benefitCard
NavigationLink(value: HomeRoute.inviteRecord) {
Label("邀请记录", systemImage: "calendar.badge.clock")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
}
.buttonStyle(.plain)
}
.padding(AppMetrics.Spacing.medium)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("摄影师邀请")
.navigationBarTitleDisplayMode(.inline)
.task { await reload(showLoading: true) }
.refreshable { await reload(showLoading: false) }
}
private var inviteCard: some View {
VStack(spacing: AppMetrics.Spacing.medium) {
if let qrImage = viewModel.qrImage {
Image(uiImage: qrImage)
.resizable()
.interpolation(.none)
.scaledToFit()
.frame(width: 240, height: 240)
} else {
Image(systemName: "qrcode")
.font(.system(size: 72))
.foregroundStyle(AppDesign.textSecondary)
.frame(width: 240, height: 240)
.background(Color(hex: 0xF3F4F6), in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
HStack {
Text("邀请码")
.foregroundStyle(AppDesign.textSecondary)
Text(viewModel.inviteCode.isEmpty ? "--" : viewModel.inviteCode)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.primary)
Spacer()
Button("复制") {
viewModel.copyInviteCode()
toastCenter.show("邀请码已复制")
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
}
HStack(spacing: AppMetrics.Spacing.small) {
Button {
viewModel.copyInviteUrl()
toastCenter.show("邀请链接已复制")
} label: {
Label("复制链接", systemImage: "link")
.frame(maxWidth: .infinity)
.frame(height: 44)
}
Button {
viewModel.copyInviteUrl()
toastCenter.show("链接已复制,可粘贴分享")
} label: {
Label("分享", systemImage: "square.and.arrow.up")
.frame(maxWidth: .infinity)
.frame(height: 44)
}
}
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .semibold))
.foregroundStyle(.white)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var ruleCard: some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
Text("收益规则")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
let rules = viewModel.rules.isEmpty ? ["直接邀请将获得订单奖励", "间接邀请将获得额外奖励", "具体规则以后端配置为准"] : viewModel.rules
ForEach(Array(rules.enumerated()), id: \.offset) { index, rule in
HStack(alignment: .top, spacing: AppMetrics.Spacing.small) {
Text("\(index + 1)")
.font(.system(size: AppMetrics.FontSize.caption, weight: .bold))
.foregroundStyle(.white)
.frame(width: 22, height: 22)
.background(AppDesign.primary, in: Circle())
Text(rule)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textPrimary)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private var benefitCard: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: AppMetrics.Spacing.small) {
ForEach(["免费剪辑修图", "免费云盘相册", "免费线上流量", "景区运营身份"], id: \.self) { title in
Text(title)
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium))
.foregroundStyle(AppDesign.primary)
.frame(maxWidth: .infinity)
.frame(height: 54)
.background(AppDesign.primarySoft, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.input))
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
}
private func reload(showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading) {
await viewModel.reload(api: inviteAPI)
}
}
}
///
struct InviteRecordView: View {
@Environment(InviteAPI.self) private var inviteAPI
@Environment(WalletAPI.self) private var walletAPI
@Environment(\.globalLoading) private var globalLoading
@State private var viewModel = InviteRecordViewModel()
var body: some View {
VStack(spacing: 0) {
header
Picker("记录类型", selection: $viewModel.tab) {
ForEach(InviteRecordTab.allCases, id: \.self) { tab in
Text(tab.title).tag(tab)
}
}
.pickerStyle(.segmented)
.padding(AppMetrics.Spacing.medium)
.onChange(of: viewModel.tab) { _, newValue in
Task { await viewModel.selectTab(newValue, inviteAPI: inviteAPI, walletAPI: walletAPI) }
}
recordList
NavigationLink(value: HomeRoute.wallet) {
Text("去钱包提现")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.button))
}
.buttonStyle(.plain)
.padding(AppMetrics.Spacing.medium)
.background(.white)
}
.background(Color(hex: 0xF5F7FA))
.navigationTitle("邀请记录")
.navigationBarTitleDisplayMode(.inline)
.task { await reload(refresh: true, showLoading: true) }
}
private var header: some View {
HStack(spacing: 0) {
metric("累计奖励金额", viewModel.totalRewardText)
Rectangle()
.fill(.white.opacity(0.6))
.frame(width: 1, height: 54)
metric("可提现金额", viewModel.withdrawableText)
}
.padding(.vertical, AppMetrics.Spacing.large)
.background(AppDesign.primary)
}
private func metric(_ title: String, _ value: String) -> some View {
VStack(spacing: 6) {
Text(title)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(.white.opacity(0.78))
Text(value)
.font(.system(size: AppMetrics.FontSize.title2, weight: .semibold))
.foregroundStyle(.white)
}
.frame(maxWidth: .infinity)
}
private var recordList: some View {
ScrollView {
LazyVStack(spacing: 0) {
if viewModel.displayRows.isEmpty && !viewModel.loading {
ContentUnavailableView("暂无记录", systemImage: "tray")
.frame(minHeight: 280)
}
ForEach(viewModel.displayRows) { row in
InviteRecordRow(row: row, tab: viewModel.tab)
}
if viewModel.hasMore {
ProgressView()
.padding(AppMetrics.Spacing.medium)
.onAppear {
Task { await reload(refresh: false, showLoading: false) }
}
}
}
.padding(.horizontal, AppMetrics.Spacing.medium)
}
.refreshable { await reload(refresh: true, showLoading: false) }
}
private func reload(refresh: Bool, showLoading: Bool) async {
await globalLoading.withOptionalLoading(showLoading && refresh) {
await viewModel.reload(inviteAPI: inviteAPI, walletAPI: walletAPI, refresh: refresh)
}
}
}
///
private struct InviteRecordRow: View {
let row: InviteDisplayRow
let tab: InviteRecordTab
var body: some View {
HStack(spacing: AppMetrics.Spacing.medium) {
if tab == .invite {
RemoteAvatarImage(urlString: row.avatar, iconSize: 26)
.frame(width: 48, height: 48)
.clipShape(Circle())
} else {
Image(systemName: "yensign.circle.fill")
.font(.system(size: 34))
.foregroundStyle(AppDesign.success)
.frame(width: 48, height: 48)
}
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(row.title)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
if tab == .invite {
Text(row.levelText)
.font(.system(size: AppMetrics.FontSize.caption, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
}
Text(tab == .invite ? row.phone : row.subtitle)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
Text(row.time)
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(AppDesign.textSecondary)
}
Spacer()
if tab == .reward {
Text(row.amount)
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(AppDesign.success)
}
}
.padding(AppMetrics.Spacing.medium)
.background(.white)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color(hex: 0xE5E7EB))
.frame(height: 1)
}
}
}