初始化
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
import 'package:like_button/like_button.dart';
|
||||
|
||||
import '../../../hj_model/comment/reply_model.dart';
|
||||
import '../../../hj_utils/api_service/common_service.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import '../../user_center_page/user_center_page.dart';
|
||||
|
||||
/// 评论下的一条回复:小头像 + 「A ▸ B」 + 正文(可带图) + 时间/点赞/回复
|
||||
class ReplyCommentItem extends StatefulWidget {
|
||||
final ReplyModel reply;
|
||||
final Function(ReplyModel)? replyHandler;
|
||||
|
||||
const ReplyCommentItem(this.reply, {super.key, this.replyHandler});
|
||||
|
||||
@override
|
||||
State<ReplyCommentItem> createState() => _ReplyCommentItemState();
|
||||
}
|
||||
|
||||
class _ReplyCommentItemState extends State<ReplyCommentItem> {
|
||||
ReplyModel get reply => widget.reply;
|
||||
late final isLikedNof = ValueNotifier(reply.isLike ?? false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isLikedNof.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (reply.userID != null) {
|
||||
Get.to(() => UserCenterPage(uid: reply.userID ?? 0));
|
||||
}
|
||||
},
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: reply.userPortrait ?? '',
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 18,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
reply.userName ?? '',
|
||||
style: TextStyle(
|
||||
color: Color(0x73FFFFFF),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
3.sizeBoxW,
|
||||
if (reply.toUserName?.isNotEmpty == true) ...[
|
||||
Image.asset(
|
||||
"arrow_grey.webp".commentPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
3.sizeBoxW,
|
||||
Text(
|
||||
reply.toUserName ?? '',
|
||||
style:
|
||||
TextStyle(color: Color(0x73FFFFFF), fontSize: 14),
|
||||
),
|
||||
]
|
||||
],
|
||||
), // 还有个类似评分的东西
|
||||
8.sizeBoxH,
|
||||
Text(
|
||||
reply.content ?? '',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (reply.image?.isNotEmpty == true)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ImageBrowserPage.open([reply.image ?? '']);
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 6),
|
||||
alignment: Alignment.centerLeft,
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: NetworkImageLoader(imageUrl: reply.image ?? ""),
|
||||
),
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
reply.createdAt?.utcToAgo() ?? "",
|
||||
style: const TextStyle(
|
||||
color: Color(0x73FFFFFF), fontSize: 12),
|
||||
),
|
||||
const Spacer(),
|
||||
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(
|
||||
reply.likeCount?.countOr("0") ?? "0",
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
GestureDetector(
|
||||
onTap: () => widget.replyHandler?.call(reply),
|
||||
child:
|
||||
Image.asset('msg_icon.png'.commentPath, width: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 评论点赞
|
||||
Future<bool> _toggleLike() async {
|
||||
final isLike = reply.isLike ?? false;
|
||||
bool result;
|
||||
if (!isLike) {
|
||||
result = await CommonService.sendLike(reply.id ?? '', 'COMMENT');
|
||||
if (result) reply.likeCount = (reply.likeCount ?? 0) + 1;
|
||||
} else {
|
||||
result = await CommonService.cancelLike(reply.id ?? '', 'COMMENT');
|
||||
if (result) reply.likeCount = (reply.likeCount ?? 1) - 1;
|
||||
}
|
||||
if (result) reply.isLike = !isLike;
|
||||
isLikedNof.value = reply.isLike!;
|
||||
return isLikedNof.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
|
||||
import 'comment_views.dart';
|
||||
|
||||
Future showCommentDialog(
|
||||
String objId, {
|
||||
String objType = "video",
|
||||
int? commentCount,
|
||||
}) async {
|
||||
return Get.bottomSheet(
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
height: screen.screenHeight * 0.72,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff141414),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.only(top: 12, bottom: 12),
|
||||
child: const SheetHandleBar(color: Color(0x1AFFFFFF)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 16),
|
||||
child: Text(
|
||||
"${commentCount ?? 0}条评论",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CommentView(objId, objType: objType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
elevation: 0.72,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/group_text_filed.dart';
|
||||
import 'package:image_pickers/image_pickers.dart';
|
||||
|
||||
/// 评论输入弹层:点空白关闭,自动聚焦拉起键盘
|
||||
class CommentInputView extends StatefulWidget {
|
||||
static const String routeName = 'comment_input';
|
||||
final String? hint;
|
||||
final TextEditingController textCtr;
|
||||
final String? imgPath;
|
||||
final Function(String?)? onImgChange;
|
||||
|
||||
const CommentInputView({
|
||||
super.key,
|
||||
this.hint,
|
||||
required this.textCtr,
|
||||
this.imgPath,
|
||||
this.onImgChange,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommentInputView> createState() => _CommentInputViewState();
|
||||
}
|
||||
|
||||
class _CommentInputViewState extends State<CommentInputView> {
|
||||
final focus = FocusNode();
|
||||
|
||||
TextEditingController get textCtr => widget.textCtr;
|
||||
//弹层内自己维护一份,选/删图先本地刷新再回调出去
|
||||
String? _imgPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_imgPath = widget.imgPath;
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
focus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pickImage() async {
|
||||
final listMedia = await ImagePickers.pickerPaths(
|
||||
uiConfig: UIConfig(uiThemeColor: Colors.white),
|
||||
galleryMode: GalleryMode.image,
|
||||
selectCount: 1,
|
||||
showCamera: false,
|
||||
);
|
||||
if (listMedia.isNotEmpty) {
|
||||
_imgPath = listMedia.first.path ?? '';
|
||||
widget.onImgChange?.call(_imgPath);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_imgPath?.isNotEmpty == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.file(
|
||||
File(_imgPath!),
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_imgPath = null;
|
||||
widget.onImgChange?.call(null);
|
||||
setState(() {});
|
||||
},
|
||||
child: Image.asset(
|
||||
"close_grey.png".commonImgPath,
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: GroupTextFiled(
|
||||
focusNode: focus,
|
||||
controller: textCtr,
|
||||
height: 32,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
textStyle:
|
||||
TextStyle(color: Colors.white, fontSize: 14),
|
||||
onSubmitted: (value) {
|
||||
Get.back(result: true);
|
||||
},
|
||||
placeholder: widget.hint ?? '请在此输入评论...',
|
||||
placeholderTextStyle: TextStyle(
|
||||
color: Color(0xff4D4D4D), fontSize: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff1A1A1A),
|
||||
borderRadius: BorderRadius.circular(40)),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 12, top: 4, bottom: 4),
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Image.asset(
|
||||
'icon_pic.webp'.commentPath,
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (textCtr.text.isEmpty) {
|
||||
showToast('请输入想要说的话哦~~');
|
||||
return;
|
||||
}
|
||||
Get.back(result: true);
|
||||
},
|
||||
child: Image.asset(
|
||||
'ic_send.webp'.commentPath,
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/comment/comment_model.dart';
|
||||
import 'package:hgdj/hj_model/comment/reply_model.dart';
|
||||
import 'package:hgdj/hj_page/comment/comment_input_view.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/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../hj_model/banner/comment_top_banner_model.dart';
|
||||
import '../../hj_model/comment/comment_list_res.dart';
|
||||
import '../../tools_base/file_upload/file_upload_tool.dart';
|
||||
import '../../tools_base/file_upload/upload_result_model.dart';
|
||||
|
||||
mixin CommentMixin on GetxController {
|
||||
final comments = <CommentModel>[];
|
||||
final commentController = TextEditingController();
|
||||
Function(int)? totalCallback;
|
||||
CommentModel? replyComment;
|
||||
ReplyModel? replyCommentModel;
|
||||
int page = 1;
|
||||
String? hintText;
|
||||
Rx<String?> selectedImagePath = Rx<String?>(null);
|
||||
RefreshController? refreshCtr;
|
||||
final quickSearchList = <CommentLink>[];
|
||||
final commentTopBanners = <CommentTopBannerModel>[];
|
||||
String? id;
|
||||
String? objType;
|
||||
RxBool isSendingMsg = false.obs;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
commentController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future sendComment() async {
|
||||
String content = commentController.text;
|
||||
if (content.isEmpty) {
|
||||
showToast('请输入评论内容哦~');
|
||||
return;
|
||||
}
|
||||
isSendingMsg.value = true;
|
||||
String? sendImagePath;
|
||||
if (selectedImagePath.value?.isNotEmpty == true) {
|
||||
try {
|
||||
ImageUploadResultModel? imgResult =
|
||||
await FileUploadTool().uploadImage(selectedImagePath.value!);
|
||||
sendImagePath = imgResult?.coverImg ?? "";
|
||||
if (sendImagePath.isEmpty) {
|
||||
isSendingMsg.value = false;
|
||||
showToast("图片上传失败!");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
isSendingMsg.value = false;
|
||||
showToast("图片上传失败!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (replyComment != null) {
|
||||
// 回复评论
|
||||
final res = await CommentService.sendReply(
|
||||
id,
|
||||
2,
|
||||
content,
|
||||
rid: replyCommentModel?.id,
|
||||
cid: replyComment?.id,
|
||||
toUserID: replyCommentModel?.toUserID,
|
||||
objType: objType,
|
||||
image: sendImagePath,
|
||||
);
|
||||
if (res != null) {
|
||||
replyComment?.replies ??= [];
|
||||
replyComment?.replies?.insert(0, res);
|
||||
replyComment?.commCount = replyComment?.replies?.length ?? 0;
|
||||
replyComment = null;
|
||||
replyCommentModel = null;
|
||||
hintText = null;
|
||||
showToast('已提交,审核通过后将展示');
|
||||
}
|
||||
update();
|
||||
} else {
|
||||
// 评论
|
||||
final res = await CommentService.sendComment(
|
||||
id,
|
||||
1,
|
||||
content,
|
||||
objType ?? '',
|
||||
image: sendImagePath,
|
||||
);
|
||||
if (res == null) {
|
||||
hintText = null;
|
||||
} else {
|
||||
showToast('已提交,审核通过后将展示');
|
||||
hintText = null;
|
||||
//插在第一条普通评论前面,置顶那几条不动
|
||||
final insertIndex =
|
||||
comments.indexWhere((e) => !e.isHideLikeOrComment);
|
||||
comments.insert(
|
||||
insertIndex == -1 ? comments.length : insertIndex, res);
|
||||
}
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
selectedImagePath.value = null;
|
||||
commentController.text = "";
|
||||
isSendingMsg.value = false;
|
||||
}
|
||||
|
||||
// isSend: true 点击了发送按钮, 如果没有发送内容弹键盘, 有内容直接发送
|
||||
readyReplyComment(CommentModel? comment,
|
||||
{ReplyModel? replyModel, bool? isSend}) async {
|
||||
replyComment = comment;
|
||||
replyCommentModel = replyModel;
|
||||
if (replyModel != null) {
|
||||
hintText = '回复:${replyModel.userName ?? ''}';
|
||||
}
|
||||
if (comment != null && hintText == null) {
|
||||
hintText = '回复:${comment.userName ?? ''}';
|
||||
}
|
||||
if (isSend == true && commentController.text.isNotEmpty) {
|
||||
update();
|
||||
sendComment();
|
||||
} else {
|
||||
final result = await Get.bottomSheet(
|
||||
CommentInputView(
|
||||
hint: hintText,
|
||||
textCtr: commentController,
|
||||
imgPath: selectedImagePath.value,
|
||||
onImgChange: (value) => selectedImagePath.value = value,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.transparent,
|
||||
enableDrag: false,
|
||||
settings: RouteSettings(name: CommentInputView.routeName),
|
||||
useRootNavigator: true,
|
||||
);
|
||||
if (result == true) {
|
||||
update();
|
||||
sendComment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取评论
|
||||
Future fetchComments(String id, {bool isRefresh = false}) async {
|
||||
try {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
final currentT = DateTimeUtil.format2utc(DateTime.now()) ?? "";
|
||||
final result =
|
||||
await CommentService.getCommentList(id, currentT, page, 20);
|
||||
totalCallback?.call(result?.total ?? 0);
|
||||
refreshCtr?.refreshCompleted();
|
||||
refreshCtr?.loadComplete();
|
||||
if (!(result?.hasNext ?? false)) {
|
||||
refreshCtr?.loadNoData();
|
||||
}
|
||||
if (page == 1) {
|
||||
comments.clear();
|
||||
quickSearchList.clear();
|
||||
// 首屏并行拉置顶 Banner;失败不影响评论列表
|
||||
_fetchCommentTopBanners();
|
||||
}
|
||||
comments.addAll(result?.list ?? []);
|
||||
if (page == 1) {
|
||||
quickSearchList.addAll(result?.quickSearchList ?? []);
|
||||
}
|
||||
page += 1;
|
||||
for (final e in comments) {
|
||||
e.hasMoreReply = (e.commCount ?? 0) > (e.replies?.length ?? 0);
|
||||
}
|
||||
} catch (e) {
|
||||
refreshCtr?.refreshCompleted();
|
||||
refreshCtr?.loadComplete();
|
||||
debugLog(e);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
/// 评论区置顶 Banner(scene=COMMENT_TOP)
|
||||
Future<void> _fetchCommentTopBanners() async {
|
||||
try {
|
||||
final list = await CommonService.fetchBannerList(scene: 'COMMENT_TOP');
|
||||
commentTopBanners
|
||||
..clear()
|
||||
..addAll(list);
|
||||
update();
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 首条置顶评论下标:官方评论或带内外链的置顶样式评论
|
||||
int get topPinnedCommentIndex {
|
||||
for (int i = 0; i < comments.length; i++) {
|
||||
final c = comments[i];
|
||||
if (c.isOfficial || c.isHideLikeOrComment) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/comment/cell/comment_item.dart';
|
||||
import 'package:hgdj/hj_page/comment/comment_mixin.dart';
|
||||
import 'package:hgdj/hj_page/comment/widget/comment_foot_view.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../home/widget/quick_search_view.dart';
|
||||
|
||||
class CommentLogic extends GetxController with CommentMixin {
|
||||
final String objId;
|
||||
|
||||
final String commentType;
|
||||
|
||||
@override
|
||||
final Function(int)? totalCallback;
|
||||
|
||||
CommentLogic(
|
||||
this.objId, {
|
||||
this.commentType = 'video',
|
||||
this.totalCallback,
|
||||
}) : super();
|
||||
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
id = objId;
|
||||
objType = commentType;
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
isLoading = false;
|
||||
fetchComments(objId);
|
||||
}
|
||||
}
|
||||
|
||||
class CommentView extends StatelessWidget {
|
||||
final bool showInput;
|
||||
final String objId;
|
||||
final String objType; // video, cartoon
|
||||
final Function(int)? totalCallback;
|
||||
|
||||
const CommentView(
|
||||
this.objId, {
|
||||
super.key,
|
||||
this.showInput = true,
|
||||
this.objType = 'video',
|
||||
this.totalCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder(
|
||||
init: CommentLogic(objId,
|
||||
commentType: objType, totalCallback: totalCallback),
|
||||
tag: objId,
|
||||
builder: (logic) => LayoutBuilder(builder: (_, cons) {
|
||||
if (cons.maxHeight < 58) return SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (refreshCtr) => logic.refreshCtr = refreshCtr,
|
||||
onLoading: (_) => logic.fetchComments(objId),
|
||||
onRefresh: (_) => logic.fetchComments(objId, isRefresh: true),
|
||||
child: () {
|
||||
if (logic.isLoading) return LoadingCenterWidget();
|
||||
if (logic.comments.isEmpty) return CErrorWidget();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (logic.quickSearchList.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
margin: EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom:
|
||||
BorderSide(color: Color(0x08ffffff)))),
|
||||
child: QuickSearchView(logic.quickSearchList),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: logic.comments.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final topIndex = logic.topPinnedCommentIndex;
|
||||
final showBanner = index == topIndex &&
|
||||
logic.commentTopBanners.isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CommentItem(
|
||||
key: ValueKey(logic.comments[index].id),
|
||||
logic.comments[index],
|
||||
objId: objId,
|
||||
topBanners:
|
||||
showBanner ? logic.commentTopBanners : null,
|
||||
replyHandler: (comment, {reply}) =>
|
||||
logic.readyReplyComment(comment,
|
||||
replyModel: reply),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
if (showInput)
|
||||
Obx(
|
||||
() => CommentFootView(
|
||||
isSending: logic.isSendingMsg.value,
|
||||
onComment: (isSend) =>
|
||||
logic.readyReplyComment(null, isSend: isSend),
|
||||
textCtr: logic.commentController,
|
||||
imgPath: logic.selectedImagePath.value,
|
||||
onImgChange: (value) => logic.selectedImagePath.value = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
global: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/group_text_filed.dart';
|
||||
import 'package:image_pickers/image_pickers.dart';
|
||||
|
||||
import '../../../hj_utils/api_service/mine_service.dart';
|
||||
import '../../mine/collec_history_buy/col_his_buy_sub_page.dart';
|
||||
|
||||
/// 底部评论条:假输入框 + 选图 + 收藏 + 发送
|
||||
class CommentFootView extends StatefulWidget {
|
||||
final VideoModel? video;
|
||||
final Function(bool isSend) onComment; //false 点的输入框(唤起输入弹层),true 点的发送
|
||||
final bool isSending;
|
||||
final TextEditingController textCtr;
|
||||
final String? imgPath;
|
||||
final Function(String?)? onImgChange;
|
||||
final bool showCollect;
|
||||
const CommentFootView({
|
||||
super.key,
|
||||
this.video,
|
||||
required this.onComment,
|
||||
this.isSending = false,
|
||||
required this.textCtr,
|
||||
this.imgPath,
|
||||
this.onImgChange,
|
||||
this.showCollect = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommentFootView> createState() => _CommentFootViewState();
|
||||
}
|
||||
|
||||
class _CommentFootViewState extends State<CommentFootView> {
|
||||
bool _isCollectLoading = false;
|
||||
|
||||
bool get isCollect => widget.video?.vidStatus?.hasCollected ?? false;
|
||||
|
||||
void _pickImage() async {
|
||||
final listMedia = await ImagePickers.pickerPaths(
|
||||
uiConfig: UIConfig(uiThemeColor: Colors.white),
|
||||
galleryMode: GalleryMode.image,
|
||||
selectCount: 1,
|
||||
showCamera: false,
|
||||
);
|
||||
if (listMedia.isNotEmpty)
|
||||
widget.onImgChange?.call(listMedia.first.path ?? '');
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onCollect() async {
|
||||
if (_isCollectLoading) return;
|
||||
_isCollectLoading = true;
|
||||
try {
|
||||
final preCollect = isCollect;
|
||||
|
||||
await MineService.postCollect(
|
||||
widget.video?.id, LoadDataType.post.apiType, !isCollect);
|
||||
|
||||
if (!preCollect) {
|
||||
widget.video?.collectCount = (widget.video?.collectCount ?? 0) + 1;
|
||||
} else {
|
||||
widget.video?.collectCount = (widget.video?.collectCount ?? 1) - 1;
|
||||
}
|
||||
_isCollectLoading = false;
|
||||
widget.video?.vidStatus?.hasCollected = !isCollect;
|
||||
if (!preCollect) {
|
||||
showToast("收藏成功");
|
||||
}
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
_isCollectLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
padding: EdgeInsets.only(
|
||||
left: 18.w, right: 18.w, bottom: screen.paddingBottom),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
height: 58,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.onComment(false),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: GroupTextFiled(
|
||||
controller: widget.textCtr,
|
||||
height: 32,
|
||||
enabled: false,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
textStyle: TextStyle(color: Colors.white, fontSize: 14),
|
||||
placeholder: '请在此输入评论...',
|
||||
placeholderTextStyle:
|
||||
TextStyle(color: Color(0xff525252), fontSize: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 4, 4),
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: widget.imgPath?.isNotEmpty == true
|
||||
? Image.file(
|
||||
File(widget.imgPath!),
|
||||
width: 30,
|
||||
height: 30,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.asset(
|
||||
'icon_pic.webp'.commentPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.imgPath?.isNotEmpty == true)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onImgChange?.call(null);
|
||||
setState(() {});
|
||||
},
|
||||
child: Image.asset(
|
||||
"close_grey.png".commonImgPath,
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.showCollect)
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _onCollect,
|
||||
child: Image.asset(
|
||||
isCollect
|
||||
? "collect_red.png".commonImgPath
|
||||
: "collect_path.png".commonImgPath,
|
||||
width: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.onComment(true),
|
||||
child: widget.isSending
|
||||
? CupertinoActivityIndicator(
|
||||
color: AppColors.actionRed,
|
||||
radius: 12,
|
||||
)
|
||||
: Image.asset('ic_send.webp'.commentPath),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/comment/comment_model.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
|
||||
/// 评论正文:命中搜索词时把该词渲染成可点的橙色链接,其余按纯文本
|
||||
class CommentRichText extends StatelessWidget {
|
||||
final CommentModel? model;
|
||||
|
||||
const CommentRichText({super.key, this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const style = TextStyle(color: Color(0xE5FFFFFF), fontSize: 14);
|
||||
final keyword = model?.searchKeyword ?? "";
|
||||
//按关键词切开,段与段之间插链接;切不出两段就当纯文本
|
||||
final parts =
|
||||
keyword.isEmpty ? <String>[] : (model?.content?.split(keyword) ?? []);
|
||||
if (model?.linkStr?.isNotEmpty != true ||
|
||||
keyword.isEmpty ||
|
||||
parts.length <= 1) {
|
||||
return Text(model?.content ?? '', style: style);
|
||||
}
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
style: style,
|
||||
children: [
|
||||
for (int i = 0; i < parts.length; i++) ...[
|
||||
TextSpan(text: parts[i]),
|
||||
if (i != parts.length - 1)
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: () => pushToPageByLink(model?.linkStr),
|
||||
child: Text(
|
||||
keyword,
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF68804), fontSize: 14, height: 1.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/banner/comment_top_banner_model.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_banner_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/card_swiper/src/swiper.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
/// 评论区置顶评论下方 Banner:支持多图轮播(含 GIF),按 linkType 跳转内外链
|
||||
class CommentTopBanner extends StatefulWidget {
|
||||
final List<CommentTopBannerModel> banners;
|
||||
final double borderRadius;
|
||||
|
||||
/// 宽高比,默认对齐站内 Banner 素材 720×150;勿用固定高度,窄/宽屏 cover 会裁切
|
||||
final double aspectRatio;
|
||||
|
||||
const CommentTopBanner({
|
||||
super.key,
|
||||
required this.banners,
|
||||
this.borderRadius = 12,
|
||||
this.aspectRatio = 720 / 150,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommentTopBanner> createState() => _CommentTopBannerState();
|
||||
}
|
||||
|
||||
class _CommentTopBannerState extends State<CommentTopBanner> {
|
||||
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
|
||||
|
||||
int get _count => widget.banners.length;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectIndex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onTap(CommentTopBannerModel item) async {
|
||||
final type = (item.linkType ?? '').toUpperCase();
|
||||
final value = item.linkValue?.trim() ?? '';
|
||||
if (type == 'NONE' || value.isEmpty) return;
|
||||
|
||||
if (type == 'EXTERNAL') {
|
||||
if (value.startsWith('http')) {
|
||||
await launchUrlToWeb(value);
|
||||
} else {
|
||||
await pushToPageByLink(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == 'INTERNAL') {
|
||||
// 兼容 video://detail?id=xxx
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri != null && uri.scheme == 'video') {
|
||||
final id = uri.queryParameters['id'];
|
||||
if (id?.isNotEmpty == true) {
|
||||
await pushToVideoPage(videoId: id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await pushToPageByLink(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_count == 0) return const SizedBox.shrink();
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: double.infinity,
|
||||
child: AspectRatio(
|
||||
aspectRatio: widget.aspectRatio,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Swiper(
|
||||
autoplay: _count > 1,
|
||||
autoplayDelay: 3000,
|
||||
loop: _count > 1,
|
||||
itemCount: _count,
|
||||
onIndexChanged: (index) => _selectIndex.value = index,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.banners[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _onTap(item),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: item.imageUrl ?? '',
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
borderRadius: 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_count > 1)
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _selectIndex,
|
||||
builder: (_, index, __) => CIndicator(
|
||||
itemCount: _count,
|
||||
selectIndex: index,
|
||||
space: 3,
|
||||
dotSize: 4,
|
||||
selectWidth: 10,
|
||||
isBarStyle: true,
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
selectColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user