// // KBSkinDetailVC.m // keyBoard #import "KBSkinDetailVC.h" #import #import "UIColor+Extension.h" #import "UICollectionViewLeftAlignedLayout.h" #import "KBSkinDetailHeaderCell.h" #import "KBSkinTagsContainerCell.h" #import "KBSkinCardCell.h" #import "KBSkinSectionTitleCell.h" #import "KBSkinBottomActionView.h" #import "KBShopVM.h" #import "KBShopThemeTagModel.h" #import "KBHUD.h" #import "KBSkinService.h" static NSString * const kHeaderCellId = @"kHeaderCellId"; static NSString * const kTagsContainerCellId = @"kTagsContainerCellId"; static NSString * const kSectionTitleCellId = @"kSectionTitleCellId"; static NSString * const kGridCellId = @"kGridCellId"; // 直接复用 KBSkinCardCell typedef NS_ENUM(NSInteger, KBSkinDetailSection) { KBSkinDetailSectionHeader = 0, KBSkinDetailSectionTags, KBSkinDetailSectionTitle, KBSkinDetailSectionGrid, KBSkinDetailSectionCount }; @interface KBSkinDetailVC () @property (nonatomic, strong) UICollectionView *collectionView; // 主列表 @property (nonatomic, strong) KBSkinBottomActionView *bottomBar; // 底部操作条 @property (nonatomic, copy) NSArray *tags; // 标签数据 @property (nonatomic, copy) NSArray *gridData; // 底部网格数据 @property (nonatomic, strong) KBShopVM *shopVM; @property (nonatomic, strong, nullable) KBShopThemeDetailModel *detailModel; @property (nonatomic, assign) BOOL isProcessingAction; @end @implementation KBSkinDetailVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.tags = @[]; self.gridData = @[ @{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" }, ]; // 1. 列表 [self.view addSubview:self.collectionView]; // 2. 底部操作条(左右 15,高 45,可点击) [self.view addSubview:self.bottomBar]; [self.bottomBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(15); make.right.equalTo(self.view).offset(-15); make.height.mas_equalTo([KBSkinBottomActionView preferredHeight]); make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-10); }]; // 列表底部距离操作条顶部 10 [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT); make.bottom.equalTo(self.bottomBar.mas_top).offset(-10); }]; [self updateBottomBarAppearance]; [self fetchThemeDetailIfNeeded]; } #pragma mark - UICollectionView DataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return KBSkinDetailSectionCount; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { switch (section) { case KBSkinDetailSectionHeader: return 1; // 顶部大卡片 case KBSkinDetailSectionTags: return self.tags.count > 0 ? 1 : 0; case KBSkinDetailSectionTitle: return 1; // 标题 case KBSkinDetailSectionGrid: return self.gridData.count; // 2 列网格 } return 0; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { switch (indexPath.section) { case KBSkinDetailSectionHeader: { KBSkinDetailHeaderCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kHeaderCellId forIndexPath:indexPath]; [cell configWithDetail:self.detailModel]; return cell; } case KBSkinDetailSectionTags: { KBSkinTagsContainerCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kTagsContainerCellId forIndexPath:indexPath]; [cell configWithTags:self.tags]; return cell; } case KBSkinDetailSectionTitle: { KBSkinSectionTitleCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSectionTitleCellId forIndexPath:indexPath]; [cell config:@"Recommended Skin"]; return cell; } case KBSkinDetailSectionGrid: { KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kGridCellId forIndexPath:indexPath]; NSDictionary *d = self.gridData[indexPath.item]; [cell configWithTitle:d[@"title"] imageURL:nil price:@"20"]; return cell; } } return [UICollectionViewCell new]; } #pragma mark - UICollectionView DelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat W = collectionView.bounds.size.width; CGFloat insetLR = 16.0; CGFloat contentW = W - insetLR * 2; switch (indexPath.section) { case KBSkinDetailSectionHeader: { // 高度 = 图片 0.58W + 文案与上下留白(≈56) CGFloat h = KBFit(264) + 30; return CGSizeMake(contentW, h); } case KBSkinDetailSectionTags: { CGFloat h = [KBSkinTagsContainerCell heightForTags:self.tags width:W]; return CGSizeMake(contentW, MAX(h, 0.1)); } case KBSkinDetailSectionTitle: { return CGSizeMake(contentW, 44); } case KBSkinDetailSectionGrid: { // 2 列 CGFloat spacing = 12.0; CGFloat itemW = floor((contentW - spacing) / 2.0); // CGFloat itemH = itemW * 0.75 + 56; return CGSizeMake(itemW, KBFit(197)); } } return CGSizeZero; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { // 控制各区上下留白,缩小 Header 与 Tags 之间的间距 switch (section) { case KBSkinDetailSectionHeader: // 顶部与导航之间 12,底部稍微缩小 return UIEdgeInsetsMake(12, 16, 4, 16); case KBSkinDetailSectionTags: // 与 Header 更贴近一些 return UIEdgeInsetsMake(14, 16, 5, 16); default: return UIEdgeInsetsMake(0, 16, 12, 16); } } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 12.0; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { // 网格区需要这个间距,其它区也保持统一 return 12.0; } #pragma mark - Lazy - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.dataSource = self; _collectionView.delegate = self; // 注册 cell [_collectionView registerClass:KBSkinDetailHeaderCell.class forCellWithReuseIdentifier:kHeaderCellId]; [_collectionView registerClass:KBSkinTagsContainerCell.class forCellWithReuseIdentifier:kTagsContainerCellId]; [_collectionView registerClass:KBSkinSectionTitleCell.class forCellWithReuseIdentifier:kSectionTitleCellId]; [_collectionView registerClass:KBSkinCardCell.class forCellWithReuseIdentifier:kGridCellId]; } return _collectionView; } #pragma mark - Lazy (Bottom Bar) - (KBSkinBottomActionView *)bottomBar { if (!_bottomBar) { _bottomBar = [[KBSkinBottomActionView alloc] init]; // 中文注释:配置文案与图标,可根据业务传入金币图/价格 [_bottomBar configWithTitle:@"Download" price:@"20" icon:nil]; KBWeakSelf _bottomBar.tapHandler = ^{ // 示例:点击下载/购买 // [KBHUD showText:@"点击了下载"]; // TODO: 在此处触发下载/购买逻辑 [weakSelf handleDownloadAction]; }; } return _bottomBar; } #pragma mark - Actions - (void)handleDownloadAction { if (self.isProcessingAction) { return; } if (self.themeId.length == 0) { [KBHUD showInfo:KBLocalized(@"主题信息缺失")]; return; } if (!self.detailModel) { [KBHUD showInfo:KBLocalized(@"正在加载主题详情")]; return; } if (self.detailModel.isPurchased) { [self requestDownload]; } else { [self purchaseCurrentTheme]; } } - (void)purchaseCurrentTheme { if (self.isProcessingAction) { return; } self.isProcessingAction = YES; [KBHUD show]; __weak typeof(self) weakSelf = self; [self.shopVM purchaseThemeWithId:self.themeId completion:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.isProcessingAction = NO; [KBHUD dismiss]; if (error || !success) { NSString *msg = error.localizedDescription ?: KBLocalized(@"购买失败"); [KBHUD showInfo:msg]; return; } weakSelf.detailModel.isPurchased = YES; [weakSelf updateBottomBarAppearance]; [weakSelf requestDownload]; }); }]; } - (void)requestDownload { if (self.isProcessingAction) { return; } self.isProcessingAction = YES; [KBHUD show]; NSMutableDictionary *skin = [NSMutableDictionary dictionary]; if (!skin[@"id"] && self.detailModel.themeId) { skin[@"id"] = self.detailModel.themeId; } if (!skin[@"name"] && self.detailModel.themeName) { skin[@"name"] = self.detailModel.themeName; } if (!skin[@"themeDownloadUrl"]) { [KBHUD showInfo:KBLocalized(@"缺少下载地址")]; return; } skin[@"themeDownloadUrl"] = self.detailModel.themeDownloadUrl; [[KBSkinService shared] applySkinWithJSON:skin fromViewController:self mode:KBSkinSourceModeRemoteZip completion:^(BOOL success) { if (success) { [KBHUD showSuccess:KBLocalized(@"已开始下载")]; } else { [KBHUD showInfo:KBLocalized(@"下载失败")]; } }]; } - (void)updateBottomBarAppearance { BOOL purchased = self.detailModel.isPurchased; if (purchased) { self.bottomBar.titleText = KBLocalized(@"Download again"); self.bottomBar.showsPrice = NO; } else { NSString *price = self.detailModel ? [NSString stringWithFormat:@"%.2f", self.detailModel.themePrice] : @"0"; self.bottomBar.titleText = KBLocalized(@"Download"); self.bottomBar.priceText = price; self.bottomBar.showsPrice = YES; UIImage *coin = [UIImage imageNamed:@"shop_jb_icon"]; if (coin) { self.bottomBar.iconImage = coin; } } } - (void)fetchThemeDetailIfNeeded { if (self.themeId.length == 0) { NSLog(@"[KBSkinDetailVC] themeId is empty, skip detail request"); return; } __weak typeof(self) weakSelf = self; [self.shopVM fetchThemeDetailWithId:self.themeId completion:^(KBShopThemeDetailModel * _Nullable detail, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { NSLog(@"[KBSkinDetailVC] fetch detail failed: %@", error); return; } if (!detail) { NSLog(@"[KBSkinDetailVC] theme detail is empty"); return; } weakSelf.detailModel = detail; NSMutableArray *tagNames = [NSMutableArray array]; for (KBShopThemeTagModel *tag in detail.themeTag) { if (tag.label.length) { [tagNames addObject:tag.label]; } } weakSelf.tags = tagNames.copy; [weakSelf updateBottomBarAppearance]; [weakSelf.collectionView reloadData]; }); }]; } - (KBShopVM *)shopVM { if (!_shopVM) { _shopVM = [[KBShopVM alloc] init]; } return _shopVM; } @end