初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,188 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/alert/video/share_media_dialog.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/comment/comment_alert.dart';
import 'package:hgdj/hj_utils/api_service/common_service.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:like_button/like_button.dart';
///图集底部操作按钮组
class AlbumActionWidget extends StatefulWidget {
final VideoModel? videoModel;
final int currentIndex; //如果传-1,则不显示查看 '1/3'之类的数据
const AlbumActionWidget({super.key, this.videoModel, this.currentIndex = 0});
@override
State<AlbumActionWidget> createState() => _AlbumActionWidgetState();
}
class _AlbumActionWidgetState extends State<AlbumActionWidget> {
VideoModel? get model => widget.videoModel;
bool get showIndex => widget.currentIndex != -1;
@override
Widget build(BuildContext context) {
return Container(
height: 70,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: showIndex ? 9 : 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_likeBtn(),
_actionItem(
'count_msg.png'.communityPath,
model?.commentCount.countOr('评论') ?? '评论',
onTap: () => showCommentDialog(model?.id ?? ''),
),
_collectBtn(),
_actionItem(
'share_grey.png'.commonImgPath,
'分享',
onTap: () => Get.dialog(ShareMediaDialog(videoModel: model),
useSafeArea: false),
),
//页码 1/3
if (showIndex)
Container(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 2),
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('${widget.currentIndex + 1}',
style: const TextStyle(
color: AppColors.actionRed, fontSize: 12)),
Text('/${model?.seriesCover?.length ?? 0}',
style: const TextStyle(
color: Color(0xff3D3D3D), fontSize: 12)),
],
),
),
],
),
);
}
//点赞
Widget _likeBtn() {
return LikeButton(
size: 20,
isLiked: model?.vidStatus?.hasLiked ?? false,
circleColor:
CircleColor(start: AppColors.actionRed, end: AppColors.actionRed),
bubblesColor: BubblesColor(
dotPrimaryColor: AppColors.actionRed,
dotSecondaryColor: AppColors.actionRed,
),
likeBuilder: (isLiked) => Image.asset(
isLiked
? 'like_red.png'.commonImgPath
: 'video_like_border.png'.videoPath,
width: 20,
height: 20,
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
),
likeCount: model?.likeCount ?? 0,
countBuilder: (count, isLiked, text) => Text(
count.countOr('点赞'),
style: TextStyle(
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
fontSize: 12),
),
onTap: (isLiked) async {
final hasLiked = model?.vidStatus?.hasLiked ?? false;
final count = model?.likeCount ?? 0;
bool isOk = false;
try {
isOk = hasLiked
? await CommonService.cancelLike(model?.id, 'PIC')
: await CommonService.sendLike(model?.id, 'PIC');
} catch (e) {
debugLog('album like $e');
}
//失败不动本地状态,返回原值让按钮回弹
if (!isOk) return hasLiked;
model?.likeCount = max(0, count + (hasLiked ? -1 : 1));
model?.vidStatus?.hasLiked = !hasLiked;
showToast(hasLiked ? '取消点赞' : '点赞成功');
if (mounted) setState(() {});
return !hasLiked;
},
);
}
//收藏
Widget _collectBtn() {
return LikeButton(
size: 20,
isLiked: model?.vidStatus?.hasCollected ?? false,
circleColor:
CircleColor(start: AppColors.actionRed, end: AppColors.actionRed),
bubblesColor: BubblesColor(
dotPrimaryColor: AppColors.actionRed,
dotSecondaryColor: AppColors.actionRed,
),
likeBuilder: (isLiked) => Image.asset(
isLiked
? 'collect_red.png'.commonImgPath
: 'collect_empty.png'.commonImgPath,
width: 20,
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
),
likeCount: model?.collectCount ?? 0,
countBuilder: (count, isLiked, text) => Text(
count.countOr('收藏'),
style: TextStyle(
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
fontSize: 12),
),
onTap: (isLiked) async {
final hasCollected = model?.vidStatus?.hasCollected ?? false;
final count = model?.collectCount ?? 0;
final isOk =
await MineService.postCollect(model?.id, 'PIC', !hasCollected);
//失败不动本地状态,返回原值让按钮回弹
if (!isOk) return hasCollected;
model?.collectCount = max(0, count + (hasCollected ? -1 : 1));
model?.vidStatus?.hasCollected = !hasCollected;
showToast(hasCollected ? '已取消收藏' : '收藏成功');
if (mounted) setState(() {});
return !hasCollected;
},
);
}
//图标 + 文字按钮(评论、分享共用)
Widget _actionItem(String imagePath, String title,
{GestureTapCallback? onTap}) {
return InkWell(
enableFeedback: false,
onTap: onTap,
//Container 撑满 70 高,是点击热区,别删
child: Container(
alignment: Alignment.center,
child: Row(
children: [
Image.asset(imagePath, width: 20, color: const Color(0xff989898)),
3.sizeBoxW,
Text(title,
style: const TextStyle(color: Color(0xff989898), fontSize: 12)),
],
),
),
);
}
}
@@ -0,0 +1,70 @@
import 'dart:math';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/history_util.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/item_positions_listener.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/scrollable_positioned_list.dart';
import '../../../alert/video/video_buy_bottom_sheet.dart';
import '../../../alert/video/vip_acg_dialog.dart';
class PicsDetailLogic extends GetxController {
// ===== 外部传入 =====
final String? id;
PicsDetailLogic(this.id);
// ===== 数据 =====
VideoModel? videoModel;
// ===== Controller =====
// 两者都没有可调用的释放入口:ItemScrollController 只做 attach/detach
// ItemPositionsListener 的实现 ItemPositionsNotifier 内部虽持有 ValueNotifier,但包没暴露 dispose。所以本类无 onClose
final itemScrollCtr = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
// ===== 派生 getter =====
List<String> get covers => videoModel?.seriesCover ?? [];
//lockType 0-已解锁 1-金币解锁 2-会员解锁
bool get isUnlocked => videoModel?.lockType == 0;
bool get isCoinLock => videoModel?.lockType == 1;
//未解锁时放出几张:freeTime 为 0(后端没配)兜底 8 张,否则按 freeTime,都不超过总张数
//freeTime 为 null 走后半段算 0 张,与原逻辑一致
int get lockCount => videoModel?.freeTime == 0
? min(8, covers.length)
: min(videoModel?.freeTime ?? 0, covers.length);
@override
void onReady() {
super.onReady();
loadData();
}
Future loadData() async {
videoModel = await VidService.getDetail(id ?? '');
if (videoModel != null) {
HistoryUtil.insert(videoModel!, MediaStyle.Pic);
}
videoModel ??= VideoModel(); //拉失败也给个空壳,让页面落到错误态而不是一直转圈
update();
}
//解锁:金币走购买弹窗,会员走开通弹窗
void showBuy() async {
if (isCoinLock) {
final res =
await Get.bottomSheet(VideoBuyBottomSheet(video: videoModel!));
if (res == true) {
videoModel?.vidStatus?.hasPaid = true;
update();
}
} else if (videoModel?.lockType == 2) {
Get.dialog(VipACGDialog());
}
}
}
@@ -0,0 +1,200 @@
import 'dart:ui';
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/hj_utils/widget_util.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/item_positions_listener.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/scrollable_positioned_list.dart';
import '../../mine/mine_share/mine_share_page.dart';
import 'album_action_widget.dart';
import 'pics_detail_logic.dart';
//图集详情页
class PicsDetailPage extends StatefulWidget {
final String? id;
const PicsDetailPage({super.key, this.id});
@override
State<PicsDetailPage> createState() => _PicsDetailPageState();
}
// 三个入口都是 Get.to(..., preventDuplicates: false),允许叠栈,用 uniqueTag 隔离各实例的 logic
class _PicsDetailPageState extends State<PicsDetailPage> with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GetBuilder<PicsDetailLogic>(
init: PicsDetailLogic(widget.id),
tag: uniqueTag,
builder: (logic) {
if (logic.videoModel == null) return LoadingCenterWidget();
//拉失败后 videoModel 是空壳,靠 id 判断
if (logic.videoModel?.id == null)
return CErrorWidget(retryOnTap: () => logic.loadData());
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//标题栏
Row(children: [
InkWell(
enableFeedback: false,
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new,
color: Colors.white, size: 24),
),
4.sizeBoxW,
Expanded(
child: Text(
logic.videoModel?.title ?? '',
style: textStyle(16, Colors.white.withValues(alpha: .9),
FontWeight.w800),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
]).paddingSymmetric(horizontal: 16, vertical: 12),
Expanded(child: _picList(logic)),
//底部操作条(页码/评论/点赞/分享),未解锁传 -1 让它藏起页码
ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: logic.itemPositionsListener.itemPositions,
builder: (_, positions, __) => AlbumActionWidget(
videoModel: logic.videoModel,
currentIndex: logic.isUnlocked
? (positions.isEmpty ? 1 : positions.first.index)
: -1,
),
),
],
);
},
),
),
);
}
//图片列表:已解锁全量、点开看大图;未解锁只放 lockCount 张,最后一张压解锁蒙层
Widget _picList(PicsDetailLogic logic) {
final covers = logic.covers;
final isUnlocked = logic.isUnlocked;
final count = isUnlocked ? covers.length : logic.lockCount;
return ScrollablePositionedList.builder(
itemPositionsListener: logic.itemPositionsListener,
itemScrollController: logic.itemScrollCtr,
itemCount: count,
itemBuilder: (_, index) {
final image =
NetworkImageLoader(imageUrl: covers[index], borderRadius: 0);
if (isUnlocked) {
return GestureDetector(
onTap: () => ImageBrowserPage.open(covers, index: index),
child: image);
}
return Stack(
children: [
InkWell(
enableFeedback: false,
onTap: () => showToast('解锁后可查看图片详情'),
child: image),
if (index == count - 1) Positioned.fill(child: _unlockMask(logic)),
],
);
},
);
}
//解锁蒙层:高度由那张图决定,图太矮时整体等比缩小,别撑破
Widget _unlockMask(PicsDetailLogic logic) {
final coins = logic.videoModel?.originCoins ?? 0;
final List<Widget> content = logic.isCoinLock
? [
//金币解锁
EasyRichText(
'你只需花费 $coins 金币',
defaultStyle: textStyle(16, Colors.white, FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '$coins',
style: textStyle(16, AppColors.actionRed, FontWeight.w400)),
],
),
16.sizeBoxH,
Text('即可解锁继续查看',
style: textStyle(12, Colors.white, FontWeight.w400)),
16.sizeBoxH,
_maskBtn('立即解锁', 84, [Color(0xffFF6E6E), Color(0xffFF4D4D)],
Colors.white, logic.showBuy),
]
: [
//会员解锁
Text('试看结束', style: textStyle(16, Colors.white, FontWeight.w400)),
16.sizeBoxH,
Text('本内容需开通VIP免费观看',
style: textStyle(16, Colors.white, FontWeight.w400)),
16.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_maskBtn(
'邀请得3日VIP',
110,
[Color(0xffFFE8BE), Color(0xffE6B764)],
Color(0xff694923),
() => Get.to(() => MineSharePage())),
18.sizeBoxW,
_maskBtn(
'开通VIP免费看',
115,
[Color(0xffFF6E6E), Color(0xffFF4D4D)],
Colors.white,
logic.showBuy),
],
),
];
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Container(
color: Colors.black.withValues(alpha: .6),
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizedBox(
width: screen.screenWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: content),
),
),
),
),
);
}
//蒙层里的胶囊按钮,三处只差宽度/渐变/文字色/回调
Widget _maskBtn(String text, double width, List<Color> colors,
Color textColor, VoidCallback onTap) {
return InkWell(
enableFeedback: false,
onTap: onTap,
child: Container(
width: width,
height: 30,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(colors: colors),
borderRadius: BorderRadius.circular(15)),
child: Text(text, style: textStyle(12, textColor, FontWeight.w400)),
),
);
}
}