store_page.dart 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. import 'package:auto_size_text/auto_size_text.dart';
  2. import 'package:carousel_slider/carousel_slider.dart';
  3. import 'package:collection/collection.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_screenutil/flutter_screenutil.dart';
  6. import 'package:get/get.dart';
  7. import 'package:keyboard/base/base_page.dart';
  8. import 'package:keyboard/data/consts/payment_type.dart';
  9. import 'package:keyboard/data/consts/web_url.dart';
  10. import 'package:keyboard/module/store/store_controller.dart';
  11. import 'package:keyboard/module/store/store_user_reviews_bean.dart';
  12. import 'package:keyboard/resource/string.gen.dart';
  13. import 'package:keyboard/widget/platform_util.dart';
  14. import '../../data/bean/goods_info.dart';
  15. import '../../resource/assets.gen.dart';
  16. import '../../resource/colors.gen.dart';
  17. import '../../router/app_pages.dart';
  18. import '../../utils/count_down_timer.dart';
  19. import '../../utils/date_util.dart';
  20. import '../../widget/horizontal_dashed_line.dart';
  21. import '../../utils/styles.dart';
  22. import '../../widget/click_text_span.dart';
  23. class StorePage extends BasePage<StoreController> {
  24. const StorePage({super.key});
  25. static start() {
  26. Get.toNamed(RoutePath.store);
  27. }
  28. @override
  29. backgroundColor() => const Color(0xFFFFF8D4);
  30. @override
  31. bool immersive() {
  32. return true;
  33. }
  34. @override
  35. Widget buildBody(BuildContext context) {
  36. Widget bottomArea;
  37. if (PlatformUtil.isIOS) {
  38. bottomArea = Column(
  39. children: [
  40. // iOS平台的隐私协议和服务条款
  41. _buildLastBottomCorner(
  42. child: Container(
  43. // margin: EdgeInsets.only(left: 16.w),
  44. child: _buildPrivacy(
  45. privacyColor: Color(0x99673300),
  46. mainAxisAlignment: MainAxisAlignment.start,
  47. ),
  48. ),
  49. ),
  50. // 恢复订阅入口
  51. _buildRecoverSubscribe(),
  52. SizedBox(height: 20.h),
  53. _buildUserReviews(),
  54. SizedBox(height: 20.h),
  55. _buildUserNotice(),
  56. ],
  57. );
  58. } else {
  59. // 安卓端,可以有会员心声和购买须知
  60. bottomArea = Column(
  61. children: [
  62. // 产品描述
  63. _buildLastBottomCorner(child: _buildVipDesc()),
  64. SizedBox(height: 32.h),
  65. _buildUserReviews(),
  66. SizedBox(height: 20.h),
  67. _buildUserNotice(),
  68. ],
  69. );
  70. }
  71. return Stack(
  72. children: [
  73. SingleChildScrollView(
  74. child: Column(
  75. children: [
  76. _buildBanner(context),
  77. SizedBox(height: 12.h),
  78. _buildGoodsCard(),
  79. bottomArea,
  80. SizedBox(height: 40.h),
  81. ],
  82. ),
  83. ),
  84. Positioned(top: 0, left: 0, right: 0, child: _buildTitle()),
  85. Positioned(bottom: 0, left: 0, right: 0, child: _buildBuyButtonCard()),
  86. ],
  87. );
  88. }
  89. _buildTitle() {
  90. return SafeArea(
  91. child: Container(
  92. alignment: Alignment.centerLeft,
  93. padding: EdgeInsets.only(top: 16.h, left: 16.w, right: 16.w),
  94. child: Row(
  95. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  96. children: [
  97. GestureDetector(
  98. onTap: controller.clickBack,
  99. child: Assets.images.iconStoreBack.image(
  100. width: 32.w,
  101. height: 32.w,
  102. ),
  103. ),
  104. Obx(() {
  105. return SizedBox(
  106. height: 32.h,
  107. child: Row(
  108. mainAxisSize: MainAxisSize.min,
  109. mainAxisAlignment: MainAxisAlignment.center,
  110. crossAxisAlignment: CrossAxisAlignment.center,
  111. children: [
  112. // 会员状态信息
  113. _buildMemberInfo(),
  114. SizedBox(width: 8.w),
  115. controller.isLogin
  116. ? Assets.images.iconMineUserLogged.image(
  117. width: 28.w,
  118. height: 28.w,
  119. )
  120. : Assets.images.iconMineUserNoLogin.image(
  121. width: 28.w,
  122. height: 28.w,
  123. ),
  124. ],
  125. ),
  126. );
  127. }),
  128. ],
  129. ),
  130. ),
  131. );
  132. }
  133. // 会员信息
  134. Widget _buildMemberInfo() {
  135. return Container(
  136. height: 32.h,
  137. alignment: Alignment.center,
  138. padding: EdgeInsets.symmetric(horizontal: 12.w),
  139. decoration: BoxDecoration(
  140. color: Colors.black.withAlpha(77),
  141. borderRadius: BorderRadius.circular(21.r),
  142. ),
  143. child: Text.rich(
  144. TextSpan(children: _getMemberStatusText()),
  145. textAlign: TextAlign.right,
  146. ),
  147. );
  148. }
  149. // 会员状态文字逻辑提取
  150. List<TextSpan> _getMemberStatusText() {
  151. // // 未登录
  152. // if (!controller.isLogin) {
  153. // return [
  154. // TextSpan(
  155. // text: StringName.memberNoLogged,
  156. // style: _vipTextStyle(isHighlight: true),
  157. // ),
  158. // TextSpan(text: StringName.memberCardNoVipDesc, style: _vipTextStyle()),
  159. // ];
  160. // }
  161. final isMember = controller.memberStatusInfo?.isMember ?? false;
  162. final isPermanent = controller.memberStatusInfo?.permanent ?? false;
  163. final username = controller.userInfo?.name ?? '';
  164. if (isMember) {
  165. if (isPermanent) {
  166. return [
  167. TextSpan(
  168. text: StringName.memberCardPermanentVipDesc1,
  169. style: _vipTextStyle(),
  170. ),
  171. TextSpan(
  172. text: StringName.memberCardPermanentVipDesc2,
  173. style: _vipTextStyle(isHighlight: true),
  174. ),
  175. ];
  176. } else {
  177. return [
  178. TextSpan(text: StringName.memberVipDesc, style: _vipTextStyle()),
  179. TextSpan(
  180. text: DateUtil.fromMillisecondsSinceEpoch(
  181. 'yyyy.MM.dd',
  182. controller.memberStatusInfo?.endTimestamp ?? 0,
  183. ),
  184. style: _vipTextStyle(isHighlight: true),
  185. ),
  186. ];
  187. }
  188. }
  189. // 登录但不是会员
  190. return [
  191. TextSpan(
  192. text: controller.userInfo?.name ?? controller.getUserName(),
  193. style: _vipTextStyle(isHighlight: true),
  194. ),
  195. TextSpan(text: StringName.memberCardNoVipDesc, style: _vipTextStyle()),
  196. ];
  197. }
  198. // 统一的会员状态文本样式
  199. TextStyle _vipTextStyle({bool isHighlight = false}) {
  200. return TextStyle(
  201. color: isHighlight ? const Color(0xFFFFD273) : Colors.white,
  202. fontSize: 12.sp,
  203. fontWeight: FontWeight.w400,
  204. );
  205. }
  206. Widget _buildGoodsCard() {
  207. return Container(
  208. margin: EdgeInsets.symmetric(horizontal: 16.w),
  209. padding: EdgeInsets.only(
  210. top: 16.h,
  211. left: 16.w,
  212. right: 16.w,
  213. bottom: 24.h,
  214. ),
  215. decoration: BoxDecoration(
  216. color: Colors.white,
  217. borderRadius: BorderRadius.only(
  218. topLeft: Radius.circular(16.r),
  219. topRight: Radius.circular(16.r),
  220. ),
  221. ),
  222. child: Column(
  223. crossAxisAlignment: CrossAxisAlignment.start,
  224. mainAxisAlignment: MainAxisAlignment.start,
  225. children: [
  226. Assets.images.iconGoodsInfoTitle.image(
  227. width: 115.w,
  228. fit: BoxFit.cover,
  229. ),
  230. SizedBox(height: 16.h),
  231. Obx(() {
  232. return Column(
  233. children:
  234. controller.filteredGoodsList.mapIndexed((index, item) {
  235. return Obx(() {
  236. return GestureDetector(
  237. onTap: () => controller.onGoodsItemClick(item),
  238. child:
  239. PlatformUtil.isIOS
  240. ? _buildGoodsItemIos(
  241. index,
  242. item,
  243. controller.selectedGoodsInfoItem?.id ==
  244. item.id,
  245. )
  246. : _buildGoodsItem(
  247. item,
  248. controller.selectedGoodsInfoItem?.id ==
  249. item.id,
  250. ),
  251. );
  252. });
  253. }).toList(),
  254. );
  255. }),
  256. // iOS平台的产品描述
  257. if (PlatformUtil.isIOS) _buildVipDesc(),
  258. // 非iOS平台,才有支付宝支付和微信支付
  259. if (!PlatformUtil.isIOS) _buildPayWayCard(),
  260. ],
  261. ),
  262. );
  263. }
  264. Widget _buildPayWayCard() {
  265. return GestureDetector(
  266. onTap: () => controller.clickPayWaySwitch(),
  267. child: Container(
  268. height: 36.h,
  269. padding: EdgeInsets.symmetric(horizontal: 10.w),
  270. decoration: ShapeDecoration(
  271. shape: RoundedRectangleBorder(
  272. side: BorderSide(width: 1, color: const Color(0xFFECEBE0)),
  273. borderRadius: BorderRadius.circular(10.r),
  274. ),
  275. ),
  276. child: Row(
  277. children: [
  278. Text(
  279. StringName.storePayWay,
  280. style: Styles.getTextStyleBlack204W400(14.sp),
  281. ),
  282. const Spacer(),
  283. Obx(() {
  284. if (controller.selectedPayWay == null) {
  285. return SizedBox.shrink();
  286. }
  287. return Row(
  288. children: [
  289. Image.asset(
  290. getPaymentIconPath(
  291. payMethod: controller.selectedPayWay!.payMethod,
  292. payPlatform: controller.selectedPayWay!.payPlatform,
  293. ),
  294. width: 20.w,
  295. height: 20.w,
  296. ),
  297. SizedBox(width: 4.w),
  298. Text(
  299. controller.selectedPayWay?.title ?? '',
  300. style: Styles.getTextStyleBlack204W400(14.sp),
  301. ),
  302. SizedBox(width: 6.w),
  303. Assets.images.iconStoreSwitchPay.image(
  304. width: 20.w,
  305. height: 20.w,
  306. fit: BoxFit.fill,
  307. ),
  308. ],
  309. );
  310. }),
  311. ],
  312. ),
  313. ),
  314. );
  315. }
  316. /// 商品-iOS端
  317. Widget _buildGoodsItemIos(int index, GoodsInfo item, bool isSelected) {
  318. // 第一个商品,才有有倒计时
  319. bool hasCountdown = index == 0;
  320. var amountText = item.amountText;
  321. if (index == 0) {
  322. if (controller.isDiscount.value) {
  323. amountText = item.firstAmountText;
  324. }
  325. }
  326. Widget contentWidget = Stack(
  327. children: [
  328. Positioned(
  329. left: 16.w,
  330. top: 0,
  331. right: 0,
  332. bottom: 0,
  333. child: Row(
  334. children: [
  335. // 价格
  336. RichText(
  337. text: TextSpan(
  338. children: [
  339. TextSpan(
  340. text: '¥',
  341. style: Styles.getTextStyleFF663300W700(14.sp),
  342. ),
  343. TextSpan(
  344. text: amountText,
  345. style: Styles.getTextStyleFF663300W700(18.sp),
  346. ),
  347. ],
  348. ),
  349. ),
  350. SizedBox(width: 18.w),
  351. // 名称和描述
  352. Column(
  353. // 垂直居中
  354. mainAxisAlignment: MainAxisAlignment.center,
  355. // 水平左对齐
  356. crossAxisAlignment: CrossAxisAlignment.start,
  357. children: [
  358. // 名称
  359. Text(
  360. item.name,
  361. style: Styles.getTextStyleFF663300W500(15.sp),
  362. ),
  363. // 描述
  364. if (item.mostDesc?.isNotEmpty == true)
  365. AutoSizeText(
  366. item.mostDesc!,
  367. style: TextStyle(
  368. color: Color(0x99673300),
  369. fontSize: 12.sp,
  370. fontWeight: FontWeight.w500,
  371. letterSpacing: -0.60,
  372. ),
  373. maxLines: 1,
  374. overflow: TextOverflow.ellipsis,
  375. // 最小字体
  376. minFontSize: 8,
  377. // 缩小步长,越小越丝滑
  378. stepGranularity: 0.5,
  379. ),
  380. ],
  381. ),
  382. ],
  383. ),
  384. ),
  385. // 勾选状态
  386. Positioned(
  387. top: 0,
  388. right: 22.w,
  389. bottom: 0,
  390. child: Image(
  391. image:
  392. isSelected
  393. ? Assets.images.iconStoreGoodsSelectedSymbolIos.provider()
  394. : Assets.images.iconStoreGoodsNormalSymbolIos.provider(),
  395. width: 20.w,
  396. height: 20.w,
  397. ),
  398. ),
  399. // 倒计时
  400. Positioned(
  401. top: 0,
  402. right: 8,
  403. child: Visibility(
  404. visible: hasCountdown,
  405. child: Row(
  406. crossAxisAlignment: CrossAxisAlignment.center,
  407. mainAxisAlignment: MainAxisAlignment.center,
  408. children: [
  409. Text(
  410. "倒计时",
  411. style: TextStyle(
  412. color: isSelected ? Color(0xFFFFECBB) : Color(0xFFFF9416),
  413. fontSize: 12.sp,
  414. fontWeight: FontWeight.w500,
  415. ),
  416. ),
  417. SizedBox(width: 4.w),
  418. Container(
  419. margin: EdgeInsets.only(top: 2.5.h),
  420. child: Obx(() {
  421. return Text(
  422. CountdownTimer.format(controller.goodsCountdown.value),
  423. style: TextStyle(
  424. color: isSelected ? ColorName.white : Color(0xFFFF9416),
  425. fontSize: 12.sp,
  426. fontWeight: FontWeight.w500,
  427. ),
  428. );
  429. }),
  430. ),
  431. ],
  432. ),
  433. ),
  434. ),
  435. ],
  436. );
  437. // 最终呈现的内容组件
  438. Widget resultWidget;
  439. if (hasCountdown) {
  440. // 有倒计时的商品,不规则,使用图片背景
  441. resultWidget = Container(
  442. decoration: BoxDecoration(
  443. image: DecorationImage(
  444. image:
  445. isSelected
  446. ? Assets.images.bgStoreGoodsItemWithCountdownSelectedIos
  447. .provider()
  448. : Assets.images.bgStoreGoodsItemWithCountdownNormalIos
  449. .provider(),
  450. fit: BoxFit.fill,
  451. ),
  452. ),
  453. child: contentWidget,
  454. );
  455. } else {
  456. // 没有倒计时的商品
  457. resultWidget = Container(
  458. decoration: BoxDecoration(
  459. shape: BoxShape.rectangle,
  460. borderRadius: BorderRadius.circular(8.r),
  461. border: Border.all(
  462. color: isSelected ? Color(0xFFFF9416) : Color(0xFFFEE86B),
  463. width: 2.w,
  464. ),
  465. ),
  466. child: contentWidget,
  467. );
  468. }
  469. // 最后面背景透出来的颜色
  470. Decoration bgDecoration;
  471. if (isSelected) {
  472. // 渐变背景
  473. bgDecoration = ShapeDecoration(
  474. gradient: LinearGradient(
  475. begin: Alignment(-0.06, 0.50),
  476. end: Alignment(1.14, 0.50),
  477. colors: [const Color(0xFFFFF895), const Color(0xFFFFE941)],
  478. ),
  479. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
  480. );
  481. } else {
  482. // 纯色背景
  483. bgDecoration = BoxDecoration(
  484. color: Color(0xFFFFFEEE),
  485. borderRadius: BorderRadius.circular(6.r),
  486. );
  487. }
  488. return Container(
  489. margin: EdgeInsets.only(bottom: 8.h),
  490. width: 296.w,
  491. height: 70.h,
  492. decoration: bgDecoration,
  493. child: Stack(
  494. children: [
  495. // 对勾图片
  496. Positioned(
  497. top: 0,
  498. right: 0,
  499. bottom: 0,
  500. child: Assets.images.bgStoreSelectedArrow1Ios.image(
  501. width: 85.w,
  502. height: double.infinity,
  503. ),
  504. ),
  505. resultWidget,
  506. ],
  507. ),
  508. );
  509. }
  510. /// 商品-Android端
  511. Widget _buildGoodsItem(GoodsInfo item, bool isSelected) {
  512. return Container(
  513. margin: EdgeInsets.only(bottom: 8.h),
  514. width: 296.w,
  515. height: 70.h,
  516. decoration: ShapeDecoration(
  517. gradient: LinearGradient(
  518. begin: Alignment(0.77, -0.00),
  519. end: Alignment(0.77, 1.00),
  520. colors:
  521. isSelected
  522. ? [const Color(0xFFFF9416), const Color(0xFFFF7813)]
  523. : [const Color(0xFFFEE057), const Color(0xFFFFC400)],
  524. ),
  525. shape: RoundedRectangleBorder(
  526. borderRadius: BorderRadius.circular(10.r),
  527. ),
  528. ),
  529. child: Row(
  530. children: [
  531. Container(
  532. width: 212.w,
  533. height: 70.h,
  534. decoration: ShapeDecoration(
  535. gradient:
  536. isSelected
  537. ? LinearGradient(
  538. begin: Alignment(-0.06, 0.50),
  539. end: Alignment(1.14, 0.50),
  540. colors: [
  541. const Color(0xFFFFF895),
  542. const Color(0xFFFFE941),
  543. ],
  544. )
  545. : null,
  546. color: isSelected ? null : const Color(0xFFFFFDEE),
  547. shape: RoundedRectangleBorder(
  548. side: BorderSide(width: 1, color: const Color(0xFFFEE86B)),
  549. borderRadius: BorderRadius.circular(isSelected ? 8 : 10.r),
  550. ),
  551. ),
  552. child: Stack(
  553. children: [
  554. if (isSelected)
  555. IgnorePointer(
  556. child: Assets.images.bgStoreSelectedItem.image(
  557. width: 212.w,
  558. height: 70.h,
  559. fit: BoxFit.fill,
  560. ),
  561. ),
  562. Padding(
  563. padding: EdgeInsets.only(left: 16.w),
  564. child: Column(
  565. crossAxisAlignment: CrossAxisAlignment.start,
  566. mainAxisAlignment: MainAxisAlignment.center,
  567. children: [
  568. Row(
  569. children: [
  570. RichText(
  571. text: TextSpan(
  572. children: [
  573. TextSpan(
  574. text: '¥',
  575. style: Styles.getTextStyleFF663300W700(14.sp),
  576. ),
  577. TextSpan(
  578. text: item.priceDescNumber,
  579. style: Styles.getTextStyleFF663300W700(18.sp),
  580. ),
  581. TextSpan(
  582. text: item.priceDescUnit,
  583. style: Styles.getTextStyleFF663300W400(13.sp),
  584. ),
  585. ],
  586. ),
  587. ),
  588. if (item.mostDesc?.isNotEmpty == true)
  589. Container(
  590. constraints: BoxConstraints(
  591. minHeight: 20.w,
  592. maxHeight: 20.w,
  593. minWidth: 40.w,
  594. maxWidth: 120.w,
  595. ),
  596. padding: EdgeInsets.only(
  597. left: 16.w,
  598. top: 2.h,
  599. bottom: 2.h,
  600. right: 4.w,
  601. ),
  602. decoration: BoxDecoration(
  603. image: DecorationImage(
  604. image: Assets.images.iconStoreMost.provider(),
  605. fit: BoxFit.fill,
  606. alignment: Alignment.bottomLeft,
  607. ),
  608. ),
  609. child: AutoSizeText(
  610. item.mostDesc!,
  611. style: TextStyle(
  612. color: Colors.white,
  613. fontSize: 12.sp,
  614. fontWeight: FontWeight.w500,
  615. letterSpacing: -0.60,
  616. ),
  617. maxLines: 1,
  618. overflow: TextOverflow.ellipsis,
  619. minFontSize: 8,
  620. // 最小字体
  621. stepGranularity: 0.5, // 缩小步长,越小越丝滑
  622. ),
  623. ),
  624. ],
  625. ),
  626. Text(
  627. item.description!,
  628. style: Styles.getTextStyle99673300W400(12.sp),
  629. ),
  630. ],
  631. ),
  632. ),
  633. ],
  634. ),
  635. ),
  636. // 右侧
  637. Expanded(
  638. child: Column(
  639. mainAxisAlignment: MainAxisAlignment.center,
  640. crossAxisAlignment: CrossAxisAlignment.center,
  641. children: [
  642. Text(
  643. item.name,
  644. style:
  645. isSelected
  646. ? Styles.getTextStyleFFECBBW500(15.sp)
  647. : Styles.getTextStyleFF663300W500(15.sp),
  648. ),
  649. Container(
  650. padding: EdgeInsets.symmetric(horizontal: 8.w),
  651. decoration: ShapeDecoration(
  652. color: isSelected ? const Color(0xFFFFECBB) : null,
  653. gradient:
  654. isSelected
  655. ? null
  656. : LinearGradient(
  657. begin: Alignment(0.77, -0.00),
  658. end: Alignment(0.77, 1.00),
  659. colors: [
  660. const Color(0xFFFF9416),
  661. const Color(0xFFFF7813),
  662. ],
  663. ),
  664. shape: RoundedRectangleBorder(
  665. borderRadius: BorderRadius.circular(
  666. isSelected ? 17.r : 10.r,
  667. ),
  668. ),
  669. ),
  670. child: Text(
  671. '¥${item.amountText}',
  672. textAlign: TextAlign.center,
  673. style:
  674. isSelected
  675. ? Styles.getTextStyleFF7F14W500(12.sp)
  676. : Styles.getTextStyleWhiteW500(12.sp),
  677. ),
  678. ),
  679. ],
  680. ),
  681. ),
  682. ],
  683. ),
  684. );
  685. }
  686. /// 最后的底部圆角
  687. Widget _buildLastBottomCorner({required Widget child}) {
  688. return Container(
  689. alignment: Alignment.centerLeft,
  690. margin: EdgeInsets.symmetric(horizontal: 16.w),
  691. padding: EdgeInsets.symmetric(horizontal: PlatformUtil.isIOS ? 4.w : 16.w),
  692. width: double.infinity,
  693. height: 36.h,
  694. decoration: ShapeDecoration(
  695. gradient: LinearGradient(
  696. begin: Alignment(0.00, 0.50),
  697. end: Alignment(1.00, 0.50),
  698. colors: [const Color(0x7FFFF3A3), const Color(0x21FFF3A3)],
  699. ),
  700. shape: RoundedRectangleBorder(
  701. borderRadius: BorderRadius.only(
  702. bottomLeft: Radius.circular(20.r),
  703. bottomRight: Radius.circular(20.r),
  704. ),
  705. ),
  706. shadows: [
  707. BoxShadow(
  708. color: Colors.black.withAlpha(10),
  709. blurRadius: 10.r,
  710. spreadRadius: 0.r,
  711. offset: Offset(0, 8.r),
  712. ),
  713. ],
  714. ),
  715. child: child,
  716. );
  717. }
  718. /// 产品描述
  719. Widget _buildVipDesc() {
  720. return Obx(() {
  721. return Text(
  722. controller.selectedGoodsInfoItem?.selectDesc ?? "",
  723. style: Styles.getTextStyle99673300W400(12.sp),
  724. );
  725. });
  726. }
  727. // 轮播图
  728. Widget _buildBanner(BuildContext context) {
  729. return SizedBox(
  730. width: double.infinity,
  731. child: Stack(
  732. children: [
  733. CarouselSlider(
  734. carouselController: controller.carouselSliderController,
  735. options: CarouselOptions(
  736. height: 280.h,
  737. viewportFraction: 1,
  738. initialPage: 0,
  739. enableInfiniteScroll: true,
  740. reverse: false,
  741. autoPlay: true,
  742. autoPlayInterval: const Duration(seconds: 3),
  743. autoPlayAnimationDuration: const Duration(milliseconds: 800),
  744. autoPlayCurve: Curves.fastOutSlowIn,
  745. scrollDirection: Axis.horizontal,
  746. onPageChanged: (index, reason) {
  747. controller.onBannerChanged(index, reason);
  748. },
  749. ),
  750. items:
  751. controller.bannerList.map((item) {
  752. return item.banner.image(
  753. width: double.infinity,
  754. fit: BoxFit.cover,
  755. );
  756. }).toList(),
  757. ),
  758. Positioned(bottom: 0, left: 0, right: 0, child: _buildIndicator()),
  759. ],
  760. ),
  761. );
  762. }
  763. // 指示器
  764. _buildIndicator() {
  765. return Container(
  766. height: 36.h,
  767. margin: EdgeInsets.symmetric(horizontal: 16.w),
  768. decoration: ShapeDecoration(
  769. color: const Color(0xE5121212),
  770. shape: RoundedRectangleBorder(
  771. borderRadius: BorderRadius.circular(21.r),
  772. ),
  773. ),
  774. child: Row(
  775. mainAxisAlignment: MainAxisAlignment.spaceAround,
  776. children:
  777. controller.bannerList.asMap().entries.map((entry) {
  778. return Obx(() {
  779. final isSelectedBanner =
  780. controller.currentBannerIndex == entry.key;
  781. return Row(
  782. mainAxisAlignment: MainAxisAlignment.spaceAround,
  783. children: [
  784. GestureDetector(
  785. onTap:
  786. () => controller.carouselSliderController
  787. .animateToPage(entry.key),
  788. child: SizedBox(
  789. width: 100.w,
  790. child: Stack(
  791. alignment: Alignment.center,
  792. clipBehavior: Clip.none,
  793. children: [
  794. if (isSelectedBanner)
  795. Positioned(
  796. top: -8.h,
  797. child: controller
  798. .bannerList[entry.key]
  799. .indicatorImg
  800. .image(
  801. width: 100.w,
  802. height: 40.h,
  803. fit: BoxFit.fill,
  804. ),
  805. )
  806. else
  807. Text(
  808. controller.bannerList[entry.key].unSelectedDesc,
  809. style: Styles.getTextStyleWhiteW400(14.sp),
  810. ),
  811. ],
  812. ),
  813. ),
  814. ),
  815. if (entry.key != controller.bannerList.length - 1)
  816. Padding(
  817. padding: EdgeInsets.only(left: 4.w),
  818. child: Assets.images.iconStoreDivider.image(
  819. width: 2.w,
  820. height: 17.h,
  821. fit: BoxFit.fill,
  822. ),
  823. ),
  824. ],
  825. );
  826. });
  827. }).toList(),
  828. ),
  829. );
  830. }
  831. Widget _buildUserReviews() {
  832. return Container(
  833. width: double.infinity,
  834. child: Column(
  835. crossAxisAlignment: CrossAxisAlignment.center,
  836. children: [
  837. Assets.images.iconStoreUserReviewsTitle.image(
  838. width: 240.w,
  839. fit: BoxFit.cover,
  840. ),
  841. SizedBox(height: 16.h),
  842. Container(
  843. width: double.infinity,
  844. decoration: BoxDecoration(
  845. borderRadius: BorderRadius.circular(19.r),
  846. gradient: LinearGradient(
  847. begin: Alignment.topCenter,
  848. end: Alignment.bottomCenter,
  849. colors: [Colors.white, Color(0xfffff8d4)],
  850. ),
  851. ),
  852. child: Column(
  853. children: [
  854. SizedBox(height: 20.h),
  855. Container(
  856. width: 326.w,
  857. decoration: BoxDecoration(
  858. borderRadius: BorderRadius.circular(19.r),
  859. image: DecorationImage(
  860. alignment: Alignment.topCenter,
  861. image: Assets.images.bgStoreUserReviews.provider(),
  862. fit: BoxFit.contain,
  863. ),
  864. ),
  865. child: Column(
  866. children: [
  867. Container(
  868. padding: EdgeInsets.only(left: 16.w),
  869. alignment: Alignment.centerLeft,
  870. height: 39.h,
  871. child: Assets.images.iconStoreUserReviewsLogo.image(
  872. width: 92.w,
  873. height: 24.h,
  874. ),
  875. ),
  876. Container(
  877. width: 326.w,
  878. decoration: ShapeDecoration(
  879. color: Colors.white,
  880. shape: RoundedRectangleBorder(
  881. borderRadius: BorderRadius.circular(16.r),
  882. ),
  883. shadows: [
  884. BoxShadow(
  885. color: Colors.black.withAlpha(10),
  886. blurRadius: 10.r,
  887. spreadRadius: 0.r,
  888. offset: Offset(0, 5.r),
  889. ),
  890. ],
  891. ),
  892. child: Column(
  893. children:
  894. controller.userReviewsList.map((item) {
  895. return _buildReviewsItem(item);
  896. }).toList(),
  897. ),
  898. ),
  899. ],
  900. ),
  901. ),
  902. ],
  903. ),
  904. ),
  905. ],
  906. ),
  907. );
  908. }
  909. Widget _buildReviewsItem(StoreUserReviewsBean item) {
  910. return Container(
  911. padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h),
  912. child: Column(
  913. crossAxisAlignment: CrossAxisAlignment.start,
  914. children: [
  915. Row(
  916. children: [
  917. item.avatar.image(width: 28.w, height: 28.h, fit: BoxFit.cover),
  918. SizedBox(width: 10.w),
  919. Text(
  920. item.userName,
  921. style: Styles.getTextStyleBlack204W500(14.sp),
  922. ),
  923. SizedBox(width: 6.w),
  924. Assets.images.iconStorePermanentMember.image(
  925. width: 70.w,
  926. height: 20.h,
  927. fit: BoxFit.cover,
  928. ),
  929. ],
  930. ),
  931. SizedBox(height: 4.h),
  932. Padding(
  933. padding: EdgeInsets.only(left: 38.w),
  934. child: Text(
  935. item.userReviews,
  936. style: TextStyle(
  937. color: Colors.black.withAlpha(153),
  938. fontSize: 12.sp,
  939. fontWeight: FontWeight.w400,
  940. ),
  941. ),
  942. ),
  943. SizedBox(height: 12.h),
  944. if (controller.userReviewsList.indexOf(item) !=
  945. controller.userReviewsList.length - 1)
  946. HorizontalDashedLine(
  947. width: 304.w,
  948. color: const Color(0XFFEDEBE1),
  949. strokeWidth: 2.h,
  950. dashLength: 4.w,
  951. dashSpace: 4.w,
  952. ),
  953. ],
  954. ),
  955. );
  956. }
  957. // 用户须知
  958. Widget _buildUserNotice() {
  959. return Container(
  960. margin: EdgeInsets.symmetric(horizontal: 16.w),
  961. decoration: BoxDecoration(borderRadius: BorderRadius.circular(16.r)),
  962. child: Column(
  963. crossAxisAlignment: CrossAxisAlignment.start,
  964. children: [
  965. Text('购买须知', style: Styles.getTextStyleFF663300W400(12.sp)),
  966. SizedBox(height: 8.h),
  967. Obx(() {
  968. return Text(
  969. controller.configRepository.memberTips.value,
  970. style: Styles.getTextStyle99673300W400(10.sp),
  971. );
  972. }),
  973. SizedBox(height: 150.h),
  974. ],
  975. ),
  976. );
  977. }
  978. /// 恢复订阅
  979. Widget _buildRecoverSubscribe() {
  980. return Container(
  981. margin: EdgeInsets.only(left: 16.w, top: 8.h, right: 16.w),
  982. child: Row(
  983. children: [
  984. Text(
  985. "⚠️ 订阅未生效?点此试试",
  986. style: TextStyle(
  987. color: Color(0xFF666355),
  988. fontSize: 13.sp,
  989. fontWeight: FontWeight.w400,
  990. ),
  991. ),
  992. GestureDetector(
  993. onTap: () {
  994. // 恢复订阅
  995. controller.clickRecoverSubscribe();
  996. },
  997. child: Text(
  998. " 恢复订阅>",
  999. style: TextStyle(
  1000. color: Color(0xFF479DF7),
  1001. fontSize: 13.sp,
  1002. fontWeight: FontWeight.w400,
  1003. ),
  1004. ),
  1005. ),
  1006. ],
  1007. ),
  1008. );
  1009. }
  1010. Widget _buildBuyButtonCard() {
  1011. return Container(
  1012. width: 360.w,
  1013. padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 11.h),
  1014. decoration: ShapeDecoration(
  1015. color: Colors.white,
  1016. shape: RoundedRectangleBorder(
  1017. borderRadius: BorderRadius.only(
  1018. topLeft: Radius.circular(24.r),
  1019. topRight: Radius.circular(24.r),
  1020. ),
  1021. ),
  1022. shadows: [
  1023. BoxShadow(
  1024. color: Color(0x99FFE498),
  1025. blurRadius: 20,
  1026. offset: Offset(0, 0),
  1027. spreadRadius: 0,
  1028. ),
  1029. ],
  1030. ),
  1031. child: Column(
  1032. children: [
  1033. GestureDetector(
  1034. onTap: controller.clickPayNow,
  1035. child: Container(
  1036. alignment: Alignment.topCenter,
  1037. width: 328.w,
  1038. height: 56.w,
  1039. decoration: ShapeDecoration(
  1040. color: const Color(0xFFFFF587),
  1041. shape: RoundedRectangleBorder(
  1042. borderRadius: BorderRadius.circular(30.55.r),
  1043. ),
  1044. ),
  1045. child: Container(
  1046. width: 328.w,
  1047. height: 54.w,
  1048. decoration: ShapeDecoration(
  1049. gradient: LinearGradient(
  1050. begin: Alignment(0.60, -0.39),
  1051. end: Alignment(0.60, 0.95),
  1052. colors: [
  1053. const Color(0xFFF95FAC),
  1054. const Color(0xFFFD5E4D),
  1055. const Color(0xFFFD5E4D),
  1056. const Color(0xFFFB8A3C),
  1057. ],
  1058. ),
  1059. shape: RoundedRectangleBorder(
  1060. borderRadius: BorderRadius.circular(30.55.r),
  1061. ),
  1062. ),
  1063. child: Center(
  1064. child: Text(
  1065. StringName.storePayNow,
  1066. style: Styles.getTextStyleWhiteW500(17.sp),
  1067. ),
  1068. ),
  1069. ),
  1070. ),
  1071. ),
  1072. // 安卓平台的隐私协议和服务条款
  1073. if (!PlatformUtil.isIOS) _buildPrivacy(),
  1074. ],
  1075. ),
  1076. );
  1077. }
  1078. /// 隐私协议和服务条款
  1079. Widget _buildPrivacy({
  1080. Color privacyColor = const Color(0xFF459FFF),
  1081. MainAxisAlignment mainAxisAlignment = MainAxisAlignment.center,
  1082. }) {
  1083. return Row(
  1084. mainAxisAlignment: mainAxisAlignment,
  1085. children: [
  1086. Obx(() {
  1087. return GestureDetector(
  1088. behavior: HitTestBehavior.opaque,
  1089. onTap: () {
  1090. controller.isAgree.value = !controller.isAgree.value;
  1091. },
  1092. child: Container(
  1093. margin: EdgeInsets.only(top: PlatformUtil.isIOS ? 0.h : 2.h),
  1094. padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 5.w),
  1095. child: Image(
  1096. image:
  1097. controller.isAgree.value
  1098. ? Assets.images.iconStoreAgreePrivacySelected.provider()
  1099. : Assets.images.iconStoreAgreePrivacyNormal.provider(),
  1100. width: 16.w,
  1101. height: 16.w,
  1102. ),
  1103. ),
  1104. );
  1105. }),
  1106. Transform.translate(
  1107. offset: Offset(-2.w, 0),
  1108. child: Text.rich(
  1109. PlatformUtil.isIOS
  1110. ? TextSpan(
  1111. children: [
  1112. TextSpan(
  1113. text: StringName.textSpanIHaveReadAndAgree,
  1114. style: TextStyle(
  1115. color: Colors.black.withAlpha(153),
  1116. fontSize: 10.sp,
  1117. fontWeight: FontWeight.w400,
  1118. ),
  1119. ),
  1120. ClickTextSpan(
  1121. text: StringName.textSpanPrivacyPolicy,
  1122. url: WebUrl.privacyPolicy,
  1123. color: privacyColor,
  1124. ),
  1125. ClickTextSpan(
  1126. text: StringName.textSpanServiceTerms,
  1127. url: WebUrl.serviceAgreement,
  1128. color: privacyColor,
  1129. ),
  1130. TextSpan(
  1131. text: StringName.textSpanAnd,
  1132. style: TextStyle(
  1133. color: Colors.black.withAlpha(153),
  1134. fontSize: 10.sp,
  1135. fontWeight: FontWeight.w400,
  1136. ),
  1137. ),
  1138. ClickTextSpan(
  1139. text: StringName.textSpanMembershipAgreement,
  1140. url: WebUrl.subscribeAgreement,
  1141. color: privacyColor,
  1142. ),
  1143. ],
  1144. )
  1145. : TextSpan(
  1146. children: [
  1147. TextSpan(
  1148. text: StringName.textSpanIHaveReadAndAgree,
  1149. style: TextStyle(
  1150. color: Colors.black.withAlpha(153),
  1151. fontSize: 10.sp,
  1152. fontWeight: FontWeight.w400,
  1153. ),
  1154. ),
  1155. ClickTextSpan(
  1156. text: StringName.textSpanPrivacyPolicy,
  1157. url: WebUrl.privacyPolicy,
  1158. color: privacyColor,
  1159. ),
  1160. TextSpan(
  1161. text: StringName.textSpanAnd,
  1162. style: TextStyle(
  1163. color: Colors.black.withAlpha(153),
  1164. fontSize: 10.sp,
  1165. fontWeight: FontWeight.w400,
  1166. ),
  1167. ),
  1168. ClickTextSpan(
  1169. text: StringName.textSpanServiceTerms,
  1170. url: WebUrl.serviceAgreement,
  1171. color: privacyColor,
  1172. ),
  1173. ],
  1174. ),
  1175. ),
  1176. ),
  1177. ],
  1178. );
  1179. }
  1180. }