Files
2026-09-15 15:44:13 +07:00

319 lines
12 KiB
Dart

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<CommentTopBannerModel>? topBanners; // 置顶评论下方配置 Banner
final Function(CommentModel, {ReplyModel? reply})? replyHandler;
const CommentItem(
this.comment, {
super.key,
required this.objId,
this.replyHandler,
this.topBanners,
});
@override
State<CommentItem> createState() => _CommentItemState();
}
class _CommentItemState extends State<CommentItem> {
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<bool> _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),
),
),
);
}
}