处理滚动底部问题
This commit is contained in:
@@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property(nonatomic, assign) NSInteger likeCount;
|
||||
|
||||
/// 是否已点赞
|
||||
@property(nonatomic, assign) BOOL isLiked;
|
||||
@property(nonatomic, assign) BOOL liked;
|
||||
|
||||
/// 创建时间(时间戳)
|
||||
@property(nonatomic, assign) NSTimeInterval createTime;
|
||||
|
||||
@@ -21,10 +21,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setLiked:(NSInteger)liked {
|
||||
// 后端返回的是 NSInteger (0/1),转换为 BOOL
|
||||
_isLiked = (liked == 1);
|
||||
}
|
||||
|
||||
- (void)setCreatedAt:(NSString *)createdAt {
|
||||
// 后端返回的是字符串时间,转换为时间戳
|
||||
|
||||
@@ -43,8 +43,14 @@ typedef NS_ENUM(NSInteger, KBAiChatMessageType) {
|
||||
/// 是否需要打字机效果(只有当前正在输入的消息才需要)
|
||||
@property (nonatomic, assign) BOOL needsTypewriterEffect;
|
||||
|
||||
/// 是否处于加载状态(用户消息)
|
||||
@property (nonatomic, assign) BOOL isLoading;
|
||||
|
||||
#pragma mark - 便捷构造方法
|
||||
|
||||
/// 创建加载中的用户消息
|
||||
+ (instancetype)loadingUserMessage;
|
||||
|
||||
/// 创建用户消息
|
||||
+ (instancetype)userMessageWithText:(NSString *)text;
|
||||
|
||||
|
||||
@@ -9,12 +9,23 @@
|
||||
|
||||
@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 {
|
||||
KBAiChatMessage *message = [[KBAiChatMessage alloc] init];
|
||||
message.type = KBAiChatMessageTypeUser;
|
||||
message.text = text;
|
||||
message.timestamp = [NSDate date];
|
||||
message.isComplete = YES;
|
||||
message.isLoading = NO;
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -28,6 +39,7 @@
|
||||
message.audioDuration = duration;
|
||||
message.audioData = audioData;
|
||||
message.isComplete = NO;
|
||||
message.isLoading = NO;
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -41,6 +53,7 @@
|
||||
message.audioDuration = 0;
|
||||
message.audioData = nil;
|
||||
message.isComplete = NO;
|
||||
message.isLoading = NO;
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -53,6 +66,7 @@
|
||||
message.audioDuration = 0;
|
||||
message.audioData = nil;
|
||||
message.isComplete = NO;
|
||||
message.isLoading = NO;
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -61,6 +75,7 @@
|
||||
message.type = KBAiChatMessageTypeTime;
|
||||
message.timestamp = timestamp;
|
||||
message.isComplete = YES;
|
||||
message.isLoading = NO;
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 添加用户消息
|
||||
- (void)addUserMessage:(NSString *)text;
|
||||
|
||||
/// 添加加载中的用户消息
|
||||
- (void)addLoadingUserMessage;
|
||||
|
||||
/// 更新最后一条用户消息
|
||||
- (void)updateLastUserMessage:(NSString *)text;
|
||||
|
||||
/// 添加 AI 消息(带语音)
|
||||
- (void)addAssistantMessage:(NSString *)text
|
||||
audioDuration:(NSTimeInterval)duration
|
||||
@@ -42,6 +48,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 标记最后一条 AI 消息完成
|
||||
- (void)markLastAssistantMessageComplete;
|
||||
|
||||
/// 标记最后一条用户消息结束加载
|
||||
- (void)markLastUserMessageLoadingComplete;
|
||||
|
||||
/// 清空所有消息
|
||||
- (void)clearMessages;
|
||||
|
||||
|
||||
@@ -132,6 +132,27 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
[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
|
||||
audioDuration:(NSTimeInterval)duration
|
||||
audioData:(NSData *)audioData {
|
||||
@@ -198,9 +219,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
for (NSInteger i = self.messages.count - 1; i >= 0; i--) {
|
||||
KBAiChatMessage *message = self.messages[i];
|
||||
if (message.type == KBAiChatMessageTypeUser) {
|
||||
message.isLoading = NO;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView layoutIfNeeded];
|
||||
[self scrollToBottomAnimated:NO];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -227,14 +251,9 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
CGFloat tableViewHeight = self.tableView.bounds.size.height;
|
||||
CGFloat bottomInset = self.tableView.contentInset.bottom;
|
||||
|
||||
// 如果内容高度小于 tableView 高度,不需要滚动
|
||||
if (contentHeight <= tableViewHeight) {
|
||||
NSLog(@"[KBChatTableView] 内容高度(%.2f) <= tableView高度(%.2f),不需要滚动", contentHeight, tableViewHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算滚动到底部的 offset
|
||||
CGFloat offsetY = contentHeight - tableViewHeight + bottomInset;
|
||||
offsetY = MAX(0, offsetY);
|
||||
|
||||
NSLog(@"[KBChatTableView] scrollToBottom - contentHeight: %.2f, tableViewHeight: %.2f, bottomInset: %.2f, offsetY: %.2f",
|
||||
contentHeight, tableViewHeight, bottomInset, offsetY);
|
||||
@@ -312,6 +331,12 @@ static const NSTimeInterval kTimestampInterval = 5 * 60; // 5 分钟
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.tableView layoutIfNeeded]; // 再次确保布局完成
|
||||
[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];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
@property (nonatomic, strong) UIView *bubbleView;
|
||||
@property (nonatomic, strong) UILabel *messageLabel;
|
||||
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
|
||||
|
||||
@end
|
||||
|
||||
@@ -44,12 +45,20 @@
|
||||
self.messageLabel.textColor = [UIColor blackColor];
|
||||
[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) {
|
||||
make.top.equalTo(self.contentView).offset(4);
|
||||
make.bottom.equalTo(self.contentView).offset(-4);
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.width.lessThanOrEqualTo(self.contentView).multipliedBy(0.75);
|
||||
// 最小高度约束,确保 loading 时气泡不消失
|
||||
make.height.greaterThanOrEqualTo(@40);
|
||||
make.width.greaterThanOrEqualTo(@50);
|
||||
}];
|
||||
|
||||
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
@@ -58,10 +67,24 @@
|
||||
make.left.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 {
|
||||
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
|
||||
|
||||
@@ -24,6 +24,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// 添加用户消息
|
||||
- (void)appendUserMessage:(NSString *)text;
|
||||
|
||||
/// 标记最后一条用户消息结束加载
|
||||
- (void)markLastUserMessageLoadingComplete;
|
||||
|
||||
/// 添加加载中的用户消息
|
||||
- (void)appendLoadingUserMessage;
|
||||
|
||||
/// 更新最后一条用户消息
|
||||
- (void)updateLastUserMessage:(NSString *)text;
|
||||
|
||||
/// 添加 AI 消息(支持打字机效果)
|
||||
- (void)appendAssistantMessage:(NSString *)text
|
||||
audioId:(nullable NSString *)audioId;
|
||||
|
||||
@@ -453,8 +453,43 @@ static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidRe
|
||||
[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 {
|
||||
// [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
|
||||
|
||||
@@ -424,12 +424,12 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||
// 更新模型状态
|
||||
if (isNowLiked) {
|
||||
// 点赞成功:喜欢数+1
|
||||
reply.isLiked = YES;
|
||||
reply.liked = YES;
|
||||
reply.likeCount = MAX(0, reply.likeCount + 1);
|
||||
NSLog(@"[KBAICommentView] 二级评论点赞成功,ID: %ld", (long)commentId);
|
||||
} else {
|
||||
// 取消点赞成功:喜欢数-1
|
||||
reply.isLiked = NO;
|
||||
reply.liked = NO;
|
||||
reply.likeCount = MAX(0, reply.likeCount - 1);
|
||||
NSLog(@"[KBAICommentView] 二级评论取消点赞成功,ID: %ld", (long)commentId);
|
||||
}
|
||||
|
||||
@@ -133,13 +133,14 @@
|
||||
self.timeLabel.text = [reply formattedTime];
|
||||
|
||||
// 点赞
|
||||
NSString *likeText = reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"";
|
||||
self.likeButton.textLabel.text = likeText;
|
||||
NSString *likeText =
|
||||
reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @"赞";
|
||||
self.likeButton.textLabel.text = likeText;
|
||||
|
||||
UIImage *likeImage = reply.isLiked ? [UIImage systemImageNamed:@"heart.fill"]
|
||||
: [UIImage systemImageNamed:@"heart"];
|
||||
self.likeButton.iconView.image = likeImage;
|
||||
self.likeButton.iconView.tintColor = reply.isLiked ? [UIColor systemRedColor] : [UIColor grayColor];
|
||||
UIImage *likeImage = reply.liked
|
||||
? [UIImage imageNamed:@"comment_sel_icon"]
|
||||
: [UIImage imageNamed:@"comment_nor_icon"];
|
||||
self.likeButton.iconView.image = likeImage;
|
||||
}
|
||||
|
||||
- (NSString *)formatLikeCount:(NSInteger)count {
|
||||
@@ -225,10 +226,10 @@
|
||||
_likeButton = [[KBTopImageButton alloc] init];
|
||||
_likeButton.iconSize = CGSizeMake(16, 16);
|
||||
_likeButton.spacing = 2;
|
||||
_likeButton.iconView.image = [UIImage systemImageNamed:@"heart"];
|
||||
_likeButton.iconView.image = [UIImage imageNamed:@"comment_nor_icon"];
|
||||
_likeButton.iconView.tintColor = [UIColor grayColor];
|
||||
_likeButton.textLabel.font = [UIFont systemFontOfSize:10];
|
||||
_likeButton.textLabel.textColor = [UIColor grayColor];
|
||||
_likeButton.textLabel.font = [UIFont systemFontOfSize:10];
|
||||
_likeButton.textLabel.textColor = [UIColor colorWithHex:0xC5BEB4];
|
||||
[_likeButton addTarget:self
|
||||
action:@selector(likeButtonTapped)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
@@ -647,6 +647,12 @@
|
||||
unsigned long long fileSize = [attributes[NSFileSize] unsignedLongLongValue];
|
||||
NSLog(@"[KBAIHomeVC] 录音完成,时长: %.2fs,大小: %llu bytes", duration, fileSize);
|
||||
|
||||
// 显示 loading 气泡
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
[currentCell appendLoadingUserMessage];
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.aiVM transcribeAudioFileAtURL:fileURL
|
||||
completion:^(KBAiSpeechTranscribeResponse * _Nullable response, NSError * _Nullable error) {
|
||||
@@ -656,9 +662,14 @@
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
|
||||
if (error) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字失败:%@", error.localizedDescription);
|
||||
[KBHUD showError:KBLocalized(@"语音转文字失败,请重试")];
|
||||
if (cell) {
|
||||
[cell updateLastUserMessage:KBLocalized(@"语音识别失败")];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -666,10 +677,19 @@
|
||||
if (transcript.length == 0) {
|
||||
NSLog(@"[KBAIHomeVC] 语音转文字结果为空");
|
||||
[KBHUD showError:KBLocalized(@"未识别到语音内容")];
|
||||
if (cell) {
|
||||
[cell updateLastUserMessage:KBLocalized(@"未识别到语音")];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf handleTranscribedText:transcript];
|
||||
// 更新 loading 气泡为实际文本
|
||||
if (cell) {
|
||||
[cell updateLastUserMessage:transcript];
|
||||
}
|
||||
|
||||
// 发送聊天请求(不追加消息,因为已经更新了)
|
||||
[strongSelf handleTranscribedText:transcript appendToUI:NO];
|
||||
});
|
||||
}];
|
||||
}
|
||||
@@ -687,6 +707,10 @@
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)handleTranscribedText:(NSString *)text {
|
||||
[self handleTranscribedText:text appendToUI:YES];
|
||||
}
|
||||
|
||||
- (void)handleTranscribedText:(NSString *)text appendToUI:(BOOL)appendToUI {
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -699,7 +723,7 @@
|
||||
}
|
||||
|
||||
KBPersonaChatCell *currentCell = [self currentPersonaCell];
|
||||
if (currentCell) {
|
||||
if (currentCell && appendToUI) {
|
||||
[currentCell appendUserMessage:text];
|
||||
}
|
||||
|
||||
@@ -723,6 +747,12 @@
|
||||
strongSelf.collectionView.scrollEnabled = YES;
|
||||
NSLog(@"[KBAIHomeVC] AI 回复完成,恢复 CollectionView 滚动");
|
||||
|
||||
// 收到响应后,隐藏用户消息的 loading
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell markLastUserMessageLoadingComplete];
|
||||
}
|
||||
|
||||
if (response.code == 50030) {
|
||||
NSString *message = response.message ?: @"";
|
||||
[strongSelf showChatLimitPopWithMessage:message];
|
||||
@@ -745,7 +775,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
KBPersonaChatCell *cell = [strongSelf currentPersonaCell];
|
||||
if (cell) {
|
||||
[cell appendAssistantMessage:aiResponse audioId:audioId];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user