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 createState() => _GuideHomeDialogState(); } class _GuideHomeDialogState extends State { 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: [ 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( 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), ), ); } }