// // KBAIHomeVC.m // keyBoard // // Created by Mac on 2026/1/26. // #import "KBAIHomeVC.h" #import "KBPersonaChatCell.h" #import "KBPersonaModel.h" #import "KBVoiceInputBar.h" #import "AiVM.h" #import @interface KBAIHomeVC () /// 人设列表容器 @property (nonatomic, strong) UICollectionView *collectionView; /// 底部语音输入栏 @property (nonatomic, strong) KBVoiceInputBar *voiceInputBar; /// 人设数据 @property (nonatomic, strong) NSMutableArray *personas; /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; /// 是否还有更多数据 @property (nonatomic, assign) BOOL hasMore; /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; /// 当前显示的索引 @property (nonatomic, assign) NSInteger currentIndex; /// 已预加载的索引集合 @property (nonatomic, strong) NSMutableSet *preloadedIndexes; /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; @end @implementation KBAIHomeVC #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.kb_navView.hidden = true; // 初始化数据 self.personas = [NSMutableArray array]; self.currentPage = 1; self.hasMore = YES; self.isLoading = NO; self.currentIndex = 0; self.preloadedIndexes = [NSMutableSet set]; self.aiVM = [[AiVM alloc] init]; [self setupUI]; [self loadPersonas]; } #pragma mark - 1:控件初始化 - (void)setupUI { [self.view addSubview:self.collectionView]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 底部语音输入栏 [self.view addSubview:self.voiceInputBar]; [self.voiceInputBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.bottom.equalTo(self.view); make.height.mas_equalTo(150); // 根据实际需要调整高度 }]; } #pragma mark - 2:数据加载 - (void)loadPersonas { if (self.isLoading) { return; } self.isLoading = YES; __weak typeof(self) weakSelf = self; [self.aiVM fetchPersonasWithPageNum:self.currentPage pageSize:10 completion:^(KBPersonaPageModel * _Nullable pageModel, NSError * _Nullable error) { weakSelf.isLoading = NO; if (error) { NSLog(@"加载人设列表失败:%@", error.localizedDescription); // TODO: 显示错误提示 return; } if (!pageModel || !pageModel.records) { NSLog(@"人设列表数据为空"); return; } // 追加数据 [weakSelf.personas addObjectsFromArray:pageModel.records]; weakSelf.hasMore = pageModel.hasMore; // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.collectionView reloadData]; // 首次加载,预加载前 3 个 if (weakSelf.currentPage == 1) { [weakSelf preloadDataForIndexes:@[@0, @1, @2]]; } }); NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@", weakSelf.personas.count, pageModel.total, pageModel.hasMore ? @"是" : @"否"); }]; } - (void)loadMorePersonas { if (!self.hasMore || self.isLoading) { return; } self.currentPage++; [self loadPersonas]; } #pragma mark - 3:预加载逻辑 - (void)preloadAdjacentCellsForIndex:(NSInteger)index { if (index < 0 || index >= self.personas.count) { return; } NSMutableArray *indexesToPreload = [NSMutableArray array]; // 上一个 if (index > 0) { [indexesToPreload addObject:@(index - 1)]; } // 当前 [indexesToPreload addObject:@(index)]; // 下一个 if (index < self.personas.count - 1) { [indexesToPreload addObject:@(index + 1)]; } [self preloadDataForIndexes:indexesToPreload]; } - (void)preloadDataForIndexes:(NSArray *)indexes { for (NSNumber *indexNum in indexes) { if ([self.preloadedIndexes containsObject:indexNum]) { continue; } [self.preloadedIndexes addObject:indexNum]; NSInteger index = [indexNum integerValue]; if (index >= self.personas.count) { continue; } NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0]; KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; if (cell) { [cell preloadDataIfNeeded]; } NSLog(@"预加载第 %ld 个人设", (long)index); } } #pragma mark - UICollectionViewDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.personas.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath]; cell.persona = self.personas[indexPath.item]; // 标记为已预加载 [self.preloadedIndexes addObject:@(indexPath.item)]; // 直接触发预加载 [cell preloadDataIfNeeded]; return cell; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { CGFloat pageHeight = scrollView.bounds.size.height; CGFloat offsetY = scrollView.contentOffset.y; NSInteger currentPage = offsetY / pageHeight; // 滑动超过 30% 就预加载 if (fmod(offsetY, pageHeight) > pageHeight * 0.3) { [self preloadAdjacentCellsForIndex:currentPage + 1]; } else { [self preloadAdjacentCellsForIndex:currentPage]; } // 接近底部时加载更多 if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) { [self loadMorePersonas]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { CGFloat pageHeight = scrollView.bounds.size.height; NSInteger currentPage = scrollView.contentOffset.y / pageHeight; self.currentIndex = currentPage; if (currentPage < self.personas.count) { NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name); } } #pragma mark - Lazy Load - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; layout.minimumLineSpacing = 0; layout.minimumInteritemSpacing = 0; layout.itemSize = [UIScreen mainScreen].bounds.size; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.pagingEnabled = YES; _collectionView.showsVerticalScrollIndicator = NO; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.delegate = self; _collectionView.dataSource = self; [_collectionView registerClass:[KBPersonaChatCell class] forCellWithReuseIdentifier:@"KBPersonaChatCell"]; if (@available(iOS 11.0, *)) { _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } } return _collectionView; } - (KBVoiceInputBar *)voiceInputBar { if (!_voiceInputBar) { _voiceInputBar = [[KBVoiceInputBar alloc] init]; _voiceInputBar.delegate = self; _voiceInputBar.statusText = @"按住按钮开始对话"; } return _voiceInputBar; } #pragma mark - KBVoiceInputBarDelegate - (void)voiceInputBarDidBeginRecording:(KBVoiceInputBar *)inputBar { NSLog(@"[KBAIHomeVC] 开始录音"); inputBar.statusText = @"正在聆听..."; // TODO: 开始录音逻辑 // 1. 检查登录状态 // 2. 连接语音识别服务 // 3. 开始录音 } - (void)voiceInputBarDidEndRecording:(KBVoiceInputBar *)inputBar { NSLog(@"[KBAIHomeVC] 结束录音"); inputBar.statusText = @"正在识别..."; // TODO: 结束录音逻辑 // 1. 停止录音 // 2. 发送音频数据 // 3. 等待识别结果 } - (void)voiceInputBarDidCancelRecording:(KBVoiceInputBar *)inputBar { NSLog(@"[KBAIHomeVC] 取消录音"); inputBar.statusText = @"已取消"; // TODO: 取消录音逻辑 // 1. 停止录音 // 2. 清理资源 } @end