初始化
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import '../community/community_tab_logic.dart';
|
||||
|
||||
/// 社区首页的排序 tab 条,标题取自 CommunityTabLogic.sortTabs
|
||||
class CommunityMiddleHeader extends StatefulWidget {
|
||||
final Function(int sort) sortOnTap;
|
||||
const CommunityMiddleHeader({super.key, required this.sortOnTap});
|
||||
|
||||
@override
|
||||
State<CommunityMiddleHeader> createState() => _CommunityMiddleHeaderState();
|
||||
}
|
||||
|
||||
class _CommunityMiddleHeaderState extends State<CommunityMiddleHeader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
//别在这里另写一份标题
|
||||
final sorts = CommunityTabLogic.sortTabs.map((e) => e.name).toList();
|
||||
late final TabController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//别用 late 惰性求值:万一没走 build 就 dispose,会在 dispose 里现建一个 Ticker
|
||||
_controller = TabController(length: sorts.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: AppColors.primaryColor,
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.sortOnTap,
|
||||
tabAlignment: TabAlignment.center,
|
||||
tabs: sorts
|
||||
.map((e) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(e),
|
||||
))
|
||||
.toList(),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55),
|
||||
fontSize: 16.sp,
|
||||
height: 1),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600),
|
||||
indicator: const BoxDecoration(),
|
||||
controller: _controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../find/rank_module/rank_badge.dart';
|
||||
import '../../home/widget/collect_status_wrapper.dart';
|
||||
import '../community_tag_page/community_tag_page.dart';
|
||||
import '../post_detail_page/post_detail_page.dart';
|
||||
import 'post_user_info_view.dart';
|
||||
import 'post_word_rich_text.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_alert.dart';
|
||||
|
||||
typedef OnItemCheckCallback = void Function(
|
||||
VideoModel videoModel, bool isChecked);
|
||||
|
||||
/// 社区帖子卡片,社区/搜索/个人主页/我的发布/收藏历史多处复用
|
||||
class CommunityPostWidget extends StatelessWidget {
|
||||
final VideoModel videoModel;
|
||||
|
||||
/// 详情页要拿整个列表做左右滑,由列表页传进来
|
||||
final List<VideoModel>? videoModels;
|
||||
final GestureTapCallback? clickCallback;
|
||||
|
||||
/// 我的发布模式:顶部换成「发布/上传于…」+ 审核状态
|
||||
final bool isMyPublish;
|
||||
final bool showTags; //是否展示标签
|
||||
final bool showLine; //展示分割线
|
||||
final bool showCheck; //展示勾选框
|
||||
final bool isChecked; //是否选中
|
||||
final OnItemCheckCallback? onItemCheck;
|
||||
final int? rankIndex; //排行
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const CommunityPostWidget({
|
||||
super.key,
|
||||
required this.videoModel,
|
||||
this.clickCallback,
|
||||
this.isMyPublish = false,
|
||||
this.videoModels,
|
||||
this.showTags = true,
|
||||
this.padding = const EdgeInsets.fromLTRB(16, 10, 16, 10),
|
||||
this.showLine = true,
|
||||
this.showCheck = false,
|
||||
this.onItemCheck,
|
||||
this.isChecked = false,
|
||||
this.rankIndex,
|
||||
});
|
||||
|
||||
//status: 0待审核 1已成功 2未通过
|
||||
String get _statusPrefix => videoModel.status == 1 ? '发布' : '上传';
|
||||
|
||||
String get _statusDesc => switch (videoModel.status) {
|
||||
0 => '待审核',
|
||||
1 => '已成功',
|
||||
2 => '未通过原因',
|
||||
_ => '未知状态', //含 5
|
||||
};
|
||||
|
||||
Color get _statusColor => videoModel.status == 2
|
||||
? const Color(0xffFD0563)
|
||||
: const Color(0xff3476FF);
|
||||
|
||||
bool get _showStatus => videoModel.status != 1 && !showCheck;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//视频帖的封面也在图集里,去重后图集只留纯图
|
||||
if (videoModel.sourceURL?.isNotEmpty == true) {
|
||||
videoModel.seriesCover?.remove(videoModel.cover ?? "");
|
||||
}
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: clickCallback ??
|
||||
() {
|
||||
//在点击时算好:PostDetailPageArgument 内部要过滤整个列表,别丢进 page builder 里重复跑
|
||||
final argument =
|
||||
PostDetailPageArgument(id: videoModel.id, models: videoModels);
|
||||
Get.to(() => PostDetailPage(argument: argument));
|
||||
},
|
||||
child: Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//顶部:我的发布出「发布于…」+ 审核态,其余出用户信息行
|
||||
if (!isMyPublish)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: PostUserInfoView(model: videoModel)),
|
||||
if (rankIndex != null) rankBadge(rankIndex!),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'$_statusPrefix于${formatTimeTwo(videoModel.realCreateAt)}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: Color(0x8CFFFFFF), fontSize: 14),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showCheck)
|
||||
GestureDetector(
|
||||
onTap: () => onItemCheck?.call(videoModel, !isChecked),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
//白底垫在勾选图标下面,选中时圆圈内部才不透出背景
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isChecked ? Colors.white : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isChecked
|
||||
? CupertinoIcons.checkmark_circle_fill
|
||||
: CupertinoIcons.circle,
|
||||
color: isChecked
|
||||
? const Color(0xffF68804)
|
||||
: const Color(0xff999999),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_showStatus)
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () async {
|
||||
if (videoModel.status == 2) {
|
||||
await CommonAlert.show(
|
||||
title: "未过审原因",
|
||||
content: '${videoModel.reason}',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(_statusDesc,
|
||||
style:
|
||||
TextStyle(color: _statusColor, fontSize: 14.sp)),
|
||||
),
|
||||
],
|
||||
),
|
||||
14.sizeBoxH,
|
||||
],
|
||||
//标题(含置顶、精华标签)
|
||||
if (videoModel.title?.trim().isNotEmpty == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PostWordRichText(videoModel: videoModel),
|
||||
),
|
||||
//封面:视频单图 / 单图 / 三宫格
|
||||
_coverView(),
|
||||
12.sizeBoxH,
|
||||
//底部:浏览、评论、点赞 + 右侧话题
|
||||
Row(
|
||||
children: [
|
||||
_countItem(videoModel.pageViewCount?.countStr,
|
||||
"eye_grey.webp".commonImgPath),
|
||||
12.sizeBoxW,
|
||||
_countItem(videoModel.commentCount?.countStr,
|
||||
"count_msg_gray.png".communityPath),
|
||||
12.sizeBoxW,
|
||||
CollectStatusWrapper(
|
||||
videoModel: videoModel,
|
||||
builder: () => _countItem(
|
||||
videoModel.likeCount?.countStr,
|
||||
videoModel.vidStatus?.hasLiked == true
|
||||
? "like_red.png".commonImgPath
|
||||
: "like_empty_gray.png".communityPath,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showTags) _tagView(),
|
||||
],
|
||||
),
|
||||
if (showLine)
|
||||
Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.only(top: 18),
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 封面区:无图集的视频帖出单封面 + 播放按钮,单图直接铺,多图走三宫格
|
||||
Widget _coverView() {
|
||||
final covers = videoModel.seriesCover ?? [];
|
||||
final isVideo = videoModel.sourceURL?.isNotEmpty == true;
|
||||
|
||||
if (covers.isEmpty && isVideo) {
|
||||
return SizedBox(
|
||||
height: 204,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: videoModel.cover ?? '', borderRadius: 6),
|
||||
Center(
|
||||
child: Image.asset("play_icon.png".communityPath,
|
||||
width: 32, height: 32)),
|
||||
Positioned(right: 0, top: 0, child: _coinBadge()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (covers.length == 1) {
|
||||
return SizedBox(
|
||||
height: 204,
|
||||
width: double.infinity,
|
||||
child: NetworkImageLoader(imageUrl: covers[0], borderRadius: 6),
|
||||
);
|
||||
}
|
||||
|
||||
//视频帖的封面被上面 remove 掉了,格子数要把它加回来
|
||||
final coverCount = covers.length + (isVideo ? 1 : 0);
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: min(3, coverCount),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 5,
|
||||
crossAxisSpacing: 5,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isLast = index == 2 || coverCount == index + 1;
|
||||
//视频帖把视频封面排在最后一格,其余格按图集顺序取
|
||||
final imgPath = (isLast && isVideo) || index >= covers.length
|
||||
? (videoModel.cover ?? "")
|
||||
: covers[index];
|
||||
//只有首尾两格带外圆角,中间格是直角
|
||||
final radius = isLast
|
||||
? const BorderRadius.only(
|
||||
topRight: Radius.circular(6), bottomRight: Radius.circular(6))
|
||||
: index == 0
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(6), bottomLeft: Radius.circular(6))
|
||||
: BorderRadius.zero;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(imageUrl: imgPath, imgBorderRadius: radius),
|
||||
if (isLast) Positioned(right: 0, top: 0, child: _coinBadge()),
|
||||
if (isLast && isVideo)
|
||||
Center(
|
||||
child: Image.asset("play_icon.png".communityPath, width: 30))
|
||||
else if (isLast && coverCount > 3)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
right: 6,
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Text(
|
||||
"+${coverCount - 3}",
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xff141414),
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//金币帖右上角的角标
|
||||
Widget _coinBadge() {
|
||||
if (videoModel.permission != 1) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(6), bottomLeft: Radius.circular(6)),
|
||||
color: Color(0xffDB361F),
|
||||
),
|
||||
child: Image.asset("coin.webp".commonImgPath, width: 14, height: 14),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _countItem(String? title, String icon) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(icon, width: 20, height: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(title ?? "",
|
||||
style: const TextStyle(color: Color(0xff999999), fontSize: 12)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 帖子标签
|
||||
Widget _tagView() {
|
||||
if (videoModel.tags?.isNotEmpty != true) return const SizedBox();
|
||||
final tagsBean = videoModel.tags!.first;
|
||||
tagsBean.newsType = videoModel.newsType;
|
||||
return GestureDetector(
|
||||
onTap: () => Get.to(() => CommunityTagDetailPage(model: tagsBean)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: const Color(0x0DFFFFFF)),
|
||||
child: Text(
|
||||
'#${tagsBean.name ?? ''}',
|
||||
style: const TextStyle(color: Color(0x8CFFFFFF), fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
|
||||
class CommunitySortHeader extends StatefulWidget {
|
||||
final Function(int sort) sortOnTap;
|
||||
final TabController? controller;
|
||||
const CommunitySortHeader({
|
||||
super.key,
|
||||
required this.sortOnTap,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommunitySortHeader> createState() => _CommunitySortHeaderState();
|
||||
}
|
||||
|
||||
class _CommunitySortHeaderState extends State<CommunitySortHeader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final sorts = ['最多收藏', '最新上架', '最新热评'];
|
||||
late TabController _controller;
|
||||
|
||||
/// 外部传进来的归外部释放,只有自己 new 的才由自己 dispose
|
||||
late final bool _isMine;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//在 initState 定死归属,别用 late 惰性求值——那时读到的可能是 rebuild 后的新 widget
|
||||
_isMine = widget.controller == null;
|
||||
_controller =
|
||||
widget.controller ?? TabController(length: sorts.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_isMine) _controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.sortOnTap,
|
||||
tabAlignment: TabAlignment.center,
|
||||
tabs: sorts
|
||||
.map((e) => Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
||||
child: Text(e),
|
||||
))
|
||||
.toList(),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .6),
|
||||
fontSize: 16.sp,
|
||||
height: 1),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600),
|
||||
// 无指示器:原 ContainerTabIndicator 为全透明,选中态仅靠文字加粗变白
|
||||
indicator: const BoxDecoration(),
|
||||
controller: _controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
|
||||
/// 关注 / 收藏按钮,加载中转菊花
|
||||
class FollowLoadingButton extends StatelessWidget {
|
||||
final bool? isFollow;
|
||||
final bool? isLoading;
|
||||
final bool? isCollectStyle;
|
||||
final double radius;
|
||||
const FollowLoadingButton({
|
||||
super.key,
|
||||
this.isFollow,
|
||||
this.isLoading,
|
||||
this.isCollectStyle,
|
||||
this.radius = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final followed = isFollow == true;
|
||||
//已关注是灰底灰字 10 号,未关注是红底白字 12 号
|
||||
final fg = followed ? const Color(0xff989898) : Colors.white;
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
color: followed ? const Color(0x0DFFFFFF) : AppColors.actionRed,
|
||||
),
|
||||
child: isLoading == true
|
||||
? const CupertinoActivityIndicator(color: Colors.white, radius: 8)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(followed ? Icons.check : Icons.add, size: 14, color: fg),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isCollectStyle == true ? "收藏" : (followed ? "已关注" : "关注"),
|
||||
style: TextStyle(color: fg, fontSize: followed ? 10 : 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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/net_image_widget.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../find/pic_collect/pics_detail_page.dart';
|
||||
|
||||
/// 图集卡片:封面 + 底部渐变条(浏览数 / 图片数) + 标题
|
||||
class PicSimpleCell extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
final int textLines;
|
||||
const PicSimpleCell({super.key, this.videoModel, this.textLines = 2});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
Get.to(PicsDetailPage(id: videoModel?.id), preventDuplicates: false),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: videoModel?.cover ?? '',
|
||||
borderRadius: 8,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(6, 8, 6, 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: 0.6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset("eye_white.webp".commonImgPath, width: 16),
|
||||
1.sizeBoxW,
|
||||
Text(
|
||||
"${videoModel?.playCount?.countStr}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white, fontSize: 10),
|
||||
),
|
||||
Spacer(),
|
||||
Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset("icon_pic.png".communityPath,
|
||||
width: 16),
|
||||
2.sizeBoxW,
|
||||
Text(
|
||||
videoModel?.seriesCover?.length.toString() ??
|
||||
"",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
"${videoModel?.title ?? ""}\n",
|
||||
maxLines: textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/community/widget/follow_loading_button.dart';
|
||||
import 'package:hgdj/hj_page/home/widget/collect_status_wrapper.dart';
|
||||
import 'package:hgdj/hj_page/home/widget/user_name_view.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
//帖子发布者信息:头像 + 昵称 + 发布时间,详情模式额外带关注按钮
|
||||
class PostUserInfoView extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
final bool isDetailStyle; // 帖子详情模式
|
||||
final double? avatarSize; // 头像边长,默认 40
|
||||
|
||||
const PostUserInfoView({
|
||||
super.key,
|
||||
required this.model,
|
||||
this.avatarSize,
|
||||
this.isDetailStyle = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostUserInfoView> createState() => _PostUserInfoViewState();
|
||||
}
|
||||
|
||||
class _PostUserInfoViewState extends State<PostUserInfoView> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
Publisher? get publisher => model.publisher;
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//无发布者、黄游帖不展示
|
||||
if (publisher == null || model.newsType == 'ADULT_GAME') {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final size = widget.avatarSize ?? 40;
|
||||
return Row(
|
||||
children: [
|
||||
//头像
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToPersonCenter(publisher?.uid),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: publisher?.portrait ?? "",
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
//昵称 + 发布时间
|
||||
Expanded(child: widget.isDetailStyle ? _nameDetail() : _nameNormal()),
|
||||
//关注按钮:仅详情模式,套 CollectStatusWrapper 同步全局关注态
|
||||
if (widget.isDetailStyle)
|
||||
CollectStatusWrapper(
|
||||
videoModel: model,
|
||||
builder: () => InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _toggleFollow,
|
||||
child: FollowLoadingButton(
|
||||
isFollow: publisher?.hasFollowed ?? false,
|
||||
isLoading: _isLoading),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//关注/取关:先锁定目标值,避免 await 期间 eventBus 改写 hasFollowed 导致取反基准漂移
|
||||
void _toggleFollow() async {
|
||||
if (_isLoading) return;
|
||||
final willFollow = !(publisher?.hasFollowed ?? false);
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final res = await MineService.getFollow(publisher?.uid, willFollow);
|
||||
if (res) publisher?.hasFollowed = willFollow;
|
||||
|
||||
_isLoading = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
//列表样式
|
||||
Widget _nameNormal() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UserNameView(
|
||||
nameColor: Colors.white,
|
||||
name: publisher?.name,
|
||||
isOfficial: publisher?.officialCert,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
formatTimeTwo(model.reviewAt),
|
||||
style: const TextStyle(color: Color(0xff999999), fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//详情样式:昵称后跟官方认证标
|
||||
Widget _nameDetail() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserNameView(
|
||||
nameColor: Colors.white,
|
||||
name: publisher?.name,
|
||||
isOfficial: publisher?.officialCert,
|
||||
),
|
||||
),
|
||||
if (publisher?.officialCert == true) ...[
|
||||
4.sizeBoxW,
|
||||
Image.asset("official_icon.png".commonImgPath,
|
||||
width: 22, height: 22),
|
||||
],
|
||||
],
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
formatTimeTwo(model.reviewAt),
|
||||
style: const TextStyle(color: Color(0x8C999999), fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
|
||||
/// 帖子标题:置顶 / 精华标签跟标题混排,用 WidgetSpan 才能跟着文字换行
|
||||
class PostWordRichText extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
|
||||
const PostWordRichText({super.key, this.videoModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (videoModel?.isTopping ?? false) _badge('置顶'),
|
||||
if (videoModel?.chosen ?? false) _badge('精华'),
|
||||
TextSpan(
|
||||
text: videoModel?.title ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
WidgetSpan _badge(String text) => WidgetSpan(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 18,
|
||||
alignment: Alignment.topCenter,
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF52C56),
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
child:
|
||||
Text(text, style: textStyle(12, Colors.white, FontWeight.w500)),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user