将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
285 lines
12 KiB
Swift
285 lines
12 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|
||
}
|