subscription_view.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:nomo/app/constants/iconfont/iconfont.dart';
  5. import 'package:nomo/config/theme/dark_theme_colors.dart';
  6. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  7. import 'package:video_player/video_player.dart';
  8. import '../../../../config/translations/strings_enum.dart';
  9. import '../../../constants/assets.dart';
  10. import '../../../widgets/info_card.dart';
  11. import '../../../widgets/ix_image.dart';
  12. import '../controllers/subscription_controller.dart';
  13. class SubscriptionView extends GetView<SubscriptionController> {
  14. const SubscriptionView({super.key});
  15. @override
  16. Widget build(BuildContext context) {
  17. return Scaffold(
  18. backgroundColor: DarkThemeColors.scaffoldBackgroundColor,
  19. body: Stack(
  20. children: [
  21. // 视频背景层(只显示顶部214高度)
  22. Obx(() {
  23. if (controller.isVideoInitialized.value) {
  24. return Positioned(
  25. top: 0,
  26. left: 0,
  27. right: 0,
  28. height: 214.w,
  29. child: ClipRect(
  30. child: FittedBox(
  31. fit: BoxFit.cover,
  32. child: SizedBox(
  33. width: controller.videoController.value.size.width,
  34. height: controller.videoController.value.size.height,
  35. child: VideoPlayer(controller.videoController),
  36. ),
  37. ),
  38. ),
  39. );
  40. }
  41. return const SizedBox.shrink();
  42. }),
  43. // 渐变遮罩层(只在视频区域)
  44. Positioned(
  45. top: 0,
  46. left: 0,
  47. right: 0,
  48. height: 214.w,
  49. child: Container(
  50. decoration: BoxDecoration(
  51. gradient: LinearGradient(
  52. begin: Alignment.topCenter,
  53. end: Alignment.bottomCenter,
  54. colors: [Colors.black.withValues(alpha: 0.6), Colors.black],
  55. stops: const [0.0, 1.0],
  56. ),
  57. ),
  58. ),
  59. ),
  60. // 内容层
  61. SafeArea(
  62. child: Column(
  63. children: [
  64. _buildAppBar(),
  65. Expanded(
  66. child: SingleChildScrollView(
  67. padding: EdgeInsets.symmetric(horizontal: 20.w),
  68. child: Column(
  69. crossAxisAlignment: CrossAxisAlignment.start,
  70. children: [
  71. 16.verticalSpaceFromWidth,
  72. _buildCurrentSubscription(),
  73. 24.verticalSpaceFromWidth,
  74. _buildPlanOptions(),
  75. // 仅 userLevel == 3 时显示套餐变更信息
  76. if (controller.showPlanChangeInfo)
  77. _buildPlanChangeInfo(),
  78. 16.verticalSpaceFromWidth,
  79. _buildPremiumFeatures(),
  80. 16.verticalSpaceFromWidth,
  81. ],
  82. ),
  83. ),
  84. ),
  85. _buildBottomSection(),
  86. ],
  87. ),
  88. ),
  89. ],
  90. ),
  91. );
  92. }
  93. // 顶部标题栏
  94. Widget _buildAppBar() {
  95. return Padding(
  96. padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
  97. child: Row(
  98. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  99. children: [
  100. SizedBox(width: 32.w),
  101. Text(
  102. Strings.subscription.tr,
  103. style: TextStyle(
  104. fontSize: 16.sp,
  105. height: 1.4,
  106. fontWeight: FontWeight.w500,
  107. color: Colors.white,
  108. ),
  109. ),
  110. GestureDetector(
  111. onTap: () => Get.back(),
  112. child: Container(
  113. width: 24.w,
  114. height: 24.w,
  115. decoration: BoxDecoration(
  116. color: Colors.white.withValues(alpha: 0.1),
  117. shape: BoxShape.circle,
  118. ),
  119. child: Icon(Icons.close_rounded, color: Colors.white, size: 16.w),
  120. ),
  121. ),
  122. ],
  123. ),
  124. );
  125. }
  126. // 当前订阅信息
  127. Widget _buildCurrentSubscription() {
  128. return Obx(() {
  129. // 判断是否有订阅
  130. if (!controller.hasCurrentSubscription) {
  131. // 没有订阅,只显示钻石图标
  132. return Center(
  133. child: IXImage(
  134. source: Assets.subscriptionDiamond,
  135. width: 92.w,
  136. height: 80.w,
  137. sourceType: ImageSourceType.asset,
  138. ),
  139. );
  140. }
  141. // 有订阅,显示当前套餐信息
  142. return Row(
  143. mainAxisAlignment: MainAxisAlignment.center,
  144. children: [
  145. // 钻石图标
  146. IXImage(
  147. source: Assets.subscriptionDiamond,
  148. width: 92.w,
  149. height: 80.w,
  150. sourceType: ImageSourceType.asset,
  151. ),
  152. 12.horizontalSpace,
  153. Expanded(
  154. child: Column(
  155. crossAxisAlignment: CrossAxisAlignment.start,
  156. children: [
  157. Row(
  158. children: [
  159. IXImage(
  160. source: Assets.subscriptionWallet,
  161. width: 20.w,
  162. height: 20.w,
  163. sourceType: ImageSourceType.asset,
  164. ),
  165. 4.horizontalSpace,
  166. Text(
  167. Strings.currentSubscription.tr,
  168. style: TextStyle(
  169. fontSize: 14.sp,
  170. height: 1.4,
  171. color: DarkThemeColors.subscriptionColor,
  172. fontWeight: FontWeight.w700,
  173. ),
  174. ),
  175. ],
  176. ),
  177. 10.verticalSpaceFromWidth,
  178. Text(
  179. controller.currentPlanPriceDisplay,
  180. style: TextStyle(
  181. fontSize: 14.sp,
  182. height: 1.4,
  183. color: Colors.white,
  184. ),
  185. ),
  186. ],
  187. ),
  188. ),
  189. ],
  190. );
  191. });
  192. }
  193. // 订阅计划选项
  194. Widget _buildPlanOptions() {
  195. return Obx(() {
  196. // 加载中状态
  197. if (controller.isLoadingPlans.value) {
  198. return Padding(
  199. padding: EdgeInsets.symmetric(vertical: 40.w),
  200. child: Center(
  201. child: CircularProgressIndicator(
  202. color: DarkThemeColors.subscriptionColor,
  203. ),
  204. ),
  205. );
  206. }
  207. // 空数据状态
  208. if (controller.planCount == 0) {
  209. return Padding(
  210. padding: EdgeInsets.symmetric(vertical: 40.w),
  211. child: Center(
  212. child: Text(
  213. '暂无可用套餐',
  214. style: TextStyle(
  215. fontSize: 14.sp,
  216. color: DarkThemeColors.hintTextColor,
  217. ),
  218. ),
  219. ),
  220. );
  221. }
  222. // 套餐列表
  223. return Column(
  224. children: List.generate(
  225. controller.planCount,
  226. (index) => _buildPlanItem(index),
  227. ),
  228. );
  229. });
  230. }
  231. Widget _buildPlanItem(int index) {
  232. return Obx(() {
  233. final isSelected = controller.selectedPlanIndex.value == index;
  234. final badge = controller.getPlanBadge(index);
  235. final badgeBgColor = controller.getPlanBadgeBgColor(index);
  236. final badgeTextColor = controller.getPlanBadgeTextColor(index);
  237. final badgeBorderColor = controller.getPlanBadgeBorderColor(index);
  238. return GestureDetector(
  239. onTap: () => controller.selectPlan(index),
  240. child: Container(
  241. margin: EdgeInsets.only(bottom: 18.w),
  242. decoration: BoxDecoration(
  243. color: DarkThemeColors.cardColor,
  244. borderRadius: BorderRadius.circular(12.r),
  245. border: Border.all(
  246. color: isSelected
  247. ? DarkThemeColors.subscriptionColor
  248. : DarkThemeColors.dividerColor,
  249. width: 2.w,
  250. ),
  251. ),
  252. child: Stack(
  253. clipBehavior: Clip.none,
  254. children: [
  255. // 主要内容
  256. Padding(
  257. padding: EdgeInsets.all(10.w),
  258. child: Row(
  259. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  260. children: [
  261. // 左侧:价格信息
  262. Expanded(
  263. child: Column(
  264. crossAxisAlignment: CrossAxisAlignment.start,
  265. children: [
  266. Text(
  267. controller.getPlanTitle(index),
  268. style: TextStyle(
  269. fontSize: 18.sp,
  270. height: 1.4,
  271. color: DarkThemeColors.bodyTextColor,
  272. fontWeight: FontWeight.w600,
  273. ),
  274. ),
  275. Text(
  276. controller.getPlanSubTitle(index),
  277. style: TextStyle(
  278. fontSize: 12.sp,
  279. height: 1.6,
  280. color: DarkThemeColors.hintTextColor,
  281. ),
  282. ),
  283. ],
  284. ),
  285. ),
  286. // 右侧:标题和选择框
  287. Row(
  288. children: [
  289. Text(
  290. controller.getPlanIntroduce(index),
  291. style: TextStyle(
  292. fontSize: 13.sp,
  293. height: 1.4,
  294. color: DarkThemeColors.bodyTextColor,
  295. ),
  296. ),
  297. 8.horizontalSpace,
  298. Container(
  299. width: 20.w,
  300. height: 20.w,
  301. decoration: BoxDecoration(
  302. shape: BoxShape.circle,
  303. border: Border.all(
  304. color: isSelected
  305. ? DarkThemeColors.primaryColor
  306. : Colors.white30,
  307. width: 1.5.w,
  308. ),
  309. color: isSelected
  310. ? DarkThemeColors.primaryColor
  311. : Colors.transparent,
  312. ),
  313. child: isSelected
  314. ? Icon(
  315. Icons.check,
  316. color: Colors.white,
  317. size: 12.w,
  318. )
  319. : null,
  320. ),
  321. ],
  322. ),
  323. ],
  324. ),
  325. ),
  326. // 标签固定在右上角,压在边框线上
  327. if (badge.isNotEmpty)
  328. Positioned(
  329. top: -11.h,
  330. right: 12.w,
  331. child: Container(
  332. padding: EdgeInsets.symmetric(horizontal: 6.w),
  333. decoration: BoxDecoration(
  334. color: badgeBgColor ?? Colors.black,
  335. borderRadius: BorderRadius.circular(4.r),
  336. border: badgeBorderColor != null
  337. ? Border.all(color: badgeBorderColor, width: 1)
  338. : null,
  339. ),
  340. child: Text(
  341. badge,
  342. style: TextStyle(
  343. fontSize: 12.sp,
  344. color: badgeTextColor ?? Colors.white,
  345. height: 1.6,
  346. ),
  347. ),
  348. ),
  349. ),
  350. ],
  351. ),
  352. ),
  353. );
  354. });
  355. }
  356. // 计划变更信息
  357. Widget _buildPlanChangeInfo() {
  358. return InfoCard(
  359. title: Strings.planChangeInfo.tr,
  360. items: [
  361. InfoItem(
  362. imageSource: Assets.subscriptionPlanChange1,
  363. title: Strings.whenItStarts.tr,
  364. description: Strings.yourNewPlanBeginsRightAway.tr,
  365. iconColor: DarkThemeColors.primaryColor,
  366. ),
  367. InfoItem(
  368. imageSource: Assets.subscriptionPlanChange2,
  369. title: Strings.whatHappensToYourBalance.tr,
  370. description: Strings.anyUnusedAmountFromYourOldPlan.tr,
  371. iconColor: DarkThemeColors.primaryColor,
  372. ),
  373. InfoItem(
  374. imageSource: Assets.subscriptionPlanChange3,
  375. title: Strings.extraTime.tr,
  376. description: Strings.youllGetExtraDays.tr,
  377. iconColor: DarkThemeColors.primaryColor,
  378. ),
  379. ],
  380. );
  381. }
  382. // Premium 功能列表
  383. Widget _buildPremiumFeatures() {
  384. return Column(
  385. crossAxisAlignment: CrossAxisAlignment.start,
  386. children: [
  387. Text(
  388. Strings.premiumsIncluded.tr,
  389. style: TextStyle(
  390. fontSize: 16.sp,
  391. color: DarkThemeColors.subscriptionColor,
  392. fontWeight: FontWeight.w500,
  393. ),
  394. ),
  395. 16.verticalSpace,
  396. Container(
  397. padding: EdgeInsets.symmetric(vertical: 4.w, horizontal: 10.w),
  398. decoration: BoxDecoration(
  399. color: DarkThemeColors.bgDisable,
  400. borderRadius: BorderRadius.circular(12.r),
  401. ),
  402. child: Column(
  403. children: [
  404. _buildFeatureItem(
  405. IconFont.icon60,
  406. Strings.unlockAllFreeLocations.tr,
  407. ),
  408. _buildFeatureItem(IconFont.icon61, Strings.unlockSmartMode.tr),
  409. _buildFeatureItem(IconFont.icon62, Strings.unlockMultiHopMode.tr),
  410. Obx(
  411. () => _buildFeatureItem(
  412. IconFont.icon63,
  413. Strings.premiumCanShareXDevices.trParams({
  414. 'count': controller.selectedPlanDeviceLimit,
  415. }),
  416. ),
  417. ),
  418. _buildFeatureItem(
  419. IconFont.icon64,
  420. Strings.ownYourOwnPrivateServer.tr,
  421. ),
  422. _buildFeatureItem(IconFont.icon65, Strings.closeAds.tr),
  423. ],
  424. ),
  425. ),
  426. ],
  427. );
  428. }
  429. Widget _buildFeatureItem(IconData icon, String title) {
  430. return SizedBox(
  431. height: 44.w,
  432. child: Row(
  433. children: [
  434. Icon(icon, color: DarkThemeColors.subscriptionColor, size: 24.w),
  435. 12.horizontalSpace,
  436. Expanded(
  437. child: Text(
  438. title,
  439. style: TextStyle(
  440. fontSize: 13.sp,
  441. color: Get.reactiveTheme.hintColor,
  442. ),
  443. ),
  444. ),
  445. Container(
  446. width: 20.w,
  447. height: 20.w,
  448. decoration: BoxDecoration(
  449. shape: BoxShape.circle,
  450. color: DarkThemeColors.subscriptionSelectColor,
  451. ),
  452. child: Icon(Icons.check, color: Colors.white, size: 12.w),
  453. ),
  454. ],
  455. ),
  456. );
  457. }
  458. // 底部按钮区域
  459. Widget _buildBottomSection() {
  460. return Container(
  461. padding: EdgeInsets.symmetric(vertical: 10.w, horizontal: 14.w),
  462. decoration: BoxDecoration(
  463. border: Border(
  464. top: BorderSide(color: Colors.white.withOpacity(0.1), width: 1),
  465. ),
  466. ),
  467. child: Column(
  468. mainAxisSize: MainAxisSize.min,
  469. children: [
  470. // 确认按钮
  471. GestureDetector(
  472. onTap: controller.subscribe,
  473. child: Container(
  474. width: double.infinity,
  475. height: 48.w,
  476. decoration: BoxDecoration(
  477. color: DarkThemeColors.backgroundColor,
  478. borderRadius: BorderRadius.circular(12.r),
  479. ),
  480. child: Center(
  481. child: Text(
  482. controller.showPlanChangeInfo
  483. ? Strings.confirmChange.tr
  484. : Strings.subscription.tr,
  485. style: TextStyle(
  486. fontSize: 16.sp,
  487. color: DarkThemeColors.subscriptionColor,
  488. fontWeight: FontWeight.w600,
  489. ),
  490. ),
  491. ),
  492. ),
  493. ),
  494. 14.verticalSpaceFromWidth,
  495. // 底部链接
  496. Row(
  497. mainAxisAlignment: MainAxisAlignment.center,
  498. children: [
  499. GestureDetector(
  500. onTap: controller.restorePurchases,
  501. child: Text(
  502. Strings.restorePurchases.tr,
  503. style: TextStyle(
  504. fontSize: 16.sp,
  505. color: DarkThemeColors.bodyTextColor,
  506. ),
  507. ),
  508. ),
  509. Text(
  510. ' | ',
  511. style: TextStyle(
  512. fontSize: 16.sp,
  513. color: DarkThemeColors.hintTextColor,
  514. ),
  515. ),
  516. GestureDetector(
  517. onTap: controller.handlePaymentIssue,
  518. child: Text(
  519. Strings.paymentIssue.tr,
  520. style: TextStyle(
  521. fontSize: 16.sp,
  522. color: DarkThemeColors.bodyTextColor,
  523. ),
  524. ),
  525. ),
  526. ],
  527. ),
  528. 14.verticalSpaceFromWidth,
  529. Row(
  530. mainAxisAlignment: MainAxisAlignment.center,
  531. children: [
  532. IXImage(
  533. source: Assets.subscriptionGreenShield,
  534. width: 20.w,
  535. height: 20.w,
  536. sourceType: ImageSourceType.asset,
  537. ),
  538. 10.horizontalSpace,
  539. Text(
  540. Strings.yearlyAutoRenewCancelAnytime.tr,
  541. style: TextStyle(
  542. fontSize: 13.sp,
  543. color: DarkThemeColors.hintTextColor,
  544. ),
  545. ),
  546. ],
  547. ),
  548. ],
  549. ),
  550. );
  551. }
  552. }