Sfoglia il codice sorgente

[feat]商店页,商品列表,增加iOS平台的样式

hezihao 7 mesi fa
parent
commit
adbbfc5be8

BIN
assets/images/bg_store_goods_item_with_countdown_normal_ios.webp


BIN
assets/images/bg_store_goods_item_with_countdown_selected_ios.webp


BIN
assets/images/bg_store_selected_arrow1_ios.webp


BIN
assets/images/bg_store_selected_arrow2_ios.webp


BIN
assets/images/icon_store_agree_privacy_normal.webp


BIN
assets/images/icon_store_agree_privacy_selected.webp


BIN
assets/images/icon_store_goods_normal_symbol_ios.webp


BIN
assets/images/icon_store_goods_selected_symbol_ios.webp


+ 12 - 0
lib/di/get_it.config.dart

@@ -98,6 +98,7 @@ import '../module/store/new_discount/new_discount_controller.dart' as _i326;
 import '../module/store/store_controller.dart' as _i344;
 import '../module/store/subscribe/recover_subscribe_controller.dart' as _i827;
 import '../module/store/suprise/goods_surprise_controller.dart' as _i935;
+import '../module/store/util/store_goods_countdown_manager.dart' as _i442;
 import '../module/user_info/user_info_controller.dart' as _i866;
 import '../module/user_profile/user_profile_controller.dart' as _i329;
 import '../module/zodiac_love_intimacy/future_week/zodiac_love_future_week_controller.dart'
@@ -182,6 +183,9 @@ extension GetItInjectableX on _i174.GetIt {
     gh.lazySingleton<_i495.WechatLoginService>(
       () => _i495.WechatLoginService(),
     );
+    gh.lazySingleton<_i442.StoreGoodsCountdownManager>(
+      () => _i442.StoreGoodsCountdownManager(),
+    );
     gh.singleton<_i361.Dio>(
       () => networkModule.createStreamDio(),
       instanceName: 'streamDio',
@@ -372,6 +376,14 @@ extension GetItInjectableX on _i174.GetIt {
       () => _i630.ZodiacLoveTodayController(
         gh<_i779.ZodiacLoveIntimacyRepository>(),
         gh<_i83.AccountRepository>(),
+ 		),
+    );
+    gh.factory<_i344.StoreController>(
+      () => _i344.StoreController(
+        gh<_i987.StoreRepository>(),
+        gh<_i83.AccountRepository>(),
+        gh<_i779.PaymentStatusManager>(),
+        gh<_i442.StoreGoodsCountdownManager>(),
       ),
     );
     gh.factoryParam<

+ 8 - 0
lib/module/store/store_controller.dart

@@ -22,6 +22,7 @@ import 'package:keyboard/module/store/store_user_reviews_bean.dart';
 import 'package:keyboard/module/store/subscribe/recover_subscribe_dialog.dart';
 import 'package:keyboard/module/store/suprise/surprise_dialog.dart';
 import 'package:keyboard/module/store/ticket/discount_ticket_dialog.dart';
+import 'package:keyboard/module/store/util/store_goods_countdown_manager.dart';
 import 'package:keyboard/utils/async_util.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 import 'package:lottie/lottie.dart';
@@ -57,6 +58,9 @@ class StoreController extends BaseController implements PaymentStatusCallback {
 
   final PaymentStatusManager paymentStatusManager;
 
+  /// 倒计时管理器
+  final StoreGoodsCountdownManager storeGoodsCountdownManager;
+
   final ConfigRepository configRepository ;
 
   final RxList<GoodsInfo> goodsInfoList = <GoodsInfo>[].obs;
@@ -134,10 +138,14 @@ class StoreController extends BaseController implements PaymentStatusCallback {
 
   int get currentBannerIndex => _currentBanner.value;
 
+  /// 商品倒计时,假的倒计时,只会第一个商品会显示
+  get goodsCountdown => storeGoodsCountdownManager.currentDuration;
+
   StoreController(
     this.storeRepository,
     this.accountRepository,
     this.paymentStatusManager,
+    this.storeGoodsCountdownManager,
       this.configRepository,
   );
 

+ 220 - 13
lib/module/store/store_page.dart

@@ -1,5 +1,6 @@
 import 'package:auto_size_text/auto_size_text.dart';
 import 'package:carousel_slider/carousel_slider.dart';
+import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
@@ -11,9 +12,12 @@ import 'package:keyboard/module/store/store_user_reviews_bean.dart';
 import 'package:keyboard/resource/string.gen.dart';
 import 'package:keyboard/widget/platform_util.dart';
 
+import '../../data/bean/goods_info.dart';
 import '../../data/consts/constants.dart';
 import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
 import '../../router/app_pages.dart';
+import '../../utils/count_down_timer.dart';
 import '../../utils/date_util.dart';
 import '../../widget/horizontal_dashed_line.dart';
 import '../../utils/styles.dart';
@@ -244,17 +248,26 @@ class StorePage extends BasePage<StoreController> {
           Obx(() {
             return Column(
               children:
-              controller.filteredGoodsList.map((item) {
-                return Obx(() {
-                  return GestureDetector(
-                    onTap: () => controller.onGoodsItemClick(item),
-                    child: _buildGoodsItem(
-                      item,
-                      controller.selectedGoodsInfoItem?.id == item.id,
-                    ),
-                  );
-                });
-              }).toList(),
+                  controller.filteredGoodsList.mapIndexed((index, item) {
+                    return Obx(() {
+                      return GestureDetector(
+                        onTap: () => controller.onGoodsItemClick(item),
+                        child:
+                            PlatformUtil.isIOS
+                                ? _buildGoodsItemIos(
+                                  index,
+                                  item,
+                                  controller.selectedGoodsInfoItem?.id ==
+                                      item.id,
+                                )
+                                : _buildGoodsItem(
+                                  item,
+                                  controller.selectedGoodsInfoItem?.id ==
+                                      item.id,
+                                ),
+                      );
+                    });
+                  }).toList(),
             );
           }),
           // iOS平台的产品描述
@@ -319,7 +332,201 @@ class StorePage extends BasePage<StoreController> {
     );
   }
 
-  Widget _buildGoodsItem(item, bool isSelected) {
+  /// 商品-iOS端
+  Widget _buildGoodsItemIos(int index, GoodsInfo item, bool isSelected) {
+    // 第一个商品,才有有倒计时
+    bool hasCountdown = index == 0;
+
+    Widget contentWidget = Stack(
+      children: [
+        Positioned(
+          left: 16.w,
+          top: 0,
+          right: 0,
+          bottom: 0,
+          child: Row(
+            children: [
+              // 价格
+              RichText(
+                text: TextSpan(
+                  children: [
+                    TextSpan(
+                      text: '¥',
+                      style: Styles.getTextStyleFF663300W700(14.sp),
+                    ),
+                    TextSpan(
+                      text: item.priceDescNumber,
+                      style: Styles.getTextStyleFF663300W700(18.sp),
+                    ),
+                  ],
+                ),
+              ),
+              SizedBox(width: 18.w),
+              // 名称和描述
+              Column(
+                // 垂直居中
+                mainAxisAlignment: MainAxisAlignment.center,
+                // 水平左对齐
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  // 名称
+                  Text(
+                    item.name,
+                    style: Styles.getTextStyleFF663300W500(15.sp),
+                  ),
+                  // 描述
+                  if (item.mostDesc?.isNotEmpty == true)
+                    AutoSizeText(
+                      item.mostDesc!,
+                      style: TextStyle(
+                        color: Color(0x99673300),
+                        fontSize: 12.sp,
+                        fontWeight: FontWeight.w500,
+                        letterSpacing: -0.60,
+                      ),
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                      // 最小字体
+                      minFontSize: 8,
+                      // 缩小步长,越小越丝滑
+                      stepGranularity: 0.5,
+                    ),
+                ],
+              ),
+            ],
+          ),
+        ),
+        // 勾选状态
+        Positioned(
+          top: 0,
+          right: 22.w,
+          bottom: 0,
+          child: Image(
+            image:
+                isSelected
+                    ? Assets.images.iconStoreGoodsSelectedSymbolIos.provider()
+                    : Assets.images.iconStoreGoodsNormalSymbolIos.provider(),
+            width: 20.w,
+            height: 20.w,
+          ),
+        ),
+        // 倒计时
+        Positioned(
+          top: 0,
+          right: 8,
+          child: Visibility(
+            visible: hasCountdown,
+            child: Row(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                Text(
+                  "倒计时",
+                  style: TextStyle(
+                    color: isSelected ? Color(0xFFFFECBB) : Color(0xFFFF9416),
+                    fontSize: 12.sp,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+                SizedBox(width: 4.w),
+                Container(
+                  margin: EdgeInsets.only(top: 2.5.h),
+                  child: Obx(() {
+                    return Text(
+                      CountdownTimer.format(controller.goodsCountdown.value),
+                      style: TextStyle(
+                        color: isSelected ? ColorName.white : Color(0xFFFF9416),
+                        fontSize: 12.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    );
+                  }),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+
+    // 最终呈现的内容组件
+    Widget resultWidget;
+    if (hasCountdown) {
+      // 有倒计时的商品,不规则,使用图片背景
+      resultWidget = Container(
+        decoration: BoxDecoration(
+          image: DecorationImage(
+            image:
+                isSelected
+                    ? Assets.images.bgStoreGoodsItemWithCountdownSelectedIos
+                        .provider()
+                    : Assets.images.bgStoreGoodsItemWithCountdownNormalIos
+                        .provider(),
+            fit: BoxFit.fill,
+          ),
+        ),
+        child: contentWidget,
+      );
+    } else {
+      // 没有倒计时的商品
+      resultWidget = Container(
+        decoration: BoxDecoration(
+          shape: BoxShape.rectangle,
+          borderRadius: BorderRadius.circular(8.r),
+          border: Border.all(
+            color: isSelected ? Color(0xFFFF9416) : Color(0xFFFEE86B),
+            width: 2.w,
+          ),
+        ),
+        child: contentWidget,
+      );
+    }
+
+    // 最后面背景透出来的颜色
+    Decoration bgDecoration;
+    if (isSelected) {
+      // 渐变背景
+      bgDecoration = ShapeDecoration(
+        gradient: LinearGradient(
+          begin: Alignment(-0.06, 0.50),
+          end: Alignment(1.14, 0.50),
+          colors: [const Color(0xFFFFF895), const Color(0xFFFFE941)],
+        ),
+        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
+      );
+    } else {
+      // 纯色背景
+      bgDecoration = BoxDecoration(
+        color: Color(0xFFFFFEEE),
+        borderRadius: BorderRadius.circular(6.r),
+      );
+    }
+
+    return Container(
+      margin: EdgeInsets.only(bottom: 8.h),
+      width: 296.w,
+      height: 70.h,
+      decoration: bgDecoration,
+      child: Stack(
+        children: [
+          // 对勾图片
+          Positioned(
+            top: 0,
+            right: 0,
+            bottom: 0,
+            child: Assets.images.bgStoreSelectedArrow1Ios.image(
+              width: 85.w,
+              height: double.infinity,
+            ),
+          ),
+          resultWidget,
+        ],
+      ),
+    );
+  }
+
+  /// 商品-Android端
+  Widget _buildGoodsItem(GoodsInfo item, bool isSelected) {
     return Container(
       margin: EdgeInsets.only(bottom: 8.h),
       width: 296.w,
@@ -949,7 +1156,7 @@ class StorePage extends BasePage<StoreController> {
           );
         }),
         Transform.translate(
-          offset: Offset(-10.w, 0),
+          offset: Offset(-2.w, 0),
           child: Text.rich(
             TextSpan(
               children: [

+ 41 - 0
lib/module/store/util/store_goods_countdown_manager.dart

@@ -0,0 +1,41 @@
+import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:injectable/injectable.dart';
+
+import '../../../utils/count_down_timer.dart';
+
+/// 商店商品倒计时管理器
+@lazySingleton
+class StoreGoodsCountdownManager {
+  /// 当前时间
+  Rx<Duration> currentDuration = Duration.zero.obs;
+
+  late CountdownTimer _timer;
+
+  StoreGoodsCountdownManager() {
+    _timer =
+        CountdownTimer(
+            initialDuration: Duration(minutes: 30),
+            autoRestart: false,
+          )
+          ..onTick = (duration) {
+            // 倒计时更新
+            currentDuration.value = duration;
+          }
+          ..onCompleted = () {
+            // 倒计时完成,重新开始
+            _timer.reset();
+            _timer.start();
+          };
+    startCountdown();
+  }
+
+  /// 开始倒计时
+  void startCountdown() {
+    _timer.start();
+  }
+
+  /// 暂停倒计时
+  void pauseCountdown() {
+    _timer.pause();
+  }
+}

+ 47 - 0
lib/resource/assets.gen.dart

@@ -297,6 +297,26 @@ class $AssetsImagesGen {
   AssetGenImage get bgSplash =>
       const AssetGenImage('assets/images/bg_splash.webp');
 
+  /// File path: assets/images/bg_store_goods_item_with_countdown_normal_ios.webp
+  AssetGenImage get bgStoreGoodsItemWithCountdownNormalIos =>
+      const AssetGenImage(
+        'assets/images/bg_store_goods_item_with_countdown_normal_ios.webp',
+      );
+
+  /// File path: assets/images/bg_store_goods_item_with_countdown_selected_ios.webp
+  AssetGenImage get bgStoreGoodsItemWithCountdownSelectedIos =>
+      const AssetGenImage(
+        'assets/images/bg_store_goods_item_with_countdown_selected_ios.webp',
+      );
+
+  /// File path: assets/images/bg_store_selected_arrow1_ios.webp
+  AssetGenImage get bgStoreSelectedArrow1Ios =>
+      const AssetGenImage('assets/images/bg_store_selected_arrow1_ios.webp');
+
+  /// File path: assets/images/bg_store_selected_arrow2_ios.webp
+  AssetGenImage get bgStoreSelectedArrow2Ios =>
+      const AssetGenImage('assets/images/bg_store_selected_arrow2_ios.webp');
+
   /// File path: assets/images/bg_store_selected_item.webp
   AssetGenImage get bgStoreSelectedItem =>
       const AssetGenImage('assets/images/bg_store_selected_item.webp');
@@ -1092,6 +1112,15 @@ class $AssetsImagesGen {
   AssetGenImage get iconStoreAgreePrivacy =>
       const AssetGenImage('assets/images/icon_store_agree_privacy.webp');
 
+  /// File path: assets/images/icon_store_agree_privacy_normal.webp
+  AssetGenImage get iconStoreAgreePrivacyNormal =>
+      const AssetGenImage('assets/images/icon_store_agree_privacy_normal.webp');
+
+  /// File path: assets/images/icon_store_agree_privacy_selected.webp
+  AssetGenImage get iconStoreAgreePrivacySelected => const AssetGenImage(
+    'assets/images/icon_store_agree_privacy_selected.webp',
+  );
+
   /// File path: assets/images/icon_store_back.webp
   AssetGenImage get iconStoreBack =>
       const AssetGenImage('assets/images/icon_store_back.webp');
@@ -1112,6 +1141,16 @@ class $AssetsImagesGen {
   AssetGenImage get iconStoreDivider =>
       const AssetGenImage('assets/images/icon_store_divider.webp');
 
+  /// File path: assets/images/icon_store_goods_normal_symbol_ios.webp
+  AssetGenImage get iconStoreGoodsNormalSymbolIos => const AssetGenImage(
+    'assets/images/icon_store_goods_normal_symbol_ios.webp',
+  );
+
+  /// File path: assets/images/icon_store_goods_selected_symbol_ios.webp
+  AssetGenImage get iconStoreGoodsSelectedSymbolIos => const AssetGenImage(
+    'assets/images/icon_store_goods_selected_symbol_ios.webp',
+  );
+
   /// File path: assets/images/icon_store_indicator1.webp
   AssetGenImage get iconStoreIndicator1 =>
       const AssetGenImage('assets/images/icon_store_indicator1.webp');
@@ -1351,6 +1390,10 @@ class $AssetsImagesGen {
     bgProfileLove,
     bgProfileSelected,
     bgSplash,
+    bgStoreGoodsItemWithCountdownNormalIos,
+    bgStoreGoodsItemWithCountdownSelectedIos,
+    bgStoreSelectedArrow1Ios,
+    bgStoreSelectedArrow2Ios,
     bgStoreSelectedItem,
     bgStoreUserReviews,
     bgSurpriseDialog,
@@ -1538,11 +1581,15 @@ class $AssetsImagesGen {
     iconScorpio,
     iconSetting,
     iconStoreAgreePrivacy,
+    iconStoreAgreePrivacyNormal,
+    iconStoreAgreePrivacySelected,
     iconStoreBack,
     iconStoreBanner1,
     iconStoreBanner2,
     iconStoreBanner3,
     iconStoreDivider,
+    iconStoreGoodsNormalSymbolIos,
+    iconStoreGoodsSelectedSymbolIos,
     iconStoreIndicator1,
     iconStoreIndicator2,
     iconStoreIndicator3,

+ 98 - 0
lib/utils/count_down_timer.dart

@@ -0,0 +1,98 @@
+import 'dart:async';
+
+/// 倒计时
+class CountdownTimer {
+  /// 倒计时时间
+  final Duration initialDuration;
+
+  /// 是否自动开始
+  final bool autoRestart;
+
+  /// 剩余时间
+  late Duration _remainingTime;
+
+  /// 最后更新时间
+  DateTime? _lastUpdate;
+
+  /// 定时器
+  Timer? _timer;
+
+  /// 是否已完成
+  bool _isCompleted = false;
+
+  /// 倒计时更新回调
+  Function(Duration)? onTick;
+
+  /// 倒计时结束回调
+  Function()? onCompleted;
+
+  CountdownTimer({
+    this.initialDuration = const Duration(minutes: 30),
+    this.autoRestart = true,
+  }) {
+    _remainingTime = initialDuration;
+  }
+
+  Duration get currentDuration => _remainingTime;
+
+  /// 开始
+  void start() {
+    _lastUpdate = DateTime.now();
+    _timer?.cancel();
+    _timer = Timer.periodic(const Duration(milliseconds: 16), _handleTick);
+  }
+
+  /// 暂停
+  void pause() {
+    _timer?.cancel();
+    _lastUpdate = null;
+  }
+
+  /// 重置
+  void reset() {
+    _timer?.cancel();
+    _remainingTime = initialDuration;
+    _isCompleted = false;
+    _lastUpdate = null;
+    onTick?.call(_remainingTime);
+  }
+
+  /// 结束倒计时
+  void dispose() {
+    _timer?.cancel();
+    _timer = null;
+  }
+
+  /// 处理更新
+  void _handleTick(Timer timer) {
+    if (_lastUpdate == null) return;
+
+    final now = DateTime.now();
+    final elapsed = now.difference(_lastUpdate!);
+
+    if (!_isCompleted) {
+      _remainingTime -= elapsed;
+      if (_remainingTime <= Duration.zero) {
+        _remainingTime = Duration.zero;
+        _isCompleted = true;
+        onCompleted?.call();
+      }
+    } else if (autoRestart) {
+      _remainingTime = initialDuration - elapsed;
+      _isCompleted = false;
+    }
+
+    _lastUpdate = now;
+    onTick?.call(_remainingTime);
+  }
+
+  /// 格式化时间,格式:分:秒:毫秒
+  static String format(Duration duration) {
+    String twoDigits(int n) => n.toString().padLeft(2, '0');
+    String threeDigits(int n) => n.toString().padLeft(3, '0');
+
+    return "${twoDigits(duration.inMinutes)}:"
+        "${twoDigits(duration.inSeconds % 60)}:"
+        "${threeDigits(duration.inMilliseconds % 1000)}";
+  }
+}