feat: add WeChat sharing and invite flow

This commit is contained in:
2026-07-07 16:53:39 +08:00
parent f1937e23d4
commit a80c181bd7
160 changed files with 7557 additions and 849 deletions

View File

@ -0,0 +1,448 @@
//
// PhotographerInviteViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class PhotographerInviteViewController: BaseViewController {
private let viewModel = PhotographerInviteViewModel()
private let inviteAPI: InviteServing
private let scrollView = UIScrollView()
private let contentStack = UIStackView()
private let qrCard = UIView()
private let qrImageView = UIImageView()
private let inviteCodeLabel = UILabel()
private let ruleStack = UIStackView()
///
init(inviteAPI: InviteServing = NetworkServices.shared.inviteAPI) {
self.inviteAPI = inviteAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "摄影师邀请"
}
override func setupUI() {
view.backgroundColor = UIColor(hex: 0xF5F6F8)
scrollView.alwaysBounceVertical = true
scrollView.refreshControl = UIRefreshControl()
contentStack.axis = .vertical
contentStack.spacing = 16
view.addSubview(scrollView)
scrollView.addSubview(contentStack)
contentStack.addArrangedSubview(makeInviteCard())
contentStack.addArrangedSubview(makeRuleCard())
contentStack.addArrangedSubview(makeBenefitCard())
contentStack.addArrangedSubview(makeInviteRecordEntry())
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
contentStack.snp.makeConstraints { make in
make.top.bottom.equalTo(scrollView.contentLayoutGuide).inset(16)
make.leading.trailing.equalTo(scrollView.frameLayoutGuide).inset(16)
make.width.equalTo(scrollView.frameLayoutGuide).offset(-32)
}
}
override func bindActions() {
scrollView.refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
viewModel.onStateChange = { [weak self] in
Task { @MainActor in
self?.applyViewModel()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await viewModel.reload(api: inviteAPI) }
}
private func makeInviteCard() -> UIView {
qrCard.backgroundColor = .white
qrCard.layer.cornerRadius = 12
qrCard.clipsToBounds = true
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.alignment = .fill
qrCard.addSubview(stack)
qrImageView.contentMode = .scaleAspectFit
qrImageView.backgroundColor = .white
qrImageView.image = UIImage(systemName: "qrcode")
qrImageView.tintColor = AppColor.textTertiary
stack.addArrangedSubview(qrImageView)
let codeRow = UIStackView()
codeRow.axis = .horizontal
codeRow.alignment = .center
codeRow.spacing = 8
inviteCodeLabel.font = .systemFont(ofSize: 16)
inviteCodeLabel.textColor = UIColor(hex: 0x4B5563)
inviteCodeLabel.numberOfLines = 1
codeRow.addArrangedSubview(inviteCodeLabel)
let copyButton = UIButton(type: .system)
copyButton.setTitle("复制", for: .normal)
copyButton.setTitleColor(.white, for: .normal)
copyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
copyButton.backgroundColor = AppColor.primary
copyButton.layer.cornerRadius = 6
copyButton.addTarget(self, action: #selector(copyCodeTapped), for: .touchUpInside)
codeRow.addArrangedSubview(copyButton)
copyButton.snp.makeConstraints { make in
make.width.equalTo(64)
make.height.equalTo(28)
}
stack.addArrangedSubview(codeRow)
let actionRow = UIStackView()
actionRow.axis = .horizontal
actionRow.spacing = 12
actionRow.distribution = .fillEqually
let shareButton = InviteIconButton(title: "分享", imageName: "ic_invite_share", backgroundColor: UIColor(hex: 0x07C160))
shareButton.addTarget(self, action: #selector(shareTapped), for: .touchUpInside)
let linkButton = InviteIconButton(title: "复制链接", imageName: "ic_invite_link", backgroundColor: AppColor.primary)
linkButton.addTarget(self, action: #selector(copyLinkTapped), for: .touchUpInside)
actionRow.addArrangedSubview(shareButton)
actionRow.addArrangedSubview(linkButton)
stack.addArrangedSubview(actionRow)
actionRow.snp.makeConstraints { make in
make.height.equalTo(44)
}
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
qrImageView.snp.makeConstraints { make in
make.height.equalTo(280)
}
return qrCard
}
private func makeRuleCard() -> UIView {
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = 12
card.layer.borderWidth = 1
card.layer.borderColor = AppColor.cardOutline.cgColor
let titleLabel = UILabel()
titleLabel.text = "收益规则"
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
titleLabel.textColor = .black
ruleStack.axis = .vertical
ruleStack.spacing = 12
let stack = UIStackView(arrangedSubviews: [titleLabel, ruleStack])
stack.axis = .vertical
stack.spacing = 12
card.addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
return card
}
private func makeBenefitCard() -> UIView {
let card = UIView()
card.backgroundColor = .white
card.layer.cornerRadius = 12
card.clipsToBounds = true
let titleLabel = UILabel()
titleLabel.text = "新人福利"
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
titleLabel.textColor = .black
let grid = UIStackView()
grid.axis = .vertical
grid.spacing = 12
let benefits: [(String, String)] = [
("免费剪辑修图", "ic_invite_benefit_free_editing"),
("免费云盘相册", "ic_invite_benefit_free_cloud"),
("免费线上流量", "ic_free_traffic"),
("景区运营身份", "ic_scenic_identity"),
("接单软件工具", "ic_order_tools"),
("免佣金特权", "ic_no_commission"),
]
for index in stride(from: 0, to: benefits.count, by: 2) {
let row = UIStackView()
row.axis = .horizontal
row.spacing = 12
row.distribution = .fillEqually
row.addArrangedSubview(InviteBenefitView(title: benefits[index].0, imageName: benefits[index].1))
row.addArrangedSubview(InviteBenefitView(title: benefits[index + 1].0, imageName: benefits[index + 1].1))
grid.addArrangedSubview(row)
}
let validityStack = UIStackView()
validityStack.axis = .vertical
validityStack.spacing = 8
["活动有效期: 2026年9月1日", "佣金有效期: 2028年1月1日"].forEach { text in
let label = UILabel()
label.text = text
label.font = .app(.body)
label.textColor = UIColor(hex: 0x7B8EAA)
validityStack.addArrangedSubview(label)
}
let stack = UIStackView(arrangedSubviews: [titleLabel, grid, validityStack])
stack.axis = .vertical
stack.spacing = 16
card.addSubview(stack)
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
return card
}
private func makeInviteRecordEntry() -> UIView {
let container = UIView()
container.backgroundColor = UIColor(hex: 0xF5F6F8)
container.snp.makeConstraints { make in
make.height.equalTo(64)
}
let button = UIButton(type: .system)
button.backgroundColor = .white
button.layer.cornerRadius = 8
button.setTitle("邀请记录", for: .normal)
button.setTitleColor(AppColor.primary, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
button.setImage(UIImage(named: "ic_invite_record")?.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = AppColor.primary
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -6, bottom: 0, right: 6)
button.addTarget(self, action: #selector(inviteRecordTapped), for: .touchUpInside)
container.addSubview(button)
button.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalToSuperview().offset(16)
make.height.equalTo(48)
}
return container
}
@MainActor
private func applyViewModel() {
if viewModel.isLoading {
showLoading()
} else {
hideLoading()
scrollView.refreshControl?.endRefreshing()
}
if let qrImage = viewModel.qrImage {
qrImageView.image = qrImage
}
let attributed = NSMutableAttributedString(
string: "邀请码: ",
attributes: [.foregroundColor: UIColor(hex: 0x4B5563)]
)
attributed.append(
NSAttributedString(
string: viewModel.inviteCode.isEmpty ? "--" : viewModel.inviteCode,
attributes: [.foregroundColor: AppColor.primary]
)
)
inviteCodeLabel.attributedText = attributed
applyRules()
if let message = viewModel.errorMessage {
showToast("获取邀请信息失败: \(message)")
}
}
private func applyRules() {
ruleStack.arrangedSubviews.forEach { view in
ruleStack.removeArrangedSubview(view)
view.removeFromSuperview()
}
viewModel.rules.enumerated().forEach { index, rule in
ruleStack.addArrangedSubview(InviteRuleRow(rule: rule, imageName: index.isMultiple(of: 2) ? "ic_invite_earnings" : "ic_user_benefits"))
}
}
@objc private func refreshPulled() {
Task { await viewModel.reload(api: inviteAPI) }
}
@objc private func copyCodeTapped() {
guard !viewModel.inviteCodeForCopy().isEmpty else {
showToast("邀请码加载中,请稍后重试")
return
}
UIPasteboard.general.string = viewModel.inviteCodeForCopy()
showToast("已复制成功")
}
@objc private func copyLinkTapped() {
guard !viewModel.inviteUrlForCopy().isEmpty else {
showToast("邀请链接加载中,请稍后重试")
return
}
UIPasteboard.general.string = viewModel.inviteUrlForCopy()
showToast("已复制成功")
}
@objc private func shareTapped() {
guard !viewModel.inviteUrl.isEmpty else {
showToast("邀请链接加载中,请稍后重试")
return
}
let payload = WeChatWebPageSharePayload(
title: "邀请你加入随心瞰商家版",
description: viewModel.inviteCode.isEmpty ? "摄影师邀请" : "邀请码:\(viewModel.inviteCode)",
url: viewModel.inviteUrl
)
WeChatShareService.shareWebPage(payload, scene: .session, thumbImage: viewModel.qrImage ?? WeChatShareService.defaultThumbImage()) { [weak self] result in
Task { @MainActor in
self?.handleShareResult(result)
}
}
}
@objc private func inviteRecordTapped() {
navigationController?.pushViewController(InviteRecordViewController(), animated: true)
}
private func handleShareResult(_ result: WeChatShareResult) {
switch result {
case .success:
showToast("已分享到微信好友")
case .cancelled:
showToast("已取消分享")
case .notInstalled:
showToast("未安装微信,无法分享")
case .unsupported:
showToast("当前微信版本不支持分享")
case .invalidPayload(let message):
showToast(message)
case .sendFailed, .unknown:
showToast("分享失败,请稍后重试")
}
}
}
///
private final class InviteIconButton: UIButton {
///
init(title: String, imageName: String, backgroundColor: UIColor) {
super.init(frame: .zero)
self.backgroundColor = backgroundColor
layer.cornerRadius = 8
setTitle(title, for: .normal)
setTitleColor(.white, for: .normal)
setImage(UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate), for: .normal)
tintColor = .white
titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
imageEdgeInsets = UIEdgeInsets(top: 0, left: -3, bottom: 0, right: 3)
titleEdgeInsets = UIEdgeInsets(top: 0, left: 3, bottom: 0, right: -3)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class InviteRuleRow: UIView {
///
init(rule: String, imageName: String) {
super.init(frame: .zero)
let iconView = UIImageView(image: UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate))
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = rule
label.font = .app(.body)
label.textColor = .black
label.numberOfLines = 0
addSubview(iconView)
addSubview(label)
iconView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalToSuperview().offset(2)
make.size.equalTo(16)
}
label.snp.makeConstraints { make in
make.top.bottom.trailing.equalToSuperview()
make.leading.equalTo(iconView.snp.trailing).offset(12)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
private final class InviteBenefitView: UIView {
///
init(title: String, imageName: String) {
super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xEFF6FF)
layer.cornerRadius = 12
let iconView = UIImageView(image: UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate))
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .app(.body)
titleLabel.textColor = .black
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
addSubview(iconView)
addSubview(titleLabel)
snp.makeConstraints { make in
make.height.equalTo(80)
}
iconView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(12)
make.size.equalTo(24)
}
titleLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(8)
make.top.equalTo(iconView.snp.bottom).offset(8)
make.bottom.lessThanOrEqualToSuperview().inset(8)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}