store_page.dart 31 KB

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