// ignore_for_file: use_build_context_synchronously import 'dart:math' as math; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hgdj/hj_utils/screen.dart'; import 'package:hgdj/tools_base/unique_tag_mixin.dart'; import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart'; import 'package:provider/provider.dart'; import '../../hj_model/splash/domain_source_model.dart'; import '../../hj_page/main_page/provider/msg_provider.dart'; import '../../hj_page/mine/mine_vip/mine_charge_vip_logic.dart'; import '../../hj_page/mine/mine_vip/pay_order_source.dart'; import '../../hj_page/mine/mine_vip/vip_card_item.dart'; import '../../hj_page/mine/mine_vip/vip_pay_button.dart'; import '../../hj_page/mine/mine_vip/vip_product_manager.dart'; import '../../hj_page/mine/mine_vip/vip_support_model.dart'; import '../../hj_page/mine/mine_vip/vip_ui_kit.dart'; import '../../tools_base/loading/loading_center_widget.dart'; import 'buy_vip_alert_logic.dart'; ///购买视频对话框 class BuyVipAlert extends StatefulWidget { final String? vipId; //外部指定卡ID(可选),不传走分层卡/升级卡兜底 final String? videoId; //在看的那条内容 id,下单时透传给服务端做归因 final PayOrderTrackInfo? orderTrack; //整份下单上下文,短剧付费墙要带 mediaId/contentId/checkoutContextId const BuyVipAlert({super.key, this.vipId, this.videoId, this.orderTrack}); static Future show( {String? vipId, String? videoId, PayOrderTrackInfo? orderTrack}) { return Get.bottomSheet( BuyVipAlert(vipId: vipId, videoId: videoId, orderTrack: orderTrack), isScrollControlled: true, ); } @override State createState() => _BuyVipAlertState(); } // 可叠弹(试看结束/超时长等多处触发),用 per-实例唯一 tag 隔离 controller class _BuyVipAlertState extends State with UniqueTagMixin { late final logic = BuyVipAlertLogic( vId: widget.vipId, videoId: widget.videoId, orderTrack: widget.orderTrack); static const _sheetRadius = 26.0; /// 顶部可见边:上→下 #66EBD097 → #15EBD097 → #00EBD097(顶边最实,圆角向下渐隐) static const _borderGradient = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0x66EBD097), Color(0x15EBD097), Color(0x00EBD097), ], ); /// 只订阅「选中卡变化」的局部刷新:标题/价格/卡片选中态/权益/支付按钮用它, /// 滑卡时不再整个弹窗重建(与会员中心页共用 [MineChargeVipLogic.kSelection]) Widget _selectionBuilder(Widget Function(BuyVipAlertLogic logic) builder) { return GetBuilder( tag: uniqueTag, id: MineChargeVipLogic.kSelection, builder: builder, ); } @override Widget build(BuildContext context) { return GetBuilder( tag: uniqueTag, init: logic, builder: (_) => Material( color: Colors.transparent, child: SizedBox( height: screen.screenHeight * 0.8, width: screen.screenWidth, child: Stack( clipBehavior: Clip.none, children: [ ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(_sheetRadius), topRight: Radius.circular(_sheetRadius), ), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xff2B2010), Color(0xff1A140B), Color(0xff100C07), ], ), ), child: Column( children: [ 16.sizeBoxH, const SheetHandleBar(color: Color(0x22FFFFFF)), 16.sizeBoxH, // 标题 + 分层倒计时(对齐设计稿:新人特惠/卡名 + 红块 HH:MM:SS) Row( mainAxisSize: MainAxisSize.min, children: [ _selectionBuilder( (_) => Text( _title, style: const TextStyle( color: Color(0xE5FFFFFF), fontSize: 16, fontWeight: FontWeight.w500), ), ), _buildCountdown(), ], ), 14.sizeBoxH, _selectionBuilder( (_) => _buildPriceInfo()), //原价 / 限时特价 / 优惠 16.sizeBoxH, Expanded( child: _buildContent(), ) ], ), ), ), // 只描顶部 + 左右上圆角;竖向渐变让顶边最亮、沿圆角向下淡出 Positioned( top: 0, left: 0, right: 0, height: _sheetRadius + 2, child: IgnorePointer( child: CustomPaint( painter: _BuyVipTopBorderPainter( gradient: _borderGradient, strokeWidth: 1, topRadius: _sheetRadius, ), ), ), ), ], ), ), ), ); } Widget _buildContent() { return Consumer( builder: (_, mgr, __) { // 与会员中心一致:仅 ACTIVE+B 用改版;A / DISABLED 用 classic final classic = !mgr.isNewVipUi; if (mgr.isLoading) return LoadingCenterWidget(); if (mgr.vipCards.isEmpty) return CErrorWidget(); return Stack( children: [ Column( children: [ // 会员卡实体 Expanded( child: SingleChildScrollView( child: Column( children: [ Padding( padding: const EdgeInsets.only(left: 6), child: CarouselSlider.builder( carouselController: logic.pageCtr, itemCount: mgr.vipCards.length + 2, itemBuilder: (_, index, __) => _selectionBuilder( (l) => l.instanceChildItem(index)), options: CarouselOptions( height: logic.cardHeight, viewportFraction: logic.itemRatio, initialPage: 0, enableInfiniteScroll: false, reverse: false, autoPlay: false, enlargeCenterPage: false, padEnds: false, clipBehavior: Clip.none, onPageChanged: (index, __) => logic.onPageChanged(index), scrollDirection: Axis.horizontal, ), ), ), // B:卡皮与核心权益标题间距略收;A 保持原 14 (classic ? 14 : 4).sizeBoxH, _buildPrivilegeSections(classic: classic), 100.sizeBoxH, ], ), ), ), ], ), Positioned( bottom: screen.paddingBottom + 12, left: 12, right: 12, child: Column( mainAxisSize: MainAxisSize.min, children: [ _selectionBuilder((l) => VipPayButton(l, classic: classic)), ], ), ) ], ); }, ); } /// 弹窗标题:新人分层固定"新人特惠",其他分层跟随当前选中卡名 String get _title { if (MineMsgProvider().payTier?.status == PayTier.newUnpay) return '新人特惠'; return logic.currentProductModel?.productName ?? '会员特惠'; } /// 价格行:原价(标题+价格删除线) / 限时特价 / 优惠 Widget _buildPriceInfo() { final p = logic.currentProductModel; if (p == null) return const SizedBox(); final original = p.originalPriceUI; final discounted = p.discountedPriceUI; const muted = TextStyle( color: Color(0x8cFFFFFF), fontSize: 14, decoration: TextDecoration.lineThrough, decorationColor: Color(0x8cFFFFFF), ); const normal = TextStyle( color: Color(0x8cFFFFFF), fontSize: 14, fontWeight: FontWeight.w500); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _priceItem("原价", "¥$original", muted, titleStyle: muted), 24.sizeBoxW, _priceItem("限时特价", "¥$discounted", normal), 24.sizeBoxW, _priceItem( "优惠", "¥${(original - discounted).clamp(0, original)}", normal), ], ); } Widget _priceItem(String title, String value, TextStyle valueStyle, {TextStyle? titleStyle}) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text(title, style: titleStyle ?? const TextStyle(color: Color(0x99FFFFFF), fontSize: 14)), 4.sizeBoxW, Text(value, style: valueStyle), ], ); } /// 分层倒计时:按 config.lastDiscountTime,有则显示(监听 tick 每秒刷新),过期自动收起 Widget _buildCountdown() { return ValueListenableBuilder( valueListenable: MineMsgProvider().tick, builder: (_, __, ___) { final c = MineMsgProvider().payTier?.config; if (c == null || !c.hasDiscountCountdown) return const SizedBox.shrink(); return Row( mainAxisSize: MainAxisSize.min, children: [ 12.sizeBoxW, _timeItem(c.discountHour), _colon(), _timeItem(c.discountMin), _colon(), _timeItem(c.discountSec), ], ); }, ); } Widget _timeItem(String value) { return Container( width: 24, height: 24, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xffE1351F), borderRadius: BorderRadius.circular(3)), child: Text(value, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500)), ); } Widget _colon() => const Padding( padding: EdgeInsets.symmetric(horizontal: 2), child: Text(":", style: TextStyle(color: Color(0xffDD001B), fontSize: 20)), ); /// 权益区:与会员中心同实验分支 —— classic=A/DISABLED,改版=ACTIVE+B Widget _buildPrivilegeSections({required bool classic}) { return _selectionBuilder((l) { final privileges = l.currentProductModel?.newPrivilege ?? []; final coreList = privileges.where((p) => p.isCore == true).toList(); final moreList = privileges.where((p) => p.isCore != true).toList(); return Column( mainAxisSize: MainAxisSize.min, children: [ classic ? _buildClassicCorePrivileges(coreList) : _buildCorePrivileges(coreList), classic ? _buildClassicMorePrivileges(moreList) : _buildMorePrivileges(moreList), ], ); }); } // 核心权益:横向滚动卡片 —— B 改版 Widget _buildCorePrivileges(List list) { if (list.isEmpty) return const SizedBox.shrink(); return Column( children: [ const VipCoreSectionTitleImage(), 16.sizeBoxH, SizedBox( height: 92, child: ListView.separated( clipBehavior: Clip.none, scrollDirection: Axis.horizontal, padding: const EdgeInsets.fromLTRB(16, 2, 16, 26), itemCount: list.length, separatorBuilder: (_, __) => 8.sizeBoxW, itemBuilder: (_, i) => VipCorePrivilegeCard(list[i]), ), ), 16.sizeBoxH, ], ); } // 核心权益:一行金边方卡横向滑动 —— A / DISABLED Widget _buildClassicCorePrivileges(List list) { if (list.isEmpty) return const SizedBox.shrink(); return Column( children: [ const VipSectionTitle('我的核心权益', classic: true), 16.sizeBoxH, SizedBox( //设计稿一行放得下 4 张多一点,第 5 张露半张提示可以划 height: 72, child: ListView.separated( clipBehavior: Clip.none, // 金边有外发光,裁了会缺一圈 scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: list.length, separatorBuilder: (_, __) => 8.sizeBoxW, itemBuilder: (_, i) => SizedBox( width: 72, child: VipCorePrivilegeCard(list[i], classic: true)), ), ), 16.sizeBoxH, ], ); } // 更多权益:深色圆角 + 四列网格 —— B 改版 Widget _buildMorePrivileges(List list) { if (list.isEmpty) return const SizedBox.shrink(); return Container( margin: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: const Color(0x0DFFFFFF), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const VipMoreSectionTitleImage(), 12.sizeBoxH, GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 6), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 0, crossAxisSpacing: 14, childAspectRatio: 66 / 110, ), itemCount: list.length, itemBuilder: (_, i) => VipProductPrivilegeItem(list[i]), ), ], ), ); } // 会员特权:直接四列网格 —— A / DISABLED Widget _buildClassicMorePrivileges(List list) { if (list.isEmpty) return const SizedBox.shrink(); return Column( children: [ const VipSectionTitle('我的会员特权', classic: true), 12.sizeBoxH, GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 18), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 12, crossAxisSpacing: 18, childAspectRatio: 63 / 100, ), itemCount: list.length, itemBuilder: (_, i) => VipProductPrivilegeItem(list[i], classic: true), ), ], ); } } /// 仅绘制底部弹窗顶部可见边界(上边 + 左右上圆角) class _BuyVipTopBorderPainter extends CustomPainter { final Gradient gradient; final double strokeWidth; final double topRadius; _BuyVipTopBorderPainter({ required this.gradient, required this.strokeWidth, required this.topRadius, }); @override void paint(Canvas canvas, Size size) { final w = size.width; final r = topRadius; final half = strokeWidth / 2; // Flutter arcTo:0=右、正角度顺时针。左上圆心(r,r) 由 π 扫到 3π/2,右上圆心(w-r,r) 由 3π/2 扫到 2π final path = Path() ..moveTo(half, r) ..arcTo( Rect.fromCircle(center: Offset(r, r), radius: r - half), math.pi, math.pi / 2, false, ) ..lineTo(w - r, half) ..arcTo( Rect.fromCircle(center: Offset(w - r, r), radius: r - half), math.pi * 1.5, math.pi / 2, false, ); final paint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..isAntiAlias = true ..strokeJoin = StrokeJoin.round ..shader = gradient.createShader(Rect.fromLTWH(0, 0, w, r + 2)); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant _BuyVipTopBorderPainter oldDelegate) { return oldDelegate.gradient != gradient || oldDelegate.strokeWidth != strokeWidth || oldDelegate.topRadius != topRadius; } }