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

166 lines
6.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_model/splash/domain_source_model.dart';
import 'package:hgdj/hj_page/home/activity_float/home_activity_float.dart';
import 'package:hgdj/hj_page/main_page/provider/msg_provider.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/fly_to_overlay.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
/// 支付分层首页弹窗:展示后端 homePage 图,点击跳会员页并选中 config.vipCard。
/// 倒计时读 config.lastDiscountTime,秒级刷新由 MineMsgProvider().tick 驱动,过期自动停
class GuideHomeDialog extends StatefulWidget {
final PayTierConfig config;
const GuideHomeDialog({super.key, required this.config});
@override
State<GuideHomeDialog> createState() => _GuideHomeDialogState();
}
class _GuideHomeDialogState extends State<GuideHomeDialog> {
final _posterKey = GlobalKey(); // 关闭动画的起飞位置
/// 关闭中:① 防连点,pop 过渡期弹窗还在树上,再点会把首页也 pop 掉
/// ② 藏掉原海报,否则它随弹窗淡出、和飞行副本重影
bool _closing = false;
PayTierConfig get config => widget.config;
/// 关闭:海报缩小飞进右下角浮窗,告诉用户同款入口收在哪
void _close() {
if (_closing) return;
_closing = true;
final target = _floatRect();
if (target != null) {
FlyToOverlay.play(
context: context,
sourceKey: _posterKey,
target: target,
// 副本只放海报,倒计时胶囊不跟着飞(缩到 60 宽也看不清)
// 占位给透明:没命中内存缓存时别飞一个灰块过去
child: NetworkImageLoader(
imageUrl: config.homePage ?? "",
fit: BoxFit.fill,
borderRadius: 0,
placeHolderWidget: const SizedBox(),
),
);
}
setState(() {}); // 原海报立刻消失,只留副本在飞
Get.back();
}
/// 落点:容器右下角一块浮窗大小的区域(浮窗都 bottomRight 对齐)。
/// 认容器不认轮播子项——子项会轮到屏幕外,容器位置才稳定。
/// 浮窗没展示时容器不存在,返回 null → 直接关不飞
Rect? _floatRect() {
final box = homeFloatKey.currentContext?.findRenderObject() as RenderBox?;
if (box == null || !box.hasSize) return null;
final origin = box.localToGlobal(Offset.zero);
const w = 60.0, h = 66.0; // 与 _LayeredFloatView 的尺寸一致
return Rect.fromLTWH(
origin.dx + box.size.width - w, origin.dy + box.size.height - h, w, h);
}
@override
Widget build(BuildContext context) {
// 点蒙层、返回键都走 _close,保证任何关闭方式都有飞入动画
// (canPop:false 只拦 maybePop_close 里的 Get.back() 是强制 pop,不会被自己拦住)
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) _close();
},
child: Center(
// 关闭中内容交给飞行副本。必须同时 IgnorePointer
// Opacity(0) 照样吃手势,退出动画那百来毫秒点到海报会再 pop 一次并跳会员页
child: IgnorePointer(
ignoring: _closing,
child: Opacity(
opacity: _closing ? 0 : 1,
child: Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: GestureDetector(
// 只回传"去开通",跳会员页交给调用方(_showPayDialog)——它要 await 到从会员页
// 返回才继续弹窗链,否则后面的首页吸底引导会弹在会员页上
onTap: () {
if (_closing) return; // 防连点:pop 过渡期弹窗还在树上,再点会把首页也 pop 掉
_closing = true;
Get.back(result: true);
},
child: AspectRatio(
aspectRatio: 3 / 4,
child: Stack(
key: _posterKey,
fit: StackFit.expand,
children: [
NetworkImageLoader(
imageUrl: config.homePage ?? "",
fit: BoxFit.fill,
borderRadius: 0,
placeHolderWidget:
Container(color: const Color(0x00000000)),
),
// 倒计时胶囊:海报右上角,听 tick 每秒刷新,过期自动收起
Positioned(
top: 0,
right: 0,
child: ValueListenableBuilder<int>(
valueListenable: MineMsgProvider().tick,
builder: (_, __, ___) =>
config.hasDiscountCountdown
? _buildCountdownPill()
: const SizedBox.shrink(),
),
),
],
),
),
),
),
20.sizeBoxH,
GestureDetector(
onTap: _close,
child: Image.asset("close_button.png".commonImgPath,
width: 32),
)
],
),
),
),
),
),
);
}
/// 倒计时胶囊:红渐变 + 奶白边,HH:MM:SS
Widget _buildCountdownPill() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xffE03017), Color(0xffF27952)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xffFFFAEE), width: 0.5),
),
child: Text(
'${config.discountHour}:${config.discountMin}:${config.discountSec}',
style: const TextStyle(
color: Color(0xffFFF0D1),
fontSize: 20,
fontWeight: FontWeight.w600),
),
);
}
}