NewHomeViewController.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. //
  2. // NewHomeViewController.m
  3. // AICity
  4. //
  5. // 新版首页控制器实现
  6. //
  7. #import "NewHomeViewController.h"
  8. #import "JXBannerView.h"
  9. #import "JXHotRecommendView.h"
  10. #import "JXAPIService.h"
  11. #import "JXShortDramaViewController.h"
  12. #import "SearchCommonBarView.h"
  13. #import "SearchResultViewController.h"
  14. #import <Masonry/Masonry.h>
  15. #import <MJRefresh/MJRefresh.h>
  16. #import <SDWebImage/SDWebImage.h>
  17. static NSString * const kHomeGridCellIdentifier = @"HomeGridCell";
  18. typedef NS_ENUM(NSInteger, HomeSectionType) {
  19. HomeSectionTypeBanner = 0, // Banner 轮播
  20. HomeSectionTypeHotRecommend, // 热门推荐
  21. HomeSectionTypeTodayRecommend // 今日推荐(3列网格)
  22. };
  23. #pragma mark - Grid Cell
  24. @interface HomeGridCell : UICollectionViewCell
  25. @property (nonatomic, strong) UIImageView *coverImageView;
  26. @property (nonatomic, strong) UILabel *titleLabel;
  27. @end
  28. @implementation HomeGridCell
  29. - (instancetype)initWithFrame:(CGRect)frame {
  30. self = [super initWithFrame:frame];
  31. if (self) {
  32. self.coverImageView = [[UIImageView alloc] init];
  33. self.coverImageView.contentMode = UIViewContentModeScaleAspectFill;
  34. self.coverImageView.clipsToBounds = YES;
  35. self.coverImageView.layer.cornerRadius = 8;
  36. [self.contentView addSubview:self.coverImageView];
  37. self.titleLabel = [[UILabel alloc] init];
  38. self.titleLabel.font = [UIFont systemFontOfSize:12];
  39. self.titleLabel.textColor = [UIColor blackColor];
  40. self.titleLabel.numberOfLines = 2;
  41. [self.contentView addSubview:self.titleLabel];
  42. [self.coverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
  43. make.top.left.right.equalTo(self.contentView);
  44. make.height.equalTo(self.coverImageView.mas_width).multipliedBy(1.5);
  45. }];
  46. [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
  47. make.top.equalTo(self.coverImageView.mas_bottom).offset(4);
  48. make.left.right.equalTo(self.contentView);
  49. }];
  50. }
  51. return self;
  52. }
  53. @end
  54. #pragma mark - New Home ViewController
  55. @interface NewHomeViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
  56. @property (nonatomic, strong) SearchCommonBarView *searchBar;
  57. @property (nonatomic, strong) UICollectionView *collectionView;
  58. // Data
  59. @property (nonatomic, strong) JXHomeHeaderData *headerData;
  60. @property (nonatomic, strong) NSMutableArray *todayRecommendList;
  61. @property (nonatomic, assign) NSInteger currentPage;
  62. // Views
  63. @property (nonatomic, strong) JXBannerView *bannerView;
  64. @property (nonatomic, strong) JXHotRecommendView *hotRecommendView;
  65. @end
  66. @implementation NewHomeViewController
  67. - (void)viewDidLoad {
  68. [super viewDidLoad];
  69. self.view.backgroundColor = [UIColor whiteColor];
  70. self.todayRecommendList = [NSMutableArray array];
  71. self.currentPage = 1;
  72. [self setupUI];
  73. [self loadHomeData];
  74. }
  75. - (void)viewWillAppear:(BOOL)animated {
  76. [super viewWillAppear:animated];
  77. [self.navigationController setNavigationBarHidden:YES animated:animated];
  78. // 启动 Banner 自动滚动
  79. if (self.bannerView) {
  80. [self.bannerView startAutoScroll];
  81. }
  82. }
  83. - (void)viewWillDisappear:(BOOL)animated {
  84. [super viewWillDisappear:animated];
  85. // 停止 Banner 自动滚动
  86. if (self.bannerView) {
  87. [self.bannerView stopAutoScroll];
  88. }
  89. }
  90. - (void)setupUI {
  91. // 搜索栏
  92. self.searchBar = [[SearchCommonBarView alloc] init];
  93. __weak typeof(self) weakSelf = self;
  94. self.searchBar.searchAction = ^(NSString * _Nonnull text) {
  95. [weakSelf performSearchWithKeyword:text];
  96. };
  97. [self.view addSubview:self.searchBar];
  98. // CollectionView
  99. UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
  100. layout.minimumLineSpacing = 12;
  101. layout.minimumInteritemSpacing = 8;
  102. layout.sectionInset = UIEdgeInsetsMake(12, 12, 12, 12);
  103. self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
  104. self.collectionView.delegate = self;
  105. self.collectionView.dataSource = self;
  106. self.collectionView.backgroundColor = [UIColor whiteColor];
  107. self.collectionView.alwaysBounceVertical = YES;
  108. [self.collectionView registerClass:[HomeGridCell class] forCellWithReuseIdentifier:kHomeGridCellIdentifier];
  109. [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CustomCell"];
  110. [self.view addSubview:self.collectionView];
  111. // 初始化数据模型
  112. if (!self.headerData) {
  113. self.headerData = [[JXHomeHeaderData alloc] init];
  114. self.headerData.banners = @[];
  115. self.headerData.hotMovies = @[];
  116. self.headerData.hotDramas = @[];
  117. NSLog(@"[NewHomeViewController] Initialized empty headerData");
  118. }
  119. // Layout
  120. [self.searchBar mas_makeConstraints:^(MASConstraintMaker *make) {
  121. make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
  122. make.left.right.equalTo(self.view);
  123. make.height.mas_equalTo(44);
  124. }];
  125. [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
  126. make.top.equalTo(self.searchBar.mas_bottom);
  127. make.left.right.bottom.equalTo(self.view);
  128. }];
  129. // 下拉刷新
  130. self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
  131. [weakSelf refreshData];
  132. }];
  133. // 上拉加载
  134. self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
  135. [weakSelf loadMoreData];
  136. }];
  137. }
  138. #pragma mark - Data Loading
  139. - (void)loadHomeData {
  140. // 加载首页头部数据
  141. NSLog(@"[NewHomeViewController] Starting to load home data");
  142. // 设置加载超时保护(15秒)
  143. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  144. if (!self.headerData || self.headerData.banners.count == 0) {
  145. NSLog(@"[NewHomeViewController] Home data loading timeout, using empty data");
  146. [self showErrorRecoveryUI:@"首页数据加载超时,显示空内容"];
  147. }
  148. });
  149. [[JXAPIService sharedService] getHomeHeaderDataWithSuccess:^(JXHomeHeaderData *headerData) {
  150. self.headerData = headerData;
  151. NSLog(@"[NewHomeViewController] Header data loaded: banners=%lu, hotMovies=%lu, hotDramas=%lu",
  152. (unsigned long)headerData.banners.count,
  153. (unsigned long)headerData.hotMovies.count,
  154. (unsigned long)headerData.hotDramas.count);
  155. [self.collectionView reloadData];
  156. } failure:^(NSError *error) {
  157. NSLog(@"[NewHomeViewController] Failed to load header data: %@", error);
  158. [self handleLoadingError:error section:@"首页头部数据"];
  159. }];
  160. // 加载今日推荐
  161. [self loadTodayRecommend];
  162. }
  163. - (void)loadTodayRecommend {
  164. NSLog(@"[NewHomeViewController] Starting to load today recommend, page=%ld", (long)self.currentPage);
  165. // 设置加载超时保护(10秒)
  166. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  167. if (self.todayRecommendList.count == 0 && self.currentPage == 1) {
  168. NSLog(@"[NewHomeViewController] Today recommend loading timeout");
  169. [self showErrorRecoveryUI:@"推荐内容加载超时"];
  170. [self.collectionView.mj_header endRefreshing];
  171. [self.collectionView.mj_footer endRefreshing];
  172. }
  173. });
  174. [[JXAPIService sharedService] getDailyRecommendWithPage:self.currentPage pageSize:9 success:^(id responseObject) {
  175. if ([responseObject[@"code"] intValue] == 0 || [responseObject[@"code"] intValue] == 200) {
  176. NSArray *list = responseObject[@"data"][@"list"];
  177. if (list && [list isKindOfClass:[NSArray class]]) {
  178. if (self.currentPage == 1) {
  179. [self.todayRecommendList removeAllObjects];
  180. }
  181. [self.todayRecommendList addObjectsFromArray:list];
  182. [self.collectionView reloadData];
  183. NSLog(@"[NewHomeViewController] Today recommend loaded: page=%ld, count=%lu",
  184. (long)self.currentPage, (unsigned long)list.count);
  185. }
  186. }
  187. [self.collectionView.mj_header endRefreshing];
  188. [self.collectionView.mj_footer endRefreshing];
  189. } failure:^(NSError *error) {
  190. NSLog(@"[NewHomeViewController] Failed to load today recommend: %@", error);
  191. [self handleLoadingError:error section:@"推荐内容"];
  192. [self.collectionView.mj_header endRefreshing];
  193. [self.collectionView.mj_footer endRefreshing];
  194. }];
  195. }
  196. #pragma mark - Error Recovery
  197. - (void)handleLoadingError:(NSError *)error section:(NSString *)sectionName {
  198. // 记录错误信息
  199. NSLog(@"[NewHomeViewController] Loading error - Section: %@, Error: %@", sectionName, error.localizedDescription);
  200. // 区分错误类型
  201. NSString *errorDesc = @"加载失败,请检查网络连接";
  202. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  203. switch (error.code) {
  204. case NSURLErrorTimedOut:
  205. errorDesc = @"加载超时,请重试";
  206. break;
  207. case NSURLErrorNotConnectedToInternet:
  208. case NSURLErrorNetworkConnectionLost:
  209. errorDesc = @"网络连接失败";
  210. break;
  211. case NSURLErrorCannotFindHost:
  212. errorDesc = @"无法连接到服务器";
  213. break;
  214. default:
  215. break;
  216. }
  217. }
  218. // 触发降级显示和用户提示
  219. [self showErrorRecoveryUI:errorDesc];
  220. }
  221. - (void)showErrorRecoveryUI:(NSString *)errorMessage {
  222. // 初始化为空数据(降级显示)
  223. if (!self.headerData) {
  224. self.headerData = [[JXHomeHeaderData alloc] init];
  225. self.headerData.banners = @[];
  226. self.headerData.hotMovies = @[];
  227. self.headerData.hotDramas = @[];
  228. NSLog(@"[NewHomeViewController] Degradation mode: empty data initialized");
  229. }
  230. // 重新加载 UI
  231. [self.collectionView reloadData];
  232. // 显示错误提示(弱通知方式,不阻挡 UI)
  233. dispatch_async(dispatch_get_main_queue(), ^{
  234. // 可选:显示简短的 toast 或横幅提示
  235. NSLog(@"[NewHomeViewController] Show degradation notice: %@", errorMessage);
  236. // 如果有必要可以显示一个小的错误横幅
  237. // [self showErrorBanner:errorMessage];
  238. });
  239. }
  240. // 显示错误提示 banner(可选,当需要时取消注释)
  241. - (void)showErrorBanner:(NSString *)message {
  242. // 创建错误提示 banner
  243. UIView *bannerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 44)];
  244. bannerView.backgroundColor = [UIColor colorWithRed:1.0 green:0.3 blue:0.3 alpha:0.9];
  245. bannerView.tag = 9999;
  246. UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(bannerView.bounds, 12, 0)];
  247. label.text = message;
  248. label.textColor = [UIColor whiteColor];
  249. label.font = [UIFont systemFontOfSize:14];
  250. label.textAlignment = NSTextAlignmentCenter;
  251. label.numberOfLines = 1;
  252. [bannerView addSubview:label];
  253. [self.view addSubview:bannerView];
  254. // 3 秒后自动移除
  255. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  256. [UIView animateWithDuration:0.3 animations:^{
  257. bannerView.alpha = 0;
  258. } completion:^(BOOL finished) {
  259. [bannerView removeFromSuperview];
  260. }];
  261. });
  262. }
  263. - (void)refreshData {
  264. self.currentPage = 1;
  265. [self loadHomeData];
  266. }
  267. - (void)loadMoreData {
  268. if (self.currentPage >= 10) {
  269. [self.collectionView.mj_footer endRefreshingWithNoMoreData];
  270. return;
  271. }
  272. self.currentPage++;
  273. [self loadTodayRecommend];
  274. }
  275. #pragma mark - Search
  276. - (void)performSearchWithKeyword:(NSString *)keyword {
  277. if (keyword.length == 0) {
  278. return;
  279. }
  280. SearchResultViewController *vc = [[SearchResultViewController alloc] init];
  281. vc.txt = keyword;
  282. vc.hidesBottomBarWhenPushed = YES;
  283. [self.navigationController pushViewController:vc animated:YES];
  284. }
  285. #pragma mark - UICollectionViewDataSource
  286. - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
  287. return 3; // Banner + HotRecommend + TodayRecommend
  288. }
  289. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  290. if (section == HomeSectionTypeBanner) {
  291. return (self.headerData.banners.count > 0) ? 1 : 0;
  292. } else if (section == HomeSectionTypeHotRecommend) {
  293. NSArray *hotItems = [self.headerData getAllHotRecommends];
  294. return (hotItems.count > 0) ? 1 : 0;
  295. } else {
  296. return self.todayRecommendList.count;
  297. }
  298. }
  299. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  300. if (indexPath.section == HomeSectionTypeTodayRecommend) {
  301. // 今日推荐网格
  302. HomeGridCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kHomeGridCellIdentifier forIndexPath:indexPath];
  303. // 防护检查:确保数据存在
  304. if (indexPath.item >= self.todayRecommendList.count) {
  305. NSLog(@"[NewHomeViewController] Warning: Index out of bounds for todayRecommendList");
  306. cell.titleLabel.text = @"";
  307. return cell;
  308. }
  309. NSDictionary *item = self.todayRecommendList[indexPath.item];
  310. if (!item || ![item isKindOfClass:[NSDictionary class]]) {
  311. NSLog(@"[NewHomeViewController] Warning: Invalid item data at index %ld", (long)indexPath.item);
  312. cell.titleLabel.text = @"";
  313. return cell;
  314. }
  315. cell.titleLabel.text = item[@"name"] ?: @"";
  316. NSString *coverUrl = item[@"v_cover_url"] ?: item[@"h_cover_url"];
  317. if (coverUrl) {
  318. [cell.coverImageView sd_setImageWithURL:[NSURL URLWithString:coverUrl]
  319. placeholderImage:nil];
  320. }
  321. return cell;
  322. } else {
  323. // Banner 和 HotRecommend 使用自定义 View
  324. UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomCell" forIndexPath:indexPath];
  325. // 清除旧的子视图
  326. for (UIView *view in cell.contentView.subviews) {
  327. [view removeFromSuperview];
  328. }
  329. if (indexPath.section == HomeSectionTypeBanner) {
  330. // Banner
  331. if (!self.bannerView) {
  332. self.bannerView = [[JXBannerView alloc] init];
  333. __weak typeof(self) weakSelf = self;
  334. self.bannerView.onBannerClick = ^(JXBannerItem *item) {
  335. [weakSelf handleItemClick:item];
  336. };
  337. }
  338. // 防护检查
  339. if (self.headerData && self.headerData.banners && self.headerData.banners.count > 0) {
  340. [self.bannerView updateBanners:self.headerData.banners];
  341. } else {
  342. [self.bannerView updateBanners:@[]];
  343. }
  344. [cell.contentView addSubview:self.bannerView];
  345. [self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
  346. make.edges.equalTo(cell.contentView);
  347. }];
  348. } else if (indexPath.section == HomeSectionTypeHotRecommend) {
  349. // 热门推荐
  350. if (!self.hotRecommendView) {
  351. self.hotRecommendView = [[JXHotRecommendView alloc] init];
  352. __weak typeof(self) weakSelf = self;
  353. self.hotRecommendView.onItemClick = ^(JXBannerItem *item) {
  354. [weakSelf handleItemClick:item];
  355. };
  356. }
  357. // 防护检查
  358. NSArray *hotItems = @[];
  359. if (self.headerData) {
  360. NSArray *temp = [self.headerData getAllHotRecommends];
  361. if (temp) {
  362. hotItems = temp;
  363. }
  364. }
  365. [self.hotRecommendView updateItems:hotItems];
  366. [cell.contentView addSubview:self.hotRecommendView];
  367. [self.hotRecommendView mas_makeConstraints:^(MASConstraintMaker *make) {
  368. make.edges.equalTo(cell.contentView);
  369. }];
  370. }
  371. return cell;
  372. }
  373. }
  374. #pragma mark - UICollectionViewDelegate
  375. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  376. if (indexPath.section == HomeSectionTypeTodayRecommend) {
  377. // 今日推荐点击
  378. // 防护检查
  379. if (indexPath.item >= self.todayRecommendList.count) {
  380. NSLog(@"[NewHomeViewController] Warning: Index out of bounds in didSelectItemAtIndexPath");
  381. return;
  382. }
  383. NSDictionary *item = self.todayRecommendList[indexPath.item];
  384. if (!item || ![item isKindOfClass:[NSDictionary class]]) {
  385. NSLog(@"[NewHomeViewController] Warning: Invalid item data in didSelectItemAtIndexPath");
  386. return;
  387. }
  388. JXBannerItem *bannerItem = [JXBannerItem modelWithDictionary:item];
  389. if (bannerItem) {
  390. [self handleItemClick:bannerItem];
  391. }
  392. }
  393. }
  394. #pragma mark - UICollectionViewDelegateFlowLayout
  395. - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  396. CGFloat width = collectionView.bounds.size.width;
  397. if (indexPath.section == HomeSectionTypeBanner) {
  398. // Banner: 全宽,16:9 比例
  399. return CGSizeMake(width, width * 9.0 / 16.0);
  400. } else if (indexPath.section == HomeSectionTypeHotRecommend) {
  401. // 热门推荐: 全宽,固定高度
  402. return CGSizeMake(width, 240);
  403. } else {
  404. // 今日推荐: 3列网格
  405. CGFloat itemWidth = (width - 12 * 2 - 8 * 2) / 3.0; // 减去左右边距和列间距
  406. return CGSizeMake(itemWidth, itemWidth * 1.7); // 封面 + 标题
  407. }
  408. }
  409. - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
  410. if (section == HomeSectionTypeBanner || section == HomeSectionTypeHotRecommend) {
  411. return UIEdgeInsetsZero;
  412. }
  413. return UIEdgeInsetsMake(12, 12, 12, 12);
  414. }
  415. - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
  416. return 12;
  417. }
  418. - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
  419. if (section == HomeSectionTypeTodayRecommend) {
  420. return 8;
  421. }
  422. return 0;
  423. }
  424. #pragma mark - Handle Click
  425. - (void)handleItemClick:(JXBannerItem *)item {
  426. // 判断是短剧还是影片
  427. if ([item.categories containsString:@"短剧"]) {
  428. NSLog(@"[NewHomeViewController] 点击短剧: jxDramaId=%ld, name=%@", (long)item.itemId, item.name);
  429. // 跳转到短剧播放器
  430. JXShortDramaViewController *vc = [[JXShortDramaViewController alloc] init];
  431. // TODO: 设置初始短剧ID和集数
  432. vc.hidesBottomBarWhenPushed = YES;
  433. [self.navigationController pushViewController:vc animated:YES];
  434. } else {
  435. NSLog(@"[NewHomeViewController] 点击影片: id=%ld, name=%@", (long)item.itemId, item.name);
  436. // TODO: 跳转到普通影片播放器
  437. // MoviePlayActivity.launchMoviePlay(requireContext(), item.itemId)
  438. }
  439. }
  440. @end