1
This commit is contained in:
@@ -40,12 +40,12 @@
|
||||
make.height.mas_equalTo(24);
|
||||
}];
|
||||
|
||||
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView).offset(16);
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.bottom.equalTo(self.contentView);
|
||||
make.height.mas_equalTo(0.5);
|
||||
}];
|
||||
// [self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
// make.left.equalTo(self.contentView).offset(16);
|
||||
// make.right.equalTo(self.contentView).offset(-16);
|
||||
// make.bottom.equalTo(self.contentView);
|
||||
// make.height.mas_equalTo(0.5);
|
||||
// }];
|
||||
}
|
||||
|
||||
#pragma mark - Configuration
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#import "AiVM.h"
|
||||
#import <MJExtension/MJExtension.h>
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <MJRefresh/MJRefresh.h>
|
||||
|
||||
static NSString *const kCommentHeaderIdentifier = @"CommentHeader";
|
||||
static NSString *const kReplyCellIdentifier = @"ReplyCell";
|
||||
@@ -33,6 +34,12 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
@property(nonatomic, strong) NSMutableArray<KBAICommentModel *> *comments;
|
||||
@property(nonatomic, assign) NSInteger totalCommentCount;
|
||||
|
||||
/// 分页参数
|
||||
@property(nonatomic, assign) NSInteger currentPage;
|
||||
@property(nonatomic, assign) NSInteger pageSize;
|
||||
@property(nonatomic, assign) BOOL isLoading;
|
||||
@property(nonatomic, assign) BOOL hasMoreData;
|
||||
|
||||
/// 键盘高度
|
||||
@property(nonatomic, assign) CGFloat keyboardHeight;
|
||||
/// 输入框底部约束
|
||||
@@ -56,6 +63,9 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.comments = [NSMutableArray array];
|
||||
self.currentPage = 1;
|
||||
self.pageSize = 20;
|
||||
self.hasMoreData = YES;
|
||||
[self setupUI];
|
||||
[self setupKeyboardObservers];
|
||||
}
|
||||
@@ -114,6 +124,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
make.left.right.equalTo(self);
|
||||
make.bottom.equalTo(self.inputView.mas_top);
|
||||
}];
|
||||
|
||||
// 上拉加载更多
|
||||
__weak typeof(self) weakSelf = self;
|
||||
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
[strongSelf loadMoreComments];
|
||||
}];
|
||||
footer.stateLabel.hidden = YES;
|
||||
footer.backgroundColor = [UIColor clearColor];
|
||||
footer.automaticallyHidden = YES;
|
||||
self.tableView.mj_footer = footer;
|
||||
}
|
||||
|
||||
#pragma mark - Keyboard Observers
|
||||
@@ -174,49 +198,87 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
#pragma mark - Data Loading
|
||||
|
||||
- (void)loadComments {
|
||||
if (self.companionId <= 0) {
|
||||
NSLog(@"[KBAICommentView] companionId 未设置,无法加载评论");
|
||||
[self showEmptyState];
|
||||
if (self.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.currentPage = 1;
|
||||
self.hasMoreData = YES;
|
||||
[self.tableView.mj_footer resetNoMoreData];
|
||||
|
||||
[self fetchCommentsAtPage:self.currentPage append:NO];
|
||||
}
|
||||
|
||||
- (void)loadMoreComments {
|
||||
if (self.isLoading) {
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.hasMoreData) {
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger nextPage = self.currentPage + 1;
|
||||
[self fetchCommentsAtPage:nextPage append:YES];
|
||||
}
|
||||
|
||||
- (void)fetchCommentsAtPage:(NSInteger)page append:(BOOL)append {
|
||||
if (self.companionId <= 0) {
|
||||
NSLog(@"[KBAICommentView] companionId 未设置,无法加载评论");
|
||||
[self showEmptyState];
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
return;
|
||||
}
|
||||
|
||||
self.isLoading = YES;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM fetchCommentsWithCompanionId:self.companionId
|
||||
pageNum:1
|
||||
pageSize:20
|
||||
pageNum:page
|
||||
pageSize:self.pageSize
|
||||
completion:^(KBCommentPageModel *pageModel, NSError *error) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
strongSelf.isLoading = NO;
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[KBAICommentView] 加载评论失败:%@", error.localizedDescription);
|
||||
// 加载失败也显示空态
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (append) {
|
||||
[strongSelf.tableView.mj_footer endRefreshing];
|
||||
} else {
|
||||
[strongSelf showEmptyStateWithError];
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[strongSelf updateCommentsWithPageModel:pageModel];
|
||||
[strongSelf updateCommentsWithPageModel:pageModel append:append];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
/// 更新评论数据(从后端返回的 KBCommentPageModel 转换为 UI 层的 KBAICommentModel)
|
||||
- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel {
|
||||
- (void)updateCommentsWithPageModel:(KBCommentPageModel *)pageModel append:(BOOL)append {
|
||||
if (!pageModel) {
|
||||
NSLog(@"[KBAICommentView] pageModel 为空");
|
||||
// 数据为空,显示空态
|
||||
[self showEmptyState];
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
return;
|
||||
}
|
||||
|
||||
self.totalCommentCount = pageModel.total;
|
||||
|
||||
if (!append) {
|
||||
[self.comments removeAllObjects];
|
||||
}
|
||||
|
||||
// 获取 tableView 宽度用于计算高度
|
||||
CGFloat tableWidth = self.tableView.bounds.size.width;
|
||||
@@ -224,7 +286,7 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
tableWidth = [UIScreen mainScreen].bounds.size.width;
|
||||
}
|
||||
|
||||
NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条", (long)pageModel.records.count, (long)pageModel.total);
|
||||
NSLog(@"[KBAICommentView] 加载到 %ld 条评论,共 %ld 条,页码:%ld/%ld", (long)pageModel.records.count, (long)pageModel.total, (long)pageModel.current, (long)pageModel.pages);
|
||||
|
||||
for (KBCommentItem *item in pageModel.records) {
|
||||
// 转换为 KBAICommentModel(使用 MJExtension)
|
||||
@@ -244,6 +306,20 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
[self updateTitle];
|
||||
[self.tableView reloadData];
|
||||
|
||||
// 更新分页状态
|
||||
self.currentPage = pageModel.current > 0 ? pageModel.current : self.currentPage;
|
||||
if (pageModel.pages > 0) {
|
||||
self.hasMoreData = pageModel.current < pageModel.pages;
|
||||
} else {
|
||||
self.hasMoreData = pageModel.records.count >= self.pageSize;
|
||||
}
|
||||
|
||||
if (self.hasMoreData) {
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
} else {
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
}
|
||||
|
||||
// 根据数据是否为空,动态控制空态显示
|
||||
if (self.comments.count == 0) {
|
||||
[self showEmptyState];
|
||||
@@ -641,6 +717,7 @@ static NSInteger const kRepliesLoadCount = 5;
|
||||
_tableView.backgroundColor = [UIColor clearColor];
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
_tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 0.01)];
|
||||
|
||||
// 关闭空数据占位,避免加载时显示"暂无数据"
|
||||
_tableView.useEmptyDataSet = NO;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView).offset(68); // 16 + 40 + 12 = 68
|
||||
make.top.equalTo(self.contentView).offset(8);
|
||||
make.width.height.mas_equalTo(28);
|
||||
make.width.height.mas_equalTo(26);
|
||||
}];
|
||||
|
||||
[self.userNameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -171,7 +171,7 @@
|
||||
if (!_avatarImageView) {
|
||||
_avatarImageView = [[UIImageView alloc] init];
|
||||
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_avatarImageView.layer.cornerRadius = 14;
|
||||
_avatarImageView.layer.cornerRadius = 13;
|
||||
_avatarImageView.layer.masksToBounds = YES;
|
||||
_avatarImageView.backgroundColor = [UIColor systemGray5Color];
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
if (!_userNameLabel) {
|
||||
_userNameLabel = [[UILabel alloc] init];
|
||||
_userNameLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
|
||||
_userNameLabel.textColor = [UIColor secondaryLabelColor];
|
||||
_userNameLabel.textColor = [UIColor colorWithHex:0x9F9F9F];
|
||||
_userNameLabel.numberOfLines = 0;
|
||||
}
|
||||
return _userNameLabel;
|
||||
@@ -191,8 +191,8 @@
|
||||
- (UILabel *)contentLabel {
|
||||
if (!_contentLabel) {
|
||||
_contentLabel = [[UILabel alloc] init];
|
||||
_contentLabel.font = [UIFont systemFontOfSize:14];
|
||||
_contentLabel.textColor = [UIColor labelColor];
|
||||
_contentLabel.font = [UIFont systemFontOfSize:12];
|
||||
_contentLabel.textColor = [UIColor whiteColor];
|
||||
_contentLabel.numberOfLines = 0;
|
||||
}
|
||||
return _contentLabel;
|
||||
@@ -201,8 +201,8 @@
|
||||
- (UILabel *)timeLabel {
|
||||
if (!_timeLabel) {
|
||||
_timeLabel = [[UILabel alloc] init];
|
||||
_timeLabel.font = [UIFont systemFontOfSize:11];
|
||||
_timeLabel.textColor = [UIColor secondaryLabelColor];
|
||||
_timeLabel.font = [UIFont systemFontOfSize:12];
|
||||
_timeLabel.textColor = [UIColor colorWithHex:0x9F9F9F];
|
||||
}
|
||||
return _timeLabel;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
[self.voiceButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView).offset(16);
|
||||
make.top.equalTo(self.contentView).offset(8);
|
||||
make.width.height.mas_equalTo(24);
|
||||
// make.width.height.mas_equalTo(24);
|
||||
make.width.height.mas_equalTo(0);
|
||||
}];
|
||||
|
||||
[self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -105,8 +106,8 @@
|
||||
|
||||
// 关键修复:messageLabel 约束必须完整,让 AutoLayout 能推导出 bubbleView 的高度
|
||||
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.bubbleView).offset(10);
|
||||
make.bottom.equalTo(self.bubbleView).offset(-10).priority(999); // 降低优先级
|
||||
make.top.equalTo(self.bubbleView).offset(5);
|
||||
make.bottom.equalTo(self.bubbleView).offset(-5).priority(999); // 降低优先级
|
||||
make.left.equalTo(self.bubbleView).offset(12);
|
||||
make.right.equalTo(self.bubbleView).offset(-12);
|
||||
// 关键修复:给 messageLabel 一个最小高度,防止高度为 0
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
// 布局约束
|
||||
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.contentView).offset(8);
|
||||
make.bottom.equalTo(self.contentView).offset(-8);
|
||||
make.top.equalTo(self.contentView).offset(1);
|
||||
make.bottom.equalTo(self.contentView).offset(-1);
|
||||
make.centerX.equalTo(self.contentView);
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -202,8 +202,14 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
// 关键修复:清空消息时停止音频播放,避免状态混乱
|
||||
[self.chatView stopPlayingAudio];
|
||||
|
||||
// 如果有缓存,直接显示
|
||||
// 确保开场白在第一条
|
||||
[self ensureOpeningMessageAtTop];
|
||||
|
||||
// 如果有消息,直接显示(包含开场白)
|
||||
if (self.messages.count > 0) {
|
||||
// 同步缓存,避免下次从缓存缺少开场白
|
||||
[[KBAIChatMessageCacheManager shared] saveMessages:self.messages
|
||||
forCompanionId:persona.personaId];
|
||||
[self.chatView reloadWithMessages:self.messages
|
||||
keepOffset:NO
|
||||
scrollToBottom:YES];
|
||||
@@ -287,15 +293,20 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
[newMessages addObject:message];
|
||||
}
|
||||
|
||||
// 插入到顶部(历史消息)
|
||||
// 插入历史消息(确保开场白始终是第一条)
|
||||
if (weakSelf.currentPage == 1) {
|
||||
// 第一页,直接赋值
|
||||
weakSelf.messages = newMessages;
|
||||
[weakSelf ensureOpeningMessageAtTop];
|
||||
} else {
|
||||
// 后续页,插入到顶部
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)];
|
||||
// 后续页,插入到开场白之后
|
||||
[weakSelf ensureOpeningMessageAtTop];
|
||||
if (newMessages.count > 0) {
|
||||
NSUInteger insertIndex = [weakSelf hasOpeningMessageAtTop] ? 1 : 0;
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertIndex, newMessages.count)];
|
||||
[weakSelf.messages insertObjects:newMessages atIndexes:indexSet];
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新 UI
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -306,7 +317,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
scrollToBottom:scrollToBottom];
|
||||
[weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
|
||||
|
||||
// ✅ 保存到缓存
|
||||
// ✅ 保存到缓存(包含开场白)
|
||||
[[KBAIChatMessageCacheManager shared] saveMessages:weakSelf.messages
|
||||
forCompanionId:companionId];
|
||||
});
|
||||
@@ -332,10 +343,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
|
||||
- (void)showOpeningMessage {
|
||||
// 显示开场白作为第一条消息
|
||||
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText];
|
||||
openingMsg.isComplete = YES;
|
||||
openingMsg.needsTypewriterEffect = NO;
|
||||
[self.messages addObject:openingMsg];
|
||||
[self ensureOpeningMessageAtTop];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.chatView reloadWithMessages:self.messages
|
||||
@@ -344,6 +352,41 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)hasOpeningMessageAtTop {
|
||||
if (self.messages.count == 0) {
|
||||
return NO;
|
||||
}
|
||||
return [self isOpeningMessage:self.messages.firstObject];
|
||||
}
|
||||
|
||||
- (BOOL)isOpeningMessage:(KBAiChatMessage *)message {
|
||||
if (!message) {
|
||||
return NO;
|
||||
}
|
||||
NSString *introText = self.persona.introText ?: @"";
|
||||
if (introText.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
return (message.type == KBAiChatMessageTypeAssistant) && [message.text isEqualToString:introText];
|
||||
}
|
||||
|
||||
- (void)ensureOpeningMessageAtTop {
|
||||
NSString *introText = self.persona.introText ?: @"";
|
||||
if (introText.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (!self.messages) {
|
||||
self.messages = [NSMutableArray array];
|
||||
}
|
||||
if ([self hasOpeningMessageAtTop]) {
|
||||
return;
|
||||
}
|
||||
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:introText];
|
||||
openingMsg.isComplete = YES;
|
||||
openingMsg.needsTypewriterEffect = NO;
|
||||
[self.messages insertObject:openingMsg atIndex:0];
|
||||
}
|
||||
|
||||
#pragma mark - 通知处理
|
||||
|
||||
/// 处理聊天会话被重置的通知
|
||||
@@ -368,12 +411,10 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
// 清空聊天视图
|
||||
[self.chatView clearMessages];
|
||||
|
||||
// 显示开场白
|
||||
if (self.persona.introText.length > 0) {
|
||||
// 显示开场白(始终保持第一条)
|
||||
[self showOpeningMessage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - 3:消息追加
|
||||
|
||||
@@ -386,6 +427,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
self.messages = [NSMutableArray array];
|
||||
}
|
||||
|
||||
[self ensureOpeningMessageAtTop];
|
||||
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
||||
[self.messages addObject:message];
|
||||
[self.chatView addMessage:message autoScroll:YES];
|
||||
@@ -401,6 +443,7 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
self.messages = [NSMutableArray array];
|
||||
}
|
||||
|
||||
[self ensureOpeningMessageAtTop];
|
||||
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
|
||||
audioId:audioId];
|
||||
message.needsTypewriterEffect = YES;
|
||||
|
||||
@@ -242,7 +242,7 @@ autoShowBusinessError:NO
|
||||
[[KBNetworkManager shared] uploadFile:API_AI_AUDIO_UPLOAD
|
||||
fileURL:fileURL
|
||||
name:@"file"
|
||||
mimeType:@"audio/mp4"
|
||||
mimeType:@"audio/m4a"
|
||||
parameters:nil
|
||||
headers:nil
|
||||
completion:^(NSDictionary *_Nullable json,
|
||||
|
||||
Reference in New Issue
Block a user