505 lines
17 KiB
Swift
505 lines
17 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|