store_page.dart 40 KB

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