// // KBPersonaChatCell.m // keyBoard // // Created by Kiro on 2026/1/26. // #import "KBPersonaChatCell.h" #import "KBAiChatMessage.h" #import "KBChatHistoryPageModel.h" #import "AiVM.h" #import "KBImagePositionButton.h" #import "KBAICommentView.h" #import "KBAIChatMessageCacheManager.h" #import #import #import @interface KBPersonaChatCell () /// 背景图 @property (nonatomic, strong) UIImageView *backgroundImageView; /// 头像 @property (nonatomic, strong) UIImageView *avatarImageView; /// 人设名称 @property (nonatomic, strong) UILabel *nameLabel; /// 开场白 @property (nonatomic, strong) UILabel *openingLabel; /// 聊天消息 @property (nonatomic, strong) NSMutableArray *messages; /// 是否已加载数据 @property (nonatomic, assign) BOOL hasLoadedData; /// 是否正在加载 @property (nonatomic, assign) BOOL isLoading; /// 当前页码 @property (nonatomic, assign) NSInteger currentPage; /// 是否还有更多历史消息 @property (nonatomic, assign) BOOL hasMoreHistory; /// AiVM 实例 @property (nonatomic, strong) AiVM *aiVM; /// 评论按钮 @property (nonatomic, strong) KBImagePositionButton *commentButton; /// 喜欢按钮 @property (nonatomic, strong) KBImagePositionButton *likeButton; /// 评论弹窗 @property (nonatomic, weak) LSTPopView *popView; @end @implementation KBPersonaChatCell #pragma mark - Lifecycle - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupUI]; } return self; } /// 关键修复:Cell 复用时不清空数据,避免重复请求 - (void)prepareForReuse { [super prepareForReuse]; // 停止音频播放 [self.chatView stopPlayingAudio]; // 重置加载状态标志(但不清空 hasLoadedData) self.isLoading = NO; // ✅ 移除了 self.hasLoadedData = NO; // 这样 Cell 复用时不会重复请求数据 } #pragma mark - 1:控件初始化 - (void)setupUI { // 背景图 [self.contentView addSubview:self.backgroundImageView]; [self.backgroundImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; // 半透明遮罩 UIView *maskView = [[UIView alloc] init]; maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3]; [self.contentView addSubview:maskView]; [maskView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; // 开场白 [self.contentView addSubview:self.openingLabel]; [self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT); make.left.equalTo(self.contentView).offset(40); make.right.equalTo(self.contentView).offset(-40); }]; // 头像 [self.contentView addSubview:self.avatarImageView]; [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(self.contentView).offset(-KB_TABBAR_HEIGHT - 50 - 20); make.left.equalTo(self.contentView).offset(20); make.size.mas_equalTo(CGSizeMake(54, 54)); }]; // 人设名称 [self.contentView addSubview:self.nameLabel]; [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.avatarImageView.mas_right).offset(5); make.centerY.equalTo(self.avatarImageView); }]; // 评论按钮(最右侧) [self.contentView addSubview:self.commentButton]; [self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.contentView).offset(-20); make.centerY.equalTo(self.avatarImageView); make.width.mas_equalTo(40); make.height.mas_equalTo(50); }]; // 喜欢按钮(评论按钮左侧,间距20px) [self.contentView addSubview:self.likeButton]; [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.commentButton.mas_left).offset(-20); make.centerY.equalTo(self.avatarImageView); make.width.mas_equalTo(40); make.height.mas_equalTo(50); }]; // 聊天列表 [self.contentView addSubview:self.chatView]; [self.chatView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT); make.left.right.equalTo(self.contentView); make.bottom.equalTo(self.avatarImageView.mas_top).offset(-10); }]; } #pragma mark - Setter - (void)setPersona:(KBPersonaModel *)persona { _persona = persona; // 重置状态 self.isLoading = NO; self.currentPage = 1; self.hasMoreHistory = YES; // ✅ 尝试从缓存加载 messages NSArray *cachedMessages = [[KBAIChatMessageCacheManager shared] messagesForCompanionId:persona.personaId]; if (cachedMessages.count > 0) { // 缓存命中,直接使用 self.messages = [cachedMessages mutableCopy]; self.hasLoadedData = YES; NSLog(@"[Cell] ✅ 从缓存加载:personaId=%ld, 消息数=%ld", (long)persona.personaId, (long)cachedMessages.count); } else { // 缓存未命中,需要请求 self.messages = [NSMutableArray array]; self.hasLoadedData = NO; NSLog(@"[Cell] ⚠️ 缓存未命中:personaId=%ld, 需要请求数据", (long)persona.personaId); } // 设置 UI [self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl] placeholderImage:[UIImage imageNamed:@"placeholder_bg"]]; [self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl] placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]]; self.nameLabel.text = persona.name; self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText; // 关键修复:清空消息时停止音频播放,避免状态混乱 [self.chatView stopPlayingAudio]; // 如果有缓存,直接显示 if (self.messages.count > 0) { [self.chatView reloadWithMessages:self.messages hasMoreHistory:self.hasMoreHistory completion:nil]; } else { [self.chatView clearMessages]; } [self.commentButton setTitle:persona.commentCount forState:UIControlStateNormal]; [self.likeButton setTitle:persona.likeCount forState:UIControlStateNormal]; self.likeButton.selected = persona.liked; } #pragma mark - 2:数据加载 - (void)preloadDataIfNeeded { if (self.hasLoadedData || self.isLoading) { return; } [self loadChatHistory]; } - (void)loadChatHistory { if (self.isLoading || !self.hasMoreHistory) { [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } self.isLoading = YES; if (self.currentPage == 1) { [self.chatView resetNoMoreData]; } // 使用 persona.personaId 作为 companionId NSInteger companionId = self.persona.personaId; __weak typeof(self) weakSelf = self; [self.aiVM fetchChatHistoryWithCompanionId:companionId pageNum:self.currentPage pageSize:20 completion:^(KBChatHistoryPageModel *pageModel, NSError *error) { weakSelf.isLoading = NO; if (error) { NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription); [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; // 如果是第一次加载失败,显示开场白 if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) { [weakSelf showOpeningMessage]; } return; } weakSelf.hasLoadedData = YES; weakSelf.hasMoreHistory = pageModel.hasMore; // 转换为 KBAiChatMessage NSMutableArray *newMessages = [NSMutableArray array]; for (KBChatHistoryModel *item in pageModel.records) { KBAiChatMessage *message; // 根据 sender 判断消息类型 // sender = 1: 用户消息(右侧) // sender = 2: AI 消息(左侧) if (item.sender == KBChatSenderUser) { // 用户消息 message = [KBAiChatMessage userMessageWithText:item.content]; } else if (item.sender == KBChatSenderAssistant) { // AI 消息 message = [KBAiChatMessage assistantMessageWithText:item.content]; } else { // 未知类型,默认为 AI 消息 NSLog(@"[KBPersonaChatCell] 未知的 sender 类型:%ld", (long)item.sender); message = [KBAiChatMessage assistantMessageWithText:item.content]; } message.isComplete = YES; message.needsTypewriterEffect = NO; [newMessages addObject:message]; } // 插入到顶部(历史消息) if (weakSelf.currentPage == 1) { // 第一页,直接赋值 weakSelf.messages = newMessages; } else { // 后续页,插入到顶部 NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)]; [weakSelf.messages insertObjects:newMessages atIndexes:indexSet]; } // 刷新 UI dispatch_async(dispatch_get_main_queue(), ^{ BOOL keepOffset = (weakSelf.currentPage != 1); BOOL scrollToBottom = (weakSelf.currentPage == 1); [weakSelf.chatView reloadWithMessages:weakSelf.messages keepOffset:keepOffset scrollToBottom:scrollToBottom]; [weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory]; // ✅ 保存到缓存 [[KBAIChatMessageCacheManager shared] saveMessages:weakSelf.messages forCompanionId:companionId]; }); weakSelf.currentPage++; NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@", (long)weakSelf.currentPage - 1, (long)newMessages.count, pageModel.hasMore ? @"是" : @"否"); }]; } - (void)loadMoreHistory { if (!self.hasMoreHistory || self.isLoading) { [self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory]; return; } self.currentPage++; [self loadChatHistory]; } - (void)showOpeningMessage { // 显示开场白作为第一条消息 KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText]; openingMsg.isComplete = YES; openingMsg.needsTypewriterEffect = NO; [self.messages addObject:openingMsg]; dispatch_async(dispatch_get_main_queue(), ^{ [self.chatView reloadWithMessages:self.messages keepOffset:NO scrollToBottom:YES]; }); } #pragma mark - 3:消息追加 - (void)appendUserMessage:(NSString *)text { if (text.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text]; [self.messages addObject:message]; [self.chatView addMessage:message autoScroll:YES]; } - (void)appendAssistantMessage:(NSString *)text audioId:(NSString *)audioId { if (text.length == 0) { return; } if (!self.messages) { self.messages = [NSMutableArray array]; } KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text audioId:audioId]; message.needsTypewriterEffect = YES; [self.messages addObject:message]; [self.chatView addMessage:message autoScroll:YES]; } - (void)updateChatViewBottomInset:(CGFloat)bottomInset { [self.chatView updateContentBottomInset:bottomInset]; } #pragma mark - KBChatTableViewDelegate - (void)chatTableViewDidScroll:(KBChatTableView *)chatView scrollView:(UIScrollView *)scrollView { CGFloat offsetY = scrollView.contentOffset.y; // 下拉到顶部,加载历史消息 if (offsetY <= -50 && !self.isLoading) { [self loadMoreHistory]; } } - (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView { [self loadMoreHistory]; } #pragma mark - Lazy Load - (UIImageView *)backgroundImageView { if (!_backgroundImageView) { _backgroundImageView = [[UIImageView alloc] init]; _backgroundImageView.contentMode = UIViewContentModeScaleAspectFill; _backgroundImageView.clipsToBounds = YES; } return _backgroundImageView; } - (UIImageView *)avatarImageView { if (!_avatarImageView) { _avatarImageView = [[UIImageView alloc] init]; _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; _avatarImageView.layer.cornerRadius = 27; _avatarImageView.layer.borderWidth = 3; _avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor; _avatarImageView.clipsToBounds = YES; } return _avatarImageView; } - (UILabel *)nameLabel { if (!_nameLabel) { _nameLabel = [[UILabel alloc] init]; _nameLabel.font = [UIFont boldSystemFontOfSize:12]; _nameLabel.textColor = [UIColor whiteColor]; _nameLabel.textAlignment = NSTextAlignmentCenter; } return _nameLabel; } - (UILabel *)openingLabel { if (!_openingLabel) { _openingLabel = [[UILabel alloc] init]; _openingLabel.font = [UIFont systemFontOfSize:14]; _openingLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; _openingLabel.textAlignment = NSTextAlignmentCenter; _openingLabel.numberOfLines = 2; } return _openingLabel; } - (KBChatTableView *)chatView { if (!_chatView) { _chatView = [[KBChatTableView alloc] init]; _chatView.backgroundColor = [UIColor clearColor]; _chatView.delegate = self; } return _chatView; } - (KBImagePositionButton *)commentButton { if (!_commentButton) { // 创建上图下文的按钮 _commentButton = [[KBImagePositionButton alloc] initWithImagePosition:KBImagePositionTop spacing:4]; // 关键修复:先设置字体,再设置文字,避免循环调用 _commentButton.titleLabel.font = [UIFont systemFontOfSize:10]; // 设置图片 [_commentButton setImage:[UIImage imageNamed:@"ai_comment_icon"] forState:UIControlStateNormal]; // 设置文字 [_commentButton setTitle:@"0" forState:UIControlStateNormal]; [_commentButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.8] forState:UIControlStateNormal]; // 添加点击事件 [_commentButton addTarget:self action:@selector(commentButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; } return _commentButton; } - (KBImagePositionButton *)likeButton { if (!_likeButton) { // 创建上图下文的按钮 _likeButton = [[KBImagePositionButton alloc] initWithImagePosition:KBImagePositionTop spacing:4]; // 关键修复:先设置字体,再设置文字,避免循环调用 _likeButton.titleLabel.font = [UIFont systemFontOfSize:10]; // 设置图片 [_likeButton setImage:[UIImage imageNamed:@"ai_live_icon"] forState:UIControlStateNormal]; [_likeButton setImage:[UIImage imageNamed:@"ai_livesel_icon"] forState:UIControlStateSelected]; // 设置文字 [_likeButton setTitle:@"0" forState:UIControlStateNormal]; [_likeButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.8] forState:UIControlStateNormal]; // 添加点击事件 [_likeButton addTarget:self action:@selector(likeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; } return _likeButton; } #pragma mark - Button Actions - (void)commentButtonTapped:(KBImagePositionButton *)sender { NSLog(@"[KBPersonaChatCell] 评论按钮点击"); // 弹出评论视图 [self showComment]; } - (void)likeButtonTapped:(KBImagePositionButton *)sender { NSLog(@"[KBPersonaChatCell] 喜欢按钮点击"); NSInteger personaId = self.persona.personaId; // 禁用按钮,防止重复点击 sender.enabled = NO; __weak typeof(self) weakSelf = self; [self.aiVM likeCompanionWithCompanionId:personaId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } dispatch_async(dispatch_get_main_queue(), ^{ // 恢复按钮可用状态 sender.enabled = YES; if (error) { NSLog(@"[KBPersonaChatCell] 点赞失败:%@", error.localizedDescription); // TODO: 显示错误提示 return; } if (response && response.code == 0) { // 获取当前喜欢数 NSInteger currentLikeCount = [strongSelf.persona.likeCount integerValue]; // response.data: true 表示点赞成功,false 表示取消点赞成功 if (response.data) { // 点赞成功,喜欢数加1 currentLikeCount += 1; sender.selected = YES; NSLog(@"[KBPersonaChatCell] 点赞成功,新喜欢数:%ld", (long)currentLikeCount); } else { // 取消点赞成功,喜欢数减1(但不能小于0) currentLikeCount = MAX(0, currentLikeCount - 1); sender.selected = NO; NSLog(@"[KBPersonaChatCell] 取消点赞成功,新喜欢数:%ld", (long)currentLikeCount); } // 更新模型数据 strongSelf.persona.likeCount = [NSString stringWithFormat:@"%ld", (long)currentLikeCount]; strongSelf.persona.liked = sender.selected; // 更新按钮显示文字 [sender setTitle:strongSelf.persona.likeCount forState:UIControlStateNormal]; } else { NSLog(@"[KBPersonaChatCell] 点赞失败:%@", response.message ?: @"未知错误"); // TODO: 显示错误提示 } }); }]; } #pragma mark - Comment View - (void)showComment { // 关闭之前的弹窗 if (self.popView) { [self.popView dismiss]; } CGFloat customViewHeight = KB_SCREEN_HEIGHT * 0.8; KBAICommentView *customView = [[KBAICommentView alloc] initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; // 设置评论视图的人设 ID customView.companionId = self.persona.personaId; // 加载评论数据 [customView loadComments]; LSTPopView *popView = [LSTPopView initWithCustomView:customView parentView:nil popStyle:LSTPopStyleSmoothFromBottom dismissStyle:LSTDismissStyleSmoothToBottom]; customView.popView = popView; popView.bgColor = [UIColor clearColor]; self.popView = popView; popView.priority = 1000; popView.isAvoidKeyboard = NO; popView.hemStyle = LSTHemStyleBottom; popView.dragStyle = LSTDragStyleY_Positive; popView.dragDistance = customViewHeight * 0.5; popView.sweepStyle = LSTSweepStyleY_Positive; popView.swipeVelocity = 1600; popView.sweepDismissStyle = LSTSweepDismissStyleSmooth; [popView pop]; } - (AiVM *)aiVM{ if (!_aiVM) { _aiVM = [[AiVM alloc] init]; } return _aiVM; } @end