JXCommentViewController.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. //
  2. // JXCommentViewController.m
  3. // AICity
  4. //
  5. // Created by TogetherWatch on 2025-10-20.
  6. //
  7. #import "JXCommentViewController.h"
  8. #import "JXAPIService.h"
  9. #import "JXCommentContent.h"
  10. #import "CommentItemCell.h"
  11. #import "CommentRpItemCell.h"
  12. #import "CommentFooterView.h"
  13. // Cell复用标识符
  14. static NSString * const kCommentCellIdentifier = @"JXCommentCell";
  15. @interface JXCommentViewController () <UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate>
  16. #pragma mark - UI组件
  17. @property (nonatomic, strong) UIView *headerView;
  18. @property (nonatomic, strong) UILabel *titleLabel;
  19. @property (nonatomic, strong) UIButton *closeButton;
  20. @property (nonatomic, strong) UITableView *tableView;
  21. @property (nonatomic, strong) UIRefreshControl *refreshControl;
  22. @property (nonatomic, strong) UIView *inputContainerView;
  23. @property (nonatomic, strong) UITextField *inputTextField;
  24. @property (nonatomic, strong) UIButton *submitButton;
  25. #pragma mark - 数据
  26. @property (nonatomic, copy) NSString *dramaId;
  27. @property (nonatomic, strong) NSMutableArray<JXCommentContent *> *commentList;
  28. @property (nonatomic, assign) NSInteger currentPage;
  29. @property (nonatomic, assign) BOOL isLoading;
  30. @property (nonatomic, assign) BOOL hasMoreData;
  31. @property (strong, nonatomic) UIView *emptyView;
  32. // 回复相关
  33. @property (nonatomic, assign) long long replyToCommentId;
  34. @property (nonatomic, copy) NSString *replyToUserName;
  35. @end
  36. @implementation JXCommentViewController
  37. #pragma mark - 初始化
  38. - (instancetype)initWithDramaId:(NSString *)dramaId {
  39. self = [super init];
  40. if (self) {
  41. _dramaId = dramaId;
  42. _currentPage = 1;
  43. _hasMoreData = YES;
  44. _commentList = [NSMutableArray array];
  45. _replyToCommentId = 0;
  46. }
  47. return self;
  48. }
  49. #pragma mark - 生命周期
  50. - (void)viewDidLoad {
  51. [super viewDidLoad];
  52. self.view.backgroundColor = [UIColor whiteColor];
  53. [self setupUI];
  54. [self loadComments];
  55. // 监听键盘
  56. [[NSNotificationCenter defaultCenter] addObserver:self
  57. selector:@selector(keyboardWillShow:)
  58. name:UIKeyboardWillShowNotification
  59. object:nil];
  60. [[NSNotificationCenter defaultCenter] addObserver:self
  61. selector:@selector(keyboardWillHide:)
  62. name:UIKeyboardWillHideNotification
  63. object:nil];
  64. }
  65. - (void)dealloc {
  66. [[NSNotificationCenter defaultCenter] removeObserver:self];
  67. }
  68. #pragma mark - UI设置
  69. - (void)setupUI {
  70. // 顶部标题栏
  71. [self setupHeader];
  72. // 评论列表
  73. [self setupTableView];
  74. // 底部输入框
  75. [self setupInputContainer];
  76. }
  77. - (void)setupHeader {
  78. self.headerView = [[UIView alloc] init];
  79. self.headerView.backgroundColor = [UIColor whiteColor];
  80. [self.view addSubview:self.headerView];
  81. // 标题
  82. self.titleLabel = [[UILabel alloc] init];
  83. self.titleLabel.text = @"全部评论";
  84. self.titleLabel.font = [UIFont boldSystemFontOfSize:15];
  85. self.titleLabel.textColor = [UIColor colorWithRed:1/255.0 green:1/255.0 blue:1/255.0 alpha:1];
  86. [self.headerView addSubview:self.titleLabel];
  87. // 关闭按钮
  88. self.closeButton = [UIButton buttonWithType:UIButtonTypeSystem];
  89. [self.closeButton setImage:[UIImage imageNamed:@"关闭-1"] forState:UIControlStateNormal];
  90. self.closeButton.tintColor = [UIColor grayColor];
  91. [self.closeButton addTarget:self action:@selector(handleClose) forControlEvents:UIControlEventTouchUpInside];
  92. [self.headerView addSubview:self.closeButton];
  93. // 约束
  94. self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
  95. self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
  96. self.closeButton.translatesAutoresizingMaskIntoConstraints = NO;
  97. [NSLayoutConstraint activateConstraints:@[
  98. [self.headerView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
  99. [self.headerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  100. [self.headerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
  101. [self.headerView.heightAnchor constraintEqualToConstant:50],
  102. [self.titleLabel.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
  103. [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor constant:16],
  104. [self.titleLabel.heightAnchor constraintEqualToConstant:50],
  105. [self.titleLabel.widthAnchor constraintEqualToConstant:200],
  106. [self.closeButton.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor constant:-8],
  107. [self.closeButton.centerYAnchor constraintEqualToAnchor:self.headerView.centerYAnchor],
  108. [self.closeButton.widthAnchor constraintEqualToConstant:50],
  109. [self.closeButton.heightAnchor constraintEqualToConstant:50]
  110. ]];
  111. }
  112. - (void)setupTableView {
  113. self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
  114. self.tableView.delegate = self;
  115. self.tableView.dataSource = self;
  116. self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
  117. self.tableView.backgroundColor = [UIColor whiteColor];
  118. [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass(CommentItemCell.class) bundle:nil] forCellReuseIdentifier:NSStringFromClass(CommentItemCell.class)];
  119. [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass(CommentRpItemCell.class) bundle:nil] forCellReuseIdentifier:NSStringFromClass(CommentRpItemCell.class)];
  120. [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass(CommentFooterView.class) bundle:nil] forHeaderFooterViewReuseIdentifier:NSStringFromClass(CommentFooterView.class)];
  121. [self.view addSubview:self.tableView];
  122. // 下拉刷新
  123. self.refreshControl = [[UIRefreshControl alloc] init];
  124. [self.refreshControl addTarget:self action:@selector(handleRefresh) forControlEvents:UIControlEventValueChanged];
  125. [self.tableView addSubview:self.refreshControl];
  126. // 约束
  127. self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
  128. [NSLayoutConstraint activateConstraints:@[
  129. [self.tableView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor],
  130. [self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  131. [self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
  132. [self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-70]
  133. ]];
  134. }
  135. - (void)setupInputContainer {
  136. self.inputContainerView = [[UIView alloc] init];
  137. self.inputContainerView.backgroundColor = UIColor.whiteColor;
  138. [self.view addSubview:self.inputContainerView];
  139. // 输入框
  140. self.inputTextField = [[UITextField alloc] init];
  141. self.inputTextField.placeholder = @"说点什么...";
  142. self.inputTextField.backgroundColor = RGBACOLOR(241, 242, 244, 1);
  143. self.inputTextField.layer.cornerRadius = 20;
  144. self.inputTextField.returnKeyType = UIReturnKeySend;
  145. self.inputTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 12, 0)];
  146. self.inputTextField.leftViewMode = UITextFieldViewModeAlways;
  147. self.inputTextField.delegate = self;
  148. [self.inputTextField addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
  149. [self.inputContainerView addSubview:self.inputTextField];
  150. self.submitButton = [UIButton buttonWithType:UIButtonTypeCustom];
  151. [self.submitButton setTitle:@"发送" forState:UIControlStateNormal];
  152. [self.submitButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
  153. [self.submitButton setTitleColor:[UIColor whiteColor] forState:UIControlStateDisabled];
  154. self.submitButton.backgroundColor = [UIColor lightGrayColor];
  155. self.submitButton.layer.cornerRadius = 20;
  156. self.submitButton.enabled = NO;
  157. [self.submitButton addTarget:self action:@selector(handleSubmit) forControlEvents:UIControlEventTouchUpInside];
  158. [self.inputContainerView addSubview:self.submitButton];
  159. // 约束
  160. self.inputContainerView.translatesAutoresizingMaskIntoConstraints = NO;
  161. self.inputTextField.translatesAutoresizingMaskIntoConstraints = NO;
  162. self.submitButton.translatesAutoresizingMaskIntoConstraints = NO;
  163. [NSLayoutConstraint activateConstraints:@[
  164. [self.inputContainerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
  165. [self.inputContainerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
  166. [self.inputContainerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-safebottom],
  167. [self.inputContainerView.heightAnchor constraintEqualToConstant:50],
  168. [self.inputTextField.leadingAnchor constraintEqualToAnchor:self.inputContainerView.leadingAnchor constant:16],
  169. [self.inputTextField.rightAnchor constraintEqualToAnchor:self.inputContainerView.rightAnchor constant:-86],
  170. [self.inputTextField.centerYAnchor constraintEqualToAnchor:self.inputContainerView.centerYAnchor],
  171. [self.inputTextField.heightAnchor constraintEqualToConstant:40],
  172. // [self.submitButton.leadingAnchor constraintEqualToAnchor:self.inputTextField.trailingAnchor constant:8],
  173. [self.submitButton.trailingAnchor constraintEqualToAnchor:self.inputContainerView.trailingAnchor constant:-16],
  174. [self.submitButton.centerYAnchor constraintEqualToAnchor:self.inputContainerView.centerYAnchor],
  175. [self.submitButton.widthAnchor constraintEqualToConstant:60],
  176. [self.submitButton.heightAnchor constraintEqualToConstant:40]
  177. ]];
  178. }
  179. #pragma mark - 数据加载
  180. - (void)loadComments {
  181. if (self.isLoading) {
  182. return;
  183. }
  184. self.isLoading = YES;
  185. [[JXAPIService sharedService] getCommentsWithDramaId:self.dramaId
  186. page:self.currentPage
  187. pageSize:20
  188. success:^(id response) {
  189. self.isLoading = NO;
  190. [self.refreshControl endRefreshing];
  191. NSDictionary *data = response[@"data"];
  192. NSArray *list = data[@"list"];
  193. // long long totalCount = [data[@"total"] longLongValue];
  194. // 更新标题
  195. self.titleLabel.text = [NSString stringWithFormat:@"全部评论"];
  196. if (list.count == 0) {
  197. self.hasMoreData = NO;
  198. return;
  199. }
  200. // 转换为模型
  201. NSMutableArray *models = [NSMutableArray array];
  202. for (NSDictionary *dict in list) {
  203. JXCommentContent *comment = [[JXCommentContent alloc] initWithDictionary:dict];
  204. [models addObject:comment];
  205. }
  206. if (self.currentPage == 1) {
  207. self.commentList = models;
  208. } else {
  209. [self.commentList addObjectsFromArray:models];
  210. }
  211. self.emptyView.hidden = NO;
  212. [self.tableView reloadData];
  213. } failure:^(NSError *error) {
  214. self.isLoading = NO;
  215. [self.refreshControl endRefreshing];
  216. NSLog(@"[JXComment] 加载评论失败: %@", error.localizedDescription);
  217. }];
  218. }
  219. - (void)handleRefresh {
  220. self.currentPage = 1;
  221. self.hasMoreData = YES;
  222. [self loadComments];
  223. }
  224. - (void)loadMoreComments {
  225. if (!self.hasMoreData || self.isLoading) {
  226. return;
  227. }
  228. self.currentPage++;
  229. [self loadComments];
  230. }
  231. #pragma mark - UITableViewDataSource
  232. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
  233. return self.commentList.count;
  234. }
  235. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  236. JXCommentContent *comment = self.commentList[section];
  237. return comment.isOpen ? comment.reply_count + 1 : 1;
  238. }
  239. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  240. if (indexPath.row == 0) {
  241. CommentItemCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(CommentItemCell.class) forIndexPath:indexPath];
  242. cell.selectionStyle = UITableViewCellSelectionStyleNone;
  243. JXCommentContent *comment = self.commentList[indexPath.section];
  244. cell.labelT.text = comment.userName.length ? comment.userName : @"用户001";
  245. cell.labelC.text = comment.content;
  246. cell.labelL.text = [NSString stringWithFormat:@"%lld",comment.likeCount];
  247. if (comment.isLiked) {
  248. cell.imgL.image = [UIImage imageNamed:@"icon-2.2(1)"];
  249. }else{
  250. cell.imgL.image = [UIImage imageNamed:@"icon-2.2 1"];
  251. }
  252. cell.likeClicked = ^{
  253. [self handleLikeComment:comment];
  254. };
  255. cell.huifuClicked = ^{
  256. [self handleReplyComment:comment];
  257. };
  258. return cell;
  259. }
  260. CommentRpItemCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(CommentRpItemCell.class) forIndexPath:indexPath];
  261. cell.selectionStyle = UITableViewCellSelectionStyleNone;
  262. // JXCommentContent *comment = self.commentList[indexPath.row];
  263. //
  264. // // 临时简单展示(后续可创建自定义Cell)
  265. // cell.textLabel.numberOfLines = 0;
  266. // NSString *likeText = comment.isLiked ? @"❤️" : @"🤍";
  267. // cell.textLabel.text = [NSString stringWithFormat:@"%@ · %@\n%@\n%@ %lld 💬 %lld",
  268. // comment.userName,
  269. // [comment formattedTime],
  270. // comment.content,
  271. // likeText,
  272. // comment.likeCount,
  273. // comment.replyCount];
  274. return cell;
  275. }
  276. - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
  277. CommentFooterView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:NSStringFromClass(CommentFooterView.class)];
  278. JXCommentContent *comment = self.commentList[section];
  279. header.btnOpen.selected = comment.isOpen;
  280. header.btnClicked = ^{
  281. comment.isOpen = !comment.isOpen;
  282. [self.tableView reloadData];
  283. };
  284. return header;
  285. }
  286. #pragma mark - UITableViewDelegate
  287. - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
  288. JXCommentContent *comment = self.commentList[section];
  289. return comment.reply_count > 0 ? 0 : 40;
  290. }
  291. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  292. return UITableViewAutomaticDimension;
  293. }
  294. - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
  295. return 80;
  296. }
  297. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  298. // [tableView deselectRowAtIndexPath:indexPath animated:YES];
  299. return;
  300. if (indexPath.row == 0) {
  301. JXCommentContent *comment = self.commentList[indexPath.row];
  302. // 显示操作菜单(点赞、回复)
  303. UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
  304. message:nil
  305. preferredStyle:UIAlertControllerStyleActionSheet];
  306. // 点赞
  307. NSString *likeTitle = comment.isLiked ? @"取消点赞" : @"点赞";
  308. [alert addAction:[UIAlertAction actionWithTitle:likeTitle
  309. style:UIAlertActionStyleDefault
  310. handler:^(UIAlertAction *action) {
  311. [self handleLikeComment:comment];
  312. }]];
  313. // 回复
  314. [alert addAction:[UIAlertAction actionWithTitle:@"回复"
  315. style:UIAlertActionStyleDefault
  316. handler:^(UIAlertAction *action) {
  317. [self handleReplyComment:comment];
  318. }]];
  319. [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
  320. [self presentViewController:alert animated:YES completion:nil];
  321. }
  322. }
  323. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  324. // 预加载
  325. CGFloat offsetY = scrollView.contentOffset.y;
  326. CGFloat contentHeight = scrollView.contentSize.height;
  327. CGFloat scrollViewHeight = scrollView.bounds.size.height;
  328. if (offsetY + scrollViewHeight >= contentHeight - 200) {
  329. // [self loadMoreComments];
  330. }
  331. }
  332. #pragma mark - 评论交互
  333. - (void)handleLikeComment:(JXCommentContent *)comment {
  334. [[JXAPIService sharedService] likeCommentWithCommentId:comment.commentId
  335. success:^(id response) {
  336. // 更新UI
  337. BOOL isLiked = [response[@"data"][@"isLiked"] boolValue];
  338. long long likeCount = [response[@"data"][@"likeCount"] longLongValue];
  339. comment.isLiked = isLiked;
  340. comment.likeCount = likeCount;
  341. [self.tableView reloadData];
  342. } failure:^(NSError *error) {
  343. NSLog(@"[JXComment] 点赞评论失败: %@", error.localizedDescription);
  344. }];
  345. }
  346. - (void)handleReplyComment:(JXCommentContent *)comment {
  347. self.replyToCommentId = comment.commentId;
  348. self.replyToUserName = comment.userName;
  349. self.inputTextField.placeholder = [NSString stringWithFormat:@"回复 @%@:", comment.userName];
  350. [self.inputTextField becomeFirstResponder];
  351. }
  352. - (void)textFieldDidChange {
  353. NSString *text = self.inputTextField.text;
  354. self.submitButton.enabled = (text.length > 0);
  355. }
  356. - (BOOL)textFieldShouldReturn:(UITextField *)textField{
  357. if (textField.text.length == 0) {
  358. [MBProgressHUD showMessage:@"请输入评论"];
  359. return NO;
  360. }
  361. [self handleSubmit];
  362. return YES;
  363. }
  364. - (void)handleSubmit {
  365. NSString *content = self.inputTextField.text;
  366. if (content.length == 0) {
  367. return;
  368. }
  369. if (self.replyToCommentId > 0) {
  370. // 回复评论
  371. [[JXAPIService sharedService] replyCommentWithCommentId:self.replyToCommentId
  372. content:content
  373. success:^(id response) {
  374. NSLog(@"[JXComment] 回复成功");
  375. [self resetInput];
  376. [self handleRefresh];
  377. } failure:^(NSError *error) {
  378. NSLog(@"[JXComment] 回复失败: %@", error.localizedDescription);
  379. }];
  380. } else {
  381. // 提交新评论
  382. [[JXAPIService sharedService] submitCommentWithDramaId:self.dramaId
  383. content:content
  384. success:^(id response) {
  385. NSLog(@"[JXComment] 评论提交成功");
  386. [self resetInput];
  387. [self handleRefresh];
  388. } failure:^(NSError *error) {
  389. NSLog(@"[JXComment] 评论提交失败: %@", error.localizedDescription);
  390. }];
  391. }
  392. }
  393. - (void)resetInput {
  394. self.inputTextField.text = @"";
  395. self.inputTextField.placeholder = @"说点什么...";
  396. self.submitButton.enabled = NO;
  397. self.replyToCommentId = 0;
  398. self.replyToUserName = nil;
  399. [self.inputTextField resignFirstResponder];
  400. }
  401. #pragma mark - 键盘处理
  402. - (void)keyboardWillShow:(NSNotification *)notification {
  403. NSDictionary *userInfo = notification.userInfo;
  404. CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  405. CGFloat keyboardHeight = keyboardFrame.size.height;
  406. [UIView animateWithDuration:0.3 animations:^{
  407. self.inputContainerView.transform = CGAffineTransformMakeTranslation(0, -keyboardHeight);
  408. self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
  409. }];
  410. }
  411. - (void)keyboardWillHide:(NSNotification *)notification {
  412. [UIView animateWithDuration:0.3 animations:^{
  413. self.inputContainerView.transform = CGAffineTransformIdentity;
  414. self.tableView.contentInset = UIEdgeInsetsZero;
  415. }];
  416. }
  417. #pragma mark - 关闭
  418. - (void)handleClose {
  419. [self dismissViewControllerAnimated:YES completion:nil];
  420. }
  421. @end