初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_purchase_membership_controller.dart';
class KRPurchaseMembershipBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRPurchaseMembershipController>(
() => KRPurchaseMembershipController(),
);
}
}
@@ -0,0 +1,506 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_subscribe_api.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../../../common/app_run_data.dart';
import '../../../model/response/kr_already_subscribe.dart';
import '../../../model/response/kr_payment_methods.dart';
import '../../../routes/app_pages.dart';
import '../../../services/api_service/kr_api.user.dart';
import '../../../utils/kr_event_bus.dart';
/// 会员购买控制器
/// 负责处理会员套餐选择、支付方式选择和订阅流程
class KRPurchaseMembershipController extends GetxController {
// 注入的服务
final KRSubscribeApi _kr_subscribeApi = KRSubscribeApi();
// 事件监听器
Worker? _kr_eventWorker;
// UI 状态
final RxBool kr_isLoading = false.obs;
final RxString kr_errorMessage = ''.obs;
final RxString kr_userEmail = ''.obs;
final RxBool kr_showPlanSelector = false.obs; // 是否显示套餐选择器
// 数据状态
final RxList<KRPackageListItem> kr_plans = <KRPackageListItem>[].obs;
final RxList<KRPaymentMethod> kr_paymentMethods = <KRPaymentMethod>[].obs;
final RxInt kr_selectedPlanIndex = 0.obs;
final RxInt kr_selectedPaymentMethodIndex = (-1).obs;
final RxInt kr_selectedDiscountIndex = (-1).obs;
// 已订阅套餐列表
var _kr_alreadySubscribe = <KRAlreadySubscribe>[];
/// 描述是否展开
final kr_isDescriptionExpanded = false.obs;
/// 当前余额
var _kr_balance = 0;
@override
void onInit() {
super.onInit();
kr_initializeData();
}
@override
void onClose() {
_kr_eventWorker?.dispose();
super.onClose();
}
/// 初始化数据
Future<void> kr_initializeData() async {
kr_userEmail.value = KRAppRunData.getInstance().kr_account.toString();
await kr_getPackageList();
// 监听所有支付相关消息
_kr_eventWorker = KREventBus().kr_listenMessages(
[KRMessageType.kr_payment, KRMessageType.kr_subscribe_update],
_kr_handleMessage,
);
}
/// 处理消息
Future<void> _kr_handleMessage(KRMessageData message) async {
switch (message.kr_type) {
case KRMessageType.kr_payment:
await _iniUserInfo();
// 只更新支付方式显示,因为支付方式标题中包含余额信息
if (kr_paymentMethods.isNotEmpty) {
final balanceMethodIndex = kr_paymentMethods
.indexWhere((method) => method.platform == 'balance');
if (balanceMethodIndex != -1) {
// 触发支付方式列表更新
kr_paymentMethods.refresh();
}
}
break;
case KRMessageType.kr_subscribe_update:
break;
default:
break;
}
}
/// 获取套餐列表和支付方式
Future<void> kr_getPackageList() async {
kr_isLoading.value = true;
kr_selectedPlanIndex.value = 0; // 重置套餐选择
kr_selectedDiscountIndex.value = -1; // 重置折扣选择
kr_selectedPaymentMethodIndex.value = -1; // 重置支付方式选择
await _iniUserInfo();
await kr_getAlreadySubscribe();
await kr_fetchPackages();
await kr_fetchPaymentMethods();
// 根据套餐数量决定是否显示套餐选择器
kr_showPlanSelector.value = kr_plans.length > 1;
kr_isLoading.value = false;
}
/// 初始化用户信息
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
_kr_balance = userInfo.balance;
},
);
}
/// 获取用户已订阅套餐
Future<void> kr_getAlreadySubscribe() async {
final either = await _kr_subscribeApi.kr_getAlreadySubscribe();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(alreadySubscribe) {
_kr_alreadySubscribe = alreadySubscribe;
KRLogUtil.kr_i(
'已订阅套餐: ${_kr_alreadySubscribe.map((e) => e.subscribeId).toList()}',
tag: 'PurchaseMembershipController');
},
);
}
/// 获取套餐列表
Future<void> kr_fetchPackages() async {
final either = await _kr_subscribeApi.kr_getPackageListList();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(packageList) {
kr_plans.value = packageList.kr_list;
// 默认选择第一个套餐
if (kr_plans.isNotEmpty) {
kr_selectedPlanIndex.value = 0;
kr_initializeSelection(kr_plans.first);
}
},
);
}
/// 获取支付方式列表
Future<void> kr_fetchPaymentMethods() async {
final either = await _kr_subscribeApi.kr_getPaymentMethods();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(paymentMethods) {
kr_paymentMethods.value = paymentMethods;
// 检查当前选择的套餐价格是否小于等于余额
if (kr_plans.isNotEmpty) {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPrice = kr_getPlanPrice(selectedPlan,
discountIndex: kr_selectedDiscountIndex.value);
// 查找余额支付方式的索引
final balanceMethodIndex = paymentMethods
.indexWhere((method) => method.platform == 'balance');
// 如果找到余额支付方式且余额足够,默认选择余额支付
if (balanceMethodIndex != -1 && selectedPrice <= _kr_balance / 100) {
kr_selectPaymentMethod(balanceMethodIndex);
} else {
// 查找第一个非余额支付方式
final nonBalanceMethodIndex = paymentMethods
.indexWhere((method) => method.platform != 'balance');
if (nonBalanceMethodIndex != -1) {
kr_selectPaymentMethod(nonBalanceMethodIndex);
}
}
}
},
);
}
/// 获取支付方式显示标题
String kr_getPaymentMethodTitle(KRPaymentMethod method) {
if (method.platform == 'balance') {
return '${method.name}${(_kr_balance / 100).toStringAsFixed(2)})';
}
return method.name;
}
/// 选择套餐
void kr_selectPlan(int planIndex, {int? discountIndex}) {
if (planIndex >= 0 && planIndex < kr_plans.length) {
kr_selectedPlanIndex.value = planIndex;
// 确保折扣索引有效
if (discountIndex != null) {
final plan = kr_plans[planIndex];
if (discountIndex >= 0 && discountIndex < plan.kr_discount.length) {
kr_selectedDiscountIndex.value = discountIndex;
} else {
// 如果传入的折扣索引无效,但有折扣选项,则默认选择第一个
if (plan.kr_discount.isNotEmpty) {
kr_selectedDiscountIndex.value = 0;
} else {
kr_selectedDiscountIndex.value = -1;
}
}
} else {
// 如果没有传入折扣索引,但有折扣选项,则默认选择第一个
final plan = kr_plans[planIndex];
if (plan.kr_discount.isNotEmpty) {
kr_selectedDiscountIndex.value = 0;
} else {
kr_selectedDiscountIndex.value = -1;
}
}
// 重置支付方式选择
kr_selectedPaymentMethodIndex.value = -1;
// 重新判断应该选择的支付方式
_kr_updatePaymentMethodSelection();
// 更新UI状态
update();
}
}
/// 更新支付方式选择
void _kr_updatePaymentMethodSelection() {
if (kr_plans.isEmpty || kr_paymentMethods.isEmpty) return;
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPrice = kr_getPlanPrice(selectedPlan,
discountIndex: kr_selectedDiscountIndex.value);
// 查找余额支付方式的索引
final balanceMethodIndex =
kr_paymentMethods.indexWhere((method) => method.platform == 'balance');
// 如果找到余额支付方式且余额足够,选择余额支付
if (balanceMethodIndex != -1 && selectedPrice <= _kr_balance / 100) {
kr_selectPaymentMethod(balanceMethodIndex);
} else {
// 查找第一个非余额支付方式
final nonBalanceMethodIndex = kr_paymentMethods
.indexWhere((method) => method.platform != 'balance');
if (nonBalanceMethodIndex != -1) {
kr_selectPaymentMethod(nonBalanceMethodIndex);
}
}
}
/// 选择支付方式
void kr_selectPaymentMethod(int index) {
if (index >= 0 && index < kr_paymentMethods.length) {
kr_selectedPaymentMethodIndex.value = index;
}
}
/// 获取当前选中的数量
int kr_getSelectedQuantity() {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
if (kr_selectedDiscountIndex.value >= 0 &&
kr_selectedDiscountIndex.value < selectedPlan.kr_discount.length) {
return selectedPlan
.kr_discount[kr_selectedDiscountIndex.value].kr_quantity;
}
return 1; // 默认数量为1
}
/// 开始订阅流程
Future<void> kr_startSubscription() async {
if (!kr_validateSubscriptionData()) return;
kr_errorMessage.value = '';
try {
await kr_processPurchaseAndCheckout();
} catch (e) {
kr_errorMessage.value = '订阅失败: ${e.toString()}';
}
}
/// 验证订阅数据
bool kr_validateSubscriptionData() {
if (kr_plans.isEmpty) {
KRCommonUtil.kr_showToast('没有可用的套餐');
return false;
}
if (kr_selectedPaymentMethodIndex.value < 0 ||
kr_selectedPaymentMethodIndex.value >= kr_paymentMethods.length) {
KRCommonUtil.kr_showToast('请选择支付方式');
return false;
}
return true;
}
/// 处理购买和结账流程
Future<void> kr_processPurchaseAndCheckout() async {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPaymentMethod =
kr_paymentMethods[kr_selectedPaymentMethodIndex.value];
// 获取选中的数量
final quantity = kr_getSelectedQuantity();
// 判断是续订还是新购
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
final subscribeId = isRenewal
? _kr_alreadySubscribe
.firstWhere(
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id)
.userSubscribeId
: 0;
// 根据判断结果调用不同的接口
final purchaseEither = isRenewal
? await _kr_subscribeApi.kr_renewal(
subscribeId,
quantity,
selectedPaymentMethod.id,
'',
)
: await _kr_subscribeApi.kr_purchase(
selectedPlan.kr_id,
quantity,
selectedPaymentMethod.id,
'',
);
purchaseEither.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(order) async {
// 所有支付方式都需要调用 checkout 接口
final checkoutEither = await _kr_subscribeApi.kr_checkout(order);
checkoutEither.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(uri) => Get.toNamed(
Routes.KR_ORDER_STATUS,
arguments: {
'url': uri,
'order': order,
'payment_type': selectedPaymentMethod.platform,
},
),
);
},
);
}
/// 获取套餐价格
double kr_getPlanPrice(KRPackageListItem plan, {int? discountIndex}) {
if (discountIndex != null &&
discountIndex >= 0 &&
discountIndex < plan.kr_discount.length) {
// 计算折扣价格
final discount = plan.kr_discount[discountIndex];
return (plan.kr_unitPrice / 100) *
discount.kr_quantity *
(discount.kr_discount / 100);
}
return plan.kr_unitPrice / 100;
}
/// 获取时间字符串
String kr_getTimeStr(KRPackageListItem plan, {int? discountIndex}) {
final quantity = discountIndex != null &&
discountIndex >= 0 &&
discountIndex < plan.kr_discount.length
? plan.kr_discount[discountIndex].kr_quantity
: 1;
if (plan.kr_unitTime == 'Month') {
return AppTranslations.kr_purchaseMembership.month(quantity);
} else if (plan.kr_unitTime == 'Year') {
return AppTranslations.kr_purchaseMembership.year(quantity);
} else if (plan.kr_unitTime == 'Day') {
return AppTranslations.kr_purchaseMembership.day(quantity);
}
return '';
}
/// 获取折扣文本
String kr_getDiscountText(KRPackageListItem plan, int discountIndex) {
if (discountIndex >= 0 && discountIndex < plan.kr_discount.length) {
final discount = plan.kr_discount[discountIndex];
// 折扣值为 100 表示原价,不需要显示折扣
if (discount.kr_discount == 100) {
return '';
}
// 计算折扣百分比(例如:95% 显示为 -5%)
final discountPercent = 100 - discount.kr_discount;
return '-${discountPercent}%';
}
return '';
}
/// 获取套餐总选项数
int kr_getTotalOptionsCount(KRPackageListItem plan) {
// 确保折扣列表不为空
if (plan.kr_discount.isEmpty) {
return 1; // 如果没有折扣选项,至少返回1个选项
}
return plan.kr_discount.length;
}
/// 初始化选择
void kr_initializeSelection(KRPackageListItem plan) {
if (plan.kr_discount.isNotEmpty) {
// 默认选择第一个选项
kr_selectedDiscountIndex.value = 0;
} else {
// 如果没有选项,设置为 -1
kr_selectedDiscountIndex.value = -1;
}
}
/// 获取当前选中套餐的描述
String kr_getSelectedPlanDescription() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return '';
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.join('');
}
/// 获取当前选中套餐的特性标题列表
List<String> kr_getSelectedPlanFeatureLabels() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.toList();
}
/// 获取当前选中套餐的详细信息
List<KRFeature> kr_getSelectedPlanFeatures() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features;
}
/// 判断当前选中的套餐是否是续订
bool kr_isRenewal() {
if (kr_plans.isEmpty || _kr_alreadySubscribe.isEmpty) return false;
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
return _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
}
/// 获取当前选中套餐的订阅按钮文字
String kr_getSubscribeButtonText() {
if (kr_plans.isEmpty) return '';
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
return isRenewal
? AppTranslations.kr_purchaseMembership.renewNow
: AppTranslations.kr_purchaseMembership.startSubscription;
}
/// 切换描述展开状态
void kr_toggleDescriptionExpanded() {
kr_isDescriptionExpanded.value = !kr_isDescriptionExpanded.value;
}
/// 获取流量限制显示文本
String kr_getTrafficLimitText(KRPackageListItem plan) {
KRLogUtil.kr_i('原始流量值: ${plan.kr_traffic}', tag: 'TrafficLimit');
if (plan.kr_traffic == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedTraffic;
}
// 将字节转换为GB
final trafficInGB = plan.kr_traffic / (1024 * 1024 * 1024);
KRLogUtil.kr_i('转换为GB后的值: $trafficInGB', tag: 'TrafficLimit');
if (trafficInGB < 1) {
return '${(trafficInGB * 1024).toStringAsFixed(0)}MB';
} else if (trafficInGB < 1024) {
return '${trafficInGB.toStringAsFixed(0)}GB';
} else {
return '${(trafficInGB / 1024).toStringAsFixed(1)}TB';
}
}
/// 获取设备限制显示文本
String kr_getDeviceLimitText(KRPackageListItem plan) {
if (plan.kr_deviceLimit == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedDevices;
}
return AppTranslations.kr_purchaseMembership
.devices(plan.kr_deviceLimit.toString());
}
}
@@ -0,0 +1,796 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_purchase_membership_controller.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import '../../../widgets/kr_network_image.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
/// 购买会员页面视图
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
const KRPurchaseMembershipView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Obx(() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.28],
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_purchaseMembership.purchasePackage,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
_kr_buildAccountSection(context),
if (controller.kr_isLoading.value)
Container(
height: MediaQuery.of(context).size.height * 0.5,
child: Center(
child: KRSimpleLoading(
color: Colors.blue,
size: 50.0,
),
),
)
else if (controller.kr_plans.isEmpty)
Container(
height: MediaQuery.of(context).size.height * 0.5,
child: Center(
child: Text(
AppTranslations.kr_purchaseMembership.noData,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
)
else
Container(
margin: EdgeInsets.symmetric(horizontal: 16.r),
child: Column(
children: [
// 套餐选择部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.selectPackage,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
if (controller.kr_plans.length > 1)
Container(
height: 32.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
itemCount: controller.kr_plans.length,
itemBuilder: (context, index) {
final plan = controller.kr_plans[index];
final isSelected = index == controller.kr_selectedPlanIndex.value;
return GestureDetector(
onTap: () => controller.kr_selectPlan(index),
child: Container(
margin: EdgeInsets.only(right: 8.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.withOpacity(0.3),
width: 1,
),
),
child: Center(
child: Text(
plan.kr_name,
style: KrAppTextStyle(
fontSize: 13,
color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
),
);
},
),
),
SizedBox(height: 16.h),
GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (Platform.isWindows || Platform.isMacOS || Platform.isLinux) ? 2.0 : 0.85,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.h,
),
itemCount: controller.kr_getTotalOptionsCount(controller.kr_plans[controller.kr_selectedPlanIndex.value]),
itemBuilder: (context, index) {
final plan = controller.kr_plans[controller.kr_selectedPlanIndex.value];
final discountIndex = plan.kr_discount.isEmpty ? null : index;
return _kr_buildPlanOptionCard(
plan,
controller.kr_selectedPlanIndex.value,
discountIndex,
context,
);
},
),
],
),
),
SizedBox(height: 16.h),
// 套餐描述部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.packageDescription,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
Obx(() {
final featureLabels = controller.kr_getSelectedPlanFeatureLabels();
final features = controller.kr_getSelectedPlanFeatures();
final isExpanded = controller.kr_isDescriptionExpanded.value;
final selectedPlan = controller.kr_plans[controller.kr_selectedPlanIndex.value];
// 添加流量和设备限制信息
final trafficAndDeviceInfo = Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${AppTranslations.kr_purchaseMembership.trafficLimit}${controller.kr_getTrafficLimitText(selectedPlan)}',
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.h),
Text(
'${AppTranslations.kr_purchaseMembership.deviceLimit}${controller.kr_getDeviceLimitText(selectedPlan)}',
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
],
),
),
);
// if (featureLabels.isEmpty) {
// return Column(
// children: [
// trafficAndDeviceInfo,
// Center(
// child: Padding(
// padding: EdgeInsets.symmetric(vertical: 16.h),
// child: Text(
// AppTranslations.kr_purchaseMembership.noData,
// style: KrAppTextStyle(
// fontSize: 14,
// color: Theme.of(context).textTheme.bodySmall?.color,
// ),
// ),
// ),
// ),
// ],
// );
// }
final displayCount = isExpanded ? featureLabels.length : (featureLabels.length > 3 ? 3 : featureLabels.length);
return Column(
children: [
trafficAndDeviceInfo,
...List.generate(displayCount, (index) {
final feature = features[index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onTap: () {
Get.dialog(
Dialog(
backgroundColor: Theme.of(Get.context!).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(16.r),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 8.h),
if (feature.kr_details.isNotEmpty)
...feature.kr_details.map((detail) => Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (detail.kr_label.isNotEmpty) ...[
Text(
detail.kr_label,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.h),
],
Text(
detail.kr_description,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
),
),
],
),
)).toList(),
SizedBox(height: 16.h),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => Get.back(),
child: Text(
AppTranslations.kr_dialog.kr_ok,
style: KrAppTextStyle(
fontSize: 14,
color: Colors.blue,
),
),
),
),
],
),
),
),
);
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Theme.of(Get.context!).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: Row(
children: [
Expanded(
child: Text(
featureLabels[index],
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
),
Container(
padding: EdgeInsets.all(4.r),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline,
size: 16.r,
color: Colors.blue,
),
),
],
),
),
),
);
}),
if (featureLabels.length > 3)
GestureDetector(
onTap: () => controller.kr_toggleDescriptionExpanded(),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isExpanded
? AppTranslations.kr_purchaseMembership.collapse
: AppTranslations.kr_purchaseMembership.expand,
style: KrAppTextStyle(
fontSize: 13,
color: Colors.blue,
),
),
SizedBox(width: 4.w),
Icon(
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
size: 16.r,
color: Colors.blue,
),
],
),
),
),
],
);
}),
],
),
),
SizedBox(height: 16.h),
// 支付方式选择部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.paymentMethod,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
Obx(() => ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: controller.kr_paymentMethods.length,
separatorBuilder: (context, index) => Divider(
height: 1.w,
indent: 44.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
itemBuilder: (context, index) {
final paymentMethod = controller.kr_paymentMethods[index];
return InkWell(
onTap: () => controller.kr_selectPaymentMethod(index),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.r, horizontal: 16.r),
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: paymentMethod.icon.isNotEmpty
? KRNetworkImage(
kr_imageUrl: paymentMethod.icon,
kr_width: 20.w,
kr_height: 20.w,
kr_placeholder: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
kr_errorWidget: Icon(
Icons.payment_rounded,
size: 20.w,
color: Colors.blue,
),
)
: Icon(
Icons.payment_rounded,
size: 20.w,
color: Colors.blue,
),
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_getPaymentMethodTitle(paymentMethod),
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
Obx(() {
final isSelected = index == controller.kr_selectedPaymentMethodIndex.value;
return Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.withOpacity(0.3),
width: 1.5,
),
),
child: isSelected
? Icon(
Icons.check,
color: Colors.white,
size: 16.r,
)
: null,
);
}),
],
),
),
);
},
)),
],
),
),
SizedBox(height: 80.h), // 为底部按钮留出空间
],
),
),
],
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _kr_buildBottomSection(context),
),
],
),
),
],
),
);
}),
);
}
// 账号部分
Widget _kr_buildAccountSection(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Text(
AppTranslations.kr_purchaseMembership.myAccount,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
Obx(() => Text(
controller.kr_userEmail.value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)),
],
),
);
}
// 套餐选项卡片
Widget _kr_buildPlanOptionCard(
KRPackageListItem plan,
int planIndex,
int? discountIndex,
BuildContext context) {
return Obx(() {
bool isSelected = planIndex == controller.kr_selectedPlanIndex.value &&
discountIndex == controller.kr_selectedDiscountIndex.value;
return GestureDetector(
onTap: () => controller.kr_selectPlan(planIndex, discountIndex: discountIndex),
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.h),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.withOpacity(0.08)
: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isSelected ? Colors.blue.withOpacity(0.3) : Colors.grey.withOpacity(0.15),
width: 1,
),
boxShadow: isSelected ? [
BoxShadow(
color: Colors.blue.withOpacity(0.08),
blurRadius: 12,
offset: Offset(0, 4),
spreadRadius: 0,
),
] : [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: Offset(0, 2),
spreadRadius: 0,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.kr_getTimeStr(plan, discountIndex: discountIndex),
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.blue
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 6.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'¥',
style: KrAppTextStyle(
fontSize: 12,
color: isSelected
? Colors.blue.withOpacity(0.8)
: Theme.of(context).textTheme.bodySmall?.color,
),
),
Text(
controller.kr_getPlanPrice(plan, discountIndex: discountIndex)
.toStringAsFixed(2),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.blue
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
SizedBox(height: 6.h),
if (discountIndex != null && plan.kr_discount.isNotEmpty) ...[
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h
),
decoration: BoxDecoration(
color: plan.kr_discount[discountIndex].kr_discount < 100
? Colors.red.withOpacity(0.08)
: Colors.transparent,
borderRadius: BorderRadius.circular(4.r),
border: plan.kr_discount[discountIndex].kr_discount < 100
? Border.all(
color: Colors.red.withOpacity(0.2),
width: 1,
)
: null,
boxShadow: plan.kr_discount[discountIndex].kr_discount < 100
? [
BoxShadow(
color: Colors.red.withOpacity(0.05),
blurRadius: 4,
offset: Offset(0, 2),
spreadRadius: 0,
),
]
: null,
),
child: Text(
controller.kr_getDiscountText(plan, discountIndex),
style: KrAppTextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: plan.kr_discount[discountIndex].kr_discount < 100
? Colors.red.withOpacity(0.9)
: Colors.transparent,
),
),
),
],
],
),
),
);
});
}
// 底部部分
Widget _kr_buildBottomSection(BuildContext context) {
// 如果正在加载或没有数据,不显示底部按钮
if (controller.kr_isLoading.value || controller.kr_plans.isEmpty || controller.kr_paymentMethods.isEmpty) {
return SizedBox.shrink();
}
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: Offset(0, -2),
blurRadius: 8,
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
KRDialog.show(
title: AppTranslations.kr_purchaseMembership.confirmPurchase,
message: AppTranslations.kr_purchaseMembership.confirmPurchaseDesc,
cancelText: AppTranslations.kr_dialog.kr_cancel,
confirmText: AppTranslations.kr_dialog.kr_confirm,
onConfirm: () => controller.kr_startSubscription(),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
controller.kr_getSubscribeButtonText(),
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
);
}
}
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
/// 套餐详情弹框
class KRPlanDetailsDialog extends StatelessWidget {
final List<KRFeature> kr_features;
const KRPlanDetailsDialog({
Key? key,
required this.kr_features,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.planDetails,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: kr_features.length,
itemBuilder: (context, index) {
final feature = kr_features[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
...feature.kr_details.map((detail) => Padding(
padding: const EdgeInsets.only(left: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle_outline,
size: 16,
color: Colors.green,
),
const SizedBox(width: 8),
Expanded(
child: Text(
detail.kr_description,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
)),
if (index < kr_features.length - 1)
const Divider(height: 24),
],
);
},
),
),
],
),
),
);
}
}