初始化
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/gradient_text.dart';
|
||||
|
||||
class UserChargeView extends StatelessWidget {
|
||||
final bool isFromCoin; // true: 来源金币充值, vip时间为灰色
|
||||
/// true:旧版文案/配色(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const UserChargeView(
|
||||
{super.key, this.isFromCoin = false, this.classic = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<GlobalStore>(
|
||||
builder: (_, provider, __) {
|
||||
if (classic) return _buildClassic(provider);
|
||||
return _buildModern(provider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModern(GlobalStore provider) {
|
||||
final vipIcon = provider.meInfo?.vipImageName ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: provider.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
provider.meInfo == null
|
||||
? '未知'
|
||||
: provider.meInfo?.name?.substring(
|
||||
0, min(provider.meInfo?.name?.length ?? 0, 9)) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color(0xffF6EEDC),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
if (!provider.isVIP)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Color(0x14FFFFFF), width: 1),
|
||||
),
|
||||
child: const Text(
|
||||
'未开通',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (vipIcon.isNotEmpty)
|
||||
Image.asset(vipIcon, height: 20),
|
||||
],
|
||||
),
|
||||
4.sizeBoxH,
|
||||
globalStore.isVIP
|
||||
? Consumer<PreSaleProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (isFromCoin) {
|
||||
return Text(
|
||||
'会员到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GradientText(
|
||||
'会员到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xffFFE8BE),
|
||||
Color(0xffE6B764),
|
||||
],
|
||||
),
|
||||
style: TextStyle(fontSize: 12),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
'开通会员 · 解锁全站尊享特权',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16);
|
||||
}
|
||||
|
||||
Widget _buildClassic(GlobalStore provider) {
|
||||
final vipIcon = provider.meInfo?.vipImageName ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: provider.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
provider.meInfo == null
|
||||
? '未知'
|
||||
: provider.meInfo?.name?.substring(
|
||||
0, min(provider.meInfo?.name?.length ?? 0, 9)) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
if (provider.isVIP && vipIcon.isNotEmpty)
|
||||
Image.asset(vipIcon, height: 20),
|
||||
],
|
||||
),
|
||||
4.sizeBoxH,
|
||||
globalStore.isVIP
|
||||
? Consumer<PreSaleProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (isFromCoin) {
|
||||
return Text(
|
||||
'到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GradientText(
|
||||
'到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xffFFE8BE),
|
||||
Color(0xffE6B764),
|
||||
],
|
||||
),
|
||||
style: TextStyle(fontSize: 12),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
'您还不是会员 开通会员 畅享特权',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
class AICouponModel {
|
||||
String? createTime;
|
||||
String? expiredTime;
|
||||
String? goodsDesc;
|
||||
String? goodsName;
|
||||
String? goodsOrigin;
|
||||
int? goodsType;
|
||||
int? goodsValue;
|
||||
String? id;
|
||||
int? status;
|
||||
int? uid;
|
||||
String? useTime;
|
||||
|
||||
AICouponModel(
|
||||
{this.createTime,
|
||||
this.expiredTime,
|
||||
this.goodsDesc,
|
||||
this.goodsName,
|
||||
this.goodsOrigin,
|
||||
this.goodsType,
|
||||
this.goodsValue,
|
||||
this.id,
|
||||
this.status,
|
||||
this.uid,
|
||||
this.useTime});
|
||||
|
||||
AICouponModel.fromJson(Map<String, dynamic> json) {
|
||||
createTime = json['createTime'];
|
||||
expiredTime = json['expiredTime'];
|
||||
goodsDesc = json['goodsDesc'];
|
||||
goodsName = json['goodsName'];
|
||||
goodsOrigin = json['goodsOrigin'];
|
||||
goodsType = json['goodsType'];
|
||||
goodsValue = json['goodsValue'];
|
||||
id = json['id'];
|
||||
status = json['status'];
|
||||
uid = json['uid'];
|
||||
useTime = json['useTime'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['createTime'] = this.createTime;
|
||||
data['expiredTime'] = this.expiredTime;
|
||||
data['goodsDesc'] = this.goodsDesc;
|
||||
data['goodsName'] = this.goodsName;
|
||||
data['goodsOrigin'] = this.goodsOrigin;
|
||||
data['goodsType'] = this.goodsType;
|
||||
data['goodsValue'] = this.goodsValue;
|
||||
data['id'] = this.id;
|
||||
data['status'] = this.status;
|
||||
data['uid'] = this.uid;
|
||||
data['useTime'] = this.useTime;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../../../hj_model/mine/exchange/recharge_list_model.dart';
|
||||
import '../../../hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
|
||||
class MineChargeCoinLogic extends GetxController {
|
||||
/// 下单来源(透传 /mine/topay 的 sourcePage),由入口传入
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 下单埋点上下文(短剧付费墙要带 mediaId/contentId/checkoutContextId 做归因)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
MineChargeCoinLogic(
|
||||
{this.sourcePage = PaySourcePage.unknown, this.orderTrack});
|
||||
|
||||
bool isInitLoading = true; // 首屏加载中
|
||||
RechargeListModel? model; // 金币充值档位列表
|
||||
RechargeTypeModel? selectedCoin; // 当前选中的金币档位
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 拉取金币充值档位,默认选中第一档
|
||||
Future<void> loadData() async {
|
||||
final res = await MineService.getChatRechargeTypes(1);
|
||||
isInitLoading = false;
|
||||
model = res;
|
||||
res?.list ??= [];
|
||||
if (res?.list?.isNotEmpty == true) {
|
||||
selectedCoin = res!.list!.first;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// 选择金币档位
|
||||
void onSelectCoin(int index) {
|
||||
selectedCoin = model?.list?[index];
|
||||
update();
|
||||
}
|
||||
|
||||
// 去支付(购买金币)
|
||||
void onGotoPay() {
|
||||
if (selectedCoin == null) {
|
||||
showToast("请选择产品");
|
||||
return;
|
||||
}
|
||||
if (selectedCoin!.rechargeTypeListUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
Get.bottomSheet(
|
||||
OnlinePayPage(
|
||||
coinRcModel: selectedCoin,
|
||||
orderTrack: (orderTrack ?? const PayOrderTrackInfo())
|
||||
.copyWith(sourcePage: sourcePage),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../make_money/mine_withdrawal_record_page.dart';
|
||||
import 'charge_user_header.dart';
|
||||
import 'mine_charge_coin_logic.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'widgets/coin_item.dart';
|
||||
|
||||
//金币充值页面
|
||||
class MineChargeCoinPage extends StatelessWidget {
|
||||
/// 下单来源,入口不传则 UNKNOWN(金币没有专属来源枚举,别拿会员的顶上)
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
const MineChargeCoinPage(
|
||||
{super.key, this.sourcePage = PaySourcePage.unknown});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeCoinLogic>(
|
||||
init: MineChargeCoinLogic(sourcePage: sourcePage),
|
||||
global: false, // 多入口各自独立:叠栈时别复用上一个页面的 controller(来源会串成上一次的)
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
body: () {
|
||||
if (controller.isInitLoading) return const LoadingCenterWidget();
|
||||
if (controller.model == null)
|
||||
return CErrorWidget(retryOnTap: () => controller.loadData());
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
// 头像 + 余额卡片:随金币列表上滑,自带折叠淡出动画
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
backgroundColor: Color(0xff0F0F0F),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
expandedHeight: kToolbarHeight + 200,
|
||||
title: Text(
|
||||
'金币充值',
|
||||
style: textStyle(16, Colors.white, FontWeight.w600),
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.recharge)),
|
||||
child: Text(
|
||||
'充值记录',
|
||||
style: TextStyle(
|
||||
color: Color(0xff666666), fontSize: 16.sp),
|
||||
),
|
||||
),
|
||||
18.w.sizeBoxW
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
kToolbarHeight.sizeBoxH,
|
||||
UserChargeView(isFromCoin: true),
|
||||
18.sizeBoxH,
|
||||
_buildWallet(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: 111 / 138,
|
||||
),
|
||||
itemCount: controller.model?.list?.length ?? 0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = controller.model!.list![index];
|
||||
final isSelected =
|
||||
controller.selectedCoin?.id == model.id;
|
||||
return CoinItem(
|
||||
model,
|
||||
onTap: () => controller.onSelectCoin(index),
|
||||
isSelected: isSelected,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: 18.sizeBoxH),
|
||||
SliverToBoxAdapter(
|
||||
child: EasyRichText(
|
||||
'*如提示【交易失败】【账户风险】等,可重新发起订单,或在15分钟后重试支付。如支付未到账,请反馈客服订单号',
|
||||
defaultStyle:
|
||||
TextStyle(color: Color(0xff666666), fontSize: 12),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '反馈客服订单号',
|
||||
style: TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontWeight: FontWeight.w500),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildBottomBar(controller),
|
||||
],
|
||||
);
|
||||
}(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 底部支付按钮 + 客服入口
|
||||
Widget _buildBottomBar(MineChargeCoinLogic controller) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => controller.onGotoPay(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'¥${controller.selectedCoin?.moneyYuan ?? 0}/立即支付',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('支付问题反馈,点击联系 ',
|
||||
style: textStyle(12, Color(0xffBFBFC1), FontWeight.w400)),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
pushToCustomService();
|
||||
},
|
||||
child: Text('在线客服',
|
||||
style: textStyle(12, Color(0xffFFD460), FontWeight.w400)),
|
||||
)
|
||||
],
|
||||
),
|
||||
// 垫上虚拟导航栏高度,避免底部内容被遮挡(edge-to-edge)
|
||||
(16 + screen.paddingBottom).sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 我的金币余额卡片
|
||||
Widget _buildWallet() {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.fromLTRB(16, 18, 16, 18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'我的金币余额',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxW,
|
||||
Consumer<GlobalStore>(builder: (_, store, __) {
|
||||
final wallet = store.wallet;
|
||||
final total = (wallet?.amount ?? 0) + (wallet?.income ?? 0);
|
||||
return Text(
|
||||
"$total",
|
||||
style: TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600),
|
||||
);
|
||||
}),
|
||||
Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.bill), opaque: false),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 30,
|
||||
width: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFFD460),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'余额明细',
|
||||
style: TextStyle(color: Color(0xff3D3D3D), fontSize: 14),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'package:carousel_slider/carousel_controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/alert/vip_guide/guide_manager.dart';
|
||||
import 'package:hgdj/alert/vip_guide/timed_popup_manager.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../alert/vip_guide/guide_countdown_dialog.dart';
|
||||
import '../../../alert/vip_guide/guide_config.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_card_item.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员卡页/购买弹窗的页面级 Logic:只管「当前选中卡 / 轮播 / 支付触发」。
|
||||
/// 会员卡列表数据(请求/缓存/组装/选卡)统一在 [VipProductManager],UI 用 Consumer 消费。
|
||||
class MineChargeVipLogic extends GetxController {
|
||||
/// 局部刷新 id:只有「跟随选中卡变化」的区域订阅它(卡片选中态 / 价格 / 权益区 / 支付按钮),
|
||||
/// 滑卡时不必整页重建(背景图、轮播本体、用户信息条、AppBar 都跟选中卡无关)
|
||||
static const kSelection = 'vip_selection';
|
||||
|
||||
/// 跳转指定选中的会员卡ID(构造传入,替代 Get.arguments)
|
||||
final String? vipID;
|
||||
|
||||
/// 下单来源(透传 /mine/topay 的 sourcePage)
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 播放页拉起时带上在看的那条内容 id,服务端按它归因;会员中心进来为空
|
||||
final String? videoId;
|
||||
|
||||
/// 入口透传的整份下单上下文,字段比 [videoId] 全时以它为准(短剧付费墙带 mediaId/contentId/checkoutContextId)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
MineChargeVipLogic(
|
||||
{this.vipID,
|
||||
this.sourcePage = PaySourcePage.vipCenter,
|
||||
this.videoId,
|
||||
this.orderTrack});
|
||||
|
||||
/// 当前选中的会员卡(页面级状态,多入口各自独立)
|
||||
VipProductModel? currentProductModel;
|
||||
|
||||
/// 引导弹窗点「立即开通」指定的卡ID,优先级高于构造传入的 vipID(用户显式选择)
|
||||
String? _guideCardId;
|
||||
|
||||
/// 会员卡轮播控制器(页面级,两个入口不能共享)
|
||||
late final pageCtr = CarouselSliderController();
|
||||
|
||||
/// 本页 VIP 卡片统计会话
|
||||
VipCardAnalyticsSession? _analytics;
|
||||
|
||||
//轮播视口占比 / 高度:B 组固定卡面 148×157 + 横向间距 10;高度含角标与底部发光
|
||||
double get itemRatio => vipProductManager.isNewVipUi
|
||||
? (148 + 10) / screen.screenWidth
|
||||
: 137.6 / screen.screenWidth;
|
||||
|
||||
double get cardHeight => vipProductManager.isNewVipUi
|
||||
? 189 // 8角标 + 157卡面 + 24底部 Glow
|
||||
: (screen.screenWidth * itemRatio) * 150 / 110.6;
|
||||
|
||||
/// 进页面是否强制拉最新:会员中心页 true(每次刷新),购买弹窗覆写为 false(优先用缓存)
|
||||
bool get preferFreshData => true;
|
||||
|
||||
/// 进入本页是否触发「优惠倒计时」引导弹窗(VIP_CENTER)。购买弹窗(BuyVipAlertLogic)覆写为 false,避免叠弹。
|
||||
bool get enableEntryGuidePopup => true;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
PreSaleProvider().refreshConfig();
|
||||
loadCards(force: preferFreshData);
|
||||
if (enableEntryGuidePopup) _tryShowEntryGuide();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// 页面/弹窗销毁 → GetBuilder 按 tag 走 Get.delete → onDelete → 这里补报 CLOSE
|
||||
_analytics?.reportCloseWithoutPurchaseIfNeeded();
|
||||
_analytics = null;
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// VIP_CENTER:开关允许时**每次**进会员页都弹优惠倒计时引导
|
||||
/// (含从别的引导弹窗点「开通」跳进来的那次,运营要追单,不做去重)。
|
||||
void _tryShowEntryGuide() {
|
||||
TimedPopupManager().trigger(
|
||||
canShow: () => GuideManager().canShow(GuideScene.vipCenter),
|
||||
onShow: () => GuideCountdownDialog.show(
|
||||
scene: GuideScene.vipCenter,
|
||||
// 已在会员页:关弹窗 + 就地选中弹窗配置的那张卡,让底部支付按钮直接对上该卡价格
|
||||
onConfirm: (card) {
|
||||
Get.back(result: true); // 对齐 show() 的返回语义:true=点了开通
|
||||
_selectCard(card);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选中并滚到指定会员卡:只驱动轮播,选中态由 [onPageChanged] 统一更新(和点卡片一致)。
|
||||
/// 列表还在加载时轮播没渲染、动不了,先记下 id 交给 [_syncSelection] 落位
|
||||
void _selectCard(VipProductModel card) {
|
||||
_guideCardId = card.productID;
|
||||
if (vipProductManager.isLoading || !pageCtr.ready) return;
|
||||
final index = vipProductManager.vipCards
|
||||
.indexWhere((e) => e.productID == card.productID);
|
||||
if (index >= 0) pageCtr.animateToPage(index, curve: Curves.fastOutSlowIn);
|
||||
}
|
||||
|
||||
/// 拉取会员卡列表并同步选中态
|
||||
Future<void> loadCards({bool force = false}) async {
|
||||
await vipProductManager.loadVipCards(force: force);
|
||||
_syncSelection();
|
||||
}
|
||||
|
||||
/// 数据就绪后按优先级确定默认选中卡,并滚动到对应位置;UI 展示后再上报曝光
|
||||
void _syncSelection() {
|
||||
currentProductModel = vipProductManager.defaultVipCard(
|
||||
vipID: _guideCardId ?? vipID, current: currentProductModel);
|
||||
final pid = currentProductModel?.productID;
|
||||
final index = pid == null
|
||||
? -1
|
||||
: vipProductManager.vipCards.indexWhere((e) => e.productID == pid);
|
||||
// 会话尽早创建:避免仅在 post-frame 里建,用户快速返回时 onClose 拿不到 session、CLOSE 丢报。
|
||||
// 必须等有卡数据再建:首次请求失败时卡列表还是空,会把「无实验」快照锁进 session,
|
||||
// 之后错误页重试成功也不会上报(快照 late final 不可变)。
|
||||
if (vipProductManager.vipCards.isNotEmpty)
|
||||
_analytics ??= VipCardAnalyticsSession();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isClosed) return;
|
||||
// 卡列表为空时页面渲染的是错误页,轮播压根没 build,controller 没 attach,
|
||||
// 而 jumpToPage 内部是 _state!.pageController!.page! 三层空断言,会直接崩
|
||||
if (index >= 0 && pageCtr.ready) pageCtr.jumpToPage(index);
|
||||
// 曝光必须在 UI 实际展示后上报
|
||||
_analytics?.reportPageViewAfterPaint();
|
||||
_analytics?.reportProductImpression(currentProductModel,
|
||||
afterPaint: true);
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
Widget instanceChildItem(int index) {
|
||||
final data = vipProductManager.vipCards;
|
||||
if (index > data.length - 1) {
|
||||
return SizedBox(
|
||||
height: cardHeight,
|
||||
width: screen.screenWidth,
|
||||
);
|
||||
}
|
||||
final model = data[index];
|
||||
final card = VipCardItem(
|
||||
model,
|
||||
isSelect: currentProductModel?.productID != null &&
|
||||
currentProductModel?.productID == model.productID,
|
||||
callback: () {
|
||||
pageCtr.animateToPage(index, curve: Curves.fastOutSlowIn);
|
||||
},
|
||||
);
|
||||
// B 组:卡槽 = 148 + 右侧 10 间距
|
||||
if (vipProductManager.isNewVipUi) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Align(alignment: Alignment.centerLeft, child: card),
|
||||
);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
/// 滑动切卡
|
||||
onPageChanged(int index) {
|
||||
final data = vipProductManager.vipCards;
|
||||
if (data.isEmpty) return;
|
||||
// 轮播 itemCount = 卡数+2(尾部占位,让最后一张能滑到最左)。padEnds:false 时 PageView 的
|
||||
// 最大页码 = itemCount - 1/viewportFraction,B 组卡槽 158 在 <395 宽的机型上会四舍五入到
|
||||
// 占位项索引,直接取 dataSource[index] 会越界,这里钳到最后一张真实卡。
|
||||
currentProductModel = data[index.clamp(0, data.length - 1)];
|
||||
_analytics?.reportProductImpression(currentProductModel);
|
||||
update([kSelection]);
|
||||
}
|
||||
|
||||
onInitiatePayAction() async {
|
||||
if (currentProductModel == null) {
|
||||
showToast("请选择充值的会员卡~");
|
||||
return;
|
||||
}
|
||||
if (currentProductModel!.rchgTypeUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
await Get.bottomSheet(
|
||||
OnlinePayPage(
|
||||
vipProductModel: currentProductModel,
|
||||
orderTrack: buildOrderTrack(),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
await PreSaleProvider().refreshConfig();
|
||||
update();
|
||||
}
|
||||
|
||||
/// 下单埋点:来源 + 卡皮 session + ACTIVE 时的实验字段(普通卡 / 预售卡共用)
|
||||
PayOrderTrackInfo buildOrderTrack() {
|
||||
final base = orderTrack;
|
||||
return PayOrderTrackInfo(
|
||||
sourcePage: base?.sourcePage ?? sourcePage,
|
||||
sourceRef: base?.sourceRef ?? videoId,
|
||||
videoId: base?.videoId ?? videoId,
|
||||
mediaId: base?.mediaId,
|
||||
contentId: base?.contentId,
|
||||
checkoutContextId: base?.checkoutContextId,
|
||||
sessionId: base?.sessionId ?? _analytics?.sessionId,
|
||||
experimentId: vipProductManager.isExperimentActive
|
||||
? (currentProductModel?.experimentId ??
|
||||
vipProductManager.experimentId)
|
||||
: null,
|
||||
experimentVariant: vipProductManager.isExperimentActive
|
||||
? (currentProductModel?.variant ?? vipProductManager.variant)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
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/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../pre_sale/pre_sale_entry.dart';
|
||||
import '../make_money/mine_withdrawal_record_page.dart';
|
||||
import 'charge_user_header.dart';
|
||||
import 'mine_charge_vip_logic.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_item.dart';
|
||||
import 'vip_pay_button.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
import 'vip_ui_kit.dart';
|
||||
|
||||
class MineChargeVipPage extends StatefulWidget {
|
||||
final String? vipID; //跳转指定选中的会员卡ID
|
||||
/// 下单来源,默认会员中心
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
const MineChargeVipPage({
|
||||
super.key,
|
||||
this.vipID,
|
||||
this.sourcePage = PaySourcePage.vipCenter,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MineChargeVipPage> createState() => _MineChargeVipPageState();
|
||||
}
|
||||
|
||||
// 多入口可叠栈,用 per-实例唯一 tag 隔离 controller,避免轮播/选中卡/onReady 串味
|
||||
class _MineChargeVipPageState extends State<MineChargeVipPage>
|
||||
with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
init: MineChargeVipLogic(
|
||||
vipID: widget.vipID, sourcePage: widget.sourcePage),
|
||||
builder: (logic) => Consumer<VipProductManager>(
|
||||
builder: (_, mgr, __) {
|
||||
// ACTIVE+B → 改版 UI;A / DISABLED / 无实验 → 旧版 UI
|
||||
final useNew = mgr.isNewVipUi;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: useNew ? const Color(0xff0F0F0F) : null,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: Colors.white),
|
||||
title: Text('会员中心', style: TextStyle(color: Colors.white)),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(RecordsPage(RecordType.recharge)),
|
||||
child: Text('充值记录',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontSize: 12)),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: useNew
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildPageBackground(mgr),
|
||||
),
|
||||
_buildBody(logic, classic: false),
|
||||
],
|
||||
)
|
||||
: _buildBody(logic, classic: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// B 组页顶背景:优先 uiConfig.backgroundImage,空则本地默认图
|
||||
Widget _buildPageBackground(VipProductManager mgr) {
|
||||
final url = mgr.vipBgImage;
|
||||
if (url != null) {
|
||||
return NetworkImageLoader(
|
||||
imageUrl: url,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
return Image.asset(
|
||||
'mine_vip_bg.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(MineChargeVipLogic logic, {required bool classic}) {
|
||||
if (vipProductManager.isLoading) return LoadingCenterWidget();
|
||||
if (vipProductManager.vipCards.isEmpty) {
|
||||
return CErrorWidget(retryOnTap: () => logic.loadCards(force: true));
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
(kToolbarHeight + screen.paddingTop).sizeBoxH,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
UserChargeView(classic: classic),
|
||||
26.sizeBoxH,
|
||||
_buildCardSlider(logic),
|
||||
if (vipProductManager.presaleGroup != null) ...[
|
||||
20.sizeBoxH,
|
||||
PreSaleVipEntry().paddingSymmetric(horizontal: 16),
|
||||
],
|
||||
// B:卡皮与核心权益标题间距略收;A 保持原 14
|
||||
(classic ? 14 : 4).sizeBoxH,
|
||||
_buildPrivileges(classic: classic),
|
||||
100.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0, left: 0, right: 0, child: _buildPay(classic: classic)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//会员卡轮播
|
||||
Widget _buildCardSlider(MineChargeVipLogic logic) {
|
||||
return CarouselSlider.builder(
|
||||
carouselController: logic.pageCtr,
|
||||
itemCount: vipProductManager.vipCards.length + 2,
|
||||
//选中态跟着 kSelection 单卡刷新,避免滑一次卡把整个轮播和页面重建一遍
|
||||
itemBuilder: (_, index, __) => GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (l) => l.instanceChildItem(index),
|
||||
),
|
||||
options: CarouselOptions(
|
||||
height: logic.cardHeight,
|
||||
viewportFraction: logic.itemRatio,
|
||||
enableInfiniteScroll: false,
|
||||
enlargeCenterPage: false,
|
||||
padEnds: false,
|
||||
// 选中卡底部 VIP/Glow Gold 不被 PageView 裁切
|
||||
clipBehavior: Clip.none,
|
||||
onPageChanged: (index, __) => logic.onPageChanged(index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//权益区:跟随选中卡,订阅 kSelection 局部刷新;newPrivilege 只遍历一次按 isCore 分组
|
||||
Widget _buildPrivileges({required bool classic}) {
|
||||
return GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (logic) {
|
||||
final privileges = logic.currentProductModel?.newPrivilege ?? [];
|
||||
final coreList = <NewPrivilege>[];
|
||||
final moreList = <NewPrivilege>[];
|
||||
for (final p in privileges) {
|
||||
(p.isCore == true ? coreList : moreList).add(p);
|
||||
}
|
||||
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: (_, index) => VipCorePrivilegeCard(list[index]),
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//核心权益:4 列金边方卡 —— A / DISABLED 旧版
|
||||
Widget _buildClassicCorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
const VipSectionTitle("我的核心权益", classic: true),
|
||||
16.sizeBoxH,
|
||||
GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) => FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: VipCorePrivilegeCard(list[index], classic: true)),
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//更多权益:深色圆角容器 + 四列网格 + 展开/收起 —— B 改版
|
||||
Widget _buildMorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return _VipMorePrivilegesPanel(
|
||||
list: list,
|
||||
title: const VipMoreSectionTitleImage(),
|
||||
);
|
||||
}
|
||||
|
||||
//会员特权:直接四列网格 —— 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: (_, index) =>
|
||||
VipProductPrivilegeItem(list[index], classic: true),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//底部支付区域:只有按钮跟选中卡走,渐变底和客服文案是静态的
|
||||
Widget _buildPay({required bool classic}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.black.withValues(alpha: 0), Colors.black],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (logic) => VipPayButton(logic, classic: classic),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
EasyRichText(
|
||||
'支付问题反馈,点击联系 在线客服',
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '在线客服',
|
||||
style: TextStyle(color: Color(0xFFFFD460), fontSize: 12),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
)
|
||||
],
|
||||
defaultStyle: TextStyle(color: Color(0xffDCDCDC), fontSize: 12),
|
||||
),
|
||||
// 垫上虚拟导航栏高度,避免底部内容被遮挡(edge-to-edge)
|
||||
(12 + screen.paddingBottom).sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更多权益:深色圆角面板 + 四列网格,默认展开,可收起为 2 行
|
||||
class _VipMorePrivilegesPanel extends StatefulWidget {
|
||||
final List<NewPrivilege> list;
|
||||
final Widget title;
|
||||
|
||||
const _VipMorePrivilegesPanel({required this.list, required this.title});
|
||||
|
||||
@override
|
||||
State<_VipMorePrivilegesPanel> createState() =>
|
||||
_VipMorePrivilegesPanelState();
|
||||
}
|
||||
|
||||
class _VipMorePrivilegesPanelState extends State<_VipMorePrivilegesPanel> {
|
||||
static const _collapsedCount = 8; // 收起时展示 2 行 × 4
|
||||
static const _radius = 24.0;
|
||||
bool _expanded = true;
|
||||
|
||||
/// 描边:上→下 #FFFBE5 30% → 0% → 30%(上下可见,左右中段淡出)
|
||||
static const _borderGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x4DFFFBE5), Color(0x00FFFBE5), Color(0x4DFFFBE5)],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canToggle = widget.list.length > _collapsedCount;
|
||||
final showCount =
|
||||
(!_expanded && canToggle) ? _collapsedCount : widget.list.length;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomPaint(
|
||||
painter: _FigmaGradientBorderPainter(
|
||||
gradient: _borderGradient,
|
||||
strokeWidth: 1,
|
||||
radius: _radius,
|
||||
inner: true,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
child: ColoredBox(
|
||||
color: const Color(0x0DFFFFFF),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 14, 12, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
widget.title,
|
||||
14.sizeBoxH,
|
||||
GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 72 / 110,
|
||||
),
|
||||
itemCount: showCount,
|
||||
itemBuilder: (_, index) =>
|
||||
VipProductPrivilegeItem(widget.list[index]),
|
||||
),
|
||||
if (canToggle) ...[
|
||||
8.sizeBoxH,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_expanded ? '收起特权' : '展开特权',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_expanded
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
size: 16,
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Figma 线性渐变描边:shader 按节点 bounds 映射(与 Figma stroke fill 一致)
|
||||
class _FigmaGradientBorderPainter extends CustomPainter {
|
||||
final Gradient gradient;
|
||||
final double strokeWidth;
|
||||
final double radius;
|
||||
final bool inner;
|
||||
|
||||
_FigmaGradientBorderPainter({
|
||||
required this.gradient,
|
||||
required this.strokeWidth,
|
||||
required this.radius,
|
||||
this.inner = true,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
// Inner:描边中心线向内缩 strokeWidth/2,使整条描边落在边界内侧
|
||||
final inset = inner ? strokeWidth / 2 : 0.0;
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
rect.deflate(inset),
|
||||
Radius.circular(math.max(0, radius - inset)),
|
||||
);
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..shader = gradient.createShader(rect);
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _FigmaGradientBorderPainter oldDelegate) {
|
||||
return oldDelegate.gradient != gradient ||
|
||||
oldDelegate.strokeWidth != strokeWidth ||
|
||||
oldDelegate.radius != radius ||
|
||||
oldDelegate.inner != inner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../assets_tool/app_colors.dart';
|
||||
import '../../../assets_tool/images.dart';
|
||||
import '../../../config/address.dart';
|
||||
import '../../../hj_model/mine/exchange/dc_model.dart';
|
||||
import '../../../hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import '../../../hj_utils/api_service/mine_service.dart';
|
||||
import '../../../hj_utils/pay/pay_manager.dart';
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
import '../../../tools_base/loading/loading_helper.dart';
|
||||
import '../../../tools_base/net/net_manager.dart';
|
||||
import '../../main_page/provider/msg_provider.dart';
|
||||
import '../../web_page/h5_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'pay_success_alert.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 在线支付弹窗:VIP 会员卡购买(vipProductModel) 或 金币充值(coinRcModel) 二选一,
|
||||
/// 底部弹出,展示支付方式列表 + 支付按钮;下单渠道分代充(dc)/线上跳链/金币三种。
|
||||
class OnlinePayPage extends StatefulWidget {
|
||||
final VipProductModel? vipProductModel; // 传此值:购买 VIP 会员卡
|
||||
final RechargeTypeModel? coinRcModel; // 传此值:充值金币
|
||||
/// 下单来源/实验/会话等埋点信息(透传 /mine/topay)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
const OnlinePayPage({
|
||||
super.key,
|
||||
this.vipProductModel,
|
||||
this.coinRcModel,
|
||||
this.orderTrack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OnlinePayPage> createState() => _OnlinePayPageState();
|
||||
}
|
||||
|
||||
class _OnlinePayPageState extends State<OnlinePayPage> {
|
||||
// 当前选中的支付方式下标
|
||||
int payIndex = 0;
|
||||
// 支付请求防重入标记
|
||||
bool isPaying = false;
|
||||
|
||||
// 加赠券:isCouponPanel 控制切到选券面板,coupon 为已选券
|
||||
bool isCouponPanel = false;
|
||||
CouponModel? coupon;
|
||||
|
||||
// 支付方式列表。会员卡与金币档位二选一,getter 每次访问都会重建列表,故每帧只取一次
|
||||
List<RchgType> get _payTypes =>
|
||||
widget.vipProductModel?.rchgTypeUI ??
|
||||
widget.coinRcModel!.rechargeTypeListUI;
|
||||
|
||||
/// 支付按钮文案:预售(尾款/升级/预订) → 会员卡(升级/普通) → 金币充值
|
||||
String get _payText {
|
||||
final vip = widget.vipProductModel;
|
||||
if (vip == null) return '¥${widget.coinRcModel?.moneyYuan ?? 0}/立即支付';
|
||||
if (!vip.isPreSale)
|
||||
return '¥${vip.discountedPriceUI}/${vip.isUpgrade == true ? '补差价升级' : '立即支付'}';
|
||||
// 预售:可付尾款优先,其次升级,最后预订
|
||||
final preSale = PreSaleProvider();
|
||||
final detail = preSale.preSaleModel?.detailModel;
|
||||
if (preSale.canPayBalance) return '¥${detail?.balanceAmount ?? 0}支付尾款';
|
||||
if (vip.isUpgrade == true) return '¥${(vip.advanceAmount ?? 0) ~/ 10}/立即升级';
|
||||
return '¥${detail?.advanceAmount ?? 0}立即预订';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
||||
decoration: ShapeDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 选中券类型支付方式时切到选券面板(本项目通常无券数据)
|
||||
child: isCouponPanel
|
||||
? ChoseCouponView(
|
||||
onSelect: (model) => setState(() {
|
||||
isCouponPanel = false;
|
||||
coupon = model;
|
||||
}),
|
||||
)
|
||||
: _payPanel(),
|
||||
);
|
||||
}
|
||||
|
||||
//支付主面板
|
||||
Widget _payPanel() {
|
||||
final types = _payTypes;
|
||||
final curType = types[payIndex]; // 当前选中的支付方式,与列表同源,build 时取
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const SheetHandleBar(),
|
||||
18.sizeBoxH,
|
||||
const Text(
|
||||
'选择支付方式',
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
//支付方式列表
|
||||
Wrap(children: [
|
||||
for (var i = 0; i < types.length; i++) _payItem(types[i], i)
|
||||
]),
|
||||
//支付小贴士
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 24.h, left: 16, right: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'支付小贴士:',
|
||||
style: textStyle(
|
||||
14, Colors.white.withValues(alpha: .9), FontWeight.w500),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Text(
|
||||
'''1.因超时支付无法到账,请重新发起。
|
||||
2.每天发起支付不能超过5次,连续发起且未支付,账号可能被加入黑名单。''',
|
||||
style: textStyle(
|
||||
12, Colors.white.withValues(alpha: .45), FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//支付按钮
|
||||
GestureDetector(
|
||||
onTap: () => _onPay(curType),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 44,
|
||||
decoration: ShapeDecoration(
|
||||
color: AppColors.actionRed,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_payText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20.sp,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
//客服入口
|
||||
Center(
|
||||
child: EasyRichText(
|
||||
'支付中如有问题,请咨询 在线客服',
|
||||
defaultStyle: textStyle(
|
||||
12, Colors.white.withValues(alpha: .6), FontWeight.w500),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '在线客服',
|
||||
style: textStyle(12, Color(0xffFFD460), FontWeight.w500),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
//单个支付方式
|
||||
Widget _payItem(RchgType type, int index) {
|
||||
final icon = type.getPayIcon();
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => setState(() {
|
||||
//券类型切到选券面板,其余直接切换选中
|
||||
if (type.type == 'coupon') {
|
||||
isCouponPanel = true;
|
||||
} else {
|
||||
payIndex = index;
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
height: 41,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: EdgeInsets.only(bottom: 20.h),
|
||||
child: Row(
|
||||
children: [
|
||||
icon.isEmpty
|
||||
? const SizedBox(width: 36, height: 36)
|
||||
: Image.asset(icon, width: 36),
|
||||
13.sizeBoxW,
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${type.typeName}",
|
||||
style: textStyle(
|
||||
16, Colors.white.withValues(alpha: .9), FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
payIndex == index
|
||||
? 'radio_sel.png'.commonImgPath
|
||||
: 'mine_withdraw_nor.png'.mineImgPath,
|
||||
width: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//按选中的支付方式下单:金币余额 / 代充 / 线上跳链
|
||||
void _onPay(RchgType type) {
|
||||
final vip = widget.vipProductModel;
|
||||
final coinRc = widget.coinRcModel;
|
||||
if (type.type == "coin") {
|
||||
_payByCoin();
|
||||
} else if (type.isOfficial == true) {
|
||||
// 正常购买:金币 / vip购买
|
||||
if (coinRc != null) {
|
||||
_payByDc(false, type, coinRc.daichong, coinRc.money);
|
||||
} else {
|
||||
_payByDc(true, type, vip?.daichong, (vip?.discountedPrice ?? 0) * 10);
|
||||
}
|
||||
} else {
|
||||
_payByLink(type);
|
||||
}
|
||||
}
|
||||
|
||||
///线上跳链支付:创建支付单后跳外部支付页
|
||||
Future<void> _payByLink(RchgType payType) async {
|
||||
final vip = widget.vipProductModel; // 非空 = 购买会员卡,空 = 购买金币
|
||||
final productId = vip != null ? vip.productID! : widget.coinRcModel!.id;
|
||||
if (isPaying) return;
|
||||
isPaying = true;
|
||||
LoadingHelper.showLoading();
|
||||
// 合并入口透传;仅实验 ACTIVE 时附带卡皮实验信息(copyWith 无法清 null,故重建)
|
||||
final base = widget.orderTrack ?? const PayOrderTrackInfo();
|
||||
final orderTrack = PayOrderTrackInfo(
|
||||
sourcePage: base.sourcePage ?? PaySourcePage.unknown,
|
||||
sourceRef: base.sourceRef,
|
||||
videoId: base.videoId,
|
||||
activityId: base.activityId,
|
||||
sessionId: base.sessionId,
|
||||
//短剧付费墙的归因字段,这里重建时漏抄会被服务端 8001 拒单
|
||||
mediaId: base.mediaId,
|
||||
contentId: base.contentId,
|
||||
checkoutContextId: base.checkoutContextId,
|
||||
experimentId: vipProductManager.isExperimentActive
|
||||
? (base.experimentId ??
|
||||
(vip == null
|
||||
? null
|
||||
: vip.experimentId ?? vipProductManager.experimentId))
|
||||
: null,
|
||||
experimentVariant: vipProductManager.isExperimentActive
|
||||
? (base.experimentVariant ??
|
||||
(vip == null ? null : vip.variant ?? vipProductManager.variant))
|
||||
: null,
|
||||
);
|
||||
final urlModel = await MineService.chargeGoldCoin(
|
||||
payType.type,
|
||||
productId: productId,
|
||||
isVip: vip != null,
|
||||
goldExtraID: coupon?.cId,
|
||||
finalPayStatus: vip?.isPreSale == true
|
||||
? PreSaleProvider().preSaleModel?.detailModel?.balancePayment
|
||||
: null,
|
||||
orderTrack: orderTrack,
|
||||
);
|
||||
LoadingHelper.dismissLoading();
|
||||
if (urlModel != null) {
|
||||
if (urlModel.mode == "url") {
|
||||
await launchUrlToWeb(urlModel.payUrl ?? '');
|
||||
} else if (urlModel.mode == "sdk") {
|
||||
showToast("没有找到支付类型为:${payType.type} 的sdk");
|
||||
Get.back();
|
||||
isPaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// VIP 已创建支付单:视为产生购买行为,关闭会员卡页不再报无购买关闭
|
||||
if (vip != null) VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
Get.back();
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
//支付成功刷新vip到期时间
|
||||
Get.dialog(
|
||||
const Center(child: PaySuccessAlert()),
|
||||
barrierColor: const Color(0x22000000),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
isPaying = false;
|
||||
}
|
||||
|
||||
///金币余额支付(仅会员卡有该方式)
|
||||
Future<void> _payByCoin() async {
|
||||
if (isPaying) return;
|
||||
isPaying = true;
|
||||
final vip = widget.vipProductModel;
|
||||
final track = widget.orderTrack;
|
||||
final experimentActive = vipProductManager.isExperimentActive;
|
||||
await PayManager().buyVip(
|
||||
vip?.productType,
|
||||
vip?.productID,
|
||||
vip?.productName,
|
||||
vip?.discountedPrice,
|
||||
source: 'online_pay',
|
||||
jumpWalletOnInsufficient: false, // 当前就在充值页,余额不足不跳转
|
||||
finalPayStatus: vip?.isPreSale == true
|
||||
? PreSaleProvider().preSaleModel?.detailModel?.balancePayment
|
||||
: null,
|
||||
// 与 /mine/topay 一致:仅实验 ACTIVE 时回传;sessionId 有则带上
|
||||
experimentId: experimentActive
|
||||
? (track?.experimentId ??
|
||||
vip?.experimentId ??
|
||||
vipProductManager.experimentId)
|
||||
: null,
|
||||
experimentVariant: experimentActive
|
||||
? (track?.experimentVariant ??
|
||||
vip?.variant ??
|
||||
vipProductManager.variant)
|
||||
: null,
|
||||
sessionId: track?.sessionId,
|
||||
//短剧付费墙开卡:金币余额支付这条也要带归因,否则只有第三方充值那条统计得到
|
||||
mediaId: track?.mediaId,
|
||||
contentId: track?.contentId,
|
||||
checkoutContextId: track?.checkoutContextId,
|
||||
onSuccess: (data) async {
|
||||
VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
showToast("购买成功");
|
||||
globalStore.refreshWallet();
|
||||
await globalStore.updateUserInfo();
|
||||
MineMsgProvider().refreshPayPopup(); //会员状态变了,重拉分层与付费引导开关(买完别再弹引导)
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
isPaying = false;
|
||||
}
|
||||
|
||||
///代充支付:拼装代充参数后跳 H5 收银台
|
||||
Future<void> _payByDc(
|
||||
bool isVip, RchgType payType, DCModel? dcModel, int? money) async {
|
||||
// daichong 是所有会员卡/金币档位共用的同一实例,下面的改写只为拼给 H5,必须在副本上做:
|
||||
// 原地改会把 payInfos 截成单条(支付方式列表变短→下标越界)、ordUrl 被重复拼 host
|
||||
final dc = dcModel!.clone();
|
||||
final payList = dc.traders![0].payInfos!;
|
||||
PayInfoModel? payInfo;
|
||||
for (final model in payList) {
|
||||
if (model.payMethod == payType.payMethod) {
|
||||
payInfo = model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//设置默认值
|
||||
if (dc.limit == 0) dc.limit = 500;
|
||||
final payMoney = money ?? 0;
|
||||
//超额度走大额通道
|
||||
if (payInfo!.payType!.contains(2) && payInfo.payType!.contains(3)) {
|
||||
payInfo.payType = (payMoney / 100) > dc.limit! ? [3] : [2];
|
||||
}
|
||||
if (payInfo.payType!.length >= 3) {
|
||||
payInfo.payType = (payMoney / 100) > dc.limit! ? [1, 3] : [1, 2];
|
||||
}
|
||||
dc.traders![0].payInfos = [payInfo];
|
||||
|
||||
final host = Address.baseHost!;
|
||||
dc.ordUrl = host + dc.ordUrl!;
|
||||
dc.traderUrl = host + dc.traderUrl!;
|
||||
dc.chargeMoney = payMoney ~/ 100;
|
||||
final channel = payType.channel!;
|
||||
dc.channel = channel;
|
||||
final token = await netManager.getToken();
|
||||
//bt64
|
||||
final data = base64Encode(utf8.encode(json.encode(dc)));
|
||||
if (isVip) VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
Get.to(H5Page(title: "代理充值", url: "$channel/?data=$data&token=$token"),
|
||||
opaque: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加赠券选择面板:拉取用户券列表,选中经 onSelect 回传(传 null 表示返回不选)
|
||||
class ChoseCouponView extends StatefulWidget {
|
||||
final Function(CouponModel? coupon) onSelect;
|
||||
|
||||
const ChoseCouponView({super.key, required this.onSelect});
|
||||
|
||||
@override
|
||||
State<ChoseCouponView> createState() => _ChoseCouponViewState();
|
||||
}
|
||||
|
||||
class _ChoseCouponViewState extends State<ChoseCouponView> {
|
||||
bool isLoading = true;
|
||||
final _dataSource = <CouponModel>[];
|
||||
int page = 1;
|
||||
|
||||
// refreshCtr 由 CustomRefreshView 创建并 dispose,本类只持引用,绝不能再 dispose
|
||||
RefreshController? refreshCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fetch());
|
||||
}
|
||||
|
||||
Future<void> _fetch({bool isRefresh = true}) async {
|
||||
if (isRefresh) page = 1;
|
||||
final res = await MineService.fetchUserCoupons(1, page: page);
|
||||
isLoading = false;
|
||||
|
||||
if (isRefresh) {
|
||||
refreshCtr?.refreshCompleted();
|
||||
_dataSource.clear();
|
||||
}
|
||||
_dataSource.length < 20
|
||||
? refreshCtr?.loadNoData()
|
||||
: refreshCtr?.loadComplete();
|
||||
_dataSource.addAll(res);
|
||||
page += 1;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: Column(
|
||||
children: [
|
||||
//顶部拖动条
|
||||
Container(
|
||||
height: 41,
|
||||
alignment: Alignment.center,
|
||||
child: const SheetHandleBar(color: Colors.black12),
|
||||
),
|
||||
//返回 + 标题
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => widget.onSelect(null),
|
||||
child: Image.asset(
|
||||
'common_back.png'.commonImgPath,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Colors.black.withValues(alpha: .9),
|
||||
),
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'选择加赠券',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onRefresh: (_) => _fetch(),
|
||||
onLoading: (_) => _fetch(isRefresh: false),
|
||||
child: () {
|
||||
if (isLoading) return const LoadingCenterWidget();
|
||||
if (_dataSource.isEmpty) return const CErrorWidget();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (_, __) => 12.sizeBoxH,
|
||||
itemCount: _dataSource.length,
|
||||
itemBuilder: (_, index) => _couponItem(_dataSource[index]),
|
||||
);
|
||||
}(),
|
||||
onInit: (ctr) => refreshCtr = ctr),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _couponItem(CouponModel model) {
|
||||
return Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.only(left: 24, right: 11),
|
||||
alignment: Alignment.centerLeft,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff891c).withValues(alpha: .4),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: Row(
|
||||
children: [
|
||||
//券面额
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'coin_icon.webp'.mineImgPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
4.sizeBoxW,
|
||||
Text(
|
||||
'${model.price ?? 0}',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 33 / 24),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'充值加送金币',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: .6), fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
23.sizeBoxW,
|
||||
SizedBox(
|
||||
width: 1,
|
||||
height: 45,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: .5),
|
||||
Colors.black.withValues(alpha: 0)
|
||||
], begin: Alignment.topCenter, end: Alignment.bottomCenter),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
//券说明 + 有效期
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'金币加购券',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'有效期:${model.expireTime?.utcToYMDHM()}',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
fontSize: 8,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
11.sizeBoxW,
|
||||
GestureDetector(
|
||||
onTap: () => widget.onSelect(model),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF68216),
|
||||
borderRadius: BorderRadius.circular(22)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
child: Text(
|
||||
'立即使用',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/// 创建订单(/mine/topay)的来源页,[value] 与后端约定一致
|
||||
enum PaySourcePage {
|
||||
homeUserSegment('HOME_USER_SEGMENT'), //用户分层首页弹窗
|
||||
homeFloatWindow('HOME_FLOAT_WINDOW'), //首页右下角浮窗(分层浮窗 + 活动浮窗内链)
|
||||
videoBottomBanner('VIDEO_BOTTOM_BANNER'), //视频播放页下方 banner
|
||||
videoBottomSheet('VIDEO_BOTTOM_SHEET'), //视频播放页底部弹窗(含播放页金币解锁)
|
||||
vipCenter('VIP_CENTER'), //会员中心
|
||||
h5Activity('H5_ACTIVITY'), //H5 活动
|
||||
dramaPaywall('DRAMA_PAYWALL'), //短剧付费墙(金币充值 / 开短剧卡)
|
||||
unknown('UNKNOWN'); //入口没指定
|
||||
|
||||
final String value;
|
||||
const PaySourcePage(this.value);
|
||||
}
|
||||
|
||||
/// 下单(/mine/topay)透传的埋点信息:来源 / 实验 / 会话等,均为非必填
|
||||
class PayOrderTrackInfo {
|
||||
/// 来源页
|
||||
final PaySourcePage? sourcePage;
|
||||
|
||||
/// 来源关联(如 videoId 文案标识)
|
||||
final String? sourceRef;
|
||||
|
||||
/// 关联视频 ID
|
||||
final String? videoId;
|
||||
|
||||
/// 活动 ID
|
||||
final String? activityId;
|
||||
|
||||
/// 实验 ID(VIP 卡皮 A/B)
|
||||
final String? experimentId;
|
||||
|
||||
/// 实验分组(对应接口 experimentVariant)
|
||||
final String? experimentVariant;
|
||||
|
||||
/// 会话 ID(与 VIP 卡片统计 session 对齐)
|
||||
final String? sessionId;
|
||||
|
||||
/// 短剧:当前剧 ID
|
||||
final String? mediaId;
|
||||
|
||||
/// 短剧:当前分集 ID
|
||||
final String? contentId;
|
||||
|
||||
/// 短剧:本次付费墙上下文,服务端据此把充值/开卡订单归因到这一集
|
||||
final String? checkoutContextId;
|
||||
|
||||
const PayOrderTrackInfo({
|
||||
this.sourcePage,
|
||||
this.sourceRef,
|
||||
this.videoId,
|
||||
this.activityId,
|
||||
this.experimentId,
|
||||
this.experimentVariant,
|
||||
this.sessionId,
|
||||
this.mediaId,
|
||||
this.contentId,
|
||||
this.checkoutContextId,
|
||||
});
|
||||
|
||||
PayOrderTrackInfo copyWith({
|
||||
PaySourcePage? sourcePage,
|
||||
String? sourceRef,
|
||||
String? videoId,
|
||||
String? activityId,
|
||||
String? experimentId,
|
||||
String? experimentVariant,
|
||||
String? sessionId,
|
||||
String? mediaId,
|
||||
String? contentId,
|
||||
String? checkoutContextId,
|
||||
}) {
|
||||
return PayOrderTrackInfo(
|
||||
sourcePage: sourcePage ?? this.sourcePage,
|
||||
sourceRef: sourceRef ?? this.sourceRef,
|
||||
videoId: videoId ?? this.videoId,
|
||||
activityId: activityId ?? this.activityId,
|
||||
experimentId: experimentId ?? this.experimentId,
|
||||
experimentVariant: experimentVariant ?? this.experimentVariant,
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
mediaId: mediaId ?? this.mediaId,
|
||||
contentId: contentId ?? this.contentId,
|
||||
checkoutContextId: checkoutContextId ?? this.checkoutContextId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_dialog.dart';
|
||||
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
|
||||
class PaySuccessAlert extends StatelessWidget {
|
||||
const PaySuccessAlert({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonDialog(
|
||||
child: _buildContent(),
|
||||
);
|
||||
}
|
||||
|
||||
_buildContent() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"支付确认",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
0.5.line,
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'''1.订单支付后,一般会在1-10分钟内到账,如超时未到账,请联系在线客服为您处理。
|
||||
2.受特殊行业限制,如支付失败可尝试重新发起订单,系统将会随机切换备用的支付通道。
|
||||
3.本APP有稳定的广告收益,产品稳定安全,请放心充值使用,如遇报毒提示忽略即可。''',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55),
|
||||
fontSize: 14,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToCustomService(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'支付遇到问题',
|
||||
style: TextStyle(color: Color(0xff999999), fontSize: 14),
|
||||
),
|
||||
2.sizeBoxW,
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
)
|
||||
],
|
||||
)),
|
||||
24.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
"支付成功",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:hgdj/hj_model/mine/vip_card_analytics_event.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_vip/vip_product_manager.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_vip/vip_support_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// VIP 卡片统计:一次进入会员卡页/购买弹窗对应一个 session,关闭时按是否下单上报。
|
||||
/// 曝光在 UI 实际展示后(post-frame)上报。
|
||||
class VipCardAnalyticsSession {
|
||||
VipCardAnalyticsSession() {
|
||||
// 进页时快照实验上下文:关闭时 manager 的原始数据可能已被重拉/失效,不能再读 live manager
|
||||
_experimentActive = vipProductManager.isExperimentActive;
|
||||
_experimentId = vipProductManager.experimentId;
|
||||
_variant = vipProductManager.variant;
|
||||
_active.add(this);
|
||||
}
|
||||
|
||||
static final _uuid = Uuid();
|
||||
static final Set<VipCardAnalyticsSession> _active = {};
|
||||
|
||||
/// 当前访问会话 ID(同一次进入会员卡 UI 内事件共用)
|
||||
final String sessionId = _uuid.v4();
|
||||
|
||||
/// 进页瞬间的实验快照(关闭上报必须用这份,避免中途 force 重拉把 live 态冲掉)
|
||||
late final bool _experimentActive;
|
||||
late final String? _experimentId;
|
||||
late final String? _variant;
|
||||
|
||||
bool _pageViewReported = false;
|
||||
bool _purchased = false;
|
||||
bool _closed = false;
|
||||
String? _lastImpressedProductId;
|
||||
|
||||
/// 同一帧内 jumpToPage + onPageChanged 去重
|
||||
String? _pendingImpressionProductId;
|
||||
|
||||
static String _nowUtc() =>
|
||||
DateTimeUtil.format2utc(DateTime.now().toUtc()) ?? '';
|
||||
|
||||
/// DISABLED / 无实验 ID:不上报(空 experimentId 会参数错误)
|
||||
bool get _canReport {
|
||||
if (!_experimentActive) return false;
|
||||
final id = _experimentId;
|
||||
return id != null && id.isNotEmpty;
|
||||
}
|
||||
|
||||
VipCardAnalyticsEvent _build({
|
||||
required String eventName,
|
||||
String? productId,
|
||||
int? price,
|
||||
}) {
|
||||
return VipCardAnalyticsEvent(
|
||||
eventId: _uuid.v4(),
|
||||
eventName: eventName,
|
||||
sessionId: sessionId,
|
||||
occurredAt: _nowUtc(),
|
||||
experimentId: _experimentId,
|
||||
variant: _variant,
|
||||
productId: productId,
|
||||
price: price,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _send(List<VipCardAnalyticsEvent> events) async {
|
||||
if (!_canReport || events.isEmpty) return;
|
||||
try {
|
||||
await MineService.reportAnalyticsEvents(events);
|
||||
} catch (e) {
|
||||
debugLog('VipCardAnalytics', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 卡皮页展示:UI 就绪后只报一次
|
||||
void reportPageViewAfterPaint() {
|
||||
if (_pageViewReported || _closed || !_canReport) return;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pageViewReported || _closed || !_canReport) return;
|
||||
_pageViewReported = true;
|
||||
_send([_build(eventName: VipCardAnalyticsEventName.pageView)]);
|
||||
});
|
||||
}
|
||||
|
||||
/// 套餐曝光:选中 / 默认选中各算一次;同 product 连续重复不重复报
|
||||
void reportProductImpression(VipProductModel? product,
|
||||
{bool afterPaint = false}) {
|
||||
final productId = product?.productID;
|
||||
if (productId == null || productId.isEmpty || _closed || !_canReport)
|
||||
return;
|
||||
if (_lastImpressedProductId == productId) return;
|
||||
|
||||
void doReport() {
|
||||
if (_closed || !_canReport) return;
|
||||
if (_lastImpressedProductId == productId) return;
|
||||
_lastImpressedProductId = productId;
|
||||
_pendingImpressionProductId = null;
|
||||
_send([
|
||||
_build(
|
||||
eventName: VipCardAnalyticsEventName.productImpression,
|
||||
productId: productId,
|
||||
price: product?.discountedPrice,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (afterPaint) {
|
||||
_pendingImpressionProductId = productId;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pendingImpressionProductId != productId) return;
|
||||
doReport();
|
||||
});
|
||||
} else {
|
||||
doReport();
|
||||
}
|
||||
}
|
||||
|
||||
/// 下单成功:本访问内关闭不再报「无购买关闭」
|
||||
void markPurchased() => _purchased = true;
|
||||
|
||||
/// 活跃会话均标记已购买(支付页成功回调用)
|
||||
static void markPurchaseOnActiveSessions() {
|
||||
for (final s in _active) {
|
||||
s.markPurchased();
|
||||
}
|
||||
}
|
||||
|
||||
/// 无购买关闭
|
||||
void reportCloseWithoutPurchaseIfNeeded() {
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
_active.remove(this);
|
||||
if (_purchased) {
|
||||
debugLog(
|
||||
'VipCardAnalytics', 'skip CLOSE_WITHOUT_PURCHASE: already purchased');
|
||||
return;
|
||||
}
|
||||
if (!_canReport) {
|
||||
debugLog(
|
||||
'VipCardAnalytics',
|
||||
'skip CLOSE_WITHOUT_PURCHASE: canReport=false active=$_experimentActive id=$_experimentId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 关闭瞬间触发上报;不 await,避免卡住 onClose,但请求走全局 http 不会随页面 dispose 取消
|
||||
_send([_build(eventName: VipCardAnalyticsEventName.closeWithoutPurchase)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../main_page/provider/msg_provider.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
import 'vip_ui_kit.dart';
|
||||
|
||||
class VipCardItem extends StatefulWidget {
|
||||
final Function()? callback;
|
||||
final bool isSelect;
|
||||
final VipProductModel model;
|
||||
|
||||
const VipCardItem(this.model,
|
||||
{super.key, this.callback, this.isSelect = false});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _VipCardItemState();
|
||||
}
|
||||
}
|
||||
|
||||
class _VipCardItemState extends State<VipCardItem> {
|
||||
/// 分层倒计时停表后(如已是会员),新人卡仍需本地秒级刷新
|
||||
final ValueNotifier<int> _localTick = ValueNotifier<int>(0);
|
||||
Timer? _localTimer;
|
||||
|
||||
/// 仅按套餐 badgeType 决定角标,不看名称 / sort / 默认选中 / actionDesc
|
||||
String get _badgeType => (widget.model.badgeType ?? '').trim().toUpperCase();
|
||||
|
||||
String? get _badgeLabel {
|
||||
switch (_badgeType) {
|
||||
case 'MOST_POPULAR':
|
||||
case 'NEW_USER_OFFER':
|
||||
final text = widget.model.badgeText?.trim() ?? '';
|
||||
if (text.isNotEmpty) return text;
|
||||
// NEW_USER_OFFER 无文案时兜底
|
||||
return _badgeType == 'NEW_USER_OFFER' ? '新人特惠' : null;
|
||||
default:
|
||||
// 角标只认 badgeType / badgeText,不回退 actionDesc / desc
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isMostPopularBadge => _badgeType == 'MOST_POPULAR';
|
||||
|
||||
/// 仅 productType==5(新人卡)展示倒计时
|
||||
bool get _isNewerOfferCard => widget.model.productType == 5;
|
||||
|
||||
/// 当前应展示的倒计时:仅新人卡;优先分层 lastDiscountTime;会员态无分层时用卡 showCountdownTime(小时) 滚动
|
||||
({String hour, String min, String sec})? get _countdownParts {
|
||||
if (!_isNewerOfferCard) return null;
|
||||
final layered = MineMsgProvider().countdownConfig;
|
||||
if (layered != null) {
|
||||
return (
|
||||
hour: layered.discountHour,
|
||||
min: layered.discountMin,
|
||||
sec: layered.discountSec
|
||||
);
|
||||
}
|
||||
final hours = widget.model.showCountdownTime ?? 0;
|
||||
if (hours <= 0) return null;
|
||||
final period = hours * 3600;
|
||||
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
var remain = period - (nowSec % period);
|
||||
if (remain <= 0) remain = period;
|
||||
final h = (remain ~/ 3600).toString().padLeft(2, '0');
|
||||
final m = ((remain ~/ 60) % 60).toString().padLeft(2, '0');
|
||||
final s = (remain % 60).toString().padLeft(2, '0');
|
||||
return (hour: h, min: m, sec: s);
|
||||
}
|
||||
|
||||
bool get _needLocalTick =>
|
||||
_isNewerOfferCard &&
|
||||
MineMsgProvider().countdownConfig == null &&
|
||||
(widget.model.showCountdownTime ?? 0) > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncLocalTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VipCardItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncLocalTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_localTimer?.cancel();
|
||||
_localTick.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncLocalTimer() {
|
||||
if (_needLocalTick) {
|
||||
_localTimer ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
_localTick.value++;
|
||||
});
|
||||
} else {
|
||||
_localTimer?.cancel();
|
||||
_localTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
String get _title => (widget.model.newName?.trim().isNotEmpty == true)
|
||||
? widget.model.newName!.trim()
|
||||
: (widget.model.productName ?? '');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_syncLocalTimer();
|
||||
// variant=B:组合卡 UI;A / DISABLED:接口卡图
|
||||
if (vipProductManager.isNewVipUi) {
|
||||
return _buildVariantBCard();
|
||||
}
|
||||
return _buildClassicImageCard();
|
||||
}
|
||||
|
||||
/// 旧版:选中/未选中靠接口下发卡面图 + 角标/新人倒计时叠加
|
||||
Widget _buildClassicImageCard() {
|
||||
final badgeLabel = _badgeLabel;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: widget.callback,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 9,
|
||||
bottom: 10,
|
||||
right: 0,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: widget.isSelect
|
||||
? (widget.model.realSelectVipImage().isEmpty
|
||||
? widget.model.realNormalVipImage()
|
||||
: widget.model.realSelectVipImage())
|
||||
: widget.model.realNormalVipImage(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
if (badgeLabel != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildBadge(badgeLabel),
|
||||
),
|
||||
if (_isNewerOfferCard)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _buildNewerTimer(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// variant=B:148×157 组合卡;选中金渐变+#B2FFF6DC 边;未选中 #211C12+#14FFFFFF 边
|
||||
Widget _buildVariantBCard() {
|
||||
final selected = widget.isSelect;
|
||||
final badgeLabel = _badgeLabel;
|
||||
const cardW = 148.0;
|
||||
const cardH = 157.0;
|
||||
const badgeTop = 8.0;
|
||||
// VIP/Glow Gold:Y10+Blur28,底部预留绘制空间,避免被轮播裁切
|
||||
const glowPadBottom = 24.0;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: widget.callback,
|
||||
child: SizedBox(
|
||||
width: cardW,
|
||||
height: cardH + badgeTop + glowPadBottom,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: badgeTop,
|
||||
height: cardH,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: selected
|
||||
? const [
|
||||
// VIP/Glow Gold:Y10 / Blur28 / Spread-6 / #DEAB54 35%
|
||||
BoxShadow(
|
||||
color: Color(0x59DEAB54),
|
||||
offset: Offset(0, 10),
|
||||
blurRadius: 28,
|
||||
spreadRadius: -6,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 底色 / 渐变
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? null : const Color(0xff211C12),
|
||||
gradient: selected
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xffFBF0CD),
|
||||
Color(0xffE5B75D)
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// 内容(含底栏 desc)
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: _buildBCardBody(selected)),
|
||||
_buildBCardFooter(selected),
|
||||
],
|
||||
),
|
||||
// 边框置顶,避免被底栏遮住;新人卡选中不描边
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: (selected && _isNewerOfferCard)
|
||||
? null
|
||||
: Border.all(
|
||||
color: selected
|
||||
? const Color(0xB2FFF6DC)
|
||||
: const Color(0x14FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (badgeLabel != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildBadge(badgeLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBCardBody(bool selected) {
|
||||
final titleColor = selected ? const Color(0xff3B2B0E) : Color(0xffF6EEDC);
|
||||
final priceColor =
|
||||
selected ? const Color(0xff3B2B0E) : const Color(0xffF6EEDC);
|
||||
final originColor =
|
||||
selected ? const Color(0xff6C644F) : const Color(0xff9A927C);
|
||||
final showOrigin = widget.model.originalPriceUI > 0 &&
|
||||
widget.model.originalPriceUI != widget.model.discountedPriceUI;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 22, 10, 8),
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: titleColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
EasyRichText(
|
||||
'¥${widget.model.discountedPriceUI}',
|
||||
defaultStyle: TextStyle(
|
||||
color: priceColor,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '¥',
|
||||
matchWordBoundaries: false,
|
||||
matchOption: 'first',
|
||||
style: TextStyle(
|
||||
color: priceColor,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showOrigin) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'¥${widget.model.originalPriceUI}',
|
||||
style: TextStyle(
|
||||
color: originColor,
|
||||
fontSize: 13,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationColor: originColor,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// 无原价时占位短横,对齐设计稿
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
'-',
|
||||
style: TextStyle(color: originColor, fontSize: 13, height: 1.2),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBCardFooter(bool selected) {
|
||||
if (!_isNewerOfferCard) return _buildBDurationFooter(selected);
|
||||
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _needLocalTick ? _localTick : MineMsgProvider().tick,
|
||||
builder: (_, __, ___) {
|
||||
final cd = _countdownParts;
|
||||
if (cd != null) {
|
||||
if (selected) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
alignment: Alignment.center,
|
||||
color: const Color(0xffC14A38),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_timerBox(cd.hour),
|
||||
_timerColon(),
|
||||
_timerBox(cd.min),
|
||||
_timerColon(),
|
||||
_timerBox(cd.sec),
|
||||
4.sizeBoxW,
|
||||
const Text(
|
||||
'结束',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
alignment: Alignment.center,
|
||||
color: const Color(0xff16140F),
|
||||
child: Text(
|
||||
'${cd.hour}:${cd.min}:${cd.sec}后失效',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff8A8A8A),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildBDurationFooter(selected);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 无倒计时时的底栏:取套餐 desc
|
||||
Widget _buildBDurationFooter(bool selected) {
|
||||
final text = widget.model.desc?.trim() ?? '';
|
||||
if (text.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
alignment: Alignment.center,
|
||||
color: selected ? const Color(0xff2B2314) : const Color(0xff1A150C),
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: selected ? const Color(0xffFFFFFF) : const Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(String label) {
|
||||
final isMostPopular = _isMostPopularBadge;
|
||||
final useB = vipProductManager.isNewVipUi;
|
||||
// B 未选中:统一深底金边/金字;B 选中 / A:优先 uiConfig.badgeStyles
|
||||
final useApiStyle = useB ? widget.isSelect : true;
|
||||
Color? apiBg;
|
||||
Color? apiFg;
|
||||
if (useApiStyle) {
|
||||
final style = vipProductManager.badgeStyleFor(_badgeType);
|
||||
apiBg = _parseHexColor(style?.backgroundColor);
|
||||
apiFg = _parseHexColor(style?.textColor);
|
||||
}
|
||||
final Color bg;
|
||||
final Color fg;
|
||||
final Border? border;
|
||||
if (useB && !widget.isSelect) {
|
||||
bg = const Color(0xCC1A1A1A);
|
||||
fg = const Color(0xFFE8C078);
|
||||
border = Border.all(color: const Color(0xFFE8C078), width: 0.8);
|
||||
} else {
|
||||
bg = apiBg ??
|
||||
(isMostPopular ? const Color(0xCC1A1A1A) : const Color(0xffE1351F));
|
||||
fg = apiFg ??
|
||||
(isMostPopular ? const Color(0xFFE8C078) : const Color(0xE5FFFFFF));
|
||||
border = (apiBg == null && isMostPopular)
|
||||
? Border.all(color: const Color(0xFFE8C078), width: 0.8)
|
||||
: null;
|
||||
}
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 111),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: bg,
|
||||
border: border,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析 #RRGGBB / RRGGBB / #AARRGGBB;非法或空返回 null
|
||||
Color? _parseHexColor(String? raw) {
|
||||
var hex = (raw ?? '').trim().toUpperCase().replaceAll('#', '');
|
||||
if (hex.isEmpty) return null;
|
||||
if (hex.length == 6) hex = 'FF$hex';
|
||||
if (hex.length != 8) return null;
|
||||
final value = int.tryParse(hex, radix: 16);
|
||||
return value == null ? null : Color(value);
|
||||
}
|
||||
|
||||
/// A / DISABLED 旧版新人倒计时:卡底橙色小标签「HH:MM:SS」
|
||||
Widget _buildNewerTimer() {
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _needLocalTick ? _localTick : MineMsgProvider().tick,
|
||||
builder: (_, __, ___) {
|
||||
final cd = _countdownParts;
|
||||
if (cd == null) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF68804),
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
child: Text(
|
||||
'${cd.hour}:${cd.min}:${cd.sec}',
|
||||
style: const TextStyle(color: Color(0xff000000), fontSize: 10),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timerBox(String value) {
|
||||
return Container(
|
||||
width: 17,
|
||||
height: 16,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timerColon() => const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Text(':',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600)),
|
||||
);
|
||||
}
|
||||
|
||||
class VipProductPrivilegeItem extends StatelessWidget {
|
||||
final NewPrivilege? model;
|
||||
final bool isShowOnePrivilege;
|
||||
|
||||
/// true:旧版白字圆图标样式(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const VipProductPrivilegeItem(
|
||||
this.model, {
|
||||
super.key,
|
||||
this.isShowOnePrivilege = false,
|
||||
this.classic = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) {
|
||||
return Column(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
),
|
||||
3.sizeBoxH,
|
||||
Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withValues(alpha: .9)),
|
||||
),
|
||||
if (!isShowOnePrivilege) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(0),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(1),
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 9,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
),
|
||||
6.sizeBoxH,
|
||||
VipGradientMask(
|
||||
child: Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
),
|
||||
if (!isShowOnePrivilege) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(0),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
_descLine(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 9,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _descLine(int index) {
|
||||
final raw = model?.privilegeDesc ?? '';
|
||||
final bySpace = raw.split(' ');
|
||||
if (bySpace.length > 1) {
|
||||
return index < bySpace.length ? bySpace[index].trim() : '';
|
||||
}
|
||||
final byLine = raw.split('\n');
|
||||
return index < byLine.length ? byLine[index].trim() : '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 核心权益卡片
|
||||
/// - classic=false:左图标 + 右标题/描述(会员中心 B / 购买弹窗)
|
||||
/// - classic=true:72×72 金边方卡(会员中心 A / DISABLED)
|
||||
class VipCorePrivilegeCard extends StatelessWidget {
|
||||
final NewPrivilege? model;
|
||||
final bool classic;
|
||||
|
||||
const VipCorePrivilegeCard(this.model, {super.key, this.classic = false});
|
||||
|
||||
static const _radius = 16.0;
|
||||
|
||||
static const _fillGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x12FFFFFF), // #FFFFFF 7.06%
|
||||
Color(0x05FFFFFF), // #FFFFFF 7.06%
|
||||
],
|
||||
);
|
||||
|
||||
static const _borderGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x29FFFFFF), // #FFFFFF 16.08%
|
||||
Color(0x0AFFFFFF), // #FFFFFF 3.92%
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) return _buildClassic();
|
||||
return Container(
|
||||
width: 168,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1AFFFFFF),
|
||||
offset: Offset(0, 1),
|
||||
blurRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x4D000000),
|
||||
offset: Offset(0, 8),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -8,
|
||||
),
|
||||
],
|
||||
),
|
||||
// 渐变描边用 CustomPaint 画 stroke,避免双层 Container「假边框」透出底层背景
|
||||
child: CustomPaint(
|
||||
painter: _GradientBorderPainter(
|
||||
gradient: _borderGradient,
|
||||
strokeWidth: 1,
|
||||
radius: _radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: _fillGradient),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildIcon(),
|
||||
10.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF6EEDC),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
_buildDesc(model?.privilegeDesc ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 旧版:正方形金框 + 深棕底图 + 图标/标题/描述
|
||||
Widget _buildClassic() {
|
||||
return Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('core_privilege_bg.webp'.videoPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xffFFF576), width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 0),
|
||||
5.sizeBoxH,
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => const LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xffFF9642), Color(0xffFFE7BD), Color(0xffFCCF36)],
|
||||
stops: [0.066, 0.49, 0.914],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Text(
|
||||
model?.privilegeName ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
3.sizeBoxH,
|
||||
Text(
|
||||
model?.privilegeDesc ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Color(0xffB7A463), fontSize: 9),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0x14FFFFFF),
|
||||
border: Border.all(color: const Color(0x66DCAD55), width: 0.8),
|
||||
),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 设计稿描述:前半灰、后半金(支持空格分隔;否则前 4 字灰、其余金)
|
||||
Widget _buildDesc(String raw) {
|
||||
if (raw.isEmpty) return const SizedBox.shrink();
|
||||
return Text(
|
||||
raw,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: Color(0xff9A927C), fontSize: 11, height: 1.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1px 内渐变描边(stroke),不占用布局厚度,避免双层 Container 透底
|
||||
class _GradientBorderPainter extends CustomPainter {
|
||||
final Gradient gradient;
|
||||
final double strokeWidth;
|
||||
final double radius;
|
||||
|
||||
_GradientBorderPainter({
|
||||
required this.gradient,
|
||||
required this.strokeWidth,
|
||||
required this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
final inset = strokeWidth / 2;
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
rect.deflate(inset),
|
||||
Radius.circular(math.max(0, radius - inset)),
|
||||
);
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..shader = gradient.createShader(rect);
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _GradientBorderPainter oldDelegate) {
|
||||
return oldDelegate.gradient != gradient ||
|
||||
oldDelegate.strokeWidth != strokeWidth ||
|
||||
oldDelegate.radius != radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_alert.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
|
||||
import 'mine_charge_vip_logic.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员支付按钮:会员卡页 MineChargeVipPage 与购买弹窗 BuyVipAlert 共用
|
||||
/// [classic] true:旧版红底/升级金橙;false:改版金渐变 CTA
|
||||
class VipPayButton extends StatelessWidget {
|
||||
final MineChargeVipLogic logic;
|
||||
final bool classic;
|
||||
|
||||
const VipPayButton(this.logic, {super.key, this.classic = false});
|
||||
|
||||
static const _ctaGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xffFBEDC2),
|
||||
Color(0xffEFCB84),
|
||||
Color(0xffD9A346),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final product = logic.currentProductModel;
|
||||
//预售首付或非升级预售 → 预售按钮(透传会员中心 sessionId / 实验字段)
|
||||
if (product?.isPreSale == true && product?.isUpgrade == false) {
|
||||
return PreSaleActivityButton(onTap: () async {
|
||||
await PreSaleProvider()
|
||||
.startPay(product!, orderTrack: logic.buildOrderTrack());
|
||||
logic.update();
|
||||
});
|
||||
}
|
||||
if (classic) return _buildClassic(product);
|
||||
return GestureDetector(
|
||||
onTap: logic.onInitiatePayAction,
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x66DEAB54),
|
||||
offset: Offset(0, 10),
|
||||
blurRadius: 30,
|
||||
spreadRadius: -6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(gradient: _ctaGradient),
|
||||
child: SizedBox.expand(),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
const Color(0x8CFFFFFF),
|
||||
const Color(0x00FFFFFF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_payText,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff3D2914),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 旧版:升级卡金橙渐变 / 普通卡红底
|
||||
Widget _buildClassic(VipProductModel? product) {
|
||||
final isUpgrade = product?.isUpgrade ?? false;
|
||||
return GestureDetector(
|
||||
onTap: logic.onInitiatePayAction,
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
gradient: isUpgrade
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFFFFAF50), Color(0xFFE75100)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
)
|
||||
: null,
|
||||
color: isUpgrade ? null : AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
_payText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String get _payText {
|
||||
final product = logic.currentProductModel;
|
||||
if (product == null) return '';
|
||||
if (product.isPreSale) {
|
||||
if (product.isUpgrade ?? false) {
|
||||
return '${(product.advanceAmount ?? 0) ~/ 10}元/立即升级';
|
||||
}
|
||||
if (PreSaleProvider().canPayBalance) {
|
||||
return '¥${PreSaleProvider().preSaleModel?.detailModel?.balanceAmount ?? 0}/支付尾款';
|
||||
}
|
||||
return '¥${PreSaleProvider().preSaleModel?.detailModel?.advanceAmount ?? 0}/立即预订';
|
||||
}
|
||||
if (product.isUpgrade ?? false) {
|
||||
return '¥${product.discountedPriceUI}/补差价升级';
|
||||
}
|
||||
return '¥${product.discountedPriceUI}/立即支付';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../../../hj_page/main_page/provider/msg_provider.dart';
|
||||
import '../../../hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员卡 / 短剧卡列表统一数据源(全站唯一):请求 / 缓存 / 组装 / 选卡兜底 / 按卡ID直接拉支付。
|
||||
/// 会员中心页、购买弹窗、分层横幅等「需要卡列表」的地方用 Consumer<VipProductManager> 消费。
|
||||
/// 页面级状态(当前选中卡 / 轮播控制器 / 支付触发)仍留在各自 Logic,避免多入口串味。
|
||||
class VipProductManager with ChangeNotifier {
|
||||
static final VipProductManager _instance = VipProductManager._();
|
||||
factory VipProductManager() => _instance;
|
||||
VipProductManager._();
|
||||
|
||||
/// 按卡 id 取卡,会员卡和短剧卡共用。id 空/匹配不上都给 null,让调用方用 `??` 往下一级兜。
|
||||
/// 空串必须当「未指定」:分层/实验/短剧默认卡没配时下发的都是空串,
|
||||
/// 不拦会跟 productID 为空的脏数据撞上
|
||||
VipProductModel? _pick(List<VipProductModel> list, String? id) =>
|
||||
(id ?? '').isEmpty
|
||||
? null
|
||||
: list.firstWhereOrNull((e) => e.productID == id);
|
||||
|
||||
// ===== 会员卡数据 =====
|
||||
|
||||
/// 原始接口数据(含代充/预售卡组/升级卡等 / 实验态)
|
||||
VipSupportModel? _raw;
|
||||
|
||||
/// 分层变化等场景标记需重拉;不清 _raw,避免已挂载会员页丢失实验态(B 皮闪回 A)
|
||||
bool _needsReload = false;
|
||||
|
||||
/// 组装后的会员卡列表(常规卡 + 符合条件的预售卡,按 sort 排序)——全站共享一份
|
||||
final vipCards = <VipProductModel>[];
|
||||
|
||||
/// 加载态
|
||||
bool isLoading = true;
|
||||
|
||||
/// 预售卡分组(会员中心页据此判断是否显示预售入口)
|
||||
VipProductListModel? presaleGroup;
|
||||
|
||||
// ===== A/B 实验 =====
|
||||
|
||||
/// 实验是否生效(ACTIVE);DISABLED 时不挂实验字段、不上报 analytics、topay 不传实验参数
|
||||
bool get isExperimentActive => _raw?.isExperimentActive == true;
|
||||
|
||||
/// 会员中心 UI:仅 ACTIVE 且 variant=B 用改版;A / DISABLED / 无实验时 variant 恒为 null,自然走旧版
|
||||
bool get isNewVipUi => (variant ?? '').trim().toUpperCase() == 'B';
|
||||
|
||||
/// 实验字段(仅 ACTIVE 时有值,下单时原样回传)
|
||||
String? get experimentId => isExperimentActive ? _raw?.experimentId : null;
|
||||
|
||||
String? get variant => isExperimentActive ? _raw?.variant : null;
|
||||
|
||||
/// 实验指定的默认卡 id。和短剧卡的 [_dramaCardId] 是两码事,别混
|
||||
String? get _abCardId => isExperimentActive ? _raw?.defaultProductId : null;
|
||||
|
||||
/// 皮肤配置:ACTIVE 时才消费;DISABLED / 无实验不走接口皮肤
|
||||
VipUiConfig? get _uiConfig => isExperimentActive ? _raw?.uiConfig : null;
|
||||
|
||||
/// 会员中心背景图:接口给了就用网络图,否则 null(走本地默认)
|
||||
String? get vipBgImage {
|
||||
final url = (_uiConfig?.backgroundImage ?? '').trim();
|
||||
return url.isEmpty ? null : url;
|
||||
}
|
||||
|
||||
/// 角标配色:按套餐 badgeType 匹配皮肤配置
|
||||
VipBadgeStyle? badgeStyleFor(String? badgeType) =>
|
||||
_uiConfig?.styleFor(badgeType);
|
||||
|
||||
// ===== 会员卡:请求 / 组装 =====
|
||||
|
||||
/// 请求会员卡列表。[force] 为 false 且缓存有效时,直接用缓存重组,不发请求。
|
||||
Future<void> loadVipCards({bool force = false}) async {
|
||||
if (_raw != null && !force && !_needsReload) {
|
||||
_rebuild();
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
final result = await MineService.getVipProduct();
|
||||
// 容错:请求失败或返回空数据时,保留上次成功的数据,只复位加载态,不覆盖 _raw/vipCards
|
||||
if (result == null || (result.list?.isEmpty ?? true)) {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
_raw = result;
|
||||
_needsReload = false;
|
||||
_rebuild();
|
||||
}
|
||||
|
||||
/// 缓存失效:标记下次 load 强制重拉;保留 _raw/vipCards,避免购卡后已挂载页实验皮闪回 A。
|
||||
/// (分层状态变化时调用)
|
||||
void invalidate() {
|
||||
_needsReload = true;
|
||||
}
|
||||
|
||||
/// 切换账号后:清掉上个用户的数据并按新用户权限重新拉取。
|
||||
/// 卡列表(尤其预售卡)由后端按用户权限/购买状态返回,切号必须重拉,否则残留上个账号的卡。
|
||||
Future<void> reloadForUser() async {
|
||||
_raw = null;
|
||||
_needsReload = true;
|
||||
vipCards.clear();
|
||||
await loadVipCards(force: true);
|
||||
}
|
||||
|
||||
/// 用 _raw 组装 vipCards(会员卡组 + 符合条件的预售卡)
|
||||
void _rebuild() {
|
||||
isLoading = false;
|
||||
presaleGroup = _raw?.list?.firstWhereOrNull((e) => e.position == '预售卡');
|
||||
vipCards
|
||||
..clear()
|
||||
..addAll(_assemble(_raw));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 纯组装:会员卡组 + 符合条件的预售卡。无副作用,供 _rebuild 与 payByVipCard 共用。
|
||||
/// 卡序统一按 sort 升序(含卡皮 ACTIVE)
|
||||
List<VipProductModel> _assemble(VipSupportModel? raw) {
|
||||
final cardGroup = raw?.list?.firstWhereOrNull((e) => e.position == '会员卡');
|
||||
final presale = raw?.list?.firstWhereOrNull((e) => e.position == '预售卡');
|
||||
final list = <VipProductModel>[...(cardGroup?.list ?? [])];
|
||||
// 优先添加预售卡(预售活动中且未付全款)
|
||||
if ((presale?.list ?? []).isNotEmpty &&
|
||||
PreSaleProvider().advanceStatus?.activityStatus == true &&
|
||||
!PreSaleProvider().isPayAll) {
|
||||
list.add(presale!.list!.first..isPreSale = true);
|
||||
}
|
||||
list.sort((a, b) => (a.sort ?? 0).compareTo(b.sort ?? 0));
|
||||
return list;
|
||||
}
|
||||
|
||||
// ===== 会员卡:选卡 / 支付 =====
|
||||
|
||||
/// 默认选中卡:外部指定卡ID > 分层卡 > 实验默认卡 > 升级差价卡 > 保留当前 > 第一张。
|
||||
/// 选中态归页面 Logic,这里只按 vipCards 计算,[vipID]/[current] 由调用方透传。
|
||||
VipProductModel? defaultVipCard({String? vipID, VipProductModel? current}) {
|
||||
if (vipCards.isEmpty) return null;
|
||||
return _pick(vipCards, vipID) ??
|
||||
_pick(vipCards, MineMsgProvider().payTier?.config?.vipCard) ??
|
||||
_pick(vipCards, _abCardId) ??
|
||||
(globalStore.meInfo?.isUpgrade == true
|
||||
? _cheapestUpgrade(vipCards)
|
||||
: null) ??
|
||||
_pick(vipCards, current?.productID) ?? // 二次刷新时保留当前选中
|
||||
vipCards.first;
|
||||
}
|
||||
|
||||
/// 按卡 ID 直接拉起支付弹窗(跳过会员中心列表页)。无数据时先请求一次。
|
||||
/// 选卡:传入 [cardId] > 分层卡 > 实验默认卡 > 升级差价卡 > 第一张
|
||||
/// [reportAnalytics] 是否上报 VIP 卡皮事件(PAGE_VIEW / 曝光 / CLOSE);视频底部分层 banner 等入口传 false
|
||||
Future<void> payByVipCard(
|
||||
String? cardId, {
|
||||
PayOrderTrackInfo? orderTrack,
|
||||
bool reportAnalytics = true,
|
||||
}) async {
|
||||
var raw = _raw;
|
||||
if (raw == null || _needsReload) {
|
||||
LoadingHelper.showLoading();
|
||||
try {
|
||||
raw = await MineService.getVipProduct();
|
||||
} finally {
|
||||
LoadingHelper.dismissLoading(); //无论成功/异常都关 loading,避免请求抛异常时卡死
|
||||
}
|
||||
// 缓存供后续复用,但不触碰 isLoading/vipCards/notify,避免打扰已挂载的会员卡列表 UI
|
||||
if (raw != null && (raw.list?.isNotEmpty ?? false)) {
|
||||
_raw = raw;
|
||||
_needsReload = false;
|
||||
}
|
||||
}
|
||||
// 用当前预售状态现算列表(不依赖共享 vipCards,也不受其重拉影响)
|
||||
final list = _assemble(raw);
|
||||
if (list.isEmpty) {
|
||||
showToast("未获取到会员卡~");
|
||||
return;
|
||||
}
|
||||
final card = _pick(list, cardId) ??
|
||||
_pick(list, MineMsgProvider().payTier?.config?.vipCard) ??
|
||||
_pick(list, _abCardId) ??
|
||||
(globalStore.meInfo?.isUpgrade == true
|
||||
? _cheapestUpgrade(list)
|
||||
: null) ??
|
||||
list.first;
|
||||
if (card.rchgTypeUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
// 直拉支付弹窗没有 MineChargeVipLogic:需要卡皮埋点时自建 session,关弹窗报 CLOSE
|
||||
final analytics = reportAnalytics ? VipCardAnalyticsSession() : null;
|
||||
final base = orderTrack ?? const PayOrderTrackInfo();
|
||||
final track = PayOrderTrackInfo(
|
||||
sourcePage: base.sourcePage ?? PaySourcePage.unknown,
|
||||
sourceRef: base.sourceRef,
|
||||
videoId: base.videoId,
|
||||
activityId: base.activityId,
|
||||
sessionId: base.sessionId ?? analytics?.sessionId,
|
||||
//短剧付费墙的归因字段,重建时漏抄就丢了(同 OnlinePayPage._payByLink)
|
||||
mediaId: base.mediaId,
|
||||
contentId: base.contentId,
|
||||
checkoutContextId: base.checkoutContextId,
|
||||
// DISABLED:正常下单但不传实验字段
|
||||
experimentId: isExperimentActive
|
||||
? (base.experimentId ?? card.experimentId ?? experimentId)
|
||||
: null,
|
||||
experimentVariant: isExperimentActive
|
||||
? (base.experimentVariant ?? card.variant ?? variant)
|
||||
: null,
|
||||
);
|
||||
analytics?.reportPageViewAfterPaint();
|
||||
analytics?.reportProductImpression(card, afterPaint: true);
|
||||
try {
|
||||
await Get.bottomSheet(
|
||||
OnlinePayPage(vipProductModel: card, orderTrack: track),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} finally {
|
||||
analytics?.reportCloseWithoutPurchaseIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/// 多张可升级卡挑一张:待付价低优先,同价预售卡优先;无可升级卡返回 null
|
||||
VipProductModel? _cheapestUpgrade(List<VipProductModel> list) {
|
||||
final upgrades = list.where((e) => e.isUpgrade == true).toList();
|
||||
if (upgrades.isEmpty) return null;
|
||||
upgrades.sort((a, b) {
|
||||
final cmp = _upgradePrice(a).compareTo(_upgradePrice(b));
|
||||
if (cmp != 0) return cmp;
|
||||
return a.isPreSale ? -1 : 1; //同价预售卡优先
|
||||
});
|
||||
return upgrades.first;
|
||||
}
|
||||
|
||||
/// 升级比价用价:预售卡用实际待付(已付首款=尾款 balanceAmount / 未付=定金 advanceAmount),普通卡用 discountedPrice(对齐 mrhs)
|
||||
int _upgradePrice(VipProductModel card) {
|
||||
if (card.isPreSale) {
|
||||
return PreSaleProvider().isPayFirst
|
||||
? (card.balanceAmount ?? 0)
|
||||
: (card.advanceAmount ?? 0);
|
||||
}
|
||||
return card.discountedPrice ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 全站唯一实例(对齐 presaleProvider / globalStore 的顶层单例引用)
|
||||
final vipProductManager = VipProductManager();
|
||||
@@ -0,0 +1,778 @@
|
||||
//会员支持model
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
|
||||
import '../../../hj_model/mine/exchange/dc_model.dart';
|
||||
|
||||
/// /vip/product 实验状态:ACTIVE 生效,DISABLED 不可用(回原套餐,无实验字段)
|
||||
abstract class VipExperimentStatus {
|
||||
static const active = 'ACTIVE';
|
||||
static const disabled = 'DISABLED';
|
||||
}
|
||||
|
||||
/// uiConfig.badgeStyles 单项:按 badgeType 匹配角标底色/字色
|
||||
class VipBadgeStyle {
|
||||
String? badgeType;
|
||||
String? backgroundColor;
|
||||
String? textColor;
|
||||
|
||||
VipBadgeStyle({this.badgeType, this.backgroundColor, this.textColor});
|
||||
|
||||
VipBadgeStyle.fromJson(Map<String, dynamic>? json) {
|
||||
badgeType = json?['badgeType']?.toString();
|
||||
backgroundColor = json?['backgroundColor']?.toString();
|
||||
textColor = json?['textColor']?.toString();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'badgeType': badgeType,
|
||||
'backgroundColor': backgroundColor,
|
||||
'textColor': textColor,
|
||||
};
|
||||
}
|
||||
|
||||
/// /vip/product.data.uiConfig:皮肤背景 + 角标样式表
|
||||
class VipUiConfig {
|
||||
String? backgroundImage;
|
||||
List<VipBadgeStyle>? badgeStyles;
|
||||
|
||||
VipUiConfig({this.backgroundImage, this.badgeStyles});
|
||||
|
||||
VipUiConfig.fromJson(Map<String, dynamic>? json) {
|
||||
backgroundImage = json?['backgroundImage']?.toString();
|
||||
final raw = json?['badgeStyles'];
|
||||
if (raw is List) {
|
||||
badgeStyles = raw
|
||||
.whereType<Map>()
|
||||
.map((e) => VipBadgeStyle.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'backgroundImage': backgroundImage,
|
||||
'badgeStyles': badgeStyles?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
/// 按 badgeType 查找样式(大小写不敏感)
|
||||
VipBadgeStyle? styleFor(String? badgeType) {
|
||||
final key = (badgeType ?? '').trim().toUpperCase();
|
||||
if (key.isEmpty) return null;
|
||||
for (final s in badgeStyles ?? const <VipBadgeStyle>[]) {
|
||||
if ((s.badgeType ?? '').trim().toUpperCase() == key) return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class VipSupportModel {
|
||||
DCModel? daichong;
|
||||
List<IntegralList>? integralList; //积分兑换列表
|
||||
bool? isNewUser;
|
||||
List<VipProductListModel>? list;
|
||||
List<VipProductModel>? upgradeableVipCardList;
|
||||
|
||||
/// A/B 实验状态:ACTIVE / DISABLED
|
||||
String? experimentStatus;
|
||||
|
||||
/// A/B 实验:实验 ID(下单时原样回传;DISABLED 时后端不返回)
|
||||
String? experimentId;
|
||||
|
||||
/// A/B 实验:分组 A / B(下单时原样回传)
|
||||
String? variant;
|
||||
|
||||
/// A/B 实验:默认选中卡 ID
|
||||
String? defaultProductId;
|
||||
|
||||
/// 皮肤 key,如 vip-card-skin-b(视觉皮肤标识,UI 分支仍以 variant 为准)
|
||||
String? skinKey;
|
||||
|
||||
/// 皮肤 UI 配置(背景图 / 角标样式表)
|
||||
VipUiConfig? uiConfig;
|
||||
|
||||
/// 实验是否生效(仅 ACTIVE 才挂实验字段 / 上报埋点 / 下单回传实验信息)
|
||||
bool get isExperimentActive => experimentStatus == VipExperimentStatus.active;
|
||||
|
||||
VipSupportModel({
|
||||
this.daichong,
|
||||
this.integralList,
|
||||
this.isNewUser,
|
||||
this.list,
|
||||
this.upgradeableVipCardList,
|
||||
this.experimentStatus,
|
||||
this.experimentId,
|
||||
this.variant,
|
||||
this.defaultProductId,
|
||||
this.skinKey,
|
||||
this.uiConfig,
|
||||
});
|
||||
|
||||
VipSupportModel.fromJson(Map<String, dynamic>? json) {
|
||||
daichong =
|
||||
json?['daichong'] != null ? DCModel.fromJson(json?['daichong']) : null;
|
||||
if (json?['integralList'] != null) {
|
||||
integralList = <IntegralList>[];
|
||||
json?['integralList'].forEach((v) {
|
||||
integralList!.add(IntegralList.fromJson(v));
|
||||
});
|
||||
}
|
||||
isNewUser = json?['isNewUser'];
|
||||
experimentStatus = json?['experimentStatus']?.toString();
|
||||
experimentId = json?['experimentId']?.toString();
|
||||
variant = json?['variant']?.toString();
|
||||
defaultProductId = json?['defaultProductId']?.toString();
|
||||
skinKey = json?['skinKey']?.toString();
|
||||
uiConfig = json?['uiConfig'] != null
|
||||
? VipUiConfig.fromJson(json?['uiConfig'])
|
||||
: null;
|
||||
|
||||
/// 升级卡
|
||||
upgradeableVipCardList = (json?['upgradeableVipCardList'] as List?)
|
||||
?.map((e) => VipProductModel.fromJson(e['product'] ?? {}))
|
||||
.toList();
|
||||
if (json?['list'] != null) {
|
||||
list = <VipProductListModel>[];
|
||||
json?['list'].forEach((v) {
|
||||
VipProductListModel vipModel =
|
||||
VipProductListModel.fromJson(v, daichong);
|
||||
list!.add(vipModel);
|
||||
});
|
||||
}
|
||||
// 仅 ACTIVE 时把实验上下文挂到卡上;DISABLED 时后端不带实验字段,也不 stamp
|
||||
if (isExperimentActive) {
|
||||
_stampAbExperiment();
|
||||
}
|
||||
}
|
||||
|
||||
void _stampAbExperiment() {
|
||||
void stamp(VipProductModel p) {
|
||||
p.experimentId = experimentId;
|
||||
p.variant = variant;
|
||||
}
|
||||
|
||||
for (final group in list ?? <VipProductListModel>[]) {
|
||||
for (final p in group.list ?? <VipProductModel>[]) {
|
||||
stamp(p);
|
||||
}
|
||||
}
|
||||
for (final p in upgradeableVipCardList ?? <VipProductModel>[]) {
|
||||
stamp(p);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (daichong != null) {
|
||||
data['daichong'] = daichong!.toJson();
|
||||
}
|
||||
if (integralList != null) {
|
||||
data['integralList'] = integralList!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['isNewUser'] = isNewUser;
|
||||
data['experimentStatus'] = experimentStatus;
|
||||
data['experimentId'] = experimentId;
|
||||
data['variant'] = variant;
|
||||
data['defaultProductId'] = defaultProductId;
|
||||
data['skinKey'] = skinKey;
|
||||
if (uiConfig != null) data['uiConfig'] = uiConfig!.toJson();
|
||||
if (list != null) {
|
||||
data['list'] = list!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class IntegralList {
|
||||
String? desc;
|
||||
int? duration;
|
||||
String? id;
|
||||
String? img;
|
||||
String? name;
|
||||
int? price;
|
||||
int? type;
|
||||
String? bgImg;
|
||||
|
||||
IntegralList(
|
||||
{this.desc,
|
||||
this.duration,
|
||||
this.id,
|
||||
this.img,
|
||||
this.name,
|
||||
this.price,
|
||||
this.type});
|
||||
|
||||
IntegralList.fromJson(Map<String, dynamic> json) {
|
||||
desc = json['desc'];
|
||||
duration = json['duration'];
|
||||
id = json['id'];
|
||||
img = json['img'];
|
||||
name = json['name'];
|
||||
price = json['price'];
|
||||
bgImg = json['bgImg'];
|
||||
type = json['type'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['desc'] = desc;
|
||||
data['duration'] = duration;
|
||||
data['id'] = id;
|
||||
data['img'] = img;
|
||||
data['name'] = name;
|
||||
data['price'] = price;
|
||||
data['type'] = type;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class VipProductListModel {
|
||||
List<VipProductModel>? list;
|
||||
String? position;
|
||||
String? positionID;
|
||||
int? showType;
|
||||
|
||||
VipProductListModel(
|
||||
{this.list, this.position, this.positionID, this.showType});
|
||||
|
||||
VipProductListModel.fromJson(Map<String, dynamic> json, DCModel? daichong) {
|
||||
if (json['list'] != null) {
|
||||
list = <VipProductModel>[];
|
||||
json['list'].forEach((v) {
|
||||
VipProductModel pModel = VipProductModel.fromJson(v);
|
||||
pModel.daichong = daichong;
|
||||
list!.add(pModel);
|
||||
});
|
||||
}
|
||||
position = json['position'];
|
||||
positionID = json['positionID'];
|
||||
showType = json['showType'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (list != null) {
|
||||
data['list'] = list!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['position'] = position;
|
||||
data['positionID'] = positionID;
|
||||
data['showType'] = showType;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class VipProductModel {
|
||||
String? actionDesc;
|
||||
|
||||
/// 角标类型,如 MOST_POPULAR
|
||||
String? badgeType;
|
||||
|
||||
/// 角标文案,如「最受欢迎」
|
||||
String? badgeText;
|
||||
int? aiUndressCount;
|
||||
String? alias;
|
||||
String? bgImg;
|
||||
int? chanSplitMod;
|
||||
int? chatPrice;
|
||||
String? createdAt;
|
||||
String? desc;
|
||||
int? discountedPrice;
|
||||
int? discountedPriceAnd;
|
||||
int? discountedPriceIos;
|
||||
int? downloadCount;
|
||||
int? duration;
|
||||
int? everyDayGiveCoin;
|
||||
String? exclusiveOffer;
|
||||
int? giveCoin;
|
||||
int? giveFruitCoin;
|
||||
int? goldVideoCouponCount;
|
||||
int? goldVideoCouponNum;
|
||||
int? goldVideoFreeDay;
|
||||
bool? isAmountPay; // true 支持金币支付
|
||||
String? newBgImg;
|
||||
String? newName;
|
||||
List<NewPrivilege>? newPrivilege;
|
||||
int? originalPrice;
|
||||
int? payVidDiscount;
|
||||
String? position;
|
||||
List<int>? privilege;
|
||||
String? privilegeDesc;
|
||||
String? productID;
|
||||
String? productName;
|
||||
int? productType; //21- 预售卡
|
||||
List<RchgType>? rchgType;
|
||||
int? serviceTime;
|
||||
int? showCountdownTime;
|
||||
int? signDays;
|
||||
int? sort;
|
||||
bool? status;
|
||||
int? timesAWeek;
|
||||
int? type; //会员卡类型 1、会员卡 2、礼包卡
|
||||
bool? unitPriceDisplay;
|
||||
String? updatedAt;
|
||||
int? videoDiscount;
|
||||
String? vipCardDesc;
|
||||
int? vipLevel;
|
||||
String? tag;
|
||||
DCModel? daichong; // 从上级数据结构手动赋值过来
|
||||
/// VIP 卡皮 A/B:由 /vip/product 响应挂到卡上,下单原样回传
|
||||
String? experimentId;
|
||||
String? variant;
|
||||
|
||||
//是否VIP升级
|
||||
bool? isUpgrade;
|
||||
//当前VIP卡名称
|
||||
String? currentVipName;
|
||||
//当前VIP卡价格
|
||||
String? currentVipPrice;
|
||||
// 原价购买价格
|
||||
int? purchasePrice;
|
||||
// 预付升级价格
|
||||
int? advanceAmount;
|
||||
// 预售尾款
|
||||
int? balanceAmount;
|
||||
|
||||
int get discountedPriceUI {
|
||||
return (discountedPrice ?? 0) ~/ 10;
|
||||
} // 现价 单位角(金币)
|
||||
|
||||
int? prepaidPrice;
|
||||
|
||||
int get prepaidPriceUI {
|
||||
return (prepaidPrice ?? 0) ~/ 10;
|
||||
}
|
||||
|
||||
int get originalPriceUI {
|
||||
return (originalPrice ?? 0) ~/ 10;
|
||||
} // 现价 单位角(金币)
|
||||
|
||||
bool isPreSale = false;
|
||||
|
||||
// 预售新增字段
|
||||
String? endBgImg;
|
||||
String? endBgSelectImg;
|
||||
|
||||
List<RchgType> get rchgTypeUI {
|
||||
List<RchgType> payList = [];
|
||||
List<String> payNameArr = [
|
||||
"支付宝(人工充值)",
|
||||
"微信(人工充值)",
|
||||
"银联(人工充值)",
|
||||
"信用卡(人工充值)",
|
||||
"花呗(人工充值)",
|
||||
"云闪付(人工充值)",
|
||||
"QQ錢包(人工充值)",
|
||||
"京东支付(人工充值)"
|
||||
];
|
||||
for (RchgType rechargeTypeBean in (rchgType ?? [])) {
|
||||
if (rechargeTypeBean.type == 'daichong') {
|
||||
if (daichong?.traders?.isNotEmpty == true) {
|
||||
PayForModel dcPayModel = daichong!.traders![0];
|
||||
if (dcPayModel.payInfos?.isNotEmpty == true) {
|
||||
for (PayInfoModel payInfoModel in dcPayModel.payInfos!) {
|
||||
var payType = RchgType();
|
||||
payType.isOfficial = true;
|
||||
payType.channel = rechargeTypeBean.channel;
|
||||
payType.incrAmount = rechargeTypeBean.incrAmount;
|
||||
payType.incTax = rechargeTypeBean.incTax;
|
||||
payType.payMethod = payInfoModel.payMethod;
|
||||
if (payInfoModel.payMethod == 101) {
|
||||
payType.type = 'alipy';
|
||||
payType.typeName = payNameArr[0];
|
||||
} else if (payInfoModel.payMethod == 102) {
|
||||
payType.type = 'wechat';
|
||||
payType.typeName = payNameArr[1];
|
||||
} else if (payInfoModel.payMethod == 103) {
|
||||
payType.type = 'union';
|
||||
payType.typeName = payNameArr[2];
|
||||
} else if (payInfoModel.payMethod == 104) {
|
||||
payType.type = 'credit';
|
||||
payType.typeName = payNameArr[3];
|
||||
} else if (payInfoModel.payMethod == 105) {
|
||||
payType.type = 'huabei';
|
||||
payType.typeName = payNameArr[4];
|
||||
} else if (payInfoModel.payMethod == 106) {
|
||||
payType.type = 'yunSanPay';
|
||||
payType.typeName = payNameArr[5];
|
||||
} else if (payInfoModel.payMethod == 107) {
|
||||
payType.type = 'qqWallet';
|
||||
payType.typeName = payNameArr[6];
|
||||
} else if (payInfoModel.payMethod == 108) {
|
||||
payType.type = 'jindongPay';
|
||||
payType.typeName = payNameArr[7];
|
||||
}
|
||||
payList.add(payType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payList.add(rechargeTypeBean);
|
||||
}
|
||||
}
|
||||
if (isAmountPay == true) {
|
||||
RchgType coinType = RchgType();
|
||||
coinType.type = "coin";
|
||||
coinType.typeName = "金币";
|
||||
payList.add(coinType);
|
||||
}
|
||||
|
||||
return payList;
|
||||
}
|
||||
|
||||
VipProductModel({
|
||||
this.actionDesc,
|
||||
this.badgeType,
|
||||
this.badgeText,
|
||||
this.aiUndressCount,
|
||||
this.alias,
|
||||
this.bgImg,
|
||||
this.chanSplitMod,
|
||||
this.chatPrice,
|
||||
this.createdAt,
|
||||
this.desc,
|
||||
this.discountedPrice,
|
||||
this.discountedPriceAnd,
|
||||
this.discountedPriceIos,
|
||||
this.downloadCount,
|
||||
this.duration,
|
||||
this.everyDayGiveCoin,
|
||||
this.exclusiveOffer,
|
||||
this.giveCoin,
|
||||
this.giveFruitCoin,
|
||||
this.goldVideoCouponCount,
|
||||
this.goldVideoCouponNum,
|
||||
this.goldVideoFreeDay,
|
||||
this.isAmountPay,
|
||||
this.newBgImg,
|
||||
this.newName,
|
||||
this.newPrivilege,
|
||||
this.originalPrice,
|
||||
this.payVidDiscount,
|
||||
this.position,
|
||||
this.privilege,
|
||||
this.privilegeDesc,
|
||||
this.productID,
|
||||
this.productName,
|
||||
this.productType,
|
||||
this.rchgType,
|
||||
this.serviceTime,
|
||||
this.showCountdownTime,
|
||||
this.signDays,
|
||||
this.sort,
|
||||
this.status,
|
||||
this.timesAWeek,
|
||||
this.type,
|
||||
this.unitPriceDisplay,
|
||||
this.updatedAt,
|
||||
this.videoDiscount,
|
||||
this.vipCardDesc,
|
||||
this.vipLevel,
|
||||
this.isPreSale = false,
|
||||
this.tag,
|
||||
this.isUpgrade,
|
||||
this.currentVipName,
|
||||
this.currentVipPrice,
|
||||
this.purchasePrice,
|
||||
this.advanceAmount,
|
||||
});
|
||||
|
||||
VipProductModel.fromJson(Map<String, dynamic> json) {
|
||||
prepaidPrice = json['prepaidPrice'];
|
||||
endBgImg = json['endBgImg'];
|
||||
endBgSelectImg = json['endBgSelectImg'];
|
||||
actionDesc = json['actionDesc'];
|
||||
badgeType = json['badgeType'];
|
||||
badgeText = json['badgeText'];
|
||||
aiUndressCount = json['aiUndressCount'];
|
||||
alias = json['alias'];
|
||||
bgImg = json['bgImg'];
|
||||
chanSplitMod = json['chanSplitMod'];
|
||||
chatPrice = json['chatPrice'];
|
||||
createdAt = json['createdAt'];
|
||||
desc = json['desc'];
|
||||
discountedPrice = json['discountedPrice'];
|
||||
discountedPriceAnd = json['discountedPriceAnd'];
|
||||
discountedPriceIos = json['discountedPriceIos'];
|
||||
downloadCount = json['downloadCount'];
|
||||
duration = json['duration'];
|
||||
everyDayGiveCoin = json['everyDayGiveCoin'];
|
||||
exclusiveOffer = json['exclusiveOffer'];
|
||||
giveCoin = json['giveCoin'];
|
||||
giveFruitCoin = json['giveFruitCoin'];
|
||||
goldVideoCouponCount = json['goldVideoCouponCount'];
|
||||
goldVideoCouponNum = json['goldVideoCouponNum'];
|
||||
goldVideoFreeDay = json['goldVideoFreeDay'];
|
||||
isAmountPay = json['isAmountPay'];
|
||||
newBgImg = json['newBgImg'];
|
||||
newName = json['newName'];
|
||||
tag = json['tag'];
|
||||
if (json['newPrivilege'] != null) {
|
||||
newPrivilege = <NewPrivilege>[];
|
||||
json['newPrivilege'].forEach((v) {
|
||||
newPrivilege!.add(NewPrivilege.fromJson(v));
|
||||
});
|
||||
}
|
||||
originalPrice = json['originalPrice'];
|
||||
payVidDiscount = json['payVidDiscount'];
|
||||
position = json['position'];
|
||||
if (json['privilege'] != null) {
|
||||
privilege = (json['privilege'] as List?)?.map((v) {
|
||||
return int.tryParse(v.toString()) ?? 0;
|
||||
}).toList();
|
||||
}
|
||||
privilegeDesc = json['privilegeDesc'];
|
||||
productID = json['productID'];
|
||||
productName = json['productName'];
|
||||
productType = json['productType'];
|
||||
if (json['rchgType'] != null) {
|
||||
rchgType = <RchgType>[];
|
||||
json['rchgType'].forEach((v) {
|
||||
rchgType!.add(RchgType.fromJson(v));
|
||||
});
|
||||
}
|
||||
serviceTime = json['serviceTime'];
|
||||
showCountdownTime = json['showCountdownTime'];
|
||||
signDays = json['signDays'];
|
||||
sort = json['sort'];
|
||||
status = json['status'];
|
||||
timesAWeek = json['timesAWeek'];
|
||||
type = json['type'];
|
||||
unitPriceDisplay = json['unitPriceDisplay'];
|
||||
updatedAt = json['updatedAt'];
|
||||
videoDiscount = json['videoDiscount'];
|
||||
vipCardDesc = json['vipCardDesc'];
|
||||
vipLevel = json['vipLevel'];
|
||||
isPreSale = false;
|
||||
isUpgrade = json['isUpgrade'];
|
||||
currentVipName = json['currentVipName'];
|
||||
currentVipPrice = "${json['currentVipPrice']}";
|
||||
purchasePrice = json['purchasePrice'];
|
||||
advanceAmount = json['advanceAmount'];
|
||||
balanceAmount = json['balanceAmount'];
|
||||
}
|
||||
|
||||
//是否有折扣
|
||||
bool hasDiscout() {
|
||||
return discountedPrice != originalPrice;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['actionDesc'] = actionDesc;
|
||||
data['badgeType'] = badgeType;
|
||||
data['badgeText'] = badgeText;
|
||||
data['aiUndressCount'] = aiUndressCount;
|
||||
data['alias'] = alias;
|
||||
data['bgImg'] = bgImg;
|
||||
data['chanSplitMod'] = chanSplitMod;
|
||||
data['chatPrice'] = chatPrice;
|
||||
data['createdAt'] = createdAt;
|
||||
data['desc'] = desc;
|
||||
data['discountedPrice'] = discountedPrice;
|
||||
data['discountedPriceAnd'] = discountedPriceAnd;
|
||||
data['discountedPriceIos'] = discountedPriceIos;
|
||||
data['downloadCount'] = downloadCount;
|
||||
data['duration'] = duration;
|
||||
data['everyDayGiveCoin'] = everyDayGiveCoin;
|
||||
data['exclusiveOffer'] = exclusiveOffer;
|
||||
data['giveCoin'] = giveCoin;
|
||||
data['giveFruitCoin'] = giveFruitCoin;
|
||||
data['goldVideoCouponCount'] = goldVideoCouponCount;
|
||||
data['goldVideoCouponNum'] = goldVideoCouponNum;
|
||||
data['goldVideoFreeDay'] = goldVideoFreeDay;
|
||||
data['isAmountPay'] = isAmountPay;
|
||||
data['newBgImg'] = newBgImg;
|
||||
data['newName'] = newName;
|
||||
if (newPrivilege != null) {
|
||||
data['newPrivilege'] = newPrivilege!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['originalPrice'] = originalPrice;
|
||||
data['payVidDiscount'] = payVidDiscount;
|
||||
data['position'] = position;
|
||||
if (privilege != null) {
|
||||
data['privilege'] = privilege!.map((v) => v).toList();
|
||||
}
|
||||
data['privilegeDesc'] = privilegeDesc;
|
||||
data['productID'] = productID;
|
||||
data['productName'] = productName;
|
||||
data['productType'] = productType;
|
||||
if (rchgType != null) {
|
||||
data['rchgType'] = rchgType!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['serviceTime'] = serviceTime;
|
||||
data['showCountdownTime'] = showCountdownTime;
|
||||
data['signDays'] = signDays;
|
||||
data['sort'] = sort;
|
||||
data['status'] = status;
|
||||
data['timesAWeek'] = timesAWeek;
|
||||
data['type'] = type;
|
||||
data['unitPriceDisplay'] = unitPriceDisplay;
|
||||
data['updatedAt'] = updatedAt;
|
||||
data['videoDiscount'] = videoDiscount;
|
||||
data['vipCardDesc'] = vipCardDesc;
|
||||
data['vipLevel'] = vipLevel;
|
||||
data['isUpgrade'] = isUpgrade;
|
||||
data['currentVipName'] = currentVipName;
|
||||
data['currentVipPrice'] = currentVipPrice;
|
||||
data['purchasePrice'] = purchasePrice;
|
||||
data['advanceAmount'] = advanceAmount;
|
||||
data['balanceAmount'] = balanceAmount;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
extension PreSale on VipProductModel {
|
||||
String realNormalVipImage() {
|
||||
if (!isPreSale) return bgImg ?? '';
|
||||
if (PreSaleProvider().canPayBalance) return endBgImg ?? '';
|
||||
return bgImg ?? '';
|
||||
}
|
||||
|
||||
String realSelectVipImage() {
|
||||
if (!isPreSale) return newBgImg ?? '';
|
||||
if (PreSaleProvider().canPayBalance) return endBgSelectImg ?? '';
|
||||
return newBgImg ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
class NewPrivilege {
|
||||
String? id;
|
||||
String? img;
|
||||
String? privilegeDesc;
|
||||
String? privilegeName;
|
||||
int? privilege;
|
||||
String? uncheckedImg;
|
||||
bool? isCore; //是否核心权益(true 进「我的核心权益」横向卡片,false 进「更多权益」网格)
|
||||
|
||||
NewPrivilege({this.img, this.privilegeDesc, this.privilegeName});
|
||||
|
||||
NewPrivilege.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
img = json['img'];
|
||||
privilegeDesc = json['privilegeDesc'];
|
||||
privilegeName = json['privilegeName'];
|
||||
privilege = json['privilege'];
|
||||
uncheckedImg = json['uncheckedImg'];
|
||||
isCore = json['isCore'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['img'] = img;
|
||||
data['privilegeDesc'] = privilegeDesc;
|
||||
data['privilegeName'] = privilegeName;
|
||||
data['uncheckedImg'] = uncheckedImg;
|
||||
data['privilege'] = privilege;
|
||||
data['isCore'] = isCore;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RchgType {
|
||||
String? channel; //渠道类型 鲨鱼 金鱼
|
||||
int? incTax; //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准
|
||||
int? incrAmount; //增加的优惠额度
|
||||
String? type; //充值方式 //coin 金币方式
|
||||
String? typeName; //支付宝,微信,银联
|
||||
|
||||
//daichong 业务字段
|
||||
bool? isOfficial = false; // 官方推荐
|
||||
int? payMethod;
|
||||
|
||||
RchgType(
|
||||
{this.channel, this.incTax, this.incrAmount, this.type, this.typeName});
|
||||
|
||||
RchgType.fromJson(Map<String, dynamic> json) {
|
||||
channel = json['channel'];
|
||||
incTax = json['incTax'];
|
||||
incrAmount = json['incrAmount'];
|
||||
type = json['type'];
|
||||
typeName = json['typeName'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['channel'] = channel;
|
||||
data['incTax'] = incTax;
|
||||
data['incrAmount'] = incrAmount;
|
||||
data['type'] = type;
|
||||
data['typeName'] = typeName;
|
||||
return data;
|
||||
}
|
||||
|
||||
String getPayIcon() {
|
||||
if (isOfficial == true) {
|
||||
// 代充
|
||||
return "pay_icon103.png".mineImgPath;
|
||||
} else if (type == 'alipay') {
|
||||
return "ic_alipay.png".mineImgPath;
|
||||
} else if (type == 'union') {
|
||||
return "pay_icon103.png".mineImgPath;
|
||||
} else if (type == 'wechat') {
|
||||
return "ic_wechat.png".mineImgPath; //AssetsSvg.SVG_PAY_ICON102;
|
||||
} else if (type == 'coin') {
|
||||
//金币
|
||||
return "ic_coin.webp".mineImgPath; //AssetsSvg.SVG_PAY_ICON102;
|
||||
} else if (type == 'usdt') {
|
||||
return "ic_usdt.png".mineImgPath;
|
||||
} else {
|
||||
return "";
|
||||
// return "ic_coupon.png".mineImgPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 加赠券
|
||||
|
||||
class CouponModel {
|
||||
String? cId;
|
||||
int? count;
|
||||
String? createTime;
|
||||
String? expireTime;
|
||||
String? id;
|
||||
String? name;
|
||||
int? price;
|
||||
int? type;
|
||||
bool? used;
|
||||
int? value;
|
||||
|
||||
CouponModel(
|
||||
{this.cId,
|
||||
this.count,
|
||||
this.createTime,
|
||||
this.expireTime,
|
||||
this.id,
|
||||
this.name,
|
||||
this.price,
|
||||
this.type,
|
||||
this.used,
|
||||
this.value});
|
||||
|
||||
CouponModel.fromJson(Map<String, dynamic> json) {
|
||||
cId = json['cId'];
|
||||
count = json['count'];
|
||||
createTime = json['createTime'];
|
||||
expireTime = json['expireTime'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
price = json['price'];
|
||||
type = json['type'];
|
||||
used = json['used'];
|
||||
value = json['value'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['cId'] = this.cId;
|
||||
data['count'] = this.count;
|
||||
data['createTime'] = this.createTime;
|
||||
data['expireTime'] = this.expireTime;
|
||||
data['id'] = this.id;
|
||||
data['name'] = this.name;
|
||||
data['price'] = this.price;
|
||||
data['type'] = this.type;
|
||||
data['used'] = this.used;
|
||||
data['value'] = this.value;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
/// VIP 金色标题渐变 #F5E7BC → #DCAD55
|
||||
/// 会员中心 / 购买弹窗 / 权益标题等金色文案共用,调色只改此处
|
||||
const LinearGradient kVipTitleGradient = LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xffF5E7BC), Color(0xffDCAD55)],
|
||||
);
|
||||
|
||||
/// 金渐变着色包装:给文字 / 线条 / 菱形套上 [kVipTitleGradient]
|
||||
class VipGradientMask extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const VipGradientMask({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) => kVipTitleGradient.createShader(bounds),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// B 组「核心权益」标题图(会员中心 / 购买弹窗共用)
|
||||
class VipCoreSectionTitleImage extends StatelessWidget {
|
||||
const VipCoreSectionTitleImage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
'vip_special_title.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// B 组「更多权益」标题图(会员中心 / 购买弹窗共用)
|
||||
class VipMoreSectionTitleImage extends StatelessWidget {
|
||||
const VipMoreSectionTitleImage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
'vip_more_title.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 区块标题:两侧「金渐变横线 + 菱形」+ 居中金渐变文字(会员中心 B / 购买弹窗共用)
|
||||
class VipSectionTitle extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
/// true:旧版橙色标题(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const VipSectionTitle(this.text, {super.key, this.classic = false});
|
||||
|
||||
static const _classicOrange = Color(0xffF68804);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_classicDeco(lineFirst: true),
|
||||
8.sizeBoxW,
|
||||
Text(text,
|
||||
style: const TextStyle(
|
||||
color: _classicOrange,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600)),
|
||||
8.sizeBoxW,
|
||||
_classicDeco(lineFirst: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildDeco(lineFirst: true),
|
||||
8.sizeBoxW,
|
||||
VipGradientMask(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
_buildDeco(lineFirst: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _classicDeco({required bool lineFirst}) {
|
||||
final line = Container(width: 40, height: 0.5, color: _classicOrange);
|
||||
final diamond = Transform.rotate(
|
||||
angle: 0.7853981633974483,
|
||||
child: Container(width: 5, height: 5, color: _classicOrange),
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: lineFirst ? [line, diamond] : [diamond, line],
|
||||
);
|
||||
}
|
||||
|
||||
// 标题两侧装饰:金渐变细横线 + 菱形(lineFirst=true 横线在外侧,false 镜像)
|
||||
Widget _buildDeco({required bool lineFirst}) {
|
||||
const line = VipGradientMask(
|
||||
child: SizedBox(
|
||||
width: 40, height: 0.5, child: ColoredBox(color: Colors.white)),
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: lineFirst ? [line, _diamond] : [_diamond, line],
|
||||
);
|
||||
}
|
||||
|
||||
// 金渐变小菱形装饰(45° 旋转的小方块)
|
||||
Widget get _diamond => Transform.rotate(
|
||||
angle: 0.7853981633974483,
|
||||
child: const VipGradientMask(
|
||||
child: SizedBox(
|
||||
width: 5, height: 5, child: ColoredBox(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
// 金币充值档位卡片
|
||||
class CoinItem extends StatelessWidget {
|
||||
final RechargeTypeModel model;
|
||||
final bool isSelected; // 是否选中
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const CoinItem(this.model, {super.key, this.onTap, this.isSelected = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final couponDesc = model.couponDesc;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: isSelected
|
||||
? Border.all(width: 1, color: const Color(0xffF9C142))
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
"${model.amount}金币",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'¥${model.moneyYuan}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF9C142), fontWeight: FontWeight.w500),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
// 左上角优惠角标
|
||||
if (couponDesc?.isNotEmpty == true)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
height: 20,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12)),
|
||||
color: Color(0xffFFD460),
|
||||
),
|
||||
child: Text(
|
||||
couponDesc!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff292929),
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
|
||||
import '../../make_money/mine_withdrawal_record_page.dart';
|
||||
import '../mine_charge_coin_logic.dart';
|
||||
import '../pay_order_source.dart';
|
||||
import 'coin_item.dart';
|
||||
|
||||
/// 金币支付底部弹窗(按设计稿:余额卡片 + 档位网格 + 立即支付)
|
||||
class CoinPayBottomSheet extends StatelessWidget {
|
||||
/// 下单来源,默认播放页底部弹窗——本弹窗只在播放页(购买弹窗/解锁蒙层)用
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 下单埋点上下文(短剧付费墙充金币要带剧/集/付费墙上下文)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
const CoinPayBottomSheet(
|
||||
{super.key,
|
||||
this.sourcePage = PaySourcePage.videoBottomSheet,
|
||||
this.orderTrack});
|
||||
|
||||
/// 弹窗展示中,防连点叠多个 bottomSheet
|
||||
static bool _isShowing = false;
|
||||
|
||||
static Future<T?> show<T>({
|
||||
PaySourcePage sourcePage = PaySourcePage.videoBottomSheet,
|
||||
PayOrderTrackInfo? orderTrack,
|
||||
}) async {
|
||||
if (_isShowing) return null;
|
||||
_isShowing = true;
|
||||
try {
|
||||
return await Get.bottomSheet<T>(
|
||||
CoinPayBottomSheet(sourcePage: sourcePage, orderTrack: orderTrack),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
} finally {
|
||||
_isShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeCoinLogic>(
|
||||
init: MineChargeCoinLogic(sourcePage: sourcePage, orderTrack: orderTrack),
|
||||
global: false,
|
||||
builder: (logic) {
|
||||
// Material 填深色底:圆角抗锯齿对着 #040018,避免透明底导致左右上角异色
|
||||
return Material(
|
||||
color: const Color(0xff040018),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: Get.height * 0.85),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xff040018), Color(0xff060606)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 顶部径向黄光:#FACC15 16% → 0%
|
||||
const Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment.topCenter,
|
||||
radius: 1.2,
|
||||
colors: [
|
||||
Color(0x29FACC15), // #FACC15 @ 16%
|
||||
Color(0x00FACC15), // #FACC15 @ 0%
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
const SheetHandleBar(),
|
||||
16.sizeBoxH,
|
||||
const Text(
|
||||
'金币支付',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
Flexible(child: _buildBody(logic)),
|
||||
_buildBottomBar(logic),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(MineChargeCoinLogic logic) {
|
||||
if (logic.isInitLoading) {
|
||||
return const SizedBox(height: 220, child: LoadingCenterWidget());
|
||||
}
|
||||
if (logic.model?.list?.isEmpty != false) {
|
||||
return SizedBox(
|
||||
height: 220,
|
||||
child: CErrorWidget(retryOnTap: () => logic.loadData()),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildWallet(),
|
||||
16.sizeBoxH,
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: 111 / 138,
|
||||
),
|
||||
itemCount: logic.model!.list!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = logic.model!.list![index];
|
||||
final isSelected = logic.selectedCoin?.id == model.id;
|
||||
return CoinItem(
|
||||
model,
|
||||
isSelected: isSelected,
|
||||
onTap: () => logic.onSelectCoin(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 我的金币余额卡片
|
||||
Widget _buildWallet() {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'我的金币余额',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxW,
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, store, __) {
|
||||
final wallet = store.wallet;
|
||||
final total = (wallet?.amount ?? 0) + (wallet?.income ?? 0);
|
||||
return Text(
|
||||
'$total',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.bill), opaque: false),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 30,
|
||||
width: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFD460),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: const Text(
|
||||
'余额明细',
|
||||
style: TextStyle(color: Color(0xff3D3D3D), fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 立即支付 + 在线客服
|
||||
Widget _buildBottomBar(MineChargeCoinLogic logic) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => logic.onGotoPay(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'¥${logic.selectedCoin?.moneyYuan ?? 0}/立即支付',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('支付问题反馈,点击联系 ',
|
||||
style: textStyle(12, const Color(0xffBFBFC1), FontWeight.w400)),
|
||||
GestureDetector(
|
||||
onTap: pushToCustomService,
|
||||
child: Text('在线客服',
|
||||
style:
|
||||
textStyle(12, const Color(0xffFFD460), FontWeight.w400)),
|
||||
),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user