// // VipRechargeView.swift // QuickLocation // // Created by 八条 on 2026/6/3. // import UIKit import RxSwift import RxCocoa class VipRechargeView: UIView { var disposeBag = DisposeBag() private func setupRx() { backBtn.rx.tap.subscribe(onNext: { _ in AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) agreementLab.rx.tapGesture.subscribe { _ in // TODO: 打开会员服务协议 }.disposed(by: disposeBag) } private func setupUI() { addSubview(scrollView) scrollView.addSubview(scrollContentView) scrollContentView.addSubview(headerBgImgView) scrollContentView.addSubview(cornerView) scrollContentView.addSubview(expenseCollectionView) scrollContentView.addSubview(vipRightsView) scrollContentView.addSubview(agreementLab) scrollContentView.addSubview(tipsLab) vipRightsView.addSubview(vipRightsTitleView) addSubview(bottomView) bottomView.addSubview(payTypeStackView) bottomView.addSubview(payBtnView) bottomView.addSubview(payPriceView) addSubview(navBarView) navBarView.addSubview(navTitleLabel) addSubview(backBtn) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerY(backBtn) .centerX() backBtn.layoutChain .top(kStatusBarHeight + 12) .left(15) .width(24) .height(24) bottomView.layoutChain .edgesHorzontal() .heightToWidth(144/375) .bottom() payTypeStackView.layoutChain .top(25) .edgesHorzontal(16) .centerX() payBtnView.layoutChain .edgesHorzontal(16) .heightToWidth(50/343) .centerY() payPriceView.layoutChain .left(32) .centerY(payBtnView, offset: -7) .height(30) scrollView.layoutChain .edges(excludingEdge: .bottom) .bottomToTopOfView(bottomView) scrollContentView.layoutChain .edges() .widthToView(scrollView) headerBgImgView.layoutChain .top(-kStatusBarHeight) .edgesHorzontal() // .edges(excludingEdge: .bottom) .heightToWidth(267/375) cornerView.layoutChain .topToBottomOfView(headerBgImgView, offset: -20) .edges(excludingEdge: .top) let expenseCollectionViewHeight = (kScreenWidth - 32 - 32) / 3 * (144/110) + 10 expenseCollectionView.layoutChain .topToView(cornerView, offset: -43) .edgesHorzontal() .height(expenseCollectionViewHeight * 1.1) vipRightsView.layoutChain .topToBottomOfView(expenseCollectionView, offset: 18) .edgesHorzontal(16) .height(302) vipRightsTitleView.layoutChain .top(14) .centerX() agreementLab.layoutChain .topToBottomOfView(vipRightsView, offset: 6) .leftToView(vipRightsView) .rightToView(vipRightsView) tipsLab.layoutChain .topToBottomOfView(agreementLab, offset: 4) .leftToView(vipRightsView) .rightToView(vipRightsView) .bottom(10) } lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .white view.alpha = 0 return view }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.text = "升级会员" label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100) return btn }() lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = UIColor(hexStr: "#F5FBFB") view.showsVerticalScrollIndicator = false view.delegate = self view.bounces = false return view }() lazy var scrollContentView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var headerBgImgView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "VipRecharge/header_bg") return view }() lazy var cornerView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#F5FBFB") view.clipsToBounds = false return view }() /// 资费 lazy var expenseCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() let spacing: CGFloat = 16 let cvWidth = kScreenWidth - 32 let itemW = (cvWidth - spacing * 2) / 3 layout.itemSize = CGSize(width: itemW, height: itemW * (144/110) + 10) layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) layout.minimumLineSpacing = spacing layout.scrollDirection = .horizontal let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(ExpenseCell.self) return cv }() /// 会员权益 lazy var vipRightsView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 10 return view }() lazy var vipRightsTitleView: UIView = { let view = UIView() view.backgroundColor = .clear let titleLab = UILabel() titleLab.text = "会员权益" titleLab.font = .systemFont(ofSize: 14, weight: .bold) titleLab.textColor = UIColor(hexStr: "#3D3D3D") titleLab.textAlignment = .center view.addSubview(titleLab) titleLab.layoutChain .edgesVertical() .centerX() let leftLine1 = UIView() leftLine1.backgroundColor = UIColor(hexStr: "#B7F34E") leftLine1.cornerRadius = 1 view.addSubview(leftLine1) leftLine1.layoutChain .left() .width(2) .height(10) .centerY() let leftLine2 = UIView() leftLine2.backgroundColor = UIColor(hexStr: "#B7F34E") leftLine2.cornerRadius = 1 view.addSubview(leftLine2) leftLine2.layoutChain .leftToRightOfView(leftLine1, offset: 4) .width(2) .edgesVertical() .rightToLeftOfView(titleLab, offset: -10) let leftLine3 = UIView() leftLine3.backgroundColor = UIColor(hexStr: "#B7F34E") leftLine3.cornerRadius = 1 view.addSubview(leftLine3) leftLine3.layoutChain .leftToRightOfView(titleLab, offset: 10) .width(2) .edgesVertical() let leftLine4 = UIView() leftLine4.backgroundColor = UIColor(hexStr: "#B7F34E") leftLine4.cornerRadius = 1 view.addSubview(leftLine4) leftLine4.layoutChain .leftToRightOfView(leftLine3, offset: 4) .width(2) .height(10) .centerY() return view }() lazy var groupCountView: UIView = { let view = UIView() view.backgroundColor = .clear // 创建圈子 let createCountView = UIView() createCountView.backgroundColor = .clear let createIcon = UIImageView(image: UIImage(named: "VipRecharge/create_count")) let createBgImg = UIImageView(image: UIImage(named: "VipRecharge/count_bg")) let createUnitLab = UILabel() createUnitLab.text = "个" createUnitLab.font = .systemFont(ofSize: 10, weight: .medium) createUnitLab.textColor = UIColor(hexStr: "#1A1A1A") let createTitleLab = UILabel() createTitleLab.text = "创建圈子" createTitleLab.font = .systemFont(ofSize: 12, weight: .medium) createTitleLab.textColor = UIColor(hexStr: "#1A1A1A") createCountView.addSubview(createIcon) createCountView.addSubview(createBgImg) createCountView.addSubview(createUnitLab) createCountView.addSubview(createTitleLab) createCountView.addSubview(createCountLab) createIcon.layoutChain .top() .centerX() .width(34).height(34) createBgImg.layoutChain .top(8) createCountLab.layoutChain .topToBottomOfView(createIcon) // 分隔线 let separator1 = UIImageView(image: UIImage(named: "VipRecharge/separator")) // 圈子人数 let separator2 = UIImageView(image: UIImage(named: "VipRecharge/separator")) return view }() lazy var createCountLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 24, weight: .bold) label.textColor = UIColor(hexStr: "#FF4F44") return label }() lazy var agreementLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = UIColor(hexStr: "#767676") label.isUserInteractionEnabled = true let text = "*开通前请阅读《会员服务协议》" let attr = NSMutableAttributedString(string: text) let range = (text as NSString).range(of: "《会员服务协议》") attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#2DBBFF"), range: range) label.attributedText = attr return label }() lazy var tipsLab: UILabel = { let label = UILabel() label.text = "*根据相关隐私保护的规定,本产品功能需双方下载并授权同意后再使用,请在自己的设备上使用,不得在未经过对方同意和授权的情况下使用,仅限家庭/亲人/朋友/情侣等熟人间使用。" label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = UIColor(hexStr: "#767676") label.numberOfLines = 0 return label }() lazy var payTypeStackView: UIStackView = { let stack = UIStackView() stack.axis = .horizontal stack.spacing = 30 // stack.distribution = .fillEqually stack.alignment = .center return stack }() var selectedPayTypeTag: Int = 0 /// 根据 pay_type 字符串动态构建支付方式 (格式: "alipay,weixin") func setupPayTypes(_ payTypeStr: String) { payTypeStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } let types = payTypeStr.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } let payTypeMap: [(String, String)] = types.compactMap { if $0 == "weixin" { return ("wechat", "微信支付") } if $0 == "alipay" { return ("alipay", "支付宝支付") } return nil } guard !payTypeMap.isEmpty else { return } selectedPayTypeTag = 0 for (idx, (icon, name)) in payTypeMap.enumerated() { let isSelected = idx == 0 let view = makePayTypeView(tag: idx, icon: icon, name: name, isSelected: isSelected) payTypeStackView.addArrangedSubview(view) } } private func makePayTypeView(tag: Int, icon: String, name: String, isSelected: Bool) -> UIView { let view = UIView() view.tag = tag view.isUserInteractionEnabled = true let checkbox = UIImageView(image: UIImage(named: isSelected ? "VipRecharge/checkbox_on" : "VipRecharge/checkbox")) checkbox.contentMode = .scaleAspectFit checkbox.tag = 100 view.addSubview(checkbox) checkbox.layoutChain.left().centerY().width(18).height(18) let payIcon = UIImageView(image: UIImage(named: "VipRecharge/\(icon)")) payIcon.contentMode = .scaleAspectFit view.addSubview(payIcon) payIcon.layoutChain.leftToRightOfView(checkbox, offset: 8).centerY().width(22).height(22) let nameLab = UILabel() nameLab.text = name nameLab.font = .systemFont(ofSize: 14, weight: .medium) nameLab.textColor = UIColor(hexStr: "#1A1A1A") view.addSubview(nameLab) nameLab.layoutChain.leftToRightOfView(payIcon, offset: 6).centerY() let tap = UITapGestureRecognizer(target: self, action: #selector(onPayTypeTap(_:))) view.addGestureRecognizer(tap) return view } @objc private func onPayTypeTap(_ gesture: UITapGestureRecognizer) { guard let tag = gesture.view?.tag, tag != selectedPayTypeTag else { return } selectedPayTypeTag = tag for view in payTypeStackView.arrangedSubviews { let isSelected = view.tag == tag if let checkbox = view.viewWithTag(100) as? UIImageView { checkbox.image = UIImage(named: isSelected ? "VipRecharge/checkbox_on" : "VipRecharge/checkbox") } } } lazy var bottomView: UIView = { let view = UIView() view.backgroundColor = .white view.layer.shadowColor = UIColor.black.cgColor view.layer.shadowOffset = CGSize(width: 0, height: -3) view.layer.shadowOpacity = 0.06 return view }() lazy var payBtnView: UIView = { let view = UIView() view.backgroundColor = .clear let bgImg = UIImageView() bgImg.image = UIImage(named: "VipRecharge/pay_bg") view.addSubview(bgImg) bgImg.layoutChain.edges() let unlockLab = UILabel() unlockLab.text = "解锁会员" unlockLab.font = .systemFont(ofSize: 16, weight: .heavy) unlockLab.textColor = .white view.addSubview(unlockLab) unlockLab.layoutChain.right(21).centerY() return view }() lazy var payPriceView: UIView = { let view = UIView() view.backgroundColor = .clear let titleLab = UILabel() titleLab.text = "合计" titleLab.font = .systemFont(ofSize: 14, weight: .bold) titleLab.textColor = UIColor(hexStr: "#1A1A1A") view.addSubview(titleLab) titleLab.layoutChain .left() .bottom() let symbolLab = UILabel() symbolLab.text = "¥" symbolLab.font = .systemFont(ofSize: 12, weight: .heavy) symbolLab.textColor = UIColor(hexStr: "#FF3B05") view.addSubview(symbolLab) symbolLab.layoutChain .leftToRightOfView(titleLab) .bottomToView(titleLab) view.addSubview(priceLab) priceLab.layoutChain .leftToRightOfView(symbolLab) .bottomToView(titleLab, offset: 3) view.addSubview(discountLab) discountLab.layoutChain .leftToRightOfView(priceLab, offset: 5) .bottomToView(titleLab) .right() return view }() lazy var priceLab: UILabel = { let label = UILabel() label.text = "0" label.font = .systemFont(ofSize: 24, weight: .heavy) label.textColor = UIColor(hexStr: "#FF3B05") return label }() /// 数字翻滚动画 — 每位数字独立向上或向下滚动 func animatePrice(to newValue: String) { guard let target = Double(newValue) else { priceLab.text = newValue return } let current = Double(priceLab.text ?? "0") ?? 0 let diff = target - current let duration = 0.6 let steps = 30 var step = 0 Timer.scheduledTimer(withTimeInterval: duration / Double(steps), repeats: true) { [weak self] t in step += 1 guard let self = self else { t.invalidate(); return } if step >= steps { t.invalidate() self.priceLab.text = newValue } else { let progress = Double(step) / Double(steps) // ease out + overshoot on increase let eased = diff > 0 ? 1 - pow(1 - progress, 3) : pow(progress, 0.5) let value = current + diff * eased self.priceLab.text = String(format: "%.0f", value) } } } lazy var discountLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .bold) label.textColor = UIColor(hexStr: "#1A1A1A") return label }() override func layoutSubviews() { super.layoutSubviews() cornerView.setNeedsLayout() cornerView.layoutIfNeeded() cornerView.setCornerRadius(corners: [.topLeft, .topRight], withCornerRadii: CGSize(width: 10, height: 10)) } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() setupRx() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - ExpenseCell final class ExpenseCell: UICollectionViewCell { private let tagBadgeView: UIView = { let iv = UIView() // iv.image = UIImage(named: "VipRecharge/expense_tips") // iv.contentMode = .scaleAspectFill iv.backgroundColor = UIColor(hexStr: "#FF6643") iv.isHidden = true return iv }() private let tagLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .bold) label.textColor = .white label.textAlignment = .center return label }() private let bgImgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "VipRecharge/expense") iv.contentMode = .scaleAspectFill return iv }() private let titleLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .bold) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center return label }() lazy var priceLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .bold) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center return label }() lazy var originPriceLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .bold) label.textColor = UIColor(hexStr: "#767676") label.textAlignment = .center return label }() lazy var tipsLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .bold) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center return label }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(bgImgView) contentView.addSubview(tagBadgeView) tagBadgeView.addSubview(tagLabel) contentView.addSubview(titleLab) contentView.addSubview(priceLab) contentView.addSubview(originPriceLab) contentView.addSubview(tipsLab) tagBadgeView.layoutChain .top().left() tagLabel.layoutChain .edgesHorzontal(10) .edgesVertical(3) bgImgView.layoutChain .top(10) .left().right().bottom() titleLab.layoutChain .centerX() .top(30) .edgesHorzontal(2) priceLab.layoutChain .centerY() .edgesHorzontal(2) originPriceLab.layoutChain .topToBottomOfView(priceLab, offset: 5) .edgesHorzontal(2) tipsLab.layoutChain .edgesHorzontal(2) .bottom(7) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(model: VipExpenseModel, isSelected: Bool) { titleLab.text = model.goods_name tagBadgeView.isHidden = model.tips.isEmpty tagLabel.text = model.tips tipsLab.text = model.tips2 let priceAttr = NSMutableAttributedString(string: "¥", attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .medium)]) priceAttr.append(NSAttributedString(string: model.price, attributes: [.font: UIFont.systemFont(ofSize: 30, weight: .medium)])) priceAttr.append(NSAttributedString(string: model.unit, attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .medium)])) priceLab.attributedText = priceAttr originPriceLab.text = "¥" + model.origin_price originPriceLab.setupStrikethroughStyle() setSelected(isSelected, animated: false) } func setSelected(_ selected: Bool, animated: Bool) { let scale: CGFloat = selected ? 1.1 : 1.0 let imageName = selected ? "VipRecharge/expense_on" : "VipRecharge/expense" bgImgView.image = UIImage(named: imageName) titleLab.textColor = selected ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#1A1A1A") priceLab.textColor = selected ? UIColor(hexStr: "#FF4F44") : UIColor(hexStr: "#1A1A1A") let animations = { self.transform = CGAffineTransform(scaleX: scale, y: scale) } if animated { UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: animations) } else { animations() } } override func layoutSubviews() { super.layoutSubviews() tagBadgeView.setNeedsLayout() tagBadgeView.layoutIfNeeded() tagBadgeView.setCornerRadius(corners: [.topLeft, .bottomRight], withCornerRadii: CGSize(width: 10, height: 10)) } } extension VipRechargeView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let maxY = scrollView.contentOffset.y let alpha = maxY / kNaviHeight navBarView.alpha = alpha < 0.0 ? 0.0 : alpha } }