Files
huangguo_android/lib/alert/vip_guide/guide_countdown_dialog.dart
2026-09-15 15:44:13 +07:00

268 lines
9.6 KiB
Dart

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<VipProductModel>?
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<bool> show(
{required GuideScene scene,
ValueChanged<VipProductModel>? onConfirm}) async {
final card = await loadGuideCard(scene);
if (card == null) return false; // 配置有误不弹
final confirmed = await Get.dialog<bool>(
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<Duration>(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<Duration>(
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)),
);
}