Files
keyboard/keyBoard/Class/AiTalk/V/KBAICommentView.m
2026-01-16 19:36:41 +08:00

525 lines
16 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 <MJExtension/MJExtension.h>
#import <Masonry/Masonry.h>
static NSString *const kCommentHeaderIdentifier = @"CommentHeader";
static NSString *const kReplyCellIdentifier = @"ReplyCell";
static NSString *const kCommentFooterIdentifier = @"CommentFooter";
@interface KBAICommentView () <UITableViewDataSource, UITableViewDelegate>
@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 totalCommentCount;
/// 键盘高度
@property(nonatomic, assign) CGFloat keyboardHeight;
/// 输入框底部约束
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
@end
@implementation KBAICommentView
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.comments = [NSMutableArray array];
[self setupUI];
[self setupKeyboardObservers];
[self loadComments];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI Setup
- (void)setupUI {
self.backgroundColor = [UIColor whiteColor];
self.layer.cornerRadius = 12;
self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
self.clipsToBounds = YES;
[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(30);
}];
[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);
}];
}
#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);
}];
[UIView animateWithDuration:duration
animations:^{
[self layoutIfNeeded];
}];
}
#pragma mark - Data Loading
- (void)loadComments {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"comments_mock"
ofType:@"json"];
if (!filePath) {
NSLog(@"[KBAICommentView] comments_mock.json not found");
return;
}
NSData *data = [NSData dataWithContentsOfFile:filePath];
if (!data) {
NSLog(@"[KBAICommentView] Failed to read comments_mock.json");
return;
}
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&error];
if (error) {
NSLog(@"[KBAICommentView] JSON parse error: %@", error);
return;
}
self.totalCommentCount = [json[@"totalCount"] integerValue];
NSArray *commentsArray = json[@"comments"];
[self.comments removeAllObjects];
// 获取 tableView 宽度用于计算高度
CGFloat tableWidth = self.tableView.bounds.size.width;
if (tableWidth <= 0) {
tableWidth = [UIScreen mainScreen].bounds.size.width;
}
for (NSDictionary *dict in commentsArray) {
KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:dict];
// 预先计算并缓存 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];
}
- (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 = ^{
// TODO: 处理点赞逻辑
reply.isLiked = !reply.isLiked;
reply.likeCount += reply.isLiked ? 1 : -1;
[weakSelf.tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
};
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 = ^{
// TODO: 处理点赞逻辑
comment.isLiked = !comment.isLiked;
comment.likeCount += comment.isLiked ? 1 : -1;
[weakSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationNone];
};
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 {
// 关闭评论视图(由外部处理)
[[NSNotificationCenter defaultCenter]
postNotificationName:@"KBAICommentViewCloseNotification"
object:nil];
}
#pragma mark - Lazy Loading
- (UIView *)headerView {
if (!_headerView) {
_headerView = [[UIView alloc] init];
_headerView.backgroundColor = [UIColor whiteColor];
}
return _headerView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
_titleLabel.textColor = [UIColor labelColor];
_titleLabel.text = @"0条评论";
}
return _titleLabel;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setImage:[UIImage systemImageNamed:@"xmark"]
forState:UIControlStateNormal];
_closeButton.tintColor = [UIColor labelColor];
[_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 whiteColor];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// 注册 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 - Send Comment
- (void)sendCommentWithText:(NSString *)text {
if (text.length == 0) return;
// 创建新评论
KBAICommentModel *newComment = [[KBAICommentModel alloc] init];
newComment.commentId = [NSUUID UUID].UUIDString;
newComment.userId = @"current_user";
newComment.userName = @"";
newComment.avatarUrl = @"";
newComment.content = text;
newComment.likeCount = 0;
newComment.isLiked = NO;
newComment.createTime = [[NSDate date] timeIntervalSince1970];
newComment.replies = @[];
// 计算高度缓存
CGFloat tableWidth = self.tableView.bounds.size.width;
if (tableWidth <= 0) {
tableWidth = [UIScreen mainScreen].bounds.size.width;
}
newComment.cachedHeaderHeight = [newComment calculateHeaderHeightWithMaxWidth:tableWidth];
// 插入到数组第一个
[self.comments insertObject:newComment atIndex:0];
self.totalCommentCount++;
[self updateTitle];
// 插入新 section
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
// 滚动到顶部
[self.tableView setContentOffset:CGPointZero animated:YES];
// 清空输入框
[self.inputView clearText];
}
@end