// // 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 #import static NSString *const kCommentHeaderIdentifier = @"CommentHeader"; static NSString *const kReplyCellIdentifier = @"ReplyCell"; static NSString *const kCommentFooterIdentifier = @"CommentFooter"; @interface KBAICommentView () @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 *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 - (void)handleFooterActionForSection:(NSInteger)section { KBAICommentModel *comment = self.comments[section]; KBAIReplyFooterState state = [comment footerState]; switch (state) { case KBAIReplyFooterStateExpand: { [self expandRepliesForSection:section]; break; } case KBAIReplyFooterStateCollapse: { [self collapseRepliesForSection:section]; break; } default: break; } } - (void)expandRepliesForSection:(NSInteger)section { KBAICommentModel *comment = self.comments[section]; // 一次性展开全部回复 [comment expandAllReplies]; // 直接刷新该 section // [UIView performWithoutAnimation:^{ [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationAutomatic]; // }]; } - (void)collapseRepliesForSection:(NSInteger)section { KBAICommentModel *comment = self.comments[section]; // 收起全部回复 [comment collapseReplies]; // 直接刷新该 section // [UIView performWithoutAnimation:^{ [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationAutomatic]; // }]; } #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) { // TODO: 发送评论 NSLog(@"[KBAICommentView] Send comment: %@", text); [weakSelf.inputView clearText]; }; } return _inputView; } @end