初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
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),
),
);
}
}