// // FAPayRetainAlert.swift // Fableon // // Created by 湖北秦九 on 2026/1/13. // import UIKit import SnapKit import YYText class FAPayRetainAlert: FABaseAlert { var model: FAPayDateModel? { didSet { self.collectionView.reloadData() self.titleLabel.text = model?.retrieve_lang?.title self.countdownView.model = model bonusTagView.update(text: model?.retrieve_lang?.subtitle ?? "") } } private var selectedIndex = FAPayRetainAlertData.defaultSelectedIndex private lazy var bgView: UIImageView = { let imageView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.background)) imageView.contentMode = .scaleToFill imageView.isUserInteractionEnabled = true return imageView }() private lazy var titleLabel: FALabel = { let label = FALabel() label.borderLineWidth = 6 label.borderColor = ._9_ED_0_FF label.text = FAPayRetainAlertData.titleText label.font = .font(ofSize: 44, weight: .init(900)).withBoldItalic() label.textStartPoint = .init(x: 0, y: 0.5) label.textEndPoint = .init(x: 1, y: 0.5) label.textColors = [ UIColor.fa_hex(0x0178FF).cgColor, UIColor.fa_hex(0x0D8AFF).cgColor ] return label }() private lazy var countdownView: PayRetainCountdownView = { let view = PayRetainCountdownView() view.update(prefix: FAPayRetainAlertData.countdownPrefix, countdown: FAPayRetainAlertData.countdown, suffix: FAPayRetainAlertData.countdownSuffix) return view }() private lazy var bonusTagView: PayRetainBonusTagView = { let view = PayRetainBonusTagView() return view }() private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical layout.minimumLineSpacing = FAPayRetainAlertData.Layout.packItemSpacing layout.minimumInteritemSpacing = 0 layout.itemSize = FAPayRetainAlertData.Layout.packItemSize let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.backgroundColor = .clear view.isScrollEnabled = false view.showsVerticalScrollIndicator = false view.dataSource = self view.delegate = self view.register(FAPayRetainPackCell.self, forCellWithReuseIdentifier: FAPayRetainPackCell.reuseIdentifier) return view }() override init(frame: CGRect) { super.init(frame: frame) fa_setupLayout() collectionView.reloadData() } @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension FAPayRetainAlert { private func fa_setupLayout() { contentWidth = FAPayRetainAlertData.Layout.contentWidth contentView.backgroundColor = .clear contentView.layer.cornerRadius = 0 contentView.layer.masksToBounds = true contentView.addSubview(bgView) contentView.addSubview(titleLabel) contentView.addSubview(countdownView) contentView.addSubview(bonusTagView) contentView.addSubview(collectionView) bgView.snp.makeConstraints { make in make.edges.equalToSuperview() } titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(143) make.centerX.equalToSuperview() make.right.lessThanOrEqualToSuperview().offset(0) } countdownView.snp.makeConstraints { make in make.left.equalToSuperview().offset(FAPayRetainAlertData.Layout.countdownLeft) make.top.equalTo(titleLabel.snp.bottom).offset(2) } bonusTagView.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(35) make.centerX.equalToSuperview() make.right.lessThanOrEqualToSuperview().offset(-10) make.height.equalTo(FAPayRetainAlertData.Layout.bonusTagHeight) } collectionView.snp.makeConstraints { make in make.left.equalToSuperview().offset(10) make.right.equalToSuperview().offset(-10) make.top.equalTo(bonusTagView.snp.bottom).offset(15) make.bottom.equalToSuperview().offset(-10) } } } // MARK: - UICollectionViewDataSource & Delegate extension FAPayRetainAlert: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.model?.list_coins?.count ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FAPayRetainPackCell.reuseIdentifier, for: indexPath) as! FAPayRetainPackCell let item = self.model?.list_coins?[indexPath.row] cell.configure(with: item, isSelected: indexPath.item == selectedIndex) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedIndex = indexPath.item collectionView.reloadData() } } // MARK: - Subviews extension FAPayRetainAlert { final class PayRetainCountdownView: UIView { var model: FAPayDateModel? { didSet { prefixLabel.text = model?.retrieve_lang?.remaining_time suffixLabel.text = model?.retrieve_lang?.miss_out } } private static let defaultCountdownSeconds = 4 * 60 * 60 private var countdownTimer: Timer? private var remainingSeconds: Int = 0 private let prefixLabel = UILabel() private let suffixLabel = UILabel() private let timeStackView = UIStackView() private let hourView = TimeBoxView() private let minuteView = TimeBoxView() private let secondView = TimeBoxView() private let colonLabel1 = UILabel() private let colonLabel2 = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupView() } deinit { stopCountdown() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 更新倒计时显示内容 func update(prefix: String, countdown: FAPayRetainAlertData.Countdown, suffix: String) { prefixLabel.text = prefix suffixLabel.text = suffix hourView.setText(countdown.hours) minuteView.setText(countdown.minutes) secondView.setText(countdown.seconds) } override func didMoveToWindow() { super.didMoveToWindow() if window != nil { startCountdown() } else { stopCountdown() } } private func setupView() { let mainStack = UIStackView() mainStack.axis = .horizontal mainStack.alignment = .center mainStack.spacing = 5 prefixLabel.font = .font(ofSize: 12, weight: .medium) prefixLabel.textColor = UIColor.fa_hex(0x0F0F0F) suffixLabel.font = .font(ofSize: 12, weight: .medium) suffixLabel.textColor = UIColor.fa_hex(0x0F0F0F) timeStackView.axis = .horizontal timeStackView.alignment = .center timeStackView.spacing = 3 colonLabel1.font = .font(ofSize: 12, weight: .medium) colonLabel1.textColor = UIColor.fa_hex(0x0F0F0F) colonLabel1.text = ":" colonLabel2.font = .font(ofSize: 12, weight: .medium) colonLabel2.textColor = UIColor.fa_hex(0x0F0F0F) colonLabel2.text = ":" timeStackView.addArrangedSubview(hourView) timeStackView.addArrangedSubview(colonLabel1) timeStackView.addArrangedSubview(minuteView) timeStackView.addArrangedSubview(colonLabel2) timeStackView.addArrangedSubview(secondView) addSubview(mainStack) mainStack.addArrangedSubview(prefixLabel) mainStack.addArrangedSubview(timeStackView) mainStack.addArrangedSubview(suffixLabel) mainStack.snp.makeConstraints { make in make.edges.equalToSuperview() } } // 每次展示时从 4 小时开始倒计时 private func startCountdown() { stopCountdown() remainingSeconds = Self.defaultCountdownSeconds updateTimeLabels() countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: YYTextWeakProxy(target: self), selector: #selector(handleCountdownTimer), userInfo: nil, repeats: true) } private func stopCountdown() { countdownTimer?.invalidate() countdownTimer = nil } @objc private func handleCountdownTimer() { guard remainingSeconds > 0 else { updateTimeLabels() stopCountdown() return } remainingSeconds -= 1 updateTimeLabels() } private func updateTimeLabels() { let hours = remainingSeconds / 3600 let minutes = (remainingSeconds % 3600) / 60 let seconds = remainingSeconds % 60 hourView.setText(String(format: "%02d", hours)) minuteView.setText(String(format: "%02d", minutes)) secondView.setText(String(format: "%02d", seconds)) } private final class TimeBoxView: UIView { private let label = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setText(_ text: String) { label.text = text } private func setupView() { backgroundColor = UIColor.fa_hex(0x0F0F0F) layer.cornerRadius = 4 layer.masksToBounds = true label.font = .font(ofSize: 12, weight: .medium) label.textColor = .FFFFFF label.textAlignment = .center addSubview(label) label.snp.makeConstraints { make in make.center.equalToSuperview() } snp.makeConstraints { make in make.width.height.equalTo(18) } } } } final class PayRetainBonusTagView: UIView { private let leftStar = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.starIcon)) private let rightStar = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.starIcon)) private let titleLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 更新标签文案 func update(text: String) { titleLabel.text = text } private func setupView() { backgroundColor = UIColor.fa_hex(0x20A1FF) layer.cornerRadius = 10 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner] titleLabel.font = .font(ofSize: 20, weight: .bold).withBoldItalic() titleLabel.textColor = .FFFFFF addSubview(leftStar) addSubview(rightStar) addSubview(titleLabel) leftStar.snp.makeConstraints { make in make.centerY.equalToSuperview() make.left.equalToSuperview().offset(8) } titleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.left.equalTo(leftStar.snp.right).offset(5) } rightStar.snp.makeConstraints { make in make.centerY.equalToSuperview() make.left.equalTo(titleLabel.snp.right).offset(5) make.right.equalToSuperview().offset(-8) } } } } // MARK: - Pack Cell final class FAPayRetainPackCell: UICollectionViewCell { static let reuseIdentifier = "FAPayRetainPackCell" private lazy var bgView: UIImageView = { let imageView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.cellBackground)) return imageView }() private let selectedBorderView = UIView() private let coinIconView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.coinIcon)) private let coinLabel = UILabel() private let bonusLabel = UILabel() private lazy var priceView: FAGradientView = { let view = FAGradientView() view.fa_colors = [ UIColor.fa_hex(0x3071FF).cgColor, UIColor.fa_hex(0x6DB6FF).cgColor ] view.fa_locations = [0, 1] view.fa_startPoint = .init(x: 0, y: 0.5) view.fa_endPoint = .init(x: 1, y: 0.5) view.layer.cornerRadius = FAPayRetainAlertData.Layout.priceSize.height / 2 view.layer.masksToBounds = true view.layer.borderWidth = 1 view.layer.borderColor = UIColor.fa_hex(0xFFFFFF).cgColor return view }() private let priceLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 绑定套餐数据和选中状态 func configure(with item: FAPayItem?, isSelected: Bool) { selectedBorderView.isHidden = !isSelected guard let item = item else { return } coinLabel.text = "\(item.coins ?? 0)" bonusLabel.text = "+\((item.ext_info?.max_total_coins_pop ?? 0) - (item.ext_info?.max_total_coins ?? 0)) " + "fableon_free_coins".localized priceLabel.text = "\(item.currency ?? "")\(item.price ?? "")" } private func setupView() { contentView.backgroundColor = .clear bgView.layer.cornerRadius = 15 bgView.layer.masksToBounds = true selectedBorderView.layer.cornerRadius = 15 selectedBorderView.layer.borderWidth = 2 selectedBorderView.layer.borderColor = UIColor.fa_hex(0x1F8FFF).cgColor selectedBorderView.isHidden = true coinIconView.contentMode = .scaleAspectFit coinLabel.font = .font(ofSize: 22, weight: .bold) coinLabel.textColor = UIColor.fa_hex(0x276CFF) bonusLabel.font = .font(ofSize: 12, weight: .medium) bonusLabel.textColor = .FFFFFF priceLabel.font = .font(ofSize: 22, weight: .bold) priceLabel.textColor = .FFFFFF let coinStack = UIStackView(arrangedSubviews: [coinIconView, coinLabel]) coinStack.axis = .horizontal coinStack.alignment = .center coinStack.spacing = 2 contentView.addSubview(bgView) contentView.addSubview(selectedBorderView) bgView.addSubview(coinStack) bgView.addSubview(bonusLabel) bgView.addSubview(priceView) priceView.addSubview(priceLabel) bgView.snp.makeConstraints { make in make.edges.equalToSuperview() } selectedBorderView.snp.makeConstraints { make in make.edges.equalToSuperview() } coinStack.snp.makeConstraints { make in make.left.equalToSuperview().offset(FAPayRetainAlertData.Layout.coinStackLeft) make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.coinStackTop) } coinIconView.snp.makeConstraints { make in make.width.height.equalTo(22) } bonusLabel.snp.makeConstraints { make in make.left.equalTo(coinStack) make.top.equalTo(coinStack.snp.bottom).offset(FAPayRetainAlertData.Layout.bonusTopSpacing) } priceView.snp.makeConstraints { make in make.right.equalToSuperview().offset(-14) make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.priceTop) make.size.equalTo(FAPayRetainAlertData.Layout.priceSize) } priceLabel.snp.makeConstraints { make in make.center.equalToSuperview() } } } private extension UIColor { // 统一十六进制色值转换,便于对齐设计稿 static func fa_hex(_ hex: UInt32, alpha: CGFloat = 1) -> UIColor { let red = CGFloat((hex >> 16) & 0xFF) / 255.0 let green = CGFloat((hex >> 8) & 0xFF) / 255.0 let blue = CGFloat(hex & 0xFF) / 255.0 return UIColor(red: red, green: green, blue: blue, alpha: alpha) } }