Files
keyboard/keyBoard/Class/AiTalk/V/Comment/KBAICommentView.m
2026-01-29 15:53:26 +08:00

1142 lines
37 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBAICommentView.m
// keyBoard
//
// Created by Mac on 2026/1/16.
//
#import "KBAICommentView.h"
#import "KBAICommentFooterView.h"
#import "KBAICommentHeaderView.h"
#import "KBAICommentInputView.h"
#import "KBAICommentModel.h"
#import "KBAIReplyCell.h"
#import "KBAIReplyModel.h"
#import "KBCommentModel.h"
#import "AiVM.h"
#import "KBUserSessionManager.h"
#import "KBUser.h"
#import <MJExtension/MJExtension.h>
#import <Masonry/Masonry.h>
#import <MJRefresh/MJRefresh.h>
static NSString *const kCommentHeaderIdentifier = @"CommentHeader";
static NSString *const kReplyCellIdentifier = @"ReplyCell";
static NSString *const kCommentFooterIdentifier = @"CommentFooter";
@interface KBAICommentView () <UITableViewDataSource, UITableViewDelegate>
@property(nonatomic, strong) UIVisualEffectView *blurBackgroundView;
@property(nonatomic, strong) UIView *headerView;
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UIButton *closeButton;
@property(nonatomic, strong) BaseTableView *tableView;
@property(nonatomic, strong) KBAICommentInputView *inputView;
@property(nonatomic, strong) NSMutableArray<KBAICommentModel *> *comments;
/// 分页参数
@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;
/// 输入框底部约束
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
/// 当前回复的目标(一级评论)
@property(nonatomic, weak) KBAICommentModel *replyToComment;
/// 当前回复的目标(二级评论)
@property(nonatomic, weak) KBAIReplyModel *replyToReply;
/// AiVM 实例
@property(nonatomic, strong) AiVM *aiVM;
@end
@implementation KBAICommentView
#pragma mark - Local Model Builders
- (NSString *)currentUserName {
KBUser *user = [KBUserSessionManager shared].currentUser;
if (user.nickName.length > 0) {
return user.nickName;
}
return @"";
}
- (NSString *)currentUserId {
KBUser *user = [KBUserSessionManager shared].currentUser;
return user.userId ?: @"";
}
- (NSString *)currentUserAvatarUrl {
KBUser *user = [KBUserSessionManager shared].currentUser;
return user.avatarUrl ?: @"";
}
- (NSString *)generateTempIdString {
long long ms = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
// 使用负数避免与后端 ID 冲突
long long tmp = -ms;
return [NSString stringWithFormat:@"%lld", tmp];
}
- (KBAICommentModel *)buildLocalNewCommentWithText:(NSString *)text
serverItem:(KBCommentItem *_Nullable)serverItem
tableWidth:(CGFloat)tableWidth {
KBAICommentModel *comment = [[KBAICommentModel alloc] init];
NSString *cid = nil;
if (serverItem && serverItem.commentId > 0) {
cid = [NSString stringWithFormat:@"%ld", (long)serverItem.commentId];
} else {
cid = [self generateTempIdString];
}
comment.commentId = cid;
comment.userId = [self currentUserId];
comment.userName = [self currentUserName];
comment.avatarUrl = [self currentUserAvatarUrl];
comment.content = text ?: @"";
comment.likeCount = 0;
comment.liked = NO;
comment.createTime = [[NSDate date] timeIntervalSince1970];
comment.replies = @[];
comment.cachedHeaderHeight =
[comment calculateHeaderHeightWithMaxWidth:tableWidth];
return comment;
}
- (KBAIReplyModel *)buildLocalNewReplyWithText:(NSString *)text
serverItem:(KBCommentItem *_Nullable)serverItem
replyToUserName:(NSString *)replyToUserName
tableWidth:(CGFloat)tableWidth {
KBAIReplyModel *reply = [[KBAIReplyModel alloc] init];
NSString *rid = nil;
if (serverItem && serverItem.commentId > 0) {
rid = [NSString stringWithFormat:@"%ld", (long)serverItem.commentId];
} else {
rid = [self generateTempIdString];
}
reply.replyId = rid;
reply.userId = [self currentUserId];
reply.userName = [self currentUserName];
reply.avatarUrl = [self currentUserAvatarUrl];
reply.content = text ?: @"";
reply.replyToUserName = replyToUserName ?: @"";
reply.likeCount = 0;
reply.liked = NO;
reply.createTime = [[NSDate date] timeIntervalSince1970];
reply.cachedCellHeight = [reply calculateCellHeightWithMaxWidth:tableWidth];
return reply;
}
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.comments = [NSMutableArray array];
self.currentPage = 1;
self.pageSize = 20;
self.hasMoreData = YES;
[self setupUI];
[self setupKeyboardObservers];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI Setup
- (void)setupUI {
// 设置背景为透明,让模糊效果可见
self.backgroundColor = [UIColor clearColor];
self.layer.cornerRadius = 12;
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
self.clipsToBounds = YES;
// 添加模糊背景(最底层)
[self addSubview:self.blurBackgroundView];
[self.blurBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
[self addSubview:self.headerView];
[self.headerView addSubview:self.titleLabel];
[self.headerView addSubview:self.closeButton];
[self addSubview:self.tableView];
[self addSubview:self.inputView];
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.right.equalTo(self);
make.height.mas_equalTo(50);
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.headerView);
}];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.headerView).offset(-16);
make.centerY.equalTo(self.headerView);
make.width.height.mas_equalTo(25);
}];
[self.inputView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.height.mas_equalTo(50);
self.inputBottomConstraint =
make.bottom.equalTo(self).offset(-KB_SafeAreaBottom());
}];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.headerView.mas_bottom);
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
- (void)setupKeyboardObservers {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification {
CGRect keyboardFrame =
[notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSTimeInterval duration =
[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]
doubleValue];
self.keyboardHeight = keyboardFrame.size.height;
[self.inputBottomConstraint uninstall];
[self.inputView mas_updateConstraints:^(MASConstraintMaker *make) {
self.inputBottomConstraint =
make.bottom.equalTo(self).offset(-self.keyboardHeight);
}];
[UIView animateWithDuration:duration
animations:^{
[self layoutIfNeeded];
}];
}
- (void)keyboardWillHide:(NSNotification *)notification {
NSTimeInterval duration =
[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]
doubleValue];
self.keyboardHeight = 0;
[self.inputBottomConstraint uninstall];
[self.inputView mas_updateConstraints:^(MASConstraintMaker *make) {
self.inputBottomConstraint =
make.bottom.equalTo(self).offset(-KB_SafeAreaBottom());
}];
[UIView animateWithDuration:duration
animations:^{
[self layoutIfNeeded];
}];
}
#pragma mark - Data Loading
- (void)loadComments {
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: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 append:append];
});
}];
}
/// 更新评论数据(从后端返回的 KBCommentPageModel 转换为 UI 层的 KBAICommentModel
- (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;
if (tableWidth <= 0) {
tableWidth = [UIScreen mainScreen].bounds.size.width;
}
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
// 注意KBCommentItem 通过 MJExtension 将后端字段 id 映射为了 commentId。
// 这里如果直接用 mj_keyValues会导致字典里只有 commentIdKBAICommentModel/KBAIReplyModel
// 的映射commentId/replyId -> id拿不到值最终 commentId/replyId 为空,进而影响发送回复时的 parentId/rootId。
NSMutableDictionary *itemKV = [[item mj_keyValues] mutableCopy];
id commentIdVal = itemKV[@"commentId"];
if (commentIdVal) {
itemKV[@"id"] = commentIdVal;
[itemKV removeObjectForKey:@"commentId"];
}
id repliesObj = itemKV[@"replies"];
if ([repliesObj isKindOfClass:[NSArray class]]) {
NSArray *replies = (NSArray *)repliesObj;
NSMutableArray *fixedReplies = [NSMutableArray arrayWithCapacity:replies.count];
for (id obj in replies) {
if (![obj isKindOfClass:[NSDictionary class]]) {
continue;
}
NSMutableDictionary *replyKV = [((NSDictionary *)obj) mutableCopy];
id replyCommentIdVal = replyKV[@"commentId"];
if (replyCommentIdVal) {
replyKV[@"id"] = replyCommentIdVal;
[replyKV removeObjectForKey:@"commentId"];
}
[fixedReplies addObject:[replyKV copy]];
}
itemKV[@"replies"] = [fixedReplies copy];
}
KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[itemKV copy]];
// 预先计算并缓存 Header 高度
comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
// 预先计算并缓存所有 Reply 高度
for (KBAIReplyModel *reply in comment.replies) {
reply.cachedCellHeight = [reply calculateCellHeightWithMaxWidth:tableWidth];
}
[self.comments addObject:comment];
}
[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];
} else {
[self hideEmptyState];
}
}
/// 显示空态视图
- (void)showEmptyState {
self.tableView.useEmptyDataSet = YES;
self.tableView.emptyTitleText = @"暂无评论";
self.tableView.emptyDescriptionText = @"快来抢沙发吧~";
self.tableView.emptyImage = nil; // 可选:设置空态图片
self.tableView.emptyVerticalOffset = -50; // 向上偏移一点
[self.tableView kb_reloadEmptyDataSet];
}
/// 显示错误空态视图
- (void)showEmptyStateWithError {
self.tableView.useEmptyDataSet = YES;
self.tableView.emptyTitleText = @"加载失败";
self.tableView.emptyDescriptionText = @"点击重新加载";
self.tableView.emptyImage = nil;
self.tableView.emptyVerticalOffset = -50;
// 点击重新加载
__weak typeof(self) weakSelf = self;
self.tableView.emptyDidTapView = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf loadComments];
}
};
[self.tableView kb_reloadEmptyDataSet];
}
/// 隐藏空态视图
- (void)hideEmptyState {
self.tableView.useEmptyDataSet = NO;
[self.tableView kb_reloadEmptyDataSet];
}
- (void)updateTitle {
NSString *countText;
if (self.totalCommentCount >= 10000) {
countText = [NSString
stringWithFormat:@"%.1fw条评论", self.totalCommentCount / 10000.0];
} else {
countText =
[NSString stringWithFormat:@"%ld条评论", (long)self.totalCommentCount];
}
self.titleLabel.text = countText;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.comments.count;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
return comment.displayedReplies.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAIReplyCell *cell =
[tableView dequeueReusableCellWithIdentifier:kReplyCellIdentifier
forIndexPath:indexPath];
KBAICommentModel *comment = self.comments[indexPath.section];
KBAIReplyModel *reply = comment.displayedReplies[indexPath.row];
[cell configureWithReply:reply];
__weak typeof(self) weakSelf = self;
cell.onLikeAction = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 获取评论 ID需要转换为 NSInteger
NSInteger commentId = [reply.replyId integerValue];
// 调用点赞接口
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", error.localizedDescription);
// TODO: 显示错误提示
return;
}
if (response && response.code == 0) {
// data = true: 点赞成功data = false: 取消点赞成功
BOOL isNowLiked = response.data;
// 更新模型状态
if (isNowLiked) {
// 点赞成功:喜欢数+1
reply.liked = YES;
reply.likeCount = MAX(0, reply.likeCount + 1);
NSLog(@"[KBAICommentView] 二级评论点赞成功ID: %ld", (long)commentId);
} else {
// 取消点赞成功:喜欢数-1
reply.liked = NO;
reply.likeCount = MAX(0, reply.likeCount - 1);
NSLog(@"[KBAICommentView] 二级评论取消点赞成功ID: %ld", (long)commentId);
}
// 刷新对应的行
[strongSelf.tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"[KBAICommentView] 二级评论点赞失败:%@", response.message ?: @"未知错误");
// TODO: 显示错误提示
}
});
}];
};
cell.onReplyAction = ^{
[weakSelf setReplyToComment:comment reply:reply];
};
return cell;
}
#pragma mark - UITableViewDelegate
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section {
KBAICommentHeaderView *header = [tableView
dequeueReusableHeaderFooterViewWithIdentifier:kCommentHeaderIdentifier];
KBAICommentModel *comment = self.comments[section];
[header configureWithComment:comment];
__weak typeof(self) weakSelf = self;
header.onLikeAction = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 获取评论 ID需要转换为 NSInteger
NSInteger commentId = [comment.commentId integerValue];
// 调用点赞接口
[strongSelf.aiVM likeCommentWithCommentId:commentId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", error.localizedDescription);
// TODO: 显示错误提示
return;
}
if (response && response.code == 0) {
// data = true: 点赞成功data = false: 取消点赞成功
BOOL isNowLiked = response.data;
// 更新模型状态
if (isNowLiked) {
// 点赞成功:喜欢数+1
comment.liked = YES;
comment.likeCount = MAX(0, comment.likeCount + 1);
NSLog(@"[KBAICommentView] 一级评论点赞成功ID: %ld", (long)commentId);
} else {
// 取消点赞成功:喜欢数-1
comment.liked = NO;
comment.likeCount = MAX(0, comment.likeCount - 1);
NSLog(@"[KBAICommentView] 一级评论取消点赞成功ID: %ld", (long)commentId);
}
// 刷新对应的 section
[strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationNone];
} else {
NSLog(@"[KBAICommentView] 一级评论点赞失败:%@", response.message ?: @"未知错误");
// TODO: 显示错误提示
}
});
}];
};
header.onReplyAction = ^{
[weakSelf setReplyToComment:comment reply:nil];
};
return header;
}
- (UIView *)tableView:(UITableView *)tableView
viewForFooterInSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
KBAIReplyFooterState state = [comment footerState];
// 无二级评论时返回空视图
if (state == KBAIReplyFooterStateHidden) {
return nil;
}
KBAICommentFooterView *footer = [tableView
dequeueReusableHeaderFooterViewWithIdentifier:kCommentFooterIdentifier];
[footer configureWithComment:comment];
__weak typeof(self) weakSelf = self;
footer.onAction = ^{
[weakSelf handleFooterActionForSection:section];
};
return footer;
}
- (CGFloat)tableView:(UITableView *)tableView
heightForHeaderInSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
return comment.cachedHeaderHeight;
}
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAICommentModel *comment = self.comments[indexPath.section];
KBAIReplyModel *reply = comment.displayedReplies[indexPath.row];
return reply.cachedCellHeight;
}
- (CGFloat)tableView:(UITableView *)tableView
heightForFooterInSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
KBAIReplyFooterState state = [comment footerState];
if (state == KBAIReplyFooterStateHidden) {
return CGFLOAT_MIN;
}
return 30;
}
#pragma mark - Footer Actions
/// 每次加载的回复数量
static NSInteger const kRepliesLoadCount = 5;
- (void)handleFooterActionForSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
KBAIReplyFooterState state = [comment footerState];
switch (state) {
case KBAIReplyFooterStateExpand:
case KBAIReplyFooterStateLoadMore: {
[self loadMoreRepliesForSection:section];
break;
}
case KBAIReplyFooterStateCollapse: {
[self collapseRepliesForSection:section];
break;
}
default:
break;
}
}
- (void)loadMoreRepliesForSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
NSInteger currentCount = comment.displayedReplies.count;
// 加载更多回复
[comment loadMoreReplies:kRepliesLoadCount];
// 计算新增的行
NSInteger newCount = comment.displayedReplies.count;
NSMutableArray *insertIndexPaths = [NSMutableArray array];
for (NSInteger i = currentCount; i < newCount; i++) {
[insertIndexPaths addObject:[NSIndexPath indexPathForRow:i
inSection:section]];
}
// 插入行(不刷新 Header避免头像闪烁
[self.tableView beginUpdates];
if (insertIndexPaths.count > 0) {
[self.tableView insertRowsAtIndexPaths:insertIndexPaths
withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self.tableView endUpdates];
// 手动刷新 Footer
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
if (footerView) {
[footerView configureWithComment:comment];
}
}
- (void)collapseRepliesForSection:(NSInteger)section {
KBAICommentModel *comment = self.comments[section];
NSInteger rowCount = comment.displayedReplies.count;
// 计算要删除的行
NSMutableArray *deleteIndexPaths = [NSMutableArray array];
for (NSInteger i = 0; i < rowCount; i++) {
[deleteIndexPaths addObject:[NSIndexPath indexPathForRow:i
inSection:section]];
}
// 收起全部回复
[comment collapseReplies];
// 删除行(不刷新 Header避免头像闪烁
[self.tableView beginUpdates];
if (deleteIndexPaths.count > 0) {
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths
withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self.tableView endUpdates];
// 手动刷新 Footer
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[self.tableView footerViewForSection:section];
if (footerView) {
[footerView configureWithComment:comment];
}
}
#pragma mark - Actions
- (void)closeButtonTapped {
[self.popView dismiss];
// 关闭评论视图(由外部处理)
// [[NSNotificationCenter defaultCenter]
// postNotificationName:@"KBAICommentViewCloseNotification"
// object:nil];
}
#pragma mark - Lazy Loading
- (UIVisualEffectView *)blurBackgroundView {
if (!_blurBackgroundView) {
// 创建模糊效果43pt 的模糊半径)
// iOS 的 UIBlurEffect 没有直接设置模糊半径的 API使用系统预设的 dark 效果
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
_blurBackgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
// 在模糊效果上叠加一个半透明黑色遮罩来调整透明度和颜色
// 颜色:#000000透明度0.31
UIView *darkOverlay = [[UIView alloc] init];
darkOverlay.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.31];
[_blurBackgroundView.contentView addSubview:darkOverlay];
[darkOverlay mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(_blurBackgroundView);
}];
}
return _blurBackgroundView;
}
- (UIView *)headerView {
if (!_headerView) {
_headerView = [[UIView alloc] init];
_headerView.backgroundColor = [UIColor clearColor];
}
return _headerView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.text = @"0条评论";
}
return _titleLabel;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setImage:[UIImage imageNamed:@"comment_close_icon"]
forState:UIControlStateNormal];
[_closeButton addTarget:self
action:@selector(closeButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
- (BaseTableView *)tableView {
if (!_tableView) {
_tableView = [[BaseTableView alloc] initWithFrame:CGRectZero
style:UITableViewStyleGrouped];
_tableView.dataSource = self;
_tableView.delegate = self;
_tableView.backgroundColor = [UIColor clearColor];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 0.01)];
// 关闭空数据占位,避免加载时显示"暂无数据"
_tableView.useEmptyDataSet = NO;
// 注册 Header/Cell/Footer
[_tableView registerClass:[KBAICommentHeaderView class]
forHeaderFooterViewReuseIdentifier:kCommentHeaderIdentifier];
[_tableView registerClass:[KBAIReplyCell class]
forCellReuseIdentifier:kReplyCellIdentifier];
[_tableView registerClass:[KBAICommentFooterView class]
forHeaderFooterViewReuseIdentifier:kCommentFooterIdentifier];
// 去掉顶部间距
if (@available(iOS 15.0, *)) {
_tableView.sectionHeaderTopPadding = 0;
}
}
return _tableView;
}
- (KBAICommentInputView *)inputView {
if (!_inputView) {
_inputView = [[KBAICommentInputView alloc] init];
_inputView.placeholder = @"说点什么...";
__weak typeof(self) weakSelf = self;
_inputView.onSend = ^(NSString *text) {
[weakSelf sendCommentWithText:text];
};
}
return _inputView;
}
#pragma mark - Reply
- (void)setReplyToComment:(KBAICommentModel *)comment
reply:(KBAIReplyModel *)reply {
self.replyToComment = comment;
self.replyToReply = reply;
if (reply) {
// 回复二级评论
self.inputView.placeholder =
[NSString stringWithFormat:@"回复 @%@", reply.userName];
} else if (comment) {
// 回复一级评论
self.inputView.placeholder =
[NSString stringWithFormat:@"回复 @%@", comment.userName];
} else {
// 普通评论
self.inputView.placeholder = @"说点什么...";
}
// 弹起键盘
[self.inputView showKeyboard];
}
- (void)clearReplyTarget {
self.replyToComment = nil;
self.replyToReply = nil;
self.inputView.placeholder = @"说点什么...";
}
#pragma mark - Send Comment
- (void)sendCommentWithText:(NSString *)text {
if (text.length == 0)
return;
CGFloat tableWidth = self.tableView.bounds.size.width;
if (tableWidth <= 0) {
tableWidth = [UIScreen mainScreen].bounds.size.width;
}
if (self.replyToComment) {
// 回复评论(添加二级评论)
[self sendReplyWithText:text tableWidth:tableWidth];
} else {
// 发送一级评论
[self sendNewCommentWithText:text tableWidth:tableWidth];
}
// 清空输入框和回复目标
[self.inputView clearText];
[self clearReplyTarget];
}
- (void)sendNewCommentWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
NSLog(@"[KBAICommentView] 发送一级评论:%@", text);
__weak typeof(self) weakSelf = self;
[self.aiVM addCommentWithCompanionId:self.companionId
content:text
parentId:nil
rootId:nil
completion:^(KBCommentItem * _Nullable newItem, NSInteger code, NSError * _Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (error || code != 0) {
NSLog(@"[KBAICommentView] 发送一级评论失败:%@", error.localizedDescription ?: @"");
return;
}
// 本地插入新评论到第一条,不再全量刷新
KBAICommentModel *localComment =
[strongSelf buildLocalNewCommentWithText:text
serverItem:newItem
tableWidth:tableWidth];
[strongSelf.comments insertObject:localComment atIndex:0];
strongSelf.totalCommentCount += 1;
[strongSelf updateTitle];
[strongSelf hideEmptyState];
[strongSelf.tableView beginUpdates];
[strongSelf.tableView
insertSections:[NSIndexSet indexSetWithIndex:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[strongSelf.tableView endUpdates];
[strongSelf.tableView setContentOffset:CGPointZero animated:YES];
});
}];
// 示例代码:
// [self.aiVM sendCommentWithCompanionId:self.companionId
// content:text
// completion:^(KBCommentItem *newItem, NSError *error) {
// if (error) {
// NSLog(@"[KBAICommentView] 发送评论失败:%@", error.localizedDescription);
// return;
// }
//
// // 转换为 KBAICommentModel
// KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[newItem mj_keyValues]];
// comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth];
//
// // 插入到数组第一个
// [self.comments insertObject:comment atIndex:0];
// self.totalCommentCount++;
// [self updateTitle];
//
// // 插入新 section
// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:0]
// withRowAnimation:UITableViewRowAnimationAutomatic];
//
// // 滚动到顶部
// [self.tableView setContentOffset:CGPointZero animated:YES];
// }];
}
- (void)sendReplyWithText:(NSString *)text tableWidth:(CGFloat)tableWidth {
KBAICommentModel *comment = self.replyToComment;
if (!comment)
return;
NSLog(@"[KBAICommentView] 回复评论 %@%@", comment.commentId, text);
NSInteger root = [comment.commentId integerValue];
NSNumber *rootId = @(root);
NSNumber *parentId = nil;
if (self.replyToReply && self.replyToReply.replyId.length > 0) {
parentId = @([self.replyToReply.replyId integerValue]);
} else {
parentId = @(root);
}
__weak typeof(self) weakSelf = self;
[self.aiVM addCommentWithCompanionId:self.companionId
content:text
parentId:parentId
rootId:rootId
completion:^(KBCommentItem * _Nullable newItem, NSInteger code, NSError * _Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (error || code != 0) {
NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription ?: @"");
return;
}
NSInteger section = [strongSelf.comments indexOfObject:comment];
if (section == NSNotFound) {
return;
}
NSInteger oldTotalReplyCount = comment.totalReplyCount;
BOOL wasFooterHidden = (oldTotalReplyCount == 0);
BOOL wasFullyExpanded =
(comment.isRepliesExpanded &&
comment.displayedReplies.count == oldTotalReplyCount);
NSString *replyToUserName = @"";
if (strongSelf.replyToReply && strongSelf.replyToReply.userName.length > 0) {
replyToUserName = strongSelf.replyToReply.userName;
} else if (comment.userName.length > 0) {
replyToUserName = comment.userName;
}
KBAIReplyModel *localReply =
[strongSelf buildLocalNewReplyWithText:text
serverItem:newItem
replyToUserName:replyToUserName
tableWidth:tableWidth];
NSArray<KBAIReplyModel *> *oldReplies = comment.replies ?: @[];
NSMutableArray<KBAIReplyModel *> *newReplies =
[NSMutableArray arrayWithArray:oldReplies];
[newReplies addObject:localReply];
comment.replies = [newReplies copy];
strongSelf.totalCommentCount += 1;
[strongSelf updateTitle];
// 若当前已完整展开,则直接插入新行;否则保持 displayedReplies 为前缀,避免破坏 loadMoreReplies 逻辑
if (wasFullyExpanded) {
[comment.displayedReplies addObject:localReply];
NSInteger newRowIndex = comment.displayedReplies.count - 1;
NSIndexPath *indexPath =
[NSIndexPath indexPathForRow:newRowIndex inSection:section];
[strongSelf.tableView beginUpdates];
[strongSelf.tableView insertRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
[strongSelf.tableView endUpdates];
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[strongSelf.tableView footerViewForSection:section];
if (footerView) {
[footerView configureWithComment:comment];
}
} else {
if (wasFooterHidden) {
[strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationNone];
} else {
KBAICommentFooterView *footerView =
(KBAICommentFooterView *)[strongSelf.tableView footerViewForSection:section];
if (footerView) {
[footerView configureWithComment:comment];
} else {
[strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationNone];
}
}
}
});
}];
// 示例代码:
// NSInteger parentId = [comment.commentId integerValue];
// [self.aiVM replyCommentWithParentId:parentId
// content:text
// completion:^(KBCommentItem *newItem, NSError *error) {
// if (error) {
// NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription);
// return;
// }
//
// // 转换为 KBAIReplyModel
// KBAIReplyModel *newReply = [KBAIReplyModel mj_objectWithKeyValues:[newItem mj_keyValues]];
// newReply.cachedCellHeight = [newReply calculateCellHeightWithMaxWidth:tableWidth];
//
// // 添加到 replies 数组
// NSMutableArray *newReplies = [NSMutableArray arrayWithArray:comment.replies];
// [newReplies addObject:newReply];
// comment.replies = newReplies;
// comment.totalReplyCount = newReplies.count;
//
// // 找到该评论的 section
// NSInteger section = [self.comments indexOfObject:comment];
// if (section == NSNotFound) return;
//
// // 如果已展开,添加到 displayedReplies 并插入行
// if (comment.isRepliesExpanded) {
// NSInteger newRowIndex = comment.displayedReplies.count;
// [comment.displayedReplies addObject:newReply];
//
// NSIndexPath *indexPath = [NSIndexPath indexPathForRow:newRowIndex inSection:section];
// [self.tableView insertRowsAtIndexPaths:@[indexPath]
// withRowAnimation:UITableViewRowAnimationAutomatic];
//
// // 刷新 Footer
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
// if (footerView) {
// [footerView configureWithComment:comment];
// }
//
// // 滚动到新回复
// [self.tableView scrollToRowAtIndexPath:indexPath
// atScrollPosition:UITableViewScrollPositionBottom
// animated:YES];
// } else {
// // 未展开,刷新 Footer 显示新的回复数
// KBAICommentFooterView *footerView = (KBAICommentFooterView *)[self.tableView footerViewForSection:section];
// if (footerView) {
// [footerView configureWithComment:comment];
// }
// }
// }];
}
- (AiVM *)aiVM {
if (!_aiVM) {
_aiVM = [[AiVM alloc] init];
}
return _aiVM;
}
@end