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

511 lines
18 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.
// ignore_for_file: use_build_context_synchronously
import 'dart:math' as math;
import 'package:carousel_slider/carousel_slider.dart';
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/gestures.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 '../../routers/jump_router.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<BuyVipAlert> createState() => _BuyVipAlertState();
}
// 可叠弹(试看结束/超时长等多处触发),用 per-实例唯一 tag 隔离 controller
class _BuyVipAlertState extends State<BuyVipAlert> 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<BuyVipAlertLogic>(
tag: uniqueTag,
id: MineChargeVipLogic.kSelection,
builder: builder,
);
}
@override
Widget build(BuildContext context) {
return GetBuilder<BuyVipAlertLogic>(
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<VipProductManager>(
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)),
12.sizeBoxH,
EasyRichText(
'支付问题反馈,点击联系 在线客服',
patternList: [
EasyRichTextPattern(
targetString: '在线客服',
style: const TextStyle(
color: Color(0xFFFFD460), fontSize: 12),
recognizer: TapGestureRecognizer()
..onTap = () => pushToCustomService(),
)
],
defaultStyle:
const TextStyle(color: Color(0xffDCDCDC), fontSize: 12),
),
],
),
)
],
);
},
);
}
/// 弹窗标题:新人分层固定"新人特惠",其他分层跟随当前选中卡名
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<int>(
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<NewPrivilege> 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<NewPrivilege> 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<NewPrivilege> 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<NewPrivilege> 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 arcTo0=右、正角度顺时针。左上圆心(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;
}
}