subscription_view.dart 19 KB

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