重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
448 lines
16 KiB
Swift
448 lines
16 KiB
Swift
//
|
||
// 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? = nil) {
|
||
self.inviteAPI = inviteAPI ?? NetworkServices.shared.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.setConfigurationImagePlacement(.leading, padding: 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)
|
||
setConfigurationImagePlacement(.leading, padding: 6)
|
||
}
|
||
|
||
@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")
|
||
}
|
||
}
|