This commit is contained in:
2025-12-11 13:16:06 +08:00
parent e39104c431
commit d348b35984
12 changed files with 387 additions and 52 deletions

View File

@@ -49,7 +49,8 @@
- (void)configWithTitle:(NSString *)title imageURL:(NSString *)url price:(NSString *)price {
self.titleLabel.text = title.length ? title : @"Dopamine";
[self.priceBtn setTitle:@"20" forState:UIControlStateNormal];
NSString *priceText = price.length ? price : @"20";
[self.priceBtn setTitle:priceText forState:UIControlStateNormal];
//
self.coverView.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
@@ -84,4 +85,3 @@
}
@end

View File

@@ -0,0 +1,20 @@
//
// KBShopStyleModel.h
// keyBoard
//
// Created by Mac on 2025/12/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBShopStyleModel : NSObject
/// 风格 id
@property (nonatomic, copy, nullable) NSString *styleId;
/// 风格名称
@property (nonatomic, copy, nullable) NSString *styleName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,18 @@
//
// KBShopStyleModel.m
// keyBoard
//
// Created by Mac on 2025/12/11.
//
#import "KBShopStyleModel.h"
@implementation KBShopStyleModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"styleId": @"id",
@"styleName": @"styleName"
};
}
@end

View File

@@ -0,0 +1,34 @@
//
// KBShopThemeModel.h
// keyBoard
//
// Created by Mac on 2025/12/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBShopThemeModel : NSObject
/// 主题 id
@property (nonatomic, copy, nullable) NSString *themeId;
/// 主题名称
@property (nonatomic, copy, nullable) NSString *themeName;
/// 价格(单位:接口返回的货币)
@property (nonatomic, assign) CGFloat themePrice;
/// 标签或风格名称
@property (nonatomic, copy, nullable) NSString *themeTag;
/// 下载量描述
@property (nonatomic, copy, nullable) NSString *themeDownload;
/// 风格类型(接口返回 themeStyle
@property (nonatomic, assign) NSInteger themeStyle;
/// 是否上架
@property (nonatomic, assign, getter=isThemeStatus) BOOL themeStatus;
/// 购买次数
@property (nonatomic, assign) NSInteger themePurchasesNumber;
/// 预览图
@property (nonatomic, copy, nullable) NSString *themePreviewImageUrl;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,16 @@
//
// KBShopThemeModel.m
// keyBoard
//
// Created by Mac on 2025/12/11.
//
#import "KBShopThemeModel.h"
@implementation KBShopThemeModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"themeId": @"id",
};
}
@end

View File

@@ -10,17 +10,25 @@
NS_ASSUME_NONNULL_BEGIN
@class KBShopStyleModel;
@class KBShopVM;
@class KBShopThemeModel;
@interface KBShopItemVC : UIViewController<JXPagerViewListViewDelegate>
/// 列表:使用 UICollectionView 展示两列皮肤卡片
@property (nonatomic, strong) UICollectionView *collectionView;
/// 数据源:简单字符串作为标题(演示用)
@property (nonatomic, strong) NSMutableArray<NSString *> *dataSource;
/// 数据源:主题列表
@property (nonatomic, strong) NSMutableArray<KBShopThemeModel *> *dataSource;
/// 是否需要上拉加载更多
@property (nonatomic, assign) BOOL isNeedFooter;
/// 是否需要下拉刷新
@property (nonatomic, assign) BOOL isNeedHeader;
/// 首次是否已刷新过(避免重复触发)
@property (nonatomic, assign) BOOL isHeaderRefreshed; // 默认为 YES
/// 当前所属风格
@property (nonatomic, strong, nullable) KBShopStyleModel *style;
/// 可复用的 ShopVM由外部传入便于共用缓存/网络配置)
@property (nonatomic, strong, nullable) KBShopVM *shopViewModel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -11,9 +11,12 @@
#import "KBSkinCardCell.h"
#import "KBSkinInstallBridge.h"
#import "KBHUD.h"
#import "KBShopVM.h"
@interface KBShopItemVC ()<UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView);
@property (nonatomic, strong) KBShopVM *internalViewModel;
@property (nonatomic, assign, getter=isLoading) BOOL loading;
@end
@implementation KBShopItemVC
@@ -21,6 +24,7 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
self.dataSource = [NSMutableArray array];
// collectionView
[self.view addSubview:self.collectionView];
@@ -28,26 +32,19 @@
make.edges.equalTo(self.view); // mas
}];
// 2
KBWeakSelf
if (self.isNeedHeader) {
self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf.collectionView.mj_header endRefreshing];
});
[weakSelf fetchThemes];
}];
}
if (self.isNeedFooter) {
self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf.dataSource addObject:KBLocalized(@"Loaded more successfully")]; //
[weakSelf.collectionView reloadData];
[weakSelf.collectionView.mj_footer endRefreshing];
});
[weakSelf fetchThemes];
}];
}
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
[self beginFirstRefresh];
}
@@ -56,23 +53,58 @@
//
- (void)beginFirstRefresh {
if (!self.isHeaderRefreshed) {
[self beginRefreshImmediately];
if (self.isHeaderRefreshed || self.isLoading) {
return;
}
if (!self.style.styleId.length) {
return;
}
[self beginRefreshImmediately];
}
- (void)beginRefreshImmediately {
if (self.isNeedHeader) {
if (self.isNeedHeader && !self.collectionView.mj_header.isRefreshing) {
[self.collectionView.mj_header beginRefreshing];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.isHeaderRefreshed = YES;
[self.collectionView reloadData];
[self.collectionView.mj_header endRefreshing];
});
} else {
self.isHeaderRefreshed = YES;
[self.collectionView reloadData];
}
[self fetchThemes];
}
- (void)fetchThemes {
if (self.isLoading) {
return;
}
NSString *styleId = self.style.styleId;
if (!styleId.length) {
return;
}
self.loading = YES;
KBShopVM *vm = self.shopViewModel;
if (!vm) {
if (!self.internalViewModel) {
self.internalViewModel = [[KBShopVM alloc] init];
}
vm = self.internalViewModel;
}
KBWeakSelf
[vm fetchThemesForStyleId:styleId completion:^(NSArray<KBShopThemeModel *> * _Nullable themes, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.loading = NO;
weakSelf.isHeaderRefreshed = YES;
if (error) {
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
[KBHUD showInfo:msg];
} else {
weakSelf.dataSource = themes.count ? themes.mutableCopy : [NSMutableArray array];
[weakSelf.collectionView reloadData];
}
if ([weakSelf.collectionView.mj_header isRefreshing]) {
[weakSelf.collectionView.mj_header endRefreshing];
}
if ([weakSelf.collectionView.mj_footer isRefreshing]) {
[weakSelf.collectionView.mj_footer endRefreshing];
}
});
}];
}
#pragma mark - UICollectionView DataSource
@@ -84,8 +116,10 @@
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBSkinCardCell" forIndexPath:indexPath];
NSString *title = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : @"Dopamine";
[cell configWithTitle:title imageURL:nil price:@"20"]; // 20
KBShopThemeModel *theme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
NSString *title = theme.themeName.length ? theme.themeName : KBLocalized(@"Themes");
NSString *price = [self kb_priceStringForTheme:theme];
[cell configWithTitle:title imageURL:nil price:price];
return cell;
}
@@ -117,8 +151,28 @@
[self kb_handleShopTapAtIndexPath:indexPath];
}
- (NSString *)kb_priceStringForTheme:(KBShopThemeModel *)theme {
if (!theme) {
return @"";
}
if (theme.themePrice <= 0.0f) {
return KBLocalized(@"Free");
}
static NSNumberFormatter *formatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSNumberFormatter alloc] init];
formatter.minimumFractionDigits = 0;
formatter.maximumFractionDigits = 2;
formatter.minimumIntegerDigits = 1;
});
NSString *priceString = [formatter stringFromNumber:@(theme.themePrice)];
return priceString ?: [NSString stringWithFormat:@"%.2f", theme.themePrice];
}
- (void)kb_handleShopTapAtIndexPath:(NSIndexPath *)indexPath {
NSString *title = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : KBLocalized(@"专属皮肤002");
KBShopThemeModel *theme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
NSString *title = theme.themeName.length ? theme.themeName : KBLocalized(@"专属皮肤002");
// 002.zip id便
static NSString * const kKBBundleSkinId002 = @"bundle_skin_fense";
[KBSkinInstallBridge publishBundleSkinRequestWithId:kKBBundleSkinId002
@@ -128,6 +182,13 @@
[KBHUD showInfo:KBLocalized(@"已通知键盘解压,切换到自定义键盘即可生效")];
}
- (void)setStyle:(KBShopStyleModel *)style {
_style = style;
if (self.isViewLoaded && !self.isHeaderRefreshed) {
[self beginFirstRefresh];
}
}
#pragma mark - UIScrollView Delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
!self.scrollCallback ?: self.scrollCallback(scrollView);

View File

@@ -18,6 +18,8 @@
#import "KBWebViewViewController.h"
#import "KBShopVM.h"
#import "KBHUD.h"
static const CGFloat JXTableHeaderViewHeight = (323);
static const CGFloat JXheightForHeaderInSection = 50;
@@ -33,11 +35,13 @@ static const CGFloat JXheightForHeaderInSection = 50;
@property (nonatomic, strong) JXCategoryTitleView *categoryView;
@property (nonatomic, strong) NSArray <NSString *> *titles;
@property (nonatomic, copy) NSArray<KBShopStyleModel *> *styles;
@property (nonatomic, strong) UIImageView *bgImageView; //
@property (nonatomic, strong) UIButton *searchBtn;
@property (nonatomic, strong) UIButton *skinBtn;
//
@property (nonatomic, assign) BOOL categoryIsWhite;
@property (nonatomic, strong) KBShopVM *shopVM;
@end
@@ -65,6 +69,7 @@ static const CGFloat JXheightForHeaderInSection = 50;
make.width.height.mas_equalTo(25);
}];
[self fetchShopStylesWithHUD:YES];
}
// Base VC
@@ -76,10 +81,8 @@ static const CGFloat JXheightForHeaderInSection = 50;
self.view.backgroundColor = [UIColor whiteColor];
self.navigationController.navigationBar.translucent = false;
self.edgesForExtendedLayout = UIRectEdgeNone;
_titles = @[KBLocalized(@"能力"), KBLocalized(@"爱好"), KBLocalized(@"队友"),
KBLocalized(@"能力2"), KBLocalized(@"爱好2"), KBLocalized(@"队友2"),
KBLocalized(@"能力"), KBLocalized(@"爱好"), KBLocalized(@"队友"),
KBLocalized(@"能力2"), KBLocalized(@"爱好2"), KBLocalized(@"队友2")];
self.styles = @[];
_titles = @[];
_userHeaderView = [[KBShopHeadView alloc] init];
_categoryView = (JXCategoryTitleView *)[[KBCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, JXheightForHeaderInSection)];
@@ -133,13 +136,7 @@ static const CGFloat JXheightForHeaderInSection = 50;
__weak typeof(self)weakSelf = self;
self.pagerView.mainTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.categoryView.titles = @[KBLocalized(@"高级能力"), KBLocalized(@"高级爱好"), KBLocalized(@"高级队友")];
self.categoryView.defaultSelectedIndex = 0;
[self.categoryView reloadData];
[self.pagerView reloadData];
[weakSelf.pagerView.mainTableView.mj_header endRefreshing];
});
[weakSelf fetchShopStylesWithHUD:NO];
}];
self.pagerView.pinSectionHeaderVerticalOffset = KB_NAV_TOTAL_HEIGHT;
@@ -194,19 +191,9 @@ static const CGFloat JXheightForHeaderInSection = 50;
list.title = self.titles[index];
list.isNeedHeader = self.isNeedHeader;
list.isNeedFooter = self.isNeedFooter;
if (index == 0) {
list.dataSource = @[KBLocalized(@"橡胶火箭"), KBLocalized(@"橡胶火箭炮"), KBLocalized(@"橡胶机关枪"), KBLocalized(@"橡胶子弹"),
KBLocalized(@"橡胶攻城炮"), KBLocalized(@"橡胶象枪"), KBLocalized(@"橡胶象枪乱打"), KBLocalized(@"橡胶灰熊铳"),
KBLocalized(@"橡胶雷神象枪"), KBLocalized(@"橡胶猿王枪"), KBLocalized(@"橡胶犀·榴弹炮"), KBLocalized(@"橡胶大蛇炮"),
KBLocalized(@"橡胶火箭"), KBLocalized(@"橡胶火箭炮"), KBLocalized(@"橡胶机关枪"), KBLocalized(@"橡胶子弹"),
KBLocalized(@"橡胶攻城炮"), KBLocalized(@"橡胶象枪"), KBLocalized(@"橡胶象枪乱打"), KBLocalized(@"橡胶灰熊铳"),
KBLocalized(@"橡胶雷神象枪"), KBLocalized(@"橡胶猿王枪"), KBLocalized(@"橡胶犀·榴弹炮"), KBLocalized(@"橡胶大蛇炮")].mutableCopy;
}else if (index == 1) {
list.dataSource = @[KBLocalized(@"吃烤肉"), KBLocalized(@"吃鸡腿肉"), KBLocalized(@"吃牛肉"), KBLocalized(@"各种肉")].mutableCopy;
}else {
list.dataSource = @[KBLocalized(@"【剑士】罗罗诺亚·索隆"), KBLocalized(@"【航海士】娜美"), KBLocalized(@"【狙击手】乌索普"),
KBLocalized(@"【厨师】香吉士"), KBLocalized(@"【船医】托尼托尼·乔巴"), KBLocalized(@"【船匠】 弗兰奇"),
KBLocalized(@"【音乐家】布鲁克"), KBLocalized(@"【考古学家】妮可·罗宾")].mutableCopy;
list.shopViewModel = self.shopVM;
if (index < self.styles.count) {
list.style = self.styles[index];
}
return list;
}
@@ -277,4 +264,46 @@ static const CGFloat JXheightForHeaderInSection = 50;
}
return _skinBtn;
}
- (KBShopVM *)shopVM {
if (!_shopVM) {
_shopVM = [[KBShopVM alloc] init];
}
return _shopVM;
}
- (void)fetchShopStylesWithHUD:(BOOL)showHUD {
if (showHUD) {
[KBHUD show];
}
__weak typeof(self) weakSelf = self;
[self.shopVM fetchAllStylesWithCompletion:^(NSArray<KBShopStyleModel *> * _Nullable styles, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (showHUD) {
[KBHUD dismiss];
}
[weakSelf.pagerView.mainTableView.mj_header endRefreshing];
if (error) {
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
[KBHUD showInfo:msg];
return;
}
[weakSelf applyStyles:styles];
});
}];
}
- (void)applyStyles:(NSArray<KBShopStyleModel *> *)styles {
self.styles = styles ?: @[];
NSMutableArray *names = [NSMutableArray array];
for (KBShopStyleModel *style in self.styles) {
NSString *name = style.styleName.length ? style.styleName : KBLocalized(@"Themes");
[names addObject:name];
}
self.titles = names.copy;
self.categoryView.titles = self.titles;
self.categoryView.defaultSelectedIndex = 0;
[self.categoryView reloadData];
[self.pagerView reloadData];
}
@end

View File

@@ -0,0 +1,34 @@
//
// KBShopVM.h
// keyBoard
//
// Created by Mac on 2025/12/9.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import "KBShopStyleModel.h"
#import "KBShopThemeModel.h"
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Models
typedef void(^KBShopStylesCompletion)(NSArray<KBShopStyleModel *> *_Nullable styles,
NSError *_Nullable error);
typedef void(^KBShopThemesCompletion)(NSArray<KBShopThemeModel *> *_Nullable themes,
NSError *_Nullable error);
@interface KBShopVM : NSObject
@property (nonatomic, copy, readonly, nullable) NSArray<KBShopStyleModel *> *styles;
/// 查询全部主题风格
- (void)fetchAllStylesWithCompletion:(KBShopStylesCompletion)completion;
/// 按风格请求主题列表
- (void)fetchThemesForStyleId:(nullable NSString *)styleId
completion:(KBShopThemesCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,85 @@
//
// KBShopVM.m
// keyBoard
//
// Created by Mac on 2025/12/9.
//
#import "KBShopVM.h"
#import "KBNetworkManager.h"
#import "KBAPI.h"
#import "KBBizCode.h"
#import <MJExtension/MJExtension.h>
@interface KBShopVM ()
@property (nonatomic, copy, readwrite, nullable) NSArray<KBShopStyleModel *> *styles;
@end
@implementation KBShopVM
- (NSError *)kb_invalidResponseError {
return [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid response")}];
}
- (NSError *)kb_invalidParameterError {
return [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid parameter")}];
}
- (void)fetchAllStylesWithCompletion:(KBShopStylesCompletion)completion {
[[KBNetworkManager shared] GET:API_THEME_LIST_ALL_STYLES
parameters:nil
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:[NSArray class]]) {
if (completion) completion(nil, [self kb_invalidResponseError]);
return;
}
NSArray<KBShopStyleModel *> *list = [KBShopStyleModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj];
self.styles = list;
if (completion) completion(list, nil);
}];
}
- (void)fetchThemesForStyleId:(nullable NSString *)styleId
completion:(KBShopThemesCompletion)completion {
if (styleId.length == 0) {
if (completion) completion(nil, [self kb_invalidParameterError]);
return;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"themeStyle"] = @([styleId integerValue]);
[[KBNetworkManager shared] GET:API_THEME_LIST_BY_STYLE
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:[NSArray class]]) {
if (completion) completion(nil, [self kb_invalidResponseError]);
return;
}
NSArray<KBShopThemeModel *> *list = [KBShopThemeModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj];
if (completion) completion(list, nil);
}];
}
@end