推荐视频评论和点赞关注

This commit is contained in:
cuiyouliang 2025-08-25 18:07:40 +08:00
parent 212fdea5a0
commit 26b8b5abf3
2 changed files with 375 additions and 258 deletions

View File

@ -195,7 +195,14 @@ class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
canPop: true,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) {
print('User navigated back');
}
},
child:Scaffold(
backgroundColor: const Color(0xFFFAF6F9), backgroundColor: const Color(0xFFFAF6F9),
body: Obx(() { body: Obx(() {
return NestedScrollViewPlus( return NestedScrollViewPlus(
@ -211,6 +218,16 @@ class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
collapsedHeight: 120.0, collapsedHeight: 120.0,
pinned: true, pinned: true,
stretch: true, stretch: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Get.back(result: {
'returnTo': '/',
'vlogerId': args['memberId'],
'followStatus': (followed.value == 1 || followed.value == 3)?true:false,
});
},
),
onStretchTrigger: () async { onStretchTrigger: () async {
logger.i('触发 stretch 拉伸'); logger.i('触发 stretch 拉伸');
// //
@ -283,6 +300,7 @@ class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
), ),
); );
}), }),
),
); );
} }

View File

@ -10,6 +10,7 @@ import 'package:get/get.dart';
import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/controller/chat_controller.dart';
import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_core.dart';
import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_message.dart';
import 'package:loopin/IM/im_service.dart' hide logger;
import 'package:loopin/api/video_api.dart'; import 'package:loopin/api/video_api.dart';
import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/my_toast.dart';
import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/network_or_asset_image.dart';
@ -46,9 +47,14 @@ class RecommendModule extends StatefulWidget {
State<RecommendModule> createState() => _RecommendModuleState(); State<RecommendModule> createState() => _RecommendModuleState();
} }
class CommentBottomSheet extends StatefulWidget { class CommentBottomSheet extends StatefulWidget {
final int videoId; final String videoId;
final Function(int) onCommentCountChanged; //
const CommentBottomSheet({super.key, required this.videoId}); const CommentBottomSheet({
super.key,
required this.videoId,
required this.onCommentCountChanged, //
});
@override @override
_CommentBottomSheetState createState() => _CommentBottomSheetState(); _CommentBottomSheetState createState() => _CommentBottomSheetState();
@ -61,9 +67,15 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
bool isLoadingMoreComments = false; bool isLoadingMoreComments = false;
bool hasMoreComments = true; bool hasMoreComments = true;
List<Map<String, dynamic>> commentList = []; List<Map<String, dynamic>> commentList = [];
int replyingCommentId = 0; String replyingCommentId = '';
String replyingCommentUser = ''; String replyingCommentUser = '';
//
Map<String, dynamic> expandedReplies = {}; // {commentId: true/false}
Map<String, List<Map<String, dynamic>>> replyData = {}; // {commentId: [replies]}
Map<String, int> replyPage = {}; //
Map<String, bool> isLoadingReplies = {}; //
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -83,7 +95,7 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
hasMoreComments = true; hasMoreComments = true;
commentList.clear(); commentList.clear();
} }
logger.d('入参vlogId-------------------->: ${widget.videoId}');
try { try {
final res = await Http.post(VideoApi.videoCommentList, data: { final res = await Http.post(VideoApi.videoCommentList, data: {
'vlogId': widget.videoId, 'vlogId': widget.videoId,
@ -91,7 +103,7 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
'size': commentPageSize, 'size': commentPageSize,
}); });
logger.d('评论接口返回: ${json.encode(res)}'); logger.d('评论接口列表返回: ${json.encode(res)}');
if (res['code'] == 200 && res['data'] != null) { if (res['code'] == 200 && res['data'] != null) {
final data = res['data']; final data = res['data'];
@ -108,7 +120,7 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
hasMoreComments = commentList.length < total; hasMoreComments = commentList.length < total;
commentPage++; commentPage++;
}); });
widget.onCommentCountChanged(total);
logger.d('成功加载 ${newComments.length} 条评论,总共 $total'); logger.d('成功加载 ${newComments.length} 条评论,总共 $total');
} }
} catch (e) { } catch (e) {
@ -120,32 +132,91 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
} }
} }
Future<void> postComment(String content, {int parentCommentId = 0}) async { Future<void> postComment(String content, {String parentCommentId = ''}) async {
try { try {
final res = await Http.post(VideoApi.doVideoComment, data: { final res = await Http.post(VideoApi.doVideoComment, data: {
'vlogId': widget.videoId, 'vlogId': widget.videoId,
'content': content, 'content': content,
'fatherCommentId': parentCommentId, 'fatherCommentId': parentCommentId.isNotEmpty ? parentCommentId : null,
}); });
if (res['code'] == 0) { if (res['code'] == 200) {
// //
if (parentCommentId.isNotEmpty) {
fetchReplies(parentCommentId, false);
//
setState(() {
final comment = commentList.firstWhere(
(c) => c['id'] == parentCommentId,
orElse: () => <String, dynamic>{}
);
if (comment != null && comment.isNotEmpty) {
comment['childCount'] = (comment['childCount'] ?? 0) + 1;
}
});
} else {
//
fetchComments(false); fetchComments(false);
MyToast().tip(
title: '评论成功',
position: 'center',
type: 'success',
);
} }
} catch (e) { widget.onCommentCountChanged(commentList.length + 1);
logger.i('发布评论失败: $e');
MyToast().tip( MyToast().tip(
title: '评论失败', title: '评论成功',
position: 'center', position: 'center',
type: 'error', type: 'success',
); );
} }
} catch (e) {
logger.i('发布评论失败: $e');
MyToast().tip(
title: '评论失败',
position: 'center',
type: 'error',
);
} }
}
//
Future<void> fetchReplies(String commentId, bool loadMore) async {
if (isLoadingReplies[commentId] == true && !loadMore) return;
setState(() {
isLoadingReplies[commentId] = true;
});
if (!loadMore) {
replyPage[commentId] = 1;
replyData[commentId] = [];
}
try {
final res = await Http.post(VideoApi.videoCommentList, data: {
'fatherCommentId': commentId,
'current': replyPage[commentId],
'size': commentPageSize,
});
if (res['code'] == 200 && res['data'] != null) {
final data = res['data'];
final List<Map<String, dynamic>> newReplies =
List<Map<String, dynamic>>.from(data['records'] ?? []);
setState(() {
if (loadMore) {
replyData[commentId]!.addAll(newReplies);
} else {
replyData[commentId] = newReplies;
}
replyPage[commentId] = replyPage[commentId]! + 1;
});
}
} catch (e) {
logger.e('获取子评论异常: $e');
} finally {
setState(() {
isLoadingReplies[commentId] = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -227,109 +298,200 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
} }
final comment = commentList[index]; final comment = commentList[index];
return ListTile( final hasReplies = comment['childCount'] > 0;
isThreeLine: true, final isExpanded = expandedReplies[comment['id']] == true;
leading: ClipRRect( final replies = replyData[comment['id']] ?? [];
borderRadius: BorderRadius.circular(50.0),
child: NetworkOrAssetImage( return Column(
imageUrl: comment['commentUserFace'] ?? 'assets/images/avatar/default.png', children: [
width: 30.0, //
height: 30.0, ListTile(
fit: BoxFit.cover, isThreeLine: true,
), leading: ClipRRect(
), borderRadius: BorderRadius.circular(50.0),
title: Row( child: NetworkOrAssetImage(
children: [ imageUrl: comment['commentUserFace'] ?? 'assets/images/avatar/default.png',
Expanded( width: 30.0,
child: Text( height: 30.0,
comment['commentUserNickname'] ?? '未知用户', fit: BoxFit.cover,
style: TextStyle(
color: Colors.grey,
fontSize: 12.0,
),
), ),
), ),
SizedBox(width: 20.0), title: Row(
GestureDetector( children: [
onTap: () { Expanded(
setState(() { child: Text(
replyingCommentId = comment['id']; comment['commentUserNickname'] ?? '未知用户',
replyingCommentUser = comment['commentUserNickname'] ?? '未知用户'; style: TextStyle(
}); color: Colors.grey,
}, fontSize: 12.0,
child: Icon(
Icons.reply,
color: Colors.black54,
size: 16.0,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 5.0),
child: Text(
comment['content'] ?? '',
style: TextStyle(
fontSize: 14.0,
),
),
),
if (comment['childCount'] > 0)
GestureDetector(
onTap: () {
setState(() {
replyingCommentId = comment['id'];
replyingCommentUser = comment['commentUserNickname'] ?? '未知用户';
});
},
child: Container(
margin: EdgeInsets.only(right: 15.0),
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.0),
),
child: Row(children: [
Text(
'${comment['childCount']}回复',
style: TextStyle(fontSize: 12.0),
), ),
Icon( ),
Icons.arrow_forward_ios,
size: 10.0,
)
]),
), ),
), SizedBox(width: 20.0),
Text( GestureDetector(
'${comment['createTime']?.toString().substring(0, 10) ?? ''}', onTap: () {
style: TextStyle(color: Colors.grey, fontSize: 12.0), setState(() {
replyingCommentId = comment['id'];
replyingCommentUser = comment['commentUserNickname'] ?? '未知用户';
});
},
child: Icon(
Icons.reply,
color: Colors.black54,
size: 16.0,
),
),
],
), ),
], subtitle: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 5.0),
child: Text(
comment['content'] ?? '',
style: TextStyle(
fontSize: 14.0,
),
),
),
if (hasReplies)
GestureDetector(
onTap: () {
setState(() {
expandedReplies[comment['id']] = !isExpanded;
if (expandedReplies[comment['id']] == true &&
(replyData[comment['id']] == null || replyData[comment['id']]!.isEmpty)) {
fetchReplies(comment['id'], false);
}
});
},
child: Container(
margin: EdgeInsets.only(right: 15.0),
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.0),
),
child: Row(children: [
Text(
'${comment['childCount']}回复',
style: TextStyle(fontSize: 12.0),
),
Icon(
isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down,
size: 16.0,
)
]),
),
),
Text(
'${comment['createTime']?.toString().substring(0, 10) ?? ''}',
style: TextStyle(color: Colors.grey, fontSize: 12.0),
),
],
),
),
//
if (isExpanded && replies.isNotEmpty)
Padding(
padding: EdgeInsets.only(left: 40.0),
child: Column(
children: [
...replies.map((reply) => ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(25.0),
child: NetworkOrAssetImage(
imageUrl: reply['commentUserFace'] ?? 'assets/images/avatar/default.png',
width: 25.0,
height: 25.0,
fit: BoxFit.cover,
),
),
title: Row(
children: [
Expanded(
child: Text(
reply['commentUserNickname'] ?? '未知用户',
style: TextStyle(
color: Colors.grey,
fontSize: 11.0,
),
),
),
GestureDetector(
onTap: () {
setState(() {
replyingCommentId = comment['id'];
replyingCommentUser = reply['commentUserNickname'] ?? '未知用户';
});
},
child: Icon(
Icons.reply,
color: Colors.black54,
size: 14.0,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 3.0),
child: Text(
reply['content'] ?? '',
style: TextStyle(fontSize: 13.0),
),
),
Text(
'${reply['createTime']?.toString().substring(0, 10) ?? ''}',
style: TextStyle(color: Colors.grey, fontSize: 11.0),
),
],
),
)).toList(),
//
if (replies.length < comment['childCount'])
Center(
child: TextButton(
onPressed: () => fetchReplies(comment['id'], true),
child: isLoadingReplies[comment['id']] == true
? CircularProgressIndicator()
: Text('加载更多回复'),
),
),
],
),
),
Divider(height: 1, color: Colors.grey[200]),
],
); );
}, },
), ),
),
), ),
), //
if (replyingCommentId > 0) if (replyingCommentId.isNotEmpty)
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
color: Colors.grey[100], color: Colors.grey[100],
child: Row( child: Row(
children: [ children: [
Text( Expanded(
'回复 @$replyingCommentUser', child: Text(
style: TextStyle(fontSize: 12.0, color: Colors.blue), '回复 @$replyingCommentUser',
style: TextStyle(fontSize: 12.0, color: Colors.blue),
overflow: TextOverflow.ellipsis,
),
), ),
Spacer(),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
replyingCommentId = 0; replyingCommentId = '';
replyingCommentUser = ''; replyingCommentUser = '';
}); });
}, },
@ -338,58 +500,58 @@ class _CommentBottomSheetState extends State<CommentBottomSheet> {
], ],
), ),
), ),
GestureDetector( GestureDetector(
child: Container( child: Container(
margin: EdgeInsets.all(10.0), margin: EdgeInsets.all(10.0),
height: 40.0, height: 40.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100],
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
), ),
child: Row( child: Row(
children: [ children: [
SizedBox(width: 15.0), SizedBox(width: 15.0),
Icon( Icon(
Icons.edit_note, Icons.edit_note,
color: Colors.black54, color: Colors.black54,
size: 16.0, size: 16.0,
), ),
SizedBox(width: 5.0), SizedBox(width: 5.0),
Text( Text(
replyingCommentId > 0 ? '回复 @$replyingCommentUser' : '说点什么...', replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...',
style: TextStyle(color: Colors.black54, fontSize: 14.0), style: TextStyle(color: Colors.black54, fontSize: 14.0),
), ),
], ],
),
), ),
onTap: () {
Navigator.push(context, FadeRoute(child: PopupReply(
hintText: replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...',
onChanged: (value) {
debugPrint('评论内容: $value');
},
onSubmitted: (value) {
if (value.isNotEmpty) {
postComment(value, parentCommentId: replyingCommentId);
setState(() {
replyingCommentId = '';
replyingCommentUser = '';
});
Navigator.pop(context);
}
},
)));
},
), ),
onTap: () { ],
Navigator.push(context, FadeRoute(child: PopupReply( ),
hintText: replyingCommentId > 0 ? '回复 @$replyingCommentUser' : '说点什么...', );
onChanged: (value) { }
debugPrint('评论内容: $value');
},
onSubmitted: (value) {
if (value.isNotEmpty) {
postComment(value, parentCommentId: replyingCommentId);
setState(() {
replyingCommentId = 0;
replyingCommentUser = '';
});
Navigator.pop(context);
}
},
)));
},
),
],
),
);
} }
}
class _RecommendModuleState extends State<RecommendModule> { class _RecommendModuleState extends State<RecommendModule> {
VideoModuleController videoModuleController = Get.put(VideoModuleController()); VideoModuleController videoModuleController = Get.put(VideoModuleController());
final ChatController chatController = Get.find<ChatController>(); final ChatController chatController = Get.find<ChatController>();
final RxMap<String, dynamic> lastReturnData = RxMap({});
// //
int page = 1; int page = 1;
final int pageSize = 10; final int pageSize = 10;
@ -421,21 +583,15 @@ class _RecommendModuleState extends State<RecommendModule> {
bool hasMoreComments = true; bool hasMoreComments = true;
List<Map<String, dynamic>> commentList = []; List<Map<String, dynamic>> commentList = [];
int currentVideoId = 0; int currentVideoId = 0;
int replyingCommentId = 0; // ID String replyingCommentId = ''; // ID
String replyingCommentUser = ''; // String replyingCommentUser = ''; //
// //
List shareList = [ List shareList = [
{'icon': 'assets/images/share-wx.png', 'label': '好友'}, {'icon': 'assets/images/share-wx.png', 'label': '好友'},
{'icon': 'assets/images/share-wx.png', 'label': '微信'},
{'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'}, {'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'},
{'icon': 'assets/images/share-link.png', 'label': '复制链接'}, {'icon': 'assets/images/share-link.png', 'label': '复制链接'},
{'icon': 'assets/images/share-download.png', 'label': '下载'}, {'icon': 'assets/images/share-download.png', 'label': '下载'},
{'icon': 'assets/images/share-download.png', 'label': '下载'},
{'icon': 'assets/images/share-download.png', 'label': '下载'},
{'icon': 'assets/images/share-download.png', 'label': '下载'},
{'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'},
{'icon': 'assets/images/share-download.png', 'label': '下载下载下载下载下载下载下载下载下载下载下载下载'},
]; ];
@override @override
@ -545,7 +701,7 @@ class _RecommendModuleState extends State<RecommendModule> {
if (videos.isNotEmpty) { if (videos.isNotEmpty) {
page++; page++;
} }
logger.i('获取新的视频数据了'); logger.i('视频数据列表------------------->$videoList');
// //
player.open( player.open(
@ -569,91 +725,6 @@ class _RecommendModuleState extends State<RecommendModule> {
} }
} }
// fetchComments
Future<void> fetchComments(int videoId, {bool loadMore = false}) async {
//
if (isLoadingMoreComments && !loadMore) return;
setState(() {
isLoadingMoreComments = true;
});
try {
final res = await Http.post(VideoApi.videoCommentList, data: {
'vlogId': videoId,
'current': loadMore ? commentPage : 1,
'size': commentPageSize,
});
logger.d('评论接口返回: ${json.encode(res)}');
if (res['code'] == 200 && res['data'] != null) {
final data = res['data'];
final List<Map<String, dynamic>> newComments =
List<Map<String, dynamic>>.from(data['records'] ?? []);
final int total = data['total'] ?? 0;
setState(() {
if (loadMore) {
commentList.addAll(newComments);
} else {
commentList = newComments;
}
hasMoreComments = commentList.length < total;
commentPage = loadMore ? commentPage + 1 : 2; //
});
logger.d('成功加载 ${newComments.length} 条评论,总共 $total');
} else {
logger.e('获取评论失败: ${res['msg']}');
MyToast().tip(
title: '获取评论失败',
position: 'center',
type: 'error',
);
}
} catch (e) {
logger.e('获取评论异常: $e');
MyToast().tip(
title: '网络异常',
position: 'center',
type: 'error',
);
} finally {
setState(() {
isLoadingMoreComments = false;
});
}
}
//
Future<void> postComment(String content, {int parentCommentId = 0}) async {
try {
final res = await Http.post(VideoApi.doVideoComment, data: {
'vlogId': currentVideoId,
'content': content,
'fatherCommentId': parentCommentId,
});
if (res['code'] == 0) {
//
fetchComments(currentVideoId, loadMore: false);
MyToast().tip(
title: '评论成功',
position: 'center',
type: 'success',
);
}
} catch (e) {
logger.i('发布评论失败: $e');
MyToast().tip(
title: '评论失败',
position: 'center',
type: 'error',
);
}
}
// //
Future<void> doUnLikeVideo(item) async { Future<void> doUnLikeVideo(item) async {
logger.d('点击了点赞按钮$item'); logger.d('点击了点赞按钮$item');
@ -688,8 +759,7 @@ Future<void> fetchComments(int videoId, {bool loadMore = false}) async {
// //
void handleComment(index) { void handleComment(index) {
final videoIdStr = videoList[index]['id'].toString(); final videoId = videoList[index]['id'];
final videoId = int.tryParse(videoIdStr) ?? 0;
logger.i('点击了评论按钮$videoId'); logger.i('点击了评论按钮$videoId');
showModalBottomSheet( showModalBottomSheet(
@ -703,7 +773,17 @@ void handleComment(index) {
), ),
context: context, context: context,
builder: (context) { builder: (context) {
return CommentBottomSheet(videoId: videoId); return CommentBottomSheet(
videoId: videoId,
onCommentCountChanged: (newCount) {
//
setState(() {
if (index < videoList.length) {
videoList[index]['commentsCounts'] = newCount;
}
});
}
);
}, },
); );
} }
@ -974,9 +1054,20 @@ void handleComment(index) {
height: 55.0, height: 55.0,
width: 48.0, width: 48.0,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: ()async {
player.pause(); player.pause();
Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); // Vloger
final result = await Get.toNamed(
'/vloger',
arguments: videoList[videoModuleController
.videoPlayIndex.value]);
if (result != null) {
//
print('返回的数据: ${result['followStatus']}');
player.play();
videoList[index]['doIFollowVloger'] = result['followStatus'];
};
}, },
child: UnconstrainedBox( child: UnconstrainedBox(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
@ -1013,10 +1104,18 @@ void handleComment(index) {
size: 14.0, size: 14.0,
), ),
), ),
onTap: () { onTap: () async {
setState(() { final vlogerId = videoList[index]['memberId'];
videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; final doIFollowVloger = videoList[index]['doIFollowVloger'];
}); //
if(doIFollowVloger == false){
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
if (res.success) {
setState(() {
videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger'];
});
}
}
}, },
), ),
), ),