This commit is contained in:
2025-12-11 16:39:22 +08:00
parent e4442afe72
commit 7f90240731
6 changed files with 207 additions and 32 deletions

View File

@@ -22,6 +22,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 图标(可选),例如金币图
@property (nonatomic, strong, nullable) UIImage *iconImage;
/// 是否展示价格区域(隐藏后仅显示标题)
@property (nonatomic, assign) BOOL showsPrice;
/// 点击回调(也可直接 addTarget 使用)
@property (nonatomic, copy, nullable) void (^tapHandler)(void);
@@ -34,4 +37,3 @@ NS_ASSUME_NONNULL_BEGIN
@end
NS_ASSUME_NONNULL_END

View File

@@ -7,10 +7,11 @@
#import "KBSkinBottomActionView.h"
@interface KBSkinBottomActionView ()
@property (nonatomic, strong) UIView *contentView; // 使
@property (nonatomic, strong) UILabel *titleLabel; //
@property (nonatomic, strong) UIView *contentView; // 使
@property (nonatomic, strong) UIStackView *stackView; // Title/Icon/Price
@property (nonatomic, strong) UILabel *titleLabel; //
@property (nonatomic, strong) UIImageView *coinImageView; //
@property (nonatomic, strong) UILabel *priceLabel; //
@property (nonatomic, strong) UILabel *priceLabel; //
@end
@implementation KBSkinBottomActionView
@@ -37,24 +38,20 @@
}];
// Title - Icon - Price
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.coinImageView];
[self.contentView addSubview:self.priceLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView);
make.centerY.equalTo(self.contentView);
[self.contentView addSubview:self.stackView];
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[self.stackView addArrangedSubview:self.titleLabel];
[self.stackView addArrangedSubview:self.coinImageView];
[self.stackView addArrangedSubview:self.priceLabel];
if (@available(iOS 11.0, *)) {
[self.stackView setCustomSpacing:8 afterView:self.titleLabel];
[self.stackView setCustomSpacing:6 afterView:self.coinImageView];
}
[self.coinImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.titleLabel.mas_right).offset(8);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(18);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.coinImageView.mas_right).offset(6);
make.right.equalTo(self.contentView);
make.centerY.equalTo(self.contentView);
}];
//
self.titleText = @"Download";
@@ -62,6 +59,7 @@
UIImage *img = [UIImage systemImageNamed:@"circle.fill"];
self.iconImage = img; //
self.coinImageView.tintColor = [UIColor colorWithRed:1.0 green:0.85 blue:0.2 alpha:1.0];
self.showsPrice = YES;
//
[self addTarget:self action:@selector(handleTap) forControlEvents:UIControlEventTouchUpInside];
}
@@ -100,7 +98,9 @@
- (void)setPriceText:(NSString *)priceText {
_priceText = [priceText copy];
self.priceLabel.text = _priceText;
if (self.showsPrice) {
self.priceLabel.text = _priceText;
}
}
- (void)setIconImage:(UIImage *)iconImage {
@@ -108,6 +108,17 @@
self.coinImageView.image = _iconImage;
}
- (void)setShowsPrice:(BOOL)showsPrice {
if (_showsPrice == showsPrice) { return; }
_showsPrice = showsPrice;
self.coinImageView.hidden = !_showsPrice;
self.priceLabel.hidden = !_showsPrice;
self.priceLabel.text = _showsPrice ? self.priceText : @"";
if (@available(iOS 11.0, *)) {
[self.stackView setCustomSpacing:_showsPrice ? 8 : 0 afterView:self.titleLabel];
}
}
#pragma mark - Lazy
- (UILabel *)titleLabel {
@@ -131,6 +142,16 @@
return _contentView;
}
- (UIStackView *)stackView {
if (!_stackView) {
_stackView = [[UIStackView alloc] init];
_stackView.axis = UILayoutConstraintAxisHorizontal;
_stackView.alignment = UIStackViewAlignmentCenter;
_stackView.spacing = 6;
}
return _stackView;
}
- (UIImageView *)coinImageView {
if (!_coinImageView) {
_coinImageView = [[UIImageView alloc] init];

View File

@@ -13,6 +13,8 @@
#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";
@@ -34,6 +36,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
@property (nonatomic, copy) NSArray<NSDictionary *> *gridData; //
@property (nonatomic, strong) KBShopVM *shopVM;
@property (nonatomic, strong, nullable) KBShopThemeDetailModel *detailModel;
@property (nonatomic, assign) BOOL isProcessingAction;
@end
@implementation KBSkinDetailVC
@@ -69,6 +72,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
make.bottom.equalTo(self.bottomBar.mas_top).offset(-10);
}];
[self updateBottomBarAppearance];
[self fetchThemeDetailIfNeeded];
}
@@ -208,7 +212,87 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
#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 {
@@ -235,6 +319,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
}
}
weakSelf.tags = tagNames.copy;
[weakSelf updateBottomBarAppearance];
[weakSelf.collectionView reloadData];
});
}];

View File

@@ -23,6 +23,10 @@ typedef void(^KBShopBalanceCompletion)(NSNumber *_Nullable balance,
NSError *_Nullable error);
typedef void(^KBShopDetailCompletion)(KBShopThemeDetailModel *_Nullable detail,
NSError *_Nullable error);
typedef void(^KBShopPurchaseCompletion)(BOOL success,
NSError *_Nullable error);
typedef void(^KBShopDownloadInfoCompletion)(NSDictionary *_Nullable info,
NSError *_Nullable error);
@interface KBShopVM : NSObject
@property (nonatomic, copy, readonly, nullable) NSArray<KBShopStyleModel *> *styles;
@@ -41,6 +45,14 @@ typedef void(^KBShopDetailCompletion)(KBShopThemeDetailModel *_Nullable detail,
- (void)fetchThemeDetailWithId:(nullable NSString *)themeId
completion:(KBShopDetailCompletion)completion;
/// 购买主题
- (void)purchaseThemeWithId:(nullable NSString *)themeId
completion:(KBShopPurchaseCompletion)completion;
/// 获取主题下载信息
- (void)fetchThemeDownloadInfoWithId:(nullable NSString *)themeId
completion:(KBShopDownloadInfoCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -13,6 +13,7 @@
@interface KBShopVM ()
@property (nonatomic, copy, readwrite, nullable) NSArray<KBShopStyleModel *> *styles;
- (id)kb_themeIdParamFromString:(NSString *)themeId;
@end
@@ -99,17 +100,17 @@
if (completion) completion(nil, [self kb_invalidResponseError]);
return;
}
NSString *balanceValue = dataObj[@"balanceDisplay"];
// NSNumber *balanceNumber = nil;
// if ([balanceValue isKindOfClass:[NSNumber class]]) {
// balanceNumber = balanceValue;
// } else if ([balanceValue isKindOfClass:[NSString class]]) {
// balanceNumber = @([(NSString *)balanceValue doubleValue]);
// }
// if (!balanceNumber) {
// balanceNumber = @(0);
// }
if (completion) completion(balanceValue, nil);
id balanceValue = dataObj[@"balance"];
NSNumber *balanceNumber = nil;
if ([balanceValue isKindOfClass:[NSNumber class]]) {
balanceNumber = balanceValue;
} else if ([balanceValue isKindOfClass:[NSString class]]) {
balanceNumber = @([(NSString *)balanceValue doubleValue]);
}
if (!balanceNumber) {
balanceNumber = @(0);
}
if (completion) completion(balanceNumber, nil);
}];
}
@@ -119,7 +120,7 @@
if (completion) completion(nil, [self kb_invalidParameterError]);
return;
}
NSDictionary *params = @{@"themeId": themeId};
NSDictionary *params = @{@"themeId": [self kb_themeIdParamFromString:themeId]};
[[KBNetworkManager shared] GET:API_THEME_DETAIL
parameters:params
headers:nil
@@ -141,4 +142,56 @@
}];
}
- (void)purchaseThemeWithId:(nullable NSString *)themeId
completion:(KBShopPurchaseCompletion)completion {
if (themeId.length == 0) {
if (completion) completion(NO, [self kb_invalidParameterError]);
return;
}
NSDictionary *body = @{@"themeId": [self kb_themeIdParamFromString:themeId]};
[[KBNetworkManager shared] POST:API_THEME_PURCHASE
jsonBody:body
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary * _Nullable json,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
if (completion) completion(error == nil, error);
}];
}
- (void)fetchThemeDownloadInfoWithId:(nullable NSString *)themeId
completion:(KBShopDownloadInfoCompletion)completion {
if (themeId.length == 0) {
if (completion) completion(nil, [self kb_invalidParameterError]);
return;
}
NSDictionary *params = @{@"themeId": [self kb_themeIdParamFromString:themeId]};
[[KBNetworkManager shared] GET:API_THEME_DOWNLOAD
parameters:params
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary * _Nullable json,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
if (error) {
if (completion) completion(nil, error);
return;
}
id dataObj = json[KBData] ?: json[@"data"];
if (![dataObj isKindOfClass:[NSDictionary class]]) {
if (completion) completion(nil, [self kb_invalidResponseError]);
return;
}
if (completion) completion((NSDictionary *)dataObj, nil);
}];
}
- (id)kb_themeIdParamFromString:(NSString *)themeId {
if (themeId.length == 0) { return @""; }
NSNumberFormatter *formatter = [NSNumberFormatter new];
NSNumber *number = [formatter numberFromString:themeId];
return number ?: themeId;
}
@end