import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hgdj/assets_tool/images.dart'; import 'package:hgdj/extension/extensions.dart'; import 'package:hgdj/hj_model/banner/comment_top_banner_model.dart'; import 'package:hgdj/hj_model/comment/comment_model.dart'; import 'package:hgdj/hj_model/comment/reply_model.dart'; import 'package:hgdj/hj_model/video_model.dart'; import 'package:hgdj/hj_page/comment/cell/reply_comment_item.dart'; import 'package:hgdj/hj_utils/api_service/comment_service.dart'; import 'package:hgdj/hj_utils/api_service/common_service.dart'; import 'package:hgdj/hj_utils/date_time_util.dart'; import 'package:hgdj/hj_utils/screen.dart'; import 'package:hgdj/tools_base/debug_log.dart'; import 'package:hgdj/tools_base/widget/image_browser_page.dart'; import 'package:like_button/like_button.dart'; import '../../../tools_base/widget/net_image_widget.dart'; import '../../home/widget/user_avatar.dart'; import '../../home/widget/user_name_view.dart'; import '../widget/comment_rich_text.dart'; import '../widget/comment_top_banner.dart'; /// 一条评论:头像 + 昵称 + 正文(可带图) + 时间/点赞/回复 + 折叠的回复列表 class CommentItem extends StatefulWidget { final CommentModel comment; final String objId; final List? topBanners; // 置顶评论下方配置 Banner final Function(CommentModel, {ReplyModel? reply})? replyHandler; const CommentItem( this.comment, { super.key, required this.objId, this.replyHandler, this.topBanners, }); @override State createState() => _CommentItemState(); } class _CommentItemState extends State { CommentModel get comment => widget.comment; late final isLikedNof = ValueNotifier(comment.isLike ?? false); bool _isLoadingMore = false; bool _isExpand = false; int _page = 1; @override void dispose() { isLikedNof.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () {}, child: Stack( clipBehavior: Clip.none, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ UserAvatar( size: 40, model: Publisher() ..uid = comment.userID ..portrait = comment.userPortrait ..superUser = (comment.superUser ?? false), showVip: false, ), //头像 12.sizeBoxW, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ UserNameView( name: comment.userName, isVip: (comment.vipLevel ?? 0) > 0, nameColor: Color(0xff9A9A9A), ), if ((comment.vipLevel ?? 0) > 0) ...[ 4.sizeBoxW, Image.asset("vip_v.webp".commentPath, width: 16), ] ], ), 8.sizeBoxH, CommentRichText(model: comment), if (comment.image?.isNotEmpty == true) GestureDetector( onTap: () { ImageBrowserPage.open([comment.image ?? '']); }, child: Container( margin: EdgeInsets.only(top: 6), alignment: Alignment.centerLeft, constraints: BoxConstraints(maxWidth: 150), child: NetworkImageLoader( imageUrl: comment.image ?? ""), ), ), // 置顶评论配置 Banner(多图轮播,支持 GIF / 内外链) if (widget.topBanners?.isNotEmpty == true) CommentTopBanner(banners: widget.topBanners!), 8.sizeBoxH, Row( children: [ Text( comment.createdAt?.utcToAgo() ?? "", style: TextStyle( color: Color(0x73FFFFFF), fontSize: 12, ), ), const Spacer(), if (!comment.isOfficial && !comment.isHideLikeOrComment) ...[ ValueListenableBuilder( valueListenable: isLikedNof, builder: (context, value, child) => Row( children: [ LikeButton( isLiked: value, onTap: (_) => _toggleLike(), size: 20, likeBuilder: (isLiked) { return Image.asset(isLiked ? 'like_red.webp'.commentPath : 'like_white.webp'.commentPath); }, ), Text( comment.likeCount?.countOr("0") ?? "0", style: TextStyle( color: Color(0xE5FFFFFF), fontSize: 12, ), ), ], ), ), 12.sizeBoxW, GestureDetector( onTap: () => widget.replyHandler?.call(comment), child: Image.asset('msg_icon.png'.commentPath, width: 20), ), ], ], ), _replyList(), // 展开更多评论 if ((comment.replies?.length ?? 0) > 0) _moreReplyBar(), ], ), ), ], ), 18.sizeBoxH, const Divider(height: 1, color: Color(0x0fffffff)), 18.sizeBoxH, ], ), if ((comment.isGodComment ?? false)) Positioned( right: 8, top: -10, child: Transform.rotate( angle: pi / 60, child: Image.asset( 'god_comment.webp'.commentPath, width: 72, height: 60, ), ), ), ], )); } /// 展开 / 收起那一行:短横线 + 文案 + 箭头 Widget _replyBarRow(String text, {bool isUp = false}) { final chevron = Image.asset("chevron_down.webp".commonImgPath, width: 16); return Row( children: [ Container(width: 20, height: 1, color: const Color(0xff989898)), 4.sizeBoxW, Text(text, style: const TextStyle( fontSize: 12, color: Color(0xff989898), fontWeight: FontWeight.w500)), 2.sizeBoxW, isUp ? Transform.rotate(angle: pi, child: chevron) : chevron, ], ); } Widget _moreReplyBar() { final replyCount = comment.replies?.length ?? 0; if (replyCount > 1 && _isExpand) { //收起:padding 在点击区里面,跟改动前一致 return GestureDetector( onTap: () => setState(() => _isExpand = false), child: Padding( padding: const EdgeInsets.only(top: 12), child: _replyBarRow("收起${comment.commCount ?? 0}条回复", isUp: true), ), ); } final diff = (comment.commCount ?? 0) - (_isExpand ? replyCount : 1); if (diff <= 0) return const SizedBox.shrink(); if (_isLoadingMore) { return const Padding( padding: EdgeInsets.only(top: 12, left: 6), child: CupertinoActivityIndicator(color: Colors.white, radius: 8), ); } //展开:padding 在点击区外面别算进热区;外面这层 Row 也不能省—— //它给 InkWell 的是无界宽约束,InkWell 才会只包住文字而不是撑满整行 return Padding( padding: const EdgeInsets.only(top: 12), child: Row( children: [ InkWell( enableFeedback: false, onTap: _expandReplies, child: _replyBarRow(replyCount > 99 ? '展开更多回复' : '展开$diff条回复'), ), ], ), ); } // 展开回复 Future _expandReplies() async { if (comment.replies?.isNotEmpty != true) { comment.commCount = 0; setState(() {}); return; } if (_isLoadingMore) return; _isLoadingMore = true; setState(() {}); _isExpand = true; try { if (comment.hasMoreReply ?? false) { final curTime = DateTimeUtil.format2utc(DateTime.now()); final fastId = comment.replies!.first.id ?? ''; final result = await CommentService.getReplyList(widget.objId, comment.id ?? '', curTime, comment.replyPage ?? _page, 5, fastId); comment.hasMoreReply = result?.hasNext ?? false; comment.replies?.addAll(result?.list ?? []); if (result?.hasNext == true) { _page += 1; comment.replyPage = (comment.replyPage ?? 0) + 1; } else { comment.commCount = comment.replies?.length ?? 0; } } } catch (e) { debugLog(e); } _isLoadingMore = false; if (mounted) setState(() {}); } Future _toggleLike() async { final isLike = comment.isLike ?? false; bool result; if (!isLike) { result = await CommonService.sendLike(comment.id ?? '', 'COMMENT'); if (result) comment.likeCount = (comment.likeCount ?? 0) + 1; } else { result = await CommonService.cancelLike(comment.id ?? '', 'COMMENT'); if (result) comment.likeCount = (comment.likeCount ?? 1) - 1; } if (result) comment.isLike = !isLike; isLikedNof.value = comment.isLike!; return isLikedNof.value; } //没展开时只露第一条 Widget _replyList() { final replies = comment.replies ?? []; if (replies.isEmpty) return const SizedBox.shrink(); final length = (!_isExpand && replies.length > 1) ? 1 : replies.length; return Column( //stretch 对齐原来 ListView 的行为:子项撑满宽度,不按内容自适应 mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: List.generate( length, (i) => ReplyCommentItem( replies[i], replyHandler: (reply) => widget.replyHandler?.call(comment, reply: reply), ), ), ); } }