处理滚动底部问题

This commit is contained in:
2026-01-29 14:42:49 +08:00
parent 25fbe9b64e
commit 32ebc6fb65
12 changed files with 174 additions and 26 deletions

View File

@@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, assign) NSInteger likeCount; @property(nonatomic, assign) NSInteger likeCount;
/// 是否已点赞 /// 是否已点赞
@property(nonatomic, assign) BOOL isLiked; @property(nonatomic, assign) BOOL liked;
/// 创建时间(时间戳) /// 创建时间(时间戳)
@property(nonatomic, assign) NSTimeInterval createTime; @property(nonatomic, assign) NSTimeInterval createTime;

View File

@@ -21,10 +21,6 @@
}; };
} }
- (void)setLiked:(NSInteger)liked {
// NSInteger (0/1) BOOL
_isLiked = (liked == 1);
}
- (void)setCreatedAt:(NSString *)createdAt { - (void)setCreatedAt:(NSString *)createdAt {
// //

View File

@@ -43,8 +43,14 @@ typedef NS_ENUM(NSInteger, KBAiChatMessageType) {
/// 是否需要打字机效果(只有当前正在输入的消息才需要) /// 是否需要打字机效果(只有当前正在输入的消息才需要)
@property (nonatomic, assign) BOOL needsTypewriterEffect; @property (nonatomic, assign) BOOL needsTypewriterEffect;
/// 是否处于加载状态(用户消息)
@property (nonatomic, assign) BOOL isLoading;
#pragma mark - 便捷构造方法 #pragma mark - 便捷构造方法
/// 创建加载中的用户消息
+ (instancetype)loadingUserMessage;
/// 创建用户消息 /// 创建用户消息
+ (instancetype)userMessageWithText:(NSString *)text; + (instancetype)userMessageWithText:(NSString *)text;

View File

@@ -9,12 +9,23 @@
@implementation KBAiChatMessage @implementation KBAiChatMessage
+ (instancetype)loadingUserMessage {
KBAiChatMessage *message = [[KBAiChatMessage alloc] init];
message.type = KBAiChatMessageTypeUser;
message.text = @"";
message.timestamp = [NSDate date];
message.isComplete = NO;
message.isLoading = YES;
return message;
}
+ (instancetype)userMessageWithText:(NSString *)text { + (instancetype)userMessageWithText:(NSString *)text {
KBAiChatMessage *message = [[KBAiChatMessage alloc] init]; KBAiChatMessage *message = [[KBAiChatMessage alloc] init];
message.type = KBAiChatMessageTypeUser; message.type = KBAiChatMessageTypeUser;
message.text = text; message.text = text;
message.timestamp = [NSDate date]; message.timestamp = [NSDate date];
message.isComplete = YES; message.isComplete = YES;
message.isLoading = NO;
return message; return message;
} }
@@ -28,6 +39,7 @@
message.audioDuration = duration; message.audioDuration = duration;
message.audioData = audioData; message.audioData = audioData;
message.isComplete = NO; message.isComplete = NO;
message.isLoading = NO;
return message; return message;
} }
@@ -41,6 +53,7 @@
message.audioDuration = 0; message.audioDuration = 0;
message.audioData = nil; message.audioData = nil;
message.isComplete = NO; message.isComplete = NO;
message.isLoading = NO;
return message; return message;
} }
@@ -53,6 +66,7 @@
message.audioDuration = 0; message.audioDuration = 0;
message.audioData = nil; message.audioData = nil;
message.isComplete = NO; message.isComplete = NO;
message.isLoading = NO;
return message; return message;
} }
@@ -61,6 +75,7 @@
message.type = KBAiChatMessageTypeTime; message.type = KBAiChatMessageTypeTime;
message.timestamp = timestamp; message.timestamp = timestamp;
message.isComplete = YES; message.isComplete = YES;
message.isLoading = NO;
return message; return message;
} }

View File

@@ -27,6 +27,12 @@ NS_ASSUME_NONNULL_BEGIN
/// 添加用户消息 /// 添加用户消息
- (void)addUserMessage:(NSString *)text; - (void)addUserMessage:(NSString *)text;
/// 添加加载中的用户消息
- (void)addLoadingUserMessage;
/// 更新最后一条用户消息
- (void)updateLastUserMessage:(NSString *)text;
/// 添加 AI 消息(带语音) /// 添加 AI 消息(带语音)
- (void)addAssistantMessage:(NSString *)text - (void)addAssistantMessage:(NSString *)text
audioDuration:(NSTimeInterval)duration audioDuration:(NSTimeInterval)duration
@@ -42,6 +48,9 @@ NS_ASSUME_NONNULL_BEGIN
/// 标记最后一条 AI 消息完成 /// 标记最后一条 AI 消息完成
- (void)markLastAssistantMessageComplete; - (void)markLastAssistantMessageComplete;
/// 标记最后一条用户消息结束加载
- (void)markLastUserMessageLoadingComplete;
/// 清空所有消息 /// 清空所有消息
- (void)clearMessages; - (void)clearMessages;

View File

@@ -132,6 +132,27 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
[self addMessage:message autoScroll:YES]; [self addMessage:message autoScroll:YES];
} }
- (void)addLoadingUserMessage {
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
[self addMessage:message autoScroll:YES];
}
- (void)updateLastUserMessage:(NSString *)text {
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
message.text = text;
message.isLoading = NO;
message.isComplete = YES;
//
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
return;
}
}
}
- (void)addAssistantMessage:(NSString *)text - (void)addAssistantMessage:(NSString *)text
audioDuration:(NSTimeInterval)duration audioDuration:(NSTimeInterval)duration
audioData:(NSData *)audioData { audioData:(NSData *)audioData {
@@ -198,9 +219,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
for (NSInteger i = self.messages.count - 1; i >= 0; i--) { for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i]; KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser) { if (message.type == KBAiChatMessageTypeUser) {
message.isLoading = NO;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] [self.tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationNone]; withRowAnimation:UITableViewRowAnimationNone];
[self.tableView layoutIfNeeded];
[self scrollToBottomAnimated:NO];
return; return;
} }
} }
@@ -227,14 +251,9 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
CGFloat tableViewHeight = self.tableView.bounds.size.height; CGFloat tableViewHeight = self.tableView.bounds.size.height;
CGFloat bottomInset = self.tableView.contentInset.bottom; CGFloat bottomInset = self.tableView.contentInset.bottom;
// tableView
if (contentHeight <= tableViewHeight) {
NSLog(@"[KBChatTableView] 内容高度(%.2f) <= tableView高度(%.2f),不需要滚动", contentHeight, tableViewHeight);
return;
}
// offset // offset
CGFloat offsetY = contentHeight - tableViewHeight + bottomInset; CGFloat offsetY = contentHeight - tableViewHeight + bottomInset;
offsetY = MAX(0, offsetY);
NSLog(@"[KBChatTableView] scrollToBottom - contentHeight: %.2f, tableViewHeight: %.2f, bottomInset: %.2f, offsetY: %.2f", NSLog(@"[KBChatTableView] scrollToBottom - contentHeight: %.2f, tableViewHeight: %.2f, bottomInset: %.2f, offsetY: %.2f",
contentHeight, tableViewHeight, bottomInset, offsetY); contentHeight, tableViewHeight, bottomInset, offsetY);
@@ -312,6 +331,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView layoutIfNeeded]; // [self.tableView layoutIfNeeded]; //
[self scrollToBottomAnimated:YES]; [self scrollToBottomAnimated:YES];
//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView layoutIfNeeded];
[self scrollToBottomAnimated:NO];
});
}); });
} }
} }

View File

@@ -13,6 +13,7 @@
@property (nonatomic, strong) UIView *bubbleView; @property (nonatomic, strong) UIView *bubbleView;
@property (nonatomic, strong) UILabel *messageLabel; @property (nonatomic, strong) UILabel *messageLabel;
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
@end @end
@@ -44,12 +45,20 @@
self.messageLabel.textColor = [UIColor blackColor]; self.messageLabel.textColor = [UIColor blackColor];
[self.bubbleView addSubview:self.messageLabel]; [self.bubbleView addSubview:self.messageLabel];
//
self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
self.loadingIndicator.hidesWhenStopped = YES;
[self.contentView addSubview:self.loadingIndicator];
// //
[self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) { [self.bubbleView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(4); make.top.equalTo(self.contentView).offset(4);
make.bottom.equalTo(self.contentView).offset(-4); make.bottom.equalTo(self.contentView).offset(-4);
make.right.equalTo(self.contentView).offset(-16); make.right.equalTo(self.contentView).offset(-16);
make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75); make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75);
// loading
make.height.greaterThanOrEqualTo(@40);
make.width.greaterThanOrEqualTo(@50);
}]; }];
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) { [self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -58,10 +67,24 @@
make.left.equalTo(self.bubbleView).offset(12); make.left.equalTo(self.bubbleView).offset(12);
make.right.equalTo(self.bubbleView).offset(-12); make.right.equalTo(self.bubbleView).offset(-12);
}]; }];
[self.loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contentView);
make.right.equalTo(self.contentView).offset(-16);
}];
} }
- (void)configureWithMessage:(KBAiChatMessage *)message { - (void)configureWithMessage:(KBAiChatMessage *)message {
self.messageLabel.text = message.text; self.messageLabel.text = message.text;
if (message.isLoading) {
self.bubbleView.hidden = YES;
[self.loadingIndicator startAnimating];
} else {
self.bubbleView.hidden = NO;
self.messageLabel.hidden = NO;
[self.loadingIndicator stopAnimating];
}
} }
@end @end

View File

@@ -24,6 +24,15 @@ NS_ASSUME_NONNULL_BEGIN
/// 添加用户消息 /// 添加用户消息
- (void)appendUserMessage:(NSString *)text; - (void)appendUserMessage:(NSString *)text;
/// 标记最后一条用户消息结束加载
- (void)markLastUserMessageLoadingComplete;
/// 添加加载中的用户消息
- (void)appendLoadingUserMessage;
/// 更新最后一条用户消息
- (void)updateLastUserMessage:(NSString *)text;
/// 添加 AI 消息(支持打字机效果) /// 添加 AI 消息(支持打字机效果)
- (void)appendAssistantMessage:(NSString *)text - (void)appendAssistantMessage:(NSString *)text
audioId:(nullable NSString *)audioId; audioId:(nullable NSString *)audioId;

View File

@@ -453,8 +453,43 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
[self.chatView addMessage:message autoScroll:YES]; [self.chatView addMessage:message autoScroll:YES];
} }
- (void)appendLoadingUserMessage {
if (!self.messages) {
self.messages = [NSMutableArray array];
}
[self ensureOpeningMessageAtTop];
KBAiChatMessage *message = [KBAiChatMessage loadingUserMessage];
[self.messages addObject:message];
[self.chatView addMessage:message autoScroll:YES];
}
- (void)updateLastUserMessage:(NSString *)text {
[self.chatView updateLastUserMessage:text];
//
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
message.text = text;
message.isLoading = NO;
message.isComplete = YES;
break;
}
}
}
- (void)markLastUserMessageLoadingComplete { - (void)markLastUserMessageLoadingComplete {
// [self.chatView markLastUserMessageLoadingComplete]; [self.chatView markLastUserMessageLoadingComplete];
//
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
KBAiChatMessage *message = self.messages[i];
if (message.type == KBAiChatMessageTypeUser && message.isLoading) {
message.isLoading = NO;
break;
}
}
} }
- (void)appendAssistantMessage:(NSString *)text - (void)appendAssistantMessage:(NSString *)text

View File

@@ -424,12 +424,12 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
// //
if (isNowLiked) { if (isNowLiked) {
// +1 // +1
reply.isLiked = YES; reply.liked = YES;
reply.likeCount = MAX(0, reply.likeCount + 1); reply.likeCount = MAX(0, reply.likeCount + 1);
NSLog(@"[KBAICommentView] 二级评论点赞成功ID: %ld", (long)commentId); NSLog(@"[KBAICommentView] 二级评论点赞成功ID: %ld", (long)commentId);
} else { } else {
// -1 // -1
reply.isLiked = NO; reply.liked = NO;
reply.likeCount = MAX(0, reply.likeCount - 1); reply.likeCount = MAX(0, reply.likeCount - 1);
NSLog(@"[KBAICommentView] 二级评论取消点赞成功ID: %ld", (long)commentId); NSLog(@"[KBAICommentView] 二级评论取消点赞成功ID: %ld", (long)commentId);
} }

View File

@@ -133,13 +133,14 @@
self.timeLabel.text = [reply formattedTime]; self.timeLabel.text = [reply formattedTime];
// //
NSString *likeText = reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @""; NSString *likeText =
self.likeButton.textLabel.text = likeText; reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"赞";
self.likeButton.textLabel.text = likeText;
UIImage *likeImage = reply.isLiked ? [UIImage systemImageNamed:@"heart.fill"] UIImage *likeImage = reply.liked
: [UIImage systemImageNamed:@"heart"]; ? [UIImage imageNamed:@"comment_sel_icon"]
self.likeButton.iconView.image = likeImage; : [UIImage imageNamed:@"comment_nor_icon"];
self.likeButton.iconView.tintColor = reply.isLiked ? [UIColor systemRedColor] : [UIColor grayColor]; self.likeButton.iconView.image = likeImage;
} }
- (NSString *)formatLikeCount:(NSInteger)count { - (NSString *)formatLikeCount:(NSInteger)count {
@@ -225,10 +226,10 @@
_likeButton = [[KBTopImageButton alloc] init]; _likeButton = [[KBTopImageButton alloc] init];
_likeButton.iconSize = CGSizeMake(16, 16); _likeButton.iconSize = CGSizeMake(16, 16);
_likeButton.spacing = 2; _likeButton.spacing = 2;
_likeButton.iconView.image = [UIImage systemImageNamed:@"heart"]; _likeButton.iconView.image = [UIImage imageNamed:@"comment_nor_icon"];
_likeButton.iconView.tintColor = [UIColor grayColor]; _likeButton.iconView.tintColor = [UIColor grayColor];
_likeButton.textLabel.font = [UIFont systemFontOfSize:10]; _likeButton.textLabel.font = [UIFont systemFontOfSize:10];
_likeButton.textLabel.textColor = [UIColor grayColor]; _likeButton.textLabel.textColor = [UIColor colorWithHex:0xC5BEB4];
[_likeButton addTarget:self [_likeButton addTarget:self
action:@selector(likeButtonTapped) action:@selector(likeButtonTapped)
forControlEvents:UIControlEventTouchUpInside]; forControlEvents:UIControlEventTouchUpInside];

View File

@@ -647,6 +647,12 @@
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue]; unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize); NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
// loading
KBPersonaChatCell *currentCell = [self currentPersonaCell];
if (currentCell) {
[currentCell appendLoadingUserMessage];
}
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self;
[self.aiVM transcribeAudioFileAtURL:fileURL [self.aiVM transcribeAudioFileAtURL:fileURL
completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) { completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) {
@@ -656,9 +662,14 @@
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
if (error) { if (error) {
NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription); NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription);
[KBHUD showError:KBLocalized(@"语音转文字失败,请重试")]; [KBHUD showError:KBLocalized(@"语音转文字失败,请重试")];
if (cell) {
[cell updateLastUserMessage:KBLocalized(@"语音识别失败")];
}
return; return;
} }
@@ -666,10 +677,19 @@
if (transcript.length == 0) { if (transcript.length == 0) {
NSLog(@"[KBAIHomeVC] 语音转文字结果为空"); NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
[KBHUD showError:KBLocalized(@"未识别到语音内容")]; [KBHUD showError:KBLocalized(@"未识别到语音内容")];
if (cell) {
[cell updateLastUserMessage:KBLocalized(@"未识别到语音")];
}
return; return;
} }
[strongSelf handleTranscribedText:transcript]; // loading
if (cell) {
[cell updateLastUserMessage:transcript];
}
//
[strongSelf handleTranscribedText:transcript appendToUI:NO];
}); });
}]; }];
} }
@@ -687,6 +707,10 @@
#pragma mark - Private #pragma mark - Private
- (void)handleTranscribedText:(NSString *)text { - (void)handleTranscribedText:(NSString *)text {
[self handleTranscribedText:text appendToUI:YES];
}
- (void)handleTranscribedText:(NSString *)text appendToUI:(BOOL)appendToUI {
if (text.length == 0) { if (text.length == 0) {
return; return;
} }
@@ -699,7 +723,7 @@
} }
KBPersonaChatCell *currentCell = [self currentPersonaCell]; KBPersonaChatCell *currentCell = [self currentPersonaCell];
if (currentCell) { if (currentCell && appendToUI) {
[currentCell appendUserMessage:text]; [currentCell appendUserMessage:text];
} }
@@ -723,6 +747,12 @@
strongSelf.collectionView.scrollEnabled = YES; strongSelf.collectionView.scrollEnabled = YES;
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动"); NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
// loading
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
if (cell) {
[cell markLastUserMessageLoadingComplete];
}
if (response.code == 50030) { if (response.code == 50030) {
NSString *message = response.message ?: @""; NSString *message = response.message ?: @"";
[strongSelf showChatLimitPopWithMessage:message]; [strongSelf showChatLimitPopWithMessage:message];
@@ -745,7 +775,6 @@
return; return;
} }
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
if (cell) { if (cell) {
[cell appendAssistantMessage:aiResponse audioId:audioId]; [cell appendAssistantMessage:aiResponse audioId:audioId];
} }