subscription_view.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:nomo/config/theme/dark_theme_colors.dart';
  5. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  6. import 'package:shimmer/shimmer.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. _buildPremiumFeatures(),
  84. 16.verticalSpaceFromWidth,
  85. ],
  86. ),
  87. ),
  88. ),
  89. _buildBottomSection(),
  90. ],
  91. ),
  92. ),
  93. ],
  94. ),
  95. );
  96. }
  97. // 顶部标题栏
  98. Widget _buildAppBar() {
  99. return Padding(
  100. padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
  101. child: Row(
  102. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  103. children: [
  104. SizedBox(width: 32.w),
  105. Text(
  106. Strings.subscription.tr,
  107. style: TextStyle(
  108. fontSize: 16.sp,
  109. height: 1.4,
  110. fontWeight: FontWeight.w500,
  111. color: Colors.white,
  112. ),
  113. ),
  114. GestureDetector(
  115. onTap: () => Get.back(),
  116. child: Container(
  117. width: 24.w,
  118. height: 24.w,
  119. decoration: BoxDecoration(
  120. color: ReactiveTheme.isLightTheme
  121. ? LightThemeColors.strokes1
  122. : Color(0xFF333333),
  123. shape: BoxShape.circle,
  124. ),
  125. child: Icon(
  126. Icons.close_rounded,
  127. color: ReactiveTheme.isLightTheme
  128. ? LightThemeColors.text2
  129. : Colors.white,
  130. size: 16.w,
  131. ),
  132. ),
  133. ),
  134. ],
  135. ),
  136. );
  137. }
  138. // 当前订阅信息
  139. Widget _buildCurrentSubscription() {
  140. return Obx(() {
  141. // 判断是否有订阅
  142. if (!controller.hasCurrentSubscription) {
  143. // 没有订阅,只显示钻石图标
  144. return Center(
  145. child: IXImage(
  146. source: Assets.subscriptionDiamond,
  147. width: 92.w,
  148. height: 80.w,
  149. sourceType: ImageSourceType.asset,
  150. ),
  151. );
  152. }
  153. // 有订阅,显示当前套餐信息
  154. return Row(
  155. mainAxisAlignment: MainAxisAlignment.center,
  156. children: [
  157. // 钻石图标
  158. IXImage(
  159. source: Assets.subscriptionDiamond,
  160. width: 92.w,
  161. height: 80.w,
  162. sourceType: ImageSourceType.asset,
  163. ),
  164. 12.horizontalSpace,
  165. Expanded(
  166. child: Column(
  167. crossAxisAlignment: CrossAxisAlignment.start,
  168. children: [
  169. Row(
  170. children: [
  171. IXImage(
  172. source: Assets.subscriptionWallet,
  173. width: 20.w,
  174. height: 20.w,
  175. sourceType: ImageSourceType.asset,
  176. ),
  177. 4.horizontalSpace,
  178. Text(
  179. Strings.currentSubscription.tr,
  180. style: TextStyle(
  181. fontSize: 14.sp,
  182. height: 1.4,
  183. color: ReactiveTheme.isLightTheme
  184. ? LightThemeColors.text1
  185. : DarkThemeColors.subscriptionColor,
  186. fontWeight: FontWeight.w700,
  187. ),
  188. ),
  189. ],
  190. ),
  191. 10.verticalSpaceFromWidth,
  192. Text(
  193. controller.currentPlanPriceDisplay,
  194. style: TextStyle(
  195. fontSize: 14.sp,
  196. height: 1.4,
  197. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  198. ),
  199. ),
  200. ],
  201. ),
  202. ),
  203. ],
  204. );
  205. });
  206. }
  207. // 订阅计划选项
  208. Widget _buildPlanOptions() {
  209. return Obx(() {
  210. // 加载中状态
  211. if (controller.isLoadingPlans.value) {
  212. return Column(
  213. children: List.generate(3, (index) => _buildPlanShimmer()),
  214. );
  215. }
  216. // 空数据状态
  217. if (controller.planCount == 0) {
  218. return Padding(
  219. padding: EdgeInsets.all(20.w),
  220. child: Center(
  221. child: Column(
  222. mainAxisAlignment: MainAxisAlignment.center,
  223. crossAxisAlignment: CrossAxisAlignment.center,
  224. children: [
  225. Image.asset(Assets.oops, width: 170.w, height: 170.w),
  226. 10.verticalSpaceFromWidth,
  227. Text(
  228. Strings.oops.tr,
  229. style: TextStyle(
  230. fontSize: 22.sp,
  231. fontWeight: FontWeight.w400,
  232. height: 1.3,
  233. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  234. ),
  235. ),
  236. 4.verticalSpace,
  237. Padding(
  238. padding: EdgeInsets.symmetric(horizontal: 32.w),
  239. child: Text(
  240. Strings.connectionIssueDetected.tr,
  241. textAlign: TextAlign.center,
  242. style: TextStyle(
  243. fontSize: 14.sp,
  244. height: 1.4,
  245. color: Get.reactiveTheme.hintColor,
  246. ),
  247. ),
  248. ),
  249. ],
  250. ),
  251. ),
  252. );
  253. }
  254. // 套餐列表
  255. return Column(
  256. children: List.generate(
  257. controller.planCount,
  258. (index) => _buildPlanItem(index),
  259. ),
  260. );
  261. });
  262. }
  263. Widget _buildPlanShimmer() {
  264. final baseColor = ReactiveTheme.isLightTheme
  265. ? Colors.grey[300]!
  266. : Colors.grey[700]!;
  267. final highlightColor = ReactiveTheme.isLightTheme
  268. ? Colors.grey[100]!
  269. : Colors.grey[600]!;
  270. return Container(
  271. margin: EdgeInsets.only(bottom: 18.w),
  272. decoration: BoxDecoration(
  273. color: Get.reactiveTheme.cardColor,
  274. borderRadius: BorderRadius.circular(12.r),
  275. border: Border.all(color: Get.reactiveTheme.dividerColor, width: 2.w),
  276. ),
  277. child: Shimmer.fromColors(
  278. baseColor: baseColor,
  279. highlightColor: highlightColor,
  280. child: Padding(
  281. padding: EdgeInsets.all(10.w),
  282. child: Row(
  283. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  284. children: [
  285. // 左侧:模拟价格信息
  286. Column(
  287. crossAxisAlignment: CrossAxisAlignment.start,
  288. children: [
  289. Container(
  290. width: 120.w,
  291. height: 18.w,
  292. decoration: BoxDecoration(
  293. color: Colors.white,
  294. borderRadius: BorderRadius.circular(4.r),
  295. ),
  296. ),
  297. 8.verticalSpace,
  298. Container(
  299. width: 80.w,
  300. height: 14.w,
  301. decoration: BoxDecoration(
  302. color: Colors.white,
  303. borderRadius: BorderRadius.circular(4.r),
  304. ),
  305. ),
  306. ],
  307. ),
  308. // 右侧:模拟标题
  309. Container(
  310. width: 60.w,
  311. height: 16.w,
  312. decoration: BoxDecoration(
  313. color: Colors.white,
  314. borderRadius: BorderRadius.circular(4.r),
  315. ),
  316. ),
  317. ],
  318. ),
  319. ),
  320. ),
  321. );
  322. }
  323. Widget _buildPlanItem(int index) {
  324. return Obx(() {
  325. final isSelected = controller.selectedPlanIndex.value == index;
  326. final badge = controller.getPlanBadge(index);
  327. final badgeBgColor = controller.getPlanBadgeBgColor(index);
  328. final badgeTextColor = controller.getPlanBadgeTextColor(index);
  329. final badgeBorderColor = controller.getPlanBadgeBorderColor(index);
  330. return GestureDetector(
  331. onTap: () => controller.selectPlan(index),
  332. child: Container(
  333. margin: EdgeInsets.only(bottom: 18.w),
  334. decoration: BoxDecoration(
  335. color: Get.reactiveTheme.cardColor,
  336. borderRadius: BorderRadius.circular(12.r),
  337. border: Border.all(
  338. color: isSelected
  339. ? DarkThemeColors.subscriptionColor
  340. : Get.reactiveTheme.dividerColor,
  341. width: 2.w,
  342. ),
  343. ),
  344. child: Stack(
  345. clipBehavior: Clip.none,
  346. children: [
  347. // 主要内容
  348. Padding(
  349. padding: EdgeInsets.all(10.w),
  350. child: Row(
  351. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  352. children: [
  353. // 左侧:价格信息
  354. Expanded(
  355. child: Column(
  356. crossAxisAlignment: CrossAxisAlignment.start,
  357. children: [
  358. Text(
  359. controller.getPlanTitle(index),
  360. style: TextStyle(
  361. fontSize: 18.sp,
  362. height: 1.4,
  363. color:
  364. Get.reactiveTheme.textTheme.bodyLarge!.color,
  365. fontWeight: FontWeight.w600,
  366. ),
  367. ),
  368. Text(
  369. controller.getPlanSubTitle(index),
  370. style: TextStyle(
  371. fontSize: 12.sp,
  372. height: 1.6,
  373. color: Get.reactiveTheme.hintColor,
  374. ),
  375. ),
  376. ],
  377. ),
  378. ),
  379. // 右侧:标题和选择框
  380. Row(
  381. children: [
  382. Text(
  383. controller.getPlanIntroduce(index),
  384. style: TextStyle(
  385. fontSize: 13.sp,
  386. height: 1.4,
  387. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  388. ),
  389. ),
  390. // 8.horizontalSpace,
  391. // Container(
  392. // width: 20.w,
  393. // height: 20.w,
  394. // decoration: BoxDecoration(
  395. // shape: BoxShape.circle,
  396. // border: Border.all(
  397. // color: isSelected
  398. // ? DarkThemeColors.primaryColor
  399. // : ReactiveTheme.isLightTheme
  400. // ? LightThemeColors.strokes1
  401. // : Colors.white30,
  402. // width: 1.5.w,
  403. // ),
  404. // color: isSelected
  405. // ? DarkThemeColors.primaryColor
  406. // : Colors.transparent,
  407. // ),
  408. // child: isSelected
  409. // ? Icon(
  410. // Icons.check,
  411. // color: Colors.white,
  412. // size: 12.w,
  413. // )
  414. // : null,
  415. // ),
  416. ],
  417. ),
  418. ],
  419. ),
  420. ),
  421. // 标签固定在右上角,压在边框线上
  422. if (badge.isNotEmpty)
  423. Positioned(
  424. top: -11.h,
  425. right: 12.w,
  426. child: Container(
  427. padding: EdgeInsets.symmetric(horizontal: 6.w),
  428. decoration: BoxDecoration(
  429. color: badgeBgColor ?? Colors.black,
  430. borderRadius: BorderRadius.circular(4.r),
  431. border: badgeBorderColor != null
  432. ? Border.all(color: badgeBorderColor, width: 1)
  433. : null,
  434. ),
  435. child: Text(
  436. badge,
  437. style: TextStyle(
  438. fontSize: 12.sp,
  439. color: badgeTextColor ?? Colors.white,
  440. height: 1.6,
  441. ),
  442. ),
  443. ),
  444. ),
  445. ],
  446. ),
  447. ),
  448. );
  449. });
  450. }
  451. // 计划变更信息
  452. Widget _buildPlanChangeInfo() {
  453. return InfoCard(
  454. title: Strings.planChangeInfo.tr,
  455. items: [
  456. InfoItem(
  457. imageSource: Assets.subscriptionPlanChange1,
  458. title: Strings.whenItStarts.tr,
  459. description: Strings.yourNewPlanBeginsRightAway.tr,
  460. iconColor: DarkThemeColors.primaryColor,
  461. ),
  462. InfoItem(
  463. imageSource: Assets.subscriptionPlanChange2,
  464. title: Strings.whatHappensToYourBalance.tr,
  465. description: Strings.anyUnusedAmountFromYourOldPlan.tr,
  466. iconColor: DarkThemeColors.primaryColor,
  467. ),
  468. InfoItem(
  469. imageSource: Assets.subscriptionPlanChange3,
  470. title: Strings.extraTime.tr,
  471. description: Strings.youllGetExtraDays.tr,
  472. iconColor: DarkThemeColors.primaryColor,
  473. ),
  474. ],
  475. );
  476. }
  477. // Premium 功能列表
  478. Widget _buildPremiumFeatures() {
  479. return Column(
  480. crossAxisAlignment: CrossAxisAlignment.start,
  481. children: [
  482. Text(
  483. Strings.premiumsIncluded.tr,
  484. style: TextStyle(
  485. fontSize: 16.sp,
  486. color: ReactiveTheme.isLightTheme
  487. ? LightThemeColors.primaryColor
  488. : DarkThemeColors.subscriptionColor,
  489. fontWeight: FontWeight.w500,
  490. ),
  491. ),
  492. 16.verticalSpace,
  493. Container(
  494. padding: EdgeInsets.symmetric(vertical: 4.w, horizontal: 10.w),
  495. decoration: BoxDecoration(
  496. color: Get.reactiveTheme.cardColor,
  497. borderRadius: BorderRadius.circular(12.r),
  498. ),
  499. child: Column(
  500. children: [
  501. Obx(
  502. () => _buildFeatureItem(
  503. Assets.equity1,
  504. Strings.equity1Title.trParams({
  505. 'count': controller.selectedPlanDeviceLimit,
  506. }),
  507. Strings.equity1Desc.trParams({
  508. 'count': controller.selectedPlanDeviceLimit,
  509. }),
  510. ),
  511. ),
  512. Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  513. _buildFeatureItem(
  514. Assets.equity2,
  515. Strings.equity2Title.tr,
  516. Strings.equity2Desc.tr,
  517. ),
  518. Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  519. _buildFeatureItem(
  520. Assets.equity3,
  521. Strings.equity3Title.tr,
  522. Strings.equity3Desc.tr,
  523. ),
  524. Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  525. _buildFeatureItem(
  526. Assets.equity4,
  527. Strings.equity4Title.tr,
  528. Strings.equity4Desc.tr,
  529. ),
  530. Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  531. _buildFeatureItem(
  532. Assets.equity5,
  533. Strings.equity5Title.tr,
  534. Strings.equity5Desc.tr,
  535. ),
  536. Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  537. _buildFeatureItem(
  538. Assets.equity6,
  539. Strings.equity6Title.tr,
  540. Strings.equity6Desc.tr,
  541. ),
  542. ],
  543. ),
  544. ),
  545. ],
  546. );
  547. }
  548. Widget _buildFeatureItem(String imagePath, String title, String subtitle) {
  549. return Padding(
  550. padding: EdgeInsets.symmetric(vertical: 10.w),
  551. child: Row(
  552. children: [
  553. Image.asset(imagePath, width: 24.w, height: 24.w),
  554. 16.horizontalSpace,
  555. Expanded(
  556. child: Column(
  557. crossAxisAlignment: CrossAxisAlignment.start,
  558. children: [
  559. Text(
  560. title,
  561. style: TextStyle(
  562. fontSize: 14.sp,
  563. fontWeight: FontWeight.w500,
  564. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  565. ),
  566. ),
  567. 4.verticalSpace,
  568. Text(
  569. subtitle,
  570. style: TextStyle(
  571. fontSize: 12.sp,
  572. color: Get.reactiveTheme.hintColor,
  573. ),
  574. ),
  575. ],
  576. ),
  577. ),
  578. ],
  579. ),
  580. );
  581. }
  582. // 底部按钮区域
  583. Widget _buildBottomSection() {
  584. return Container(
  585. padding: EdgeInsets.symmetric(vertical: 10.w, horizontal: 14.w),
  586. decoration: BoxDecoration(
  587. border: Border(
  588. top: BorderSide(color: Get.reactiveTheme.dividerColor, width: 1),
  589. ),
  590. ),
  591. child: Column(
  592. mainAxisSize: MainAxisSize.min,
  593. children: [
  594. // 确认按钮
  595. GestureDetector(
  596. onTap: controller.subscribe,
  597. child: Container(
  598. width: double.infinity,
  599. height: 48.w,
  600. decoration: BoxDecoration(
  601. color: DarkThemeColors.backgroundColor,
  602. borderRadius: BorderRadius.circular(12.r),
  603. ),
  604. child: Center(
  605. child: Text(
  606. controller.showPlanChangeInfo
  607. ? Strings.confirmChange.tr
  608. : Strings.subscription.tr,
  609. style: TextStyle(
  610. fontSize: 16.sp,
  611. color: DarkThemeColors.subscriptionColor,
  612. fontWeight: FontWeight.w600,
  613. ),
  614. ),
  615. ),
  616. ),
  617. ),
  618. 14.verticalSpaceFromWidth,
  619. // 底部链接
  620. if (controller.apiController.fp.channel == 'google' ||
  621. controller.apiController.fp.channel == 'apple') ...[
  622. Row(
  623. mainAxisAlignment: MainAxisAlignment.center,
  624. children: [
  625. GestureDetector(
  626. onTap: controller.restorePurchases,
  627. child: Text(
  628. Strings.restorePurchases.tr,
  629. style: TextStyle(
  630. fontSize: 16.sp,
  631. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  632. ),
  633. ),
  634. ),
  635. // Text(
  636. // ' | ',
  637. // style: TextStyle(
  638. // fontSize: 16.sp,
  639. // color: Get.reactiveTheme.hintColor,
  640. // ),
  641. // ),
  642. // GestureDetector(
  643. // onTap: controller.handlePaymentIssue,
  644. // child: Text(
  645. // Strings.paymentIssue.tr,
  646. // style: TextStyle(
  647. // fontSize: 16.sp,
  648. // color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  649. // ),
  650. // ),
  651. // ),
  652. ],
  653. ),
  654. 14.verticalSpaceFromWidth,
  655. Row(
  656. mainAxisAlignment: MainAxisAlignment.center,
  657. children: [
  658. IXImage(
  659. source: Assets.subscriptionGreenShield,
  660. width: 20.w,
  661. height: 20.w,
  662. sourceType: ImageSourceType.asset,
  663. ),
  664. 10.horizontalSpace,
  665. Text(
  666. Strings.yearlyAutoRenewCancelAnytime.tr,
  667. style: TextStyle(
  668. fontSize: 13.sp,
  669. color: Get.reactiveTheme.hintColor,
  670. ),
  671. ),
  672. ],
  673. ),
  674. ],
  675. ],
  676. ),
  677. );
  678. }
  679. }