重构排队设置与变更日志交互,补充 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")
|
|
}
|
|
}
|