Files
suixinkan_ios_new/suixinkan/Features/Invite/Views/InviteViews.swift
汉秋 a528d9b0d7 接入微信分享 SDK 并配置 Universal Link。
集成 WechatOpenSDK、www.zhifly.cn/suixinkan/ 通用链接与邀请页分享能力,附带 AASA 部署文件与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:02:37 +08:00

300 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) private var inviteAPI
@EnvironmentObject private var toastCenter: ToastCenter
@Environment(\.globalLoading) private var globalLoading
@StateObject private var viewModel = PhotographerInviteViewModel()
@State private var isPresentingShareSheet = false
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) }
.sheet(isPresented: $isPresentingShareSheet) {
WeChatShareSheet(
title: "邀请你加入随心瞰商家版",
description: viewModel.inviteCode.isEmpty ? "摄影师邀请" : "邀请码:\(viewModel.inviteCode)",
url: viewModel.inviteUrl,
thumbImage: viewModel.qrImage
) { message in
toastCenter.show(message)
}
.presentationDetents([.medium])
}
}
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 {
guard !viewModel.inviteUrl.isEmpty else {
toastCenter.show("邀请链接加载中,请稍后重试")
return
}
isPresentingShareSheet = true
} 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) private var inviteAPI
@Environment(\.walletAPI) private var walletAPI
@Environment(\.globalLoading) private var globalLoading
@StateObject 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 {
AppContentUnavailableView("暂无记录", 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)
}
}
}