store_page.dart 40 KB

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