import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hgdj/hj_utils/screen.dart'; import 'package:hgdj/tools_base/widget/net_image_widget.dart'; import '../../assets_tool/images.dart'; import '../../hj_page/mine/mine_vip/pay_order_source.dart'; import '../../hj_page/mine/mine_vip/vip_support_model.dart'; import '../../hj_page/mine/widgets/gradient_text.dart'; import 'guide_common.dart'; import 'guide_config.dart'; import 'guide_manager.dart'; /// 优惠倒计时:时长前端写死,起点由 app 启动时 reset 一次,回前台不重置 class PromoCountdown { static const Duration _duration = Duration(hours: 2); static DateTime? _deadline; static DateTime get _end => _deadline ??= DateTime.now().add(_duration); /// 剩余时长,归零停在 0 static Duration get remain { final d = _end.difference(DateTime.now()); return d.isNegative ? Duration.zero : d; } /// app 启动时调一次(见 main),进程存活期间自然倒数 static void reset() => _deadline = DateTime.now().add(_duration); } /// 优惠倒计时弹窗(居中):主图骑顶 + 优惠文案 + 倒计时 + 开通按钮。 /// 长视频返回挽留 / 进会员页时触发 class GuideCountdownDialog extends StatelessWidget { final VipProductModel card; final ValueChanged? onConfirm; // 默认跳会员页选卡;已在会员页的场景自己处理(关弹窗+就地选中该卡) final PaySourcePage sourcePage; final String cover; // 场景下发的主图,相对路径由 ImageCacheManager 拼域名 const GuideCountdownDialog({ super.key, required this.card, required this.sourcePage, required this.cover, this.onConfirm, }); /// [scene] 决定主图/卡/下单来源,调用方须先用开关判过能弹 /// 返回 true = 点了「立即开通」(点X/点蒙层/没弹成都是 false),返回挽留场景据此决定要不要放行返回 static Future show( {required GuideScene scene, ValueChanged? onConfirm}) async { final card = await loadGuideCard(scene); if (card == null) return false; // 配置有误不弹 final confirmed = await Get.dialog( GuideCountdownDialog( card: card, sourcePage: guideSourcePage(scene), cover: GuideManager().configOf(scene)?.cover ?? '', //已校验非空 onConfirm: onConfirm, ), ); return confirmed ?? false; } @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 311, child: Stack( clipBehavior: Clip.none, alignment: Alignment.topCenter, children: [ // 主体(顶部留 61 给主图骑顶溢出) Container( margin: const EdgeInsets.only(top: 61), padding: const EdgeInsets.fromLTRB(24, 80, 24, 24), decoration: BoxDecoration( color: const Color(0xff0F0F0F), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 现在下单获得[优惠]福利:「优惠」金渐变、更大号 Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ const Text('现在下单获得', style: TextStyle( color: Colors.white, fontSize: 19, fontWeight: FontWeight.w800)), 2.sizeBoxW, const GradientText( '优惠', gradient: LinearGradient(colors: [ Color(0xffFFE3A0), Color(0xffF0A93C) ]), style: TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.w800), ), 2.sizeBoxW, const Text('福利', style: TextStyle( color: Colors.white, fontSize: 19, fontWeight: FontWeight.w800)), ], ), 15.sizeBoxH, const _CountdownRow(), 15.sizeBoxH, GestureDetector( onTap: () { if (onConfirm != null) { onConfirm!(card); } else { goVipDetail(card, sourcePage: sourcePage); } }, child: Container( width: 269, height: 52, alignment: Alignment.center, decoration: BoxDecoration( gradient: const LinearGradient(colors: [ Color(0xffFFB03F), Color(0xffF2680C) ]), borderRadius: BorderRadius.circular(26), boxShadow: [ BoxShadow( color: const Color(0x73F2680C), blurRadius: 18, offset: const Offset(0, 6)) ], // rgba(242,104,12,0.45) ), child: Text( '立即开通 · 仅需¥${card.discountedPriceUI}', style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.w700), ), ), ), ], ), ), // 主图骑在顶部 Positioned( top: 0, child: SizedBox( width: 270, height: 122, child: NetworkImageLoader( imageUrl: cover, fit: BoxFit.fill, borderRadius: 8), ), ), ], ), ), 20.sizeBoxH, GestureDetector( onTap: () => Get.back(), child: Image.asset("close_button.png".commonImgPath, width: 32), ) ], ), ), ); } } /// 距优惠结束 HH:MM:SS:每秒只重建这一行,归零后停表 class _CountdownRow extends StatefulWidget { const _CountdownRow(); @override State<_CountdownRow> createState() => _CountdownRowState(); } class _CountdownRowState extends State<_CountdownRow> { final _remain = ValueNotifier(PromoCountdown.remain); Timer? _timer; @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (t) { final r = PromoCountdown.remain; if (r == Duration.zero) t.cancel(); //归零停表(含弹窗打开时就已过期) if (r.inSeconds == _remain.value.inSeconds) return; //秒数没变不通知 _remain.value = r; }); } @override void dispose() { _timer?.cancel(); _remain.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ const Text('距优惠结束', style: TextStyle( color: Color(0xffB8AE9C), fontSize: 12, fontWeight: FontWeight.w500)), 8.sizeBoxW, ValueListenableBuilder( valueListenable: _remain, builder: (_, r, __) => Row( mainAxisSize: MainAxisSize.min, children: [ _timeCell(r.inHours), _colon, _timeCell(r.inMinutes % 60), _colon, _timeCell(r.inSeconds % 60), ], ), ), ], ); } // 时/分/秒块 Widget _timeCell(int v) => Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4), decoration: BoxDecoration( color: const Color(0xffE23B2E), borderRadius: BorderRadius.circular(6)), child: Text( v.toString().padLeft(2, '0'), style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w700), ), ); Widget get _colon => const Padding( padding: EdgeInsets.symmetric(horizontal: 3), child: Text(':', style: TextStyle( color: Color(0xffE85B4F), fontSize: 14, fontWeight: FontWeight.w700)), ); }