From b4c1d9074c7fadd54962de72805aaef48e3cc9bd Mon Sep 17 00:00:00 2001 From: zeng Date: Tue, 13 Jan 2026 15:39:15 +0800 Subject: [PATCH] 1 --- Fableon.xcodeproj/project.pbxproj | 4 + .../Object/Libs/Alert/FAPayRetainAlert.swift | 427 +++++++++++++++++- .../Libs/Alert/FAPayRetainAlertData.swift | 74 +++ 3 files changed, 488 insertions(+), 17 deletions(-) create mode 100644 Fableon/Object/Libs/Alert/FAPayRetainAlertData.swift diff --git a/Fableon.xcodeproj/project.pbxproj b/Fableon.xcodeproj/project.pbxproj index ed72432..54e7634 100644 --- a/Fableon.xcodeproj/project.pbxproj +++ b/Fableon.xcodeproj/project.pbxproj @@ -249,6 +249,7 @@ F3074OOM73GNB5015IC89537 /* HDConfigItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3T20B86PXY9HW2Y7L4511N3 /* HDConfigItem.swift */; }; F30E153206675C3SJ3974VL1 /* OJQUnechoSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3J8413H97ZK53EE37TFP314 /* OJQUnechoSectionView.swift */; }; F311250SJ7I3DZ74X009990N /* XJSelectorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39X1FMZ9NDT2QB4U8D373J2 /* XJSelectorCell.swift */; }; + F3140D182F162CFB003DA73F /* FAPayRetainAlertData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3140D172F162CFB003DA73F /* FAPayRetainAlertData.swift */; }; F31451S3X15B941342J922Q3 /* TConfigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3Z059707KM265K231NMO03T /* TConfigCell.swift */; }; F31E84U5Y6YW6430Z54X1K40 /* UCHVion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F360D0Y798545M4486S46K5D /* UCHVion.swift */; }; F31N0S2575E3YC5AOG80WE90 /* TYElyon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36Y0U188W3S4QIX41011QW2 /* TYElyon.swift */; }; @@ -627,6 +628,7 @@ F30R21783575L1F273O32B2I /* DREychainCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DREychainCell.swift; sourceTree = ""; }; F31320M8CX3D775445170788 /* PYMOastCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PYMOastCell.swift; sourceTree = ""; }; F3135K5RQO5DC4S5HWPW523M /* KZOgin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KZOgin.swift; sourceTree = ""; }; + F3140D172F162CFB003DA73F /* FAPayRetainAlertData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAPayRetainAlertData.swift; sourceTree = ""; }; F315105341210LF4533E00H8 /* KCFAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCFAlignment.swift; sourceTree = ""; }; F316495030P82IE60B93U07Q /* ADCheckMageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADCheckMageView.swift; sourceTree = ""; }; F31UNU954Q82475250X9P70C /* LKVRecommendedHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LKVRecommendedHomeView.swift; sourceTree = ""; }; @@ -770,6 +772,7 @@ 03E9A73F2EB49BE6000D1067 /* FAUpdatesAlert.swift */, 03E9A7472EB5D2CB000D1067 /* FALogoutAlert.swift */, 035589322F161E6E00FAEF4A /* FAPayRetainAlert.swift */, + F3140D172F162CFB003DA73F /* FAPayRetainAlertData.swift */, ); path = Alert; sourceTree = ""; @@ -2192,6 +2195,7 @@ 03E239652EAA1945004A8CEC /* SceneDelegate.swift in Sources */, 039CE6092EAA2F71007B5EED /* FAAdjustStateManager.swift in Sources */, F333U95746V7VK13QI9275B3 /* UMenuTransformerCell.swift in Sources */, + F3140D182F162CFB003DA73F /* FAPayRetainAlertData.swift in Sources */, F3Q234J5M18F1Q5G2948P056 /* NPVBoutModalController.swift in Sources */, F38Y7NBX3M0RE4O142264411 /* EZEFlow.swift in Sources */, F363P024H4W1T8LN2546W883 /* YBanner.swift in Sources */, diff --git a/Fableon/Object/Libs/Alert/FAPayRetainAlert.swift b/Fableon/Object/Libs/Alert/FAPayRetainAlert.swift index 7eda7ae..2f2b9ab 100644 --- a/Fableon/Object/Libs/Alert/FAPayRetainAlert.swift +++ b/Fableon/Object/Libs/Alert/FAPayRetainAlert.swift @@ -6,51 +6,444 @@ // import UIKit +import SnapKit class FAPayRetainAlert: FABaseAlert { + private var selectedIndex = FAPayRetainAlertData.defaultSelectedIndex + private lazy var bgView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "pay_retain_bg_image")) + let imageView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.background)) + imageView.contentMode = .scaleToFill + imageView.isUserInteractionEnabled = true return imageView }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - + + private lazy var titleLabel: FALabel = { + let label = FALabel() + 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(0x81CAFF).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() + view.update(text: FAPayRetainAlertData.bonusTitle) + 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.cornerRadius = FAPayRetainAlertData.Layout.cardCornerRadius + contentView.layer.masksToBounds = true + closeButton.layer.cornerRadius = 12 + closeButton.layer.masksToBounds = true + closeButton.layer.borderWidth = 1.3 + closeButton.layer.borderColor = UIColor.fa_hex(0xCACACA).cgColor + closeButton.tintColor = UIColor.fa_hex(0xCACACA) + 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(160) + make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.titleTop) make.centerX.equalToSuperview() make.right.lessThanOrEqualToSuperview().offset(-1) } + + countdownView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(FAPayRetainAlertData.Layout.countdownLeft) + make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.countdownTop) + } + + bonusTagView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(FAPayRetainAlertData.Layout.bonusTagLeft) + make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.bonusTagTop) + make.height.equalTo(FAPayRetainAlertData.Layout.bonusTagHeight) + } + + collectionView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(FAPayRetainAlertData.Layout.packListLeft) + make.top.equalToSuperview().offset(FAPayRetainAlertData.Layout.packListTop) + make.width.equalTo(FAPayRetainAlertData.Layout.packItemSize.width) + make.height.equalTo(FAPayRetainAlertData.Layout.packListHeight) + } + + closeButton.snp.updateConstraints { make in + make.width.height.equalTo(24) + } + + contentView.snp.makeConstraints { make in + make.height.equalTo(FAPayRetainAlertData.Layout.contentHeight) + } + } + +} + +// MARK: - UICollectionViewDataSource & Delegate +extension FAPayRetainAlert: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return FAPayRetainAlertData.packItems.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FAPayRetainPackCell.reuseIdentifier, for: indexPath) as! FAPayRetainPackCell + let item = FAPayRetainAlertData.packItems[indexPath.item] + 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 { + 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() + } + + 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) + } + + private func setupView() { + let mainStack = UIStackView() + mainStack.axis = .horizontal + mainStack.alignment = .center + mainStack.spacing = 5 + + prefixLabel.font = .font(ofSize: 12, weight: .medium).withBoldItalic() + prefixLabel.textColor = UIColor.fa_hex(0x0F0F0F) + + suffixLabel.font = .font(ofSize: 12, weight: .medium).withBoldItalic() + 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() + } + } + + 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 stackView = UIStackView() + 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] + + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = 5 + + titleLabel.font = .font(ofSize: 20, weight: .bold).withBoldItalic() + titleLabel.textColor = .FFFFFF + + [leftStar, titleLabel, rightStar].forEach { stackView.addArrangedSubview($0) } + [leftStar, rightStar].forEach { imageView in + imageView.contentMode = .scaleAspectFit + imageView.snp.makeConstraints { make in + make.width.height.equalTo(9) + } + } + + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(8) + make.right.equalToSuperview().offset(-8) + make.top.equalToSuperview().offset(4) + make.bottom.equalToSuperview().offset(-4) + } + } + } +} + +// MARK: - Pack Cell +final class FAPayRetainPackCell: UICollectionViewCell { + static let reuseIdentifier = "FAPayRetainPackCell" + + private let bgView = FAGradientView() + private let selectedBorderView = UIView() + private let giftImageView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.giftIcon)) + private let coinIconView = UIImageView(image: UIImage(named: FAPayRetainAlertData.Assets.coinIcon)) + private let coinLabel = UILabel() + private let bonusLabel = UILabel() + private let priceView = FAGradientView() + 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: FAPayRetainAlertData.PackItem, isSelected: Bool) { + coinLabel.text = "\(item.coinCount)" + bonusLabel.text = item.bonusText + priceLabel.text = item.priceText + selectedBorderView.isHidden = !isSelected + } + + private func setupView() { + contentView.backgroundColor = .clear + + bgView.fa_colors = [ + UIColor.fa_hex(0xF6F9FF).cgColor, + UIColor.fa_hex(0x4583FF).cgColor + ] + bgView.fa_locations = [0, 1] + bgView.fa_startPoint = .init(x: 0.5, y: 0) + bgView.fa_endPoint = .init(x: 0.5, y: 1) + 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 + + giftImageView.contentMode = .scaleAspectFit + + 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 + + priceView.fa_colors = [ + UIColor.fa_hex(0x3071FF).cgColor, + UIColor.fa_hex(0x4583FF).cgColor + ] + priceView.fa_locations = [0, 1] + priceView.fa_startPoint = .init(x: 0, y: 0.5) + priceView.fa_endPoint = .init(x: 1, y: 0.5) + priceView.layer.cornerRadius = FAPayRetainAlertData.Layout.priceSize.height / 2 + priceView.layer.masksToBounds = true + priceView.layer.borderWidth = 1 + priceView.layer.borderColor = UIColor.fa_hex(0xFFFFFF).cgColor + + 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(giftImageView) + 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() + } + + giftImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(6) + make.centerY.equalToSuperview() + make.width.height.equalTo(FAPayRetainAlertData.Layout.giftSize) + } + + 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) } - } diff --git a/Fableon/Object/Libs/Alert/FAPayRetainAlertData.swift b/Fableon/Object/Libs/Alert/FAPayRetainAlertData.swift new file mode 100644 index 0000000..de41bd0 --- /dev/null +++ b/Fableon/Object/Libs/Alert/FAPayRetainAlertData.swift @@ -0,0 +1,74 @@ +// +// FAPayRetainAlertData.swift +// Fableon +// +// Created by 湖北秦九 on 2026/1/13. +// + +import UIKit + +enum FAPayRetainAlertData { + // 弹窗文案与列表数据统一管理,避免在视图内部硬编码 + static let titleText = "Exclusive Gift" + static let countdownPrefix = "Last" + static let countdownSuffix = "Don't Miss Out!" + static let bonusTitle = "New User Bonus Bundle" + + static let countdown = Countdown(hours: "03", minutes: "29", seconds: "55") + + static let packItems: [PackItem] = [ + PackItem(coinCount: 3500, bonusText: "+700 free coins", priceText: "$4.99"), + PackItem(coinCount: 3500, bonusText: "+700 free coins", priceText: "$4.99"), + PackItem(coinCount: 3500, bonusText: "+700 free coins", priceText: "$4.99") + ] + + static let defaultSelectedIndex = 2 + static let defaultCenterIndex = 1 + + struct Countdown { + let hours: String + let minutes: String + let seconds: String + } + + struct PackItem { + let coinCount: Int + let bonusText: String + let priceText: String + } + + struct Assets { + // 资源名集中管理,便于从 Assets.xcassets 替换 + static let background = "pay_retain_bg_image" + static let giftIcon = "pay_retain_gift" + static let coinIcon = "pay_retain_coin" + static let starIcon = "pay_retain_star" + } + + struct Layout { + // 设计稿对应的尺寸/间距配置 + static let contentWidth: CGFloat = 326 + static let contentHeight: CGFloat = 515 + static let cardCornerRadius: CGFloat = 22 + + static let titleTop: CGFloat = 30 + static let countdownTop: CGFloat = 72 + static let countdownLeft: CGFloat = 10 + static let bonusTagTop: CGFloat = 105 + static let bonusTagLeft: CGFloat = 22 + static let bonusTagHeight: CGFloat = 24 + + static let packListTop: CGFloat = 152 + static let packListLeft: CGFloat = 10 + static let packItemSize = CGSize(width: 306, height: 68) + static let packItemSpacing: CGFloat = 10 + static let packListHeight: CGFloat = 224 + + static let priceSize = CGSize(width: 97, height: 40) + static let priceTop: CGFloat = 14 + static let coinStackLeft: CGFloat = 62 + static let coinStackTop: CGFloat = 15 + static let bonusTopSpacing: CGFloat = 5 + static let giftSize: CGFloat = 46 + } +}