511 lines
18 KiB
Dart
511 lines
18 KiB
Dart
// 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 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;
|
||
}
|
||
}
|