From b4e4b7b60691dd7b912fb4acacc00e13bfc6c1ce Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Wed, 4 Feb 2026 12:48:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86svip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyBoard.xcodeproj/project.pbxproj | 24 ++ keyBoard/Class/Pay/V/KBSvipBenefitBgView.h | 16 + keyBoard/Class/Pay/V/KBSvipBenefitBgView.m | 21 ++ keyBoard/Class/Pay/V/KBSvipBenefitCell.h | 17 + keyBoard/Class/Pay/V/KBSvipBenefitCell.m | 88 +++++ keyBoard/Class/Pay/V/KBSvipFlowLayout.h | 19 + keyBoard/Class/Pay/V/KBSvipFlowLayout.m | 90 +++++ keyBoard/Class/Pay/V/KBSvipSubscribeCell.h | 19 + keyBoard/Class/Pay/V/KBSvipSubscribeCell.m | 130 +++++++ keyBoard/Class/Pay/VC/KBPaySvipVC.m | 403 ++++++++++++++++++++- 10 files changed, 817 insertions(+), 10 deletions(-) create mode 100644 keyBoard/Class/Pay/V/KBSvipBenefitBgView.h create mode 100644 keyBoard/Class/Pay/V/KBSvipBenefitBgView.m create mode 100644 keyBoard/Class/Pay/V/KBSvipBenefitCell.h create mode 100644 keyBoard/Class/Pay/V/KBSvipBenefitCell.m create mode 100644 keyBoard/Class/Pay/V/KBSvipFlowLayout.h create mode 100644 keyBoard/Class/Pay/V/KBSvipFlowLayout.m create mode 100644 keyBoard/Class/Pay/V/KBSvipSubscribeCell.h create mode 100644 keyBoard/Class/Pay/V/KBSvipSubscribeCell.m diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 4e846f8..41bd2b8 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -241,6 +241,10 @@ 04F4C0AA2F32274000E8F08C /* KBPayMainVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0A92F32274000E8F08C /* KBPayMainVC.m */; }; 04F4C0AD2F32288600E8F08C /* KBPaySvipVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AC2F32288600E8F08C /* KBPaySvipVC.m */; }; 04F4C0B02F322EF200E8F08C /* PagingViewTableHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0AF2F322EF200E8F08C /* PagingViewTableHeaderView.m */; }; + 04F4C0B52F33053800E8F08C /* KBSvipBenefitCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0B22F33053800E8F08C /* KBSvipBenefitCell.m */; }; + 04F4C0B62F33053800E8F08C /* KBSvipSubscribeCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0B42F33053800E8F08C /* KBSvipSubscribeCell.m */; }; + 04F4C0BB2F3306CF00E8F08C /* KBSvipFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0BA2F3306CF00E8F08C /* KBSvipFlowLayout.m */; }; + 04F4C0BC2F3306CF00E8F08C /* KBSvipBenefitBgView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04F4C0B82F3306CF00E8F08C /* KBSvipBenefitBgView.m */; }; 04FC95672EB0546C007BD342 /* KBKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95652EB0546C007BD342 /* KBKey.m */; }; 04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; }; 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; }; @@ -739,6 +743,14 @@ 04F4C0AC2F32288600E8F08C /* KBPaySvipVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPaySvipVC.m; sourceTree = ""; }; 04F4C0AE2F322EF200E8F08C /* PagingViewTableHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PagingViewTableHeaderView.h; sourceTree = ""; }; 04F4C0AF2F322EF200E8F08C /* PagingViewTableHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PagingViewTableHeaderView.m; sourceTree = ""; }; + 04F4C0B12F33053800E8F08C /* KBSvipBenefitCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSvipBenefitCell.h; sourceTree = ""; }; + 04F4C0B22F33053800E8F08C /* KBSvipBenefitCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSvipBenefitCell.m; sourceTree = ""; }; + 04F4C0B32F33053800E8F08C /* KBSvipSubscribeCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSvipSubscribeCell.h; sourceTree = ""; }; + 04F4C0B42F33053800E8F08C /* KBSvipSubscribeCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSvipSubscribeCell.m; sourceTree = ""; }; + 04F4C0B72F3306CF00E8F08C /* KBSvipBenefitBgView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSvipBenefitBgView.h; sourceTree = ""; }; + 04F4C0B82F3306CF00E8F08C /* KBSvipBenefitBgView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSvipBenefitBgView.m; sourceTree = ""; }; + 04F4C0B92F3306CF00E8F08C /* KBSvipFlowLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSvipFlowLayout.h; sourceTree = ""; }; + 04F4C0BA2F3306CF00E8F08C /* KBSvipFlowLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSvipFlowLayout.m; sourceTree = ""; }; 04FC953A2EAFAE56007BD342 /* KeyBoardPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyBoardPrefixHeader.pch; sourceTree = ""; }; 04FC95642EB0546C007BD342 /* KBKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKey.h; sourceTree = ""; }; 04FC95652EB0546C007BD342 /* KBKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKey.m; sourceTree = ""; }; @@ -968,6 +980,14 @@ 04122FB22EC73C0100EF7AB3 /* KBVipReviewListCell.m */, 04F4C0AE2F322EF200E8F08C /* PagingViewTableHeaderView.h */, 04F4C0AF2F322EF200E8F08C /* PagingViewTableHeaderView.m */, + 04F4C0B12F33053800E8F08C /* KBSvipBenefitCell.h */, + 04F4C0B22F33053800E8F08C /* KBSvipBenefitCell.m */, + 04F4C0B32F33053800E8F08C /* KBSvipSubscribeCell.h */, + 04F4C0B42F33053800E8F08C /* KBSvipSubscribeCell.m */, + 04F4C0B72F3306CF00E8F08C /* KBSvipBenefitBgView.h */, + 04F4C0B82F3306CF00E8F08C /* KBSvipBenefitBgView.m */, + 04F4C0B92F3306CF00E8F08C /* KBSvipFlowLayout.h */, + 04F4C0BA2F3306CF00E8F08C /* KBSvipFlowLayout.m */, ); path = V; sourceTree = ""; @@ -2540,6 +2560,8 @@ 047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */, 04E038DD2F20C420002CA5A0 /* VoiceChatStreamingManager.m in Sources */, 04E038DE2F20C420002CA5A0 /* VoiceChatWebSocketClient.m in Sources */, + 04F4C0B52F33053800E8F08C /* KBSvipBenefitCell.m in Sources */, + 04F4C0B62F33053800E8F08C /* KBSvipSubscribeCell.m in Sources */, 049FB2262EC3136D00FAB05D /* KBPersonInfoItemCell.m in Sources */, 048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */, 049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */, @@ -2609,6 +2631,8 @@ 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */, 0477BE042EBC83130055D639 /* HomeMainVC.m in Sources */, 0477BDFD2EBC6A170055D639 /* HomeHotVC.m in Sources */, + 04F4C0BB2F3306CF00E8F08C /* KBSvipFlowLayout.m in Sources */, + 04F4C0BC2F3306CF00E8F08C /* KBSvipBenefitBgView.m in Sources */, 04E039522F2387D2002CA5A0 /* KBAiChatMessage.m in Sources */, 0460869A2F19238500757C95 /* KBAiWaveformView.m in Sources */, 0460869C2F19238500757C95 /* KBAiRecordButton.m in Sources */, diff --git a/keyBoard/Class/Pay/V/KBSvipBenefitBgView.h b/keyBoard/Class/Pay/V/KBSvipBenefitBgView.h new file mode 100644 index 0000000..2bfe447 --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipBenefitBgView.h @@ -0,0 +1,16 @@ +// +// KBSvipBenefitBgView.h +// keyBoard +// +// 权益列表背景装饰视图 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSvipBenefitBgView : UICollectionReusableView + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/V/KBSvipBenefitBgView.m b/keyBoard/Class/Pay/V/KBSvipBenefitBgView.m new file mode 100644 index 0000000..6af130d --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipBenefitBgView.m @@ -0,0 +1,21 @@ +// +// KBSvipBenefitBgView.m +// keyBoard +// +// 权益列表背景装饰视图:白色背景 + 15圆角 +// + +#import "KBSvipBenefitBgView.h" + +@implementation KBSvipBenefitBgView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.backgroundColor = [UIColor whiteColor]; + self.layer.cornerRadius = 15; + self.layer.masksToBounds = YES; + } + return self; +} + +@end diff --git a/keyBoard/Class/Pay/V/KBSvipBenefitCell.h b/keyBoard/Class/Pay/V/KBSvipBenefitCell.h new file mode 100644 index 0000000..c082aae --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipBenefitCell.h @@ -0,0 +1,17 @@ +// +// KBSvipBenefitCell.h +// keyBoard +// +// SVIP 权益项 Cell:左侧图标 + 文字 + 右侧勾选 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSvipBenefitCell : UICollectionViewCell +/// 配置权益项 +- (void)configWithIcon:(NSString *)iconName title:(NSString *)title; +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/V/KBSvipBenefitCell.m b/keyBoard/Class/Pay/V/KBSvipBenefitCell.m new file mode 100644 index 0000000..b3b9cbb --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipBenefitCell.m @@ -0,0 +1,88 @@ +// +// KBSvipBenefitCell.m +// keyBoard +// +// SVIP 权益项样式:左侧图标 + 文字 + 右侧勾选 +// + +#import "KBSvipBenefitCell.h" + +@interface KBSvipBenefitCell () +@property (nonatomic, strong) UIImageView *iconView; // 左侧图标 +@property (nonatomic, strong) UILabel *titleLabel; // 权益文字 +@property (nonatomic, strong) UIImageView *checkView; // 右侧勾选 +@end + +@implementation KBSvipBenefitCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor clearColor]; + + [self.contentView addSubview:self.iconView]; + [self.contentView addSubview:self.titleLabel]; + [self.contentView addSubview:self.checkView]; + + [self.iconView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(16); + make.centerY.equalTo(self.contentView); + make.width.height.mas_equalTo(40); + }]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.iconView.mas_right).offset(12); + make.centerY.equalTo(self.contentView); + make.right.lessThanOrEqualTo(self.checkView.mas_left).offset(-12); + }]; + + [self.checkView mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.contentView).offset(-16); + make.centerY.equalTo(self.contentView); + make.width.height.mas_equalTo(20); + }]; + } + return self; +} + +- (void)configWithIcon:(NSString *)iconName title:(NSString *)title { + if (iconName.length) { + self.iconView.image = [UIImage imageNamed:iconName]; + } + self.titleLabel.text = title.length ? title : @""; +} + +#pragma mark - Lazy +- (UIImageView *)iconView { + if (!_iconView) { + _iconView = [UIImageView new]; + _iconView.contentMode = UIViewContentModeScaleAspectFit; + _iconView.layer.cornerRadius = 8; + _iconView.clipsToBounds = YES; + } + return _iconView; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [UILabel new]; + _titleLabel.textColor = [UIColor colorWithHex:KBBlackValue]; + _titleLabel.font = [KBFont regular:14]; + } + return _titleLabel; +} + +- (UIImageView *)checkView { + if (!_checkView) { + _checkView = [UIImageView new]; + _checkView.contentMode = UIViewContentModeScaleAspectFit; + // 使用 SF Symbol 勾选图标 + if (@available(iOS 13.0, *)) { + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]; + _checkView.image = [UIImage systemImageNamed:@"checkmark" withConfiguration:config]; + } + _checkView.tintColor = [UIColor colorWithHex:KBColorValue]; + } + return _checkView; +} + +@end diff --git a/keyBoard/Class/Pay/V/KBSvipFlowLayout.h b/keyBoard/Class/Pay/V/KBSvipFlowLayout.h new file mode 100644 index 0000000..762aa91 --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipFlowLayout.h @@ -0,0 +1,19 @@ +// +// KBSvipFlowLayout.h +// keyBoard +// +// SVIP 页面自定义布局,支持 Section 背景装饰视图 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSvipFlowLayout : UICollectionViewFlowLayout + +/// 需要添加背景的 Section 索引(默认 Section 1) +@property (nonatomic, assign) NSInteger decorationSection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/V/KBSvipFlowLayout.m b/keyBoard/Class/Pay/V/KBSvipFlowLayout.m new file mode 100644 index 0000000..c004a6b --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipFlowLayout.m @@ -0,0 +1,90 @@ +// +// KBSvipFlowLayout.m +// keyBoard +// +// SVIP 页面自定义布局,支持 Section 背景装饰视图 +// + +#import "KBSvipFlowLayout.h" +#import "KBSvipBenefitBgView.h" + +static NSString * const kKBSvipDecorationViewKind = @"KBSvipBenefitBgDecoration"; + +@interface KBSvipFlowLayout () +@property (nonatomic, strong) NSMutableArray *decorationAttributes; +@end + +@implementation KBSvipFlowLayout + +- (instancetype)init { + if (self = [super init]) { + _decorationSection = 1; // 默认 Section 1 需要背景 + [self registerClass:[KBSvipBenefitBgView class] forDecorationViewOfKind:kKBSvipDecorationViewKind]; + } + return self; +} + +- (void)prepareLayout { + [super prepareLayout]; + + self.decorationAttributes = [NSMutableArray array]; + + NSInteger numberOfSections = [self.collectionView numberOfSections]; + if (self.decorationSection >= numberOfSections) { return; } + + NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:self.decorationSection]; + if (numberOfItems == 0) { return; } + + // 获取 Section Header 的布局属性 + UICollectionViewLayoutAttributes *headerAttr = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:self.decorationSection]]; + + // 获取第一个和最后一个 item 的布局属性 + UICollectionViewLayoutAttributes *firstItemAttr = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:self.decorationSection]]; + UICollectionViewLayoutAttributes *lastItemAttr = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:numberOfItems - 1 inSection:self.decorationSection]]; + + if (!firstItemAttr || !lastItemAttr) { return; } + + // 计算背景区域 + UIEdgeInsets sectionInset = self.sectionInset; + if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) { + sectionInset = [(id)self.collectionView.delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:self.decorationSection]; + } + + CGFloat minY = CGRectGetMinY(headerAttr.frame); + CGFloat maxY = CGRectGetMaxY(lastItemAttr.frame) + 16; // 底部留 16 间距 + CGFloat x = sectionInset.left; + CGFloat width = self.collectionView.bounds.size.width - sectionInset.left - sectionInset.right; + + CGRect decorationFrame = CGRectMake(x, minY, width, maxY - minY); + + UICollectionViewLayoutAttributes *decorationAttr = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:kKBSvipDecorationViewKind withIndexPath:[NSIndexPath indexPathForItem:0 inSection:self.decorationSection]]; + decorationAttr.frame = decorationFrame; + decorationAttr.zIndex = -1; // 放在最底层 + + [self.decorationAttributes addObject:decorationAttr]; +} + +- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { + NSMutableArray *attrs = [[super layoutAttributesForElementsInRect:rect] mutableCopy]; + + for (UICollectionViewLayoutAttributes *decorationAttr in self.decorationAttributes) { + if (CGRectIntersectsRect(rect, decorationAttr.frame)) { + [attrs addObject:decorationAttr]; + } + } + + return attrs; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if ([elementKind isEqualToString:kKBSvipDecorationViewKind]) { + for (UICollectionViewLayoutAttributes *attr in self.decorationAttributes) { + if (attr.indexPath.section == indexPath.section) { + return attr; + } + } + } + return [super layoutAttributesForDecorationViewOfKind:elementKind atIndexPath:indexPath]; +} + +@end diff --git a/keyBoard/Class/Pay/V/KBSvipSubscribeCell.h b/keyBoard/Class/Pay/V/KBSvipSubscribeCell.h new file mode 100644 index 0000000..ed7a9cb --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipSubscribeCell.h @@ -0,0 +1,19 @@ +// +// KBSvipSubscribeCell.h +// keyBoard +// +// SVIP 订阅选项 Cell:横向排列,选中时绿色边框 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSvipSubscribeCell : UICollectionViewCell +/// 配置展示文案 +- (void)configTitle:(NSString *)title price:(NSString *)price strike:(nullable NSString *)strike; +/// 同步选中态 +- (void)applySelected:(BOOL)selected animated:(BOOL)animated; +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/V/KBSvipSubscribeCell.m b/keyBoard/Class/Pay/V/KBSvipSubscribeCell.m new file mode 100644 index 0000000..851fa3a --- /dev/null +++ b/keyBoard/Class/Pay/V/KBSvipSubscribeCell.m @@ -0,0 +1,130 @@ +// +// KBSvipSubscribeCell.m +// keyBoard +// +// SVIP 订阅选项样式,横向排列,选中时绿色边框 +// + +#import "KBSvipSubscribeCell.h" + +@interface KBSvipSubscribeCell () +@property (nonatomic, strong) UIView *cardView; // 白色卡片背景 +@property (nonatomic, strong) UILabel *titleLabel; // "1 Week" / "1 Month" / "1 Year" +@property (nonatomic, strong) UILabel *priceLabel; // "$6.90" +@property (nonatomic, strong) UILabel *strikeLabel; // 删除线原价 +@end + +@implementation KBSvipSubscribeCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor clearColor]; + + [self.contentView addSubview:self.cardView]; + [self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.contentView); + }]; + + [self.cardView addSubview:self.titleLabel]; + [self.cardView addSubview:self.priceLabel]; + [self.cardView addSubview:self.strikeLabel]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView); + make.top.equalTo(self.cardView).offset(12); + }]; + [self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView); + make.top.equalTo(self.titleLabel.mas_bottom).offset(8); + }]; + [self.strikeLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView); + make.top.equalTo(self.priceLabel.mas_bottom).offset(4); + }]; + + // 基于 CALayer 的边框 + self.cardView.layer.borderWidth = 1.5; + self.cardView.layer.borderColor = [UIColor colorWithWhite:0.9 alpha:1.0].CGColor; + } + return self; +} + +- (void)prepareForReuse { + [super prepareForReuse]; + [self applySelected:NO animated:NO]; +} + +- (void)setSelected:(BOOL)selected { + [super setSelected:selected]; + [self applySelected:selected animated:NO]; +} + +- (void)configTitle:(NSString *)title price:(NSString *)price strike:(nullable NSString *)strike { + self.titleLabel.text = title.length ? title : @"1 Month"; + self.priceLabel.text = price.length ? price : @"$6.90"; + self.strikeLabel.hidden = (strike.length == 0); + if (strike.length) { + NSDictionary *attr = @{ + NSStrikethroughStyleAttributeName: @(NSUnderlineStyleSingle), + NSForegroundColorAttributeName: [UIColor colorWithHex:0x999999] + }; + self.strikeLabel.attributedText = [[NSAttributedString alloc] initWithString:strike attributes:attr]; + } +} + +- (void)applySelected:(BOOL)selected animated:(BOOL)animated { + CGColorRef color = (selected ? [UIColor colorWithHex:KBColorValue].CGColor : [UIColor colorWithWhite:0.9 alpha:1.0].CGColor); + UIColor *bgColor = selected ? [UIColor colorWithHex:0xE8FFF6] : [UIColor whiteColor]; + void (^changes)(void) = ^{ + self.cardView.layer.borderColor = color; + self.cardView.backgroundColor = bgColor; + }; + if (animated) { + [UIView animateWithDuration:0.2 animations:changes]; + } else { + changes(); + } +} + +#pragma mark - Lazy +- (UIView *)cardView { + if (!_cardView) { + _cardView = [UIView new]; + _cardView.backgroundColor = [UIColor whiteColor]; + _cardView.layer.cornerRadius = 12; + _cardView.clipsToBounds = YES; + } + return _cardView; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [UILabel new]; + _titleLabel.text = @"1 Month"; + _titleLabel.textColor = [UIColor colorWithHex:KBBlackValue]; + _titleLabel.font = [KBFont medium:13]; + } + return _titleLabel; +} + +- (UILabel *)priceLabel { + if (!_priceLabel) { + _priceLabel = [UILabel new]; + _priceLabel.text = @"$6.90"; + _priceLabel.textColor = [UIColor colorWithHex:KBBlackValue]; + _priceLabel.font = [KBFont bold:22]; + } + return _priceLabel; +} + +- (UILabel *)strikeLabel { + if (!_strikeLabel) { + _strikeLabel = [UILabel new]; + _strikeLabel.text = @"$4.49"; + _strikeLabel.font = [KBFont regular:12]; + _strikeLabel.textColor = [UIColor colorWithHex:0x999999]; + } + return _strikeLabel; +} + +@end diff --git a/keyBoard/Class/Pay/VC/KBPaySvipVC.m b/keyBoard/Class/Pay/VC/KBPaySvipVC.m index 598fdb9..9714eeb 100644 --- a/keyBoard/Class/Pay/VC/KBPaySvipVC.m +++ b/keyBoard/Class/Pay/VC/KBPaySvipVC.m @@ -6,30 +6,374 @@ // #import "KBPaySvipVC.h" +#import "KBSvipSubscribeCell.h" +#import "KBSvipBenefitCell.h" +#import "KBSvipFlowLayout.h" +#import "PayVM.h" +#import "KBPayProductModel.h" +#import "KBBizCode.h" +#import "keyBoard-Swift.h" +#import -@interface KBPaySvipVC () +static NSString * const kKBSvipSubscribeCellId = @"kKBSvipSubscribeCellId"; +static NSString * const kKBSvipBenefitCellId = @"kKBSvipBenefitCellId"; +static NSString * const kKBSvipBenefitHeaderId = @"kKBSvipBenefitHeaderId"; + +@interface KBPaySvipVC () @property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView); + +/// 1:UI 控件 @property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) UIButton *payButton; +@property (nonatomic, strong) UILabel *agreementLabel; +@property (nonatomic, strong) UIButton *agreementButton; + +/// 2:数据 +@property (nonatomic, strong) NSArray *plans; +@property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, strong) NSArray *benefits; +@property (nonatomic, strong) PayVM *payVM; @end @implementation KBPaySvipVC +#pragma mark - Life Cycle + - (void)viewDidLoad { [super viewDidLoad]; - // Do any additional setup after loading the view. - // 懒加载 collectionView,并添加到视图 + + /// 1:控件初始化 + [self setupUI]; + /// 2:数据初始化 + [self setupData]; + /// 3:加载数据 + [self loadData]; +} + +#pragma mark - 1:控件初始化 + +- (void)setupUI { + self.view.backgroundColor = [UIColor colorWithHex:0xF6F7FB]; + + [self.view addSubview:self.payButton]; + [self.view addSubview:self.agreementLabel]; + [self.view addSubview:self.agreementButton]; [self.view addSubview:self.collectionView]; + + [self.agreementButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.view); + make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM - 15); + }]; + + [self.agreementLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.view); + make.bottom.equalTo(self.agreementButton.mas_top).offset(0); + }]; + + [self.payButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(24); + make.right.equalTo(self.view).offset(-24); + make.bottom.equalTo(self.agreementLabel.mas_top).offset(-14); + make.height.mas_equalTo(58); + }]; + [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { - make.edges.equalTo(self.view); // mas 布局:铺满 + make.left.right.top.equalTo(self.view); + make.bottom.equalTo(self.payButton.mas_top).offset(-16); }]; } +#pragma mark - 2:数据初始化 + +- (void)setupData { + self.payVM = [PayVM new]; + self.plans = @[]; + self.selectedIndex = NSNotFound; + + // 权益列表数据(使用现有图标资源) + self.benefits = @[ + @{@"icon": @"pay_ai_icon", @"title": KBLocalized(@"Wireless Sub-ai Dialogue")}, + @{@"icon": @"pay_keyboard_icon", @"title": KBLocalized(@"Personalized Keyboard")}, + @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Chat Persona")}, + @{@"icon": @"pay_emotion_icon", @"title": KBLocalized(@"Emotional Counseling")}, + @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Longer Chat History")}, + @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Unlimited Chatting")}, + @{@"icon": @"pay_ai_icon", @"title": KBLocalized(@"Chat Without Speed Limits")}, + @{@"icon": @"pay_vip_icon", @"title": KBLocalized(@"Coming Soon")}, + ]; +} + +#pragma mark - 3:加载数据 + +- (void)loadData { + __weak typeof(self) weakSelf = self; + [self.payVM fetchSubscriptionProductsNeedShow:YES completion:^(NSInteger sta, NSString * _Nullable msg, NSArray * _Nullable products) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (!self) { return; } + if (sta != KBBizCodeSuccess || ![products isKindOfClass:NSArray.class]) { + self.plans = @[]; + self.selectedIndex = NSNotFound; + [self.collectionView reloadData]; + return; + } + self.plans = products ?: @[]; + self.selectedIndex = self.plans.count > 0 ? 1 : NSNotFound; // 默认选中第二个(1 Month) + [self.collectionView reloadData]; + [self selectCurrentPlanAnimated:NO]; + [self prepareStoreKitWithPlans:self.plans]; + }); + }]; +} + +#pragma mark - Private Methods + +- (void)prepareStoreKitWithPlans:(NSArray *)plans { + if (![plans isKindOfClass:NSArray.class] || plans.count == 0) { return; } + NSMutableArray *ids = [NSMutableArray array]; + for (KBPayProductModel *plan in plans) { + if (![plan isKindOfClass:KBPayProductModel.class]) { continue; } + if (plan.productId.length) { + [ids addObject:plan.productId]; + } + } + if (ids.count == 0) { return; } + [[KBStoreKitBridge shared] prepareWithProductIds:ids completion:nil]; +} + +- (void)selectCurrentPlanAnimated:(BOOL)animated { + if (self.selectedIndex == NSNotFound) { return; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.plans.count) { return; } + NSIndexPath *ip = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; + [self.collectionView selectItemAtIndexPath:ip animated:animated scrollPosition:UICollectionViewScrollPositionNone]; + KBSvipSubscribeCell *cell = (KBSvipSubscribeCell *)[self.collectionView cellForItemAtIndexPath:ip]; + if ([cell isKindOfClass:KBSvipSubscribeCell.class]) { + [cell applySelected:YES animated:animated]; + } +} + +- (KBPayProductModel *)currentSelectedPlan { + if (self.selectedIndex == NSNotFound) { return nil; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.plans.count) { return nil; } + return self.plans[self.selectedIndex]; +} + +- (NSString *)displayTitleForPlan:(KBPayProductModel *)plan { + if (!plan) { return @""; } + if (plan.productDescription.length) { return plan.productDescription; } + NSString *name = plan.name ?: @""; + NSString *unit = plan.unit ?: @""; + if (name.length && unit.length) { return [NSString stringWithFormat:@"%@ %@", name, unit]; } + if (name.length) { return name; } + if (unit.length) { return unit; } + return KBLocalized(@"Subscription"); +} + +#pragma mark - Actions + +- (void)onTapPayButton { + KBPayProductModel *plan = [self currentSelectedPlan]; + if (!plan) { + [KBHUD showInfo:KBLocalized(@"Please select a product")]; + return; + } + + NSMutableDictionary *extra = [NSMutableDictionary dictionary]; + if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) { + extra[@"product_id"] = plan.productId; + } + [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_svip_pay_btn" + pageId:@"svip_pay" + elementId:@"pay_btn" + extra:extra.copy + completion:nil]; + + NSString *productId = plan.productId; + if (productId.length == 0) { + [KBHUD showInfo:KBLocalized(@"Product unavailable")]; + return; + } + + [KBHUD show]; + __weak typeof(self) weakSelf = self; + [[KBStoreKitBridge shared] purchaseWithProductId:productId completion:^(BOOL success, NSString * _Nullable message) { + __strong typeof(weakSelf) self = weakSelf; + [KBHUD dismiss]; + [KBHUD showInfo:KBLocalized(message)]; + if (!self || !success) { return; } + [self selectCurrentPlanAnimated:NO]; + }]; +} + +- (void)agreementButtonAction { + [KBHUD showInfo:KBLocalized(@"Open agreement")]; +} + +#pragma mark - UICollectionView DataSource + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + // Section 0: 订阅选项(横向) + // Section 1: 权益列表 + return 2; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + if (section == 0) { + return self.plans.count; + } + return self.benefits.count; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0) { + KBSvipSubscribeCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBSvipSubscribeCellId forIndexPath:indexPath]; + if (indexPath.item < self.plans.count) { + KBPayProductModel *plan = self.plans[indexPath.item]; + NSString *title = [self displayTitleForPlan:plan]; + NSString *price = [plan priceDisplayText]; + [cell configTitle:title price:price strike:nil]; + [cell applySelected:(indexPath.item == self.selectedIndex) animated:NO]; + } + return cell; + } else { + KBSvipBenefitCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBSvipBenefitCellId forIndexPath:indexPath]; + if (indexPath.item < self.benefits.count) { + NSDictionary *benefit = self.benefits[indexPath.item]; + [cell configWithIcon:benefit[@"icon"] title:benefit[@"title"]]; + } + return cell; + } +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 1 && [kind isEqualToString:UICollectionElementKindSectionHeader]) { + UICollectionReusableView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBSvipBenefitHeaderId forIndexPath:indexPath]; + // 清除旧的子视图 + for (UIView *subview in header.subviews) { + [subview removeFromSuperview]; + } + // 添加标题 + UILabel *titleLabel = [UILabel new]; + titleLabel.text = KBLocalized(@"Membership Benefits"); + titleLabel.textColor = [UIColor colorWithHex:0x999999]; + titleLabel.font = [KBFont medium:13]; + titleLabel.textAlignment = NSTextAlignmentCenter; + [header addSubview:titleLabel]; + [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(header); + }]; + // 左右横线 + UIView *leftLine = [UIView new]; + leftLine.backgroundColor = [UIColor colorWithHex:0xE5E5E5]; + [header addSubview:leftLine]; + [leftLine mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(titleLabel.mas_left).offset(-12); + make.centerY.equalTo(header); + make.width.mas_equalTo(40); + make.height.mas_equalTo(1); + }]; + UIView *rightLine = [UIView new]; + rightLine.backgroundColor = [UIColor colorWithHex:0xE5E5E5]; + [header addSubview:rightLine]; + [rightLine mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(titleLabel.mas_right).offset(12); + make.centerY.equalTo(header); + make.width.mas_equalTo(40); + make.height.mas_equalTo(1); + }]; + return header; + } + return [UICollectionReusableView new]; +} + +#pragma mark - UICollectionView Delegate + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section != 0 || indexPath.item >= self.plans.count) { return; } + if (self.selectedIndex == indexPath.item) { return; } + + NSInteger old = self.selectedIndex; + self.selectedIndex = indexPath.item; + + KBPayProductModel *plan = self.plans[indexPath.item]; + NSMutableDictionary *extra = [NSMutableDictionary dictionary]; + extra[@"index"] = @(indexPath.item); + if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) { + extra[@"product_id"] = plan.productId; + } + [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_svip_select_plan" + pageId:@"svip_pay" + elementId:@"plan_item" + extra:extra.copy + completion:nil]; + + KBSvipSubscribeCell *newCell = (KBSvipSubscribeCell *)[collectionView cellForItemAtIndexPath:indexPath]; + [newCell applySelected:YES animated:YES]; + + if (old >= 0 && old < self.plans.count) { + NSIndexPath *oldIP = [NSIndexPath indexPathForItem:old inSection:0]; + KBSvipSubscribeCell *oldCell = (KBSvipSubscribeCell *)[collectionView cellForItemAtIndexPath:oldIP]; + [oldCell applySelected:NO animated:YES]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0 && indexPath.item < self.plans.count && [cell isKindOfClass:KBSvipSubscribeCell.class]) { + BOOL sel = (indexPath.item == self.selectedIndex); + KBSvipSubscribeCell *c = (KBSvipSubscribeCell *)cell; + if (sel) { + [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; + } + [c applySelected:sel animated:NO]; + } +} + +#pragma mark - FlowLayout + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + CGFloat screenW = KB_SCREEN_WIDTH; + if (indexPath.section == 0) { + // 订阅选项:横向三等分 + CGFloat itemW = (screenW - 32 - 20) / 3.0; // 左右各16,间距10*2 + return CGSizeMake(itemW, 90); + } else { + // 权益项 + return CGSizeMake(screenW - 32, 56); + } +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { + if (section == 1) { + return CGSizeMake(KB_SCREEN_WIDTH, 50); + } + return CGSizeZero; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { + if (section == 0) { return 10; } + return 0; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { + if (section == 0) { return 10; } + return 0; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + if (section == 0) { + return UIEdgeInsetsMake(16, 16, 10, 16); + } + return UIEdgeInsetsMake(0, 16, 20, 16); +} + #pragma mark - UIScrollView Delegate(转发给分页容器) + - (void)scrollViewDidScroll:(UIScrollView *)scrollView { !self.scrollCallback ?: self.scrollCallback(scrollView); } #pragma mark - JXPagingViewListViewDelegate + - (UIView *)listView { return self.view; } - (UIScrollView *)listScrollView { return self.collectionView; } - (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback { self.scrollCallback = callback; } @@ -38,21 +382,60 @@ - (void)listWillDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } - (void)listDidDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } - #pragma mark - Lazy + - (UICollectionView *)collectionView { if (!_collectionView) { - UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; + KBSvipFlowLayout *layout = [KBSvipFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.decorationSection = 1; // Section 1 添加背景 _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor clearColor]; -// _collectionView.dataSource = self; -// _collectionView.delegate = self; - // 注册皮肤卡片 cell -// [_collectionView registerClass:KBSkinCardCell.class forCellWithReuseIdentifier:@"KBSkinCardCell"]; // 复用标识 + _collectionView.dataSource = self; + _collectionView.delegate = self; + _collectionView.alwaysBounceVertical = YES; + _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + [_collectionView registerClass:KBSvipSubscribeCell.class forCellWithReuseIdentifier:kKBSvipSubscribeCellId]; + [_collectionView registerClass:KBSvipBenefitCell.class forCellWithReuseIdentifier:kKBSvipBenefitCellId]; + [_collectionView registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBSvipBenefitHeaderId]; } return _collectionView; } +- (UIButton *)payButton { + if (!_payButton) { + _payButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_payButton setTitle:KBLocalized(@"Recharge Now") forState:UIControlStateNormal]; + [_payButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal]; + _payButton.titleLabel.font = [KBFont medium:15]; + _payButton.layer.cornerRadius = 29; + _payButton.clipsToBounds = YES; + _payButton.backgroundColor = [UIColor colorWithHex:0x222222]; + [_payButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [_payButton addTarget:self action:@selector(onTapPayButton) forControlEvents:UIControlEventTouchUpInside]; + } + return _payButton; +} + +- (UILabel *)agreementLabel { + if (!_agreementLabel) { + _agreementLabel = [UILabel new]; + _agreementLabel.text = KBLocalized(@"By Clicking \"pay\", You Indicate Your Agreement To The"); + _agreementLabel.font = [KBFont regular:12]; + _agreementLabel.textColor = [UIColor colorWithHex:KBBlackValue]; + } + return _agreementLabel; +} + +- (UIButton *)agreementButton { + if (!_agreementButton) { + _agreementButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_agreementButton setTitle:KBLocalized(@"《Embership Agreement》") forState:UIControlStateNormal]; + [_agreementButton setTitleColor:[UIColor colorWithHex:KBColorValue] forState:UIControlStateNormal]; + _agreementButton.titleLabel.font = [KBFont regular:12]; + [_agreementButton addTarget:self action:@selector(agreementButtonAction) forControlEvents:UIControlEventTouchUpInside]; + } + return _agreementButton; +} @end