// // 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") } }