Переглянути джерело

fix:优会员中的倒计时。

“HeShaoZe” 4 місяців тому
батько
коміт
ae65c69dc0

BIN
assets/images/icon_member_avatar.webp


+ 3 - 3
lib/data/bean/member_status_info.dart

@@ -35,7 +35,7 @@ class MemberStatusInfo {
   /// 获取会员等级描述
   static String getLevelDesc(MemberStatusInfo? info) {
     if (info == null || info.expired == true) {
-      return '未开通';
+      return "";//'未开通';
     }
     if (info.level > 0 && info.level < 100) {
       return '试用会员';
@@ -43,7 +43,7 @@ class MemberStatusInfo {
     // 根据等级返回描述
     switch (info.level) {
       case 0:
-        return '未开通';
+        return "";//'未开通';
       case 100:
         return '日卡会员';
       case 700:
@@ -57,7 +57,7 @@ class MemberStatusInfo {
       case 3660000:
         return '终身会员';
       default:
-        return '未知会员等级';
+        return "";//'未知会员等级';
     }
   }
 }

+ 3 - 0
lib/module/member/member_controller.dart

@@ -39,6 +39,7 @@ import '../../utils/payment_status_manager.dart';
 import '../../widget/animated_switcher_widget.dart';
 import '../browser/browser_view.dart';
 import '../mine/mine_controller.dart';
+import 'member_discount_countdown_widget.dart';
 import 'member_evaluate_bean.dart';
 import 'member_evaluation_pop_up_dialog.dart';
 import 'member_first_week_discount_dialog.dart';
@@ -86,6 +87,8 @@ class MemberController extends BaseController implements PaymentStatusCallback {
 
   final RxList<PayItemBean> payItemList = <PayItemBean>[].obs;
 
+  final RxBool isShowCount = RxBool(true);
+
   CancelableFuture? _memberDataFuture;
 
   ///广告弹窗防抖

+ 228 - 0
lib/module/member/member_discount_countdown_widget.dart

@@ -0,0 +1,228 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import 'dart:async';
+
+import '../../utils/mmkv_util.dart';
+
+final String MemberDiscountCountdownTimeCount = 'key_member_discount_countdown_time_count';
+
+class MemberDiscountCountdownWidget extends StatefulWidget {
+  final Duration duration; // 倒计时总时长(默认1小时)
+  final TextStyle? textStyle;
+  final Duration animationDuration;
+  final Widget? placeholder;
+  final Widget? expiredWidget; // 已过期时显示的组件
+  final VoidCallback? onExpired; // 倒计时结束回调
+
+  const MemberDiscountCountdownWidget({
+    Key? key,
+    this.duration = const Duration(hours: 1),
+    this.textStyle,
+    this.animationDuration = const Duration(milliseconds: 500),
+    this.placeholder,
+    this.expiredWidget,
+    this.onExpired,
+  }) : super(key: key);
+
+  @override
+  _MemberDiscountCountdownWidgetState createState() => _MemberDiscountCountdownWidgetState();
+}
+
+class _MemberDiscountCountdownWidgetState extends State<MemberDiscountCountdownWidget> with SingleTickerProviderStateMixin {
+  // 1. 将_timer改为可空类型,移除late
+  Timer? _timer;
+  bool _isVisible = true;
+  bool _isExpired = false;
+  DateTime? _startTime;
+  Duration? _remainingDuration;
+  bool _isLoading = true;
+
+  @override
+  void initState() {
+    super.initState();
+    _loadStartTime();
+  }
+
+  Future<void> _loadStartTime() async {
+    try {
+      final savedTime = KVUtil.getInt(MemberDiscountCountdownTimeCount, 0);
+      print("savetimefdifjd---${savedTime}");
+      if (savedTime != null && savedTime > 0) {
+        _startTime = DateTime.fromMillisecondsSinceEpoch(savedTime);
+        _updateRemainingTime();
+
+        if (_remainingDuration != null && _remainingDuration!.isNegative) {
+          _handleExpired();
+        } else {
+          _startTimer();
+        }
+      } else {
+        _startNewCountdown();
+      }
+    } catch (e) {
+      print("加载倒计时时间出错: $e");
+      // 异常时调用修复后的_handleExpiredOver
+      _handleExpiredOver();
+    } finally {
+      if (mounted) {
+        setState(() {
+          _isLoading = false;
+        });
+      }
+    }
+  }
+
+  void _startNewCountdown() {
+    _startTime = DateTime.now();
+    _saveStartTime();
+    _updateRemainingTime();
+    _startTimer();
+  }
+
+  Future<void> _saveStartTime() async {
+    try {
+      KVUtil.putInt(MemberDiscountCountdownTimeCount, _startTime!.millisecondsSinceEpoch);
+    } catch (e) {
+      print("保存倒计时时间出错: $e");
+    }
+  }
+
+  void _startTimer() {
+    // 2. 先取消可能存在的旧定时器(避免重复创建)
+    _timer?.cancel();
+    // 3. 重新创建定时器并赋值
+    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
+      if (mounted) {
+        setState(() {
+          _updateRemainingTime();
+          if (_remainingDuration != null && _remainingDuration!.isNegative) {
+            _handleExpired();
+          }
+        });
+      }
+    });
+  }
+
+  void _updateRemainingTime() {
+    if (_startTime == null) return;
+    final now = DateTime.now();
+    final endTime = _startTime!.add(widget.duration);
+    _remainingDuration = endTime.difference(now);
+  }
+
+  void _handleExpired() {
+    // 4. 安全取消定时器(允许_timer为null)
+    _timer?.cancel();
+    _isExpired = true;
+    if (mounted) {
+      widget.onExpired?.call();
+    }
+  }
+
+  void _handleExpiredOver() {
+    // 5. 修复这里!使用?避免对null调用cancel()
+    _timer?.cancel();
+    _isExpired = true;
+    widget.onExpired?.call();
+  }
+
+  @override
+  void dispose() {
+    // 6. 销毁时安全取消
+    _timer?.cancel();
+    super.dispose();
+  }
+
+  // 以下代码不变...
+  String _formatDuration(Duration duration) {
+    final hours = duration.inHours;
+    final minutes = duration.inMinutes.remainder(60);
+    final seconds = duration.inSeconds.remainder(60);
+    return '${hours.toString().padLeft(2, '0')} : '
+        '${minutes.toString().padLeft(2, '0')} : '
+        '${seconds.toString().padLeft(2, '0')}';
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_isLoading) {
+      return const CircularProgressIndicator();
+    }
+    if (_isExpired) {
+      return widget.expiredWidget ?? const SizedBox.shrink();
+    }
+    if (!_isVisible) {
+      return widget.placeholder ?? const SizedBox.shrink();
+    }
+    if (_remainingDuration == null || _remainingDuration!.isNegative) {
+      return _createCellWidget("00","00","00");
+    }
+    final hours = _remainingDuration!.inHours;
+    final minutes = _remainingDuration!.inMinutes.remainder(60);
+    final seconds = _remainingDuration!.inSeconds.remainder(60);
+    final hoursStr = hours.toString().padLeft(2, '0');
+    final minutesStr = minutes.toString().padLeft(2, '0');
+    final secondsStr = seconds.toString().padLeft(2, '0');
+    return _createCellWidget(hoursStr, minutesStr, secondsStr);
+  }
+
+  Widget _createCellWidget(String hoursStr, String minutesStr, String secondsStr) {
+    return Container(
+      child: Row(
+        children: [
+          Text(
+            "优惠限时",
+            style: TextStyle(
+                color: "#333333".color,
+                fontSize: 11.sp,
+                fontWeight: FontWeight.w700),
+          ),
+          SizedBox(width: 5.w),
+          _createItemWidget(hoursStr),
+          _createItemMidele(),
+          _createItemWidget(minutesStr),
+          _createItemMidele(),
+          _createItemWidget(secondsStr),
+        ],
+      ),
+    );
+  }
+
+  Widget _createItemWidget(String timeStr) {
+    return Container(
+      padding: EdgeInsets.only(left: 2.w,right: 2.w),
+      decoration: BoxDecoration(
+          borderRadius: BorderRadius.all(Radius.circular(3)),
+          color: "#8B79F3".color
+      ),
+      child: Center(
+        child: Text(
+          timeStr,
+          style: TextStyle(
+            fontSize: 11.sp,
+            color: ColorName.white,
+            backgroundColor: "#8B79F3".color,
+            fontWeight: FontWeight.w500,
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _createItemMidele() {
+    return Container(
+      alignment: Alignment.center,
+      width: 4.w,
+      child: Center(
+        child: Text(
+            ":",
+            style: TextStyle(
+                fontSize: 11.sp,
+                color: "#8B79F3".color,
+                fontWeight: FontWeight.w400)),
+      ),
+    );
+  }
+}

+ 62 - 22
lib/module/member/member_page.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
@@ -22,6 +23,7 @@ import '../../resource/string.gen.dart';
 import '../../router/app_pages.dart';
 import '../../utils/date_util.dart';
 import '../../widget/animated_switcher_widget.dart';
+import 'member_discount_countdown_widget.dart';
 import 'member_evaluate_bean.dart';
 import 'member_header_cycle_widget.dart';
 
@@ -89,7 +91,6 @@ class MemberPage extends BasePage<MemberController> {
                                     topRight: Radius.circular(20.w)),
                                 image: DecorationImage(
                                   image: Assets.images.iconMemberVipMiddleBg.provider(),
-                                  fit: BoxFit.fill,
                                 )
                             ),
                             width: double.infinity,
@@ -150,7 +151,7 @@ class MemberPage extends BasePage<MemberController> {
                                 SizedBox(height: 100.w)
                               ],
                             ),
-                          )
+                          ),
                         ],
                       ),
                     )
@@ -468,9 +469,18 @@ class MemberPage extends BasePage<MemberController> {
   Widget buildUserInfoView() {
     return Row(
       children: [
-        SizedBox(width: 20.w),
-        Assets.images.iconMemberAvatar.image(width: 40.w, height: 40.w),
-        SizedBox(width: 10.w),
+        SizedBox(width: 18.w),
+        controller.isLogin ?  ClipOval(
+          child: Container(
+            width: 32.w,
+            height: 32.w,
+            child: CachedNetworkImage(
+              imageUrl: controller.memberStatusInfo?.avatar ?? "",
+              fit: BoxFit.cover,
+            ),
+          ),
+        ) : Assets.images.iconMemberAvatar.image(width: 32.w, height: 32.w),
+        SizedBox(width: 7.w),
         Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
@@ -487,32 +497,62 @@ class MemberPage extends BasePage<MemberController> {
                         fontWeight: FontWeight.bold)),
               );
             }),
-            buildMemberCardVipDesc()
+            Container(
+              height: 13.w,
+              child: Row(
+                // 主轴默认左对齐,通过Spacer推挤右侧内容
+                children: [
+                  // 左侧内容:会员等级描述 + VIP卡片描述
+                  Visibility(
+                    visible: MemberStatusInfo.getLevelDesc(controller.memberStatusInfo).isNotEmpty,
+                    child: Text(
+                        MemberStatusInfo.getLevelDesc(controller.memberStatusInfo),
+                        style: TextStyle(
+                            fontSize: 11.sp,
+                            fontWeight: FontWeight.w700,
+                            color: '#9144F8'.color
+                        )
+                    ),
+                  ),
+                  buildMemberCardVipDesc(),
+                  Container(
+                    child: MemberDiscountCountdownWidget(
+                      onExpired: () {
+                        print("sssfsdfs");
+                        controller.isShowCount.value = false;
+                      },
+                    ),
+                  )
+                ],
+              ),
+            )
           ],
         ),
-        Spacer(),
-        Container(
-            decoration: BoxDecoration(
-              color: '#272F51'.color,
-              border: Border.all(color: '#99CAB0F0'.color, width: 1.w),
-              borderRadius: BorderRadius.circular(100.w),
-            ),
-            padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 6.w),
-            child: Obx(() {
-              return Text(
-                  MemberStatusInfo.getLevelDesc(controller.memberStatusInfo),
-                  style: TextStyle(fontSize: 12.sp, color: '#D2CCFF'.color));
-            })),
-        SizedBox(width: 18.w)
+        SizedBox(width: 20.w)
       ],
     );
   }
+  // Spacer(),
+  // Container(
+  //     decoration: BoxDecoration(
+  //       color: '#272F51'.color,
+  //       border: Border.all(color: '#99CAB0F0'.color, width: 1.w),
+  //       borderRadius: BorderRadius.circular(100.w),
+  //     ),
+  //     padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 6.w),
+  //     child: Obx(() {
+  //       return Text(
+  //           MemberStatusInfo.getLevelDesc(controller.memberStatusInfo),
+  //           style: TextStyle(fontSize: 12.sp, color: '#D2CCFF'.color));
+  //     })),
 
   Widget buildMemberCardVipDesc() {
     return Obx(() {
       String desc = '';
       if (!controller.isLogin) {
-        desc = StringName.memberCardNoLoginDesc;
+        //desc = StringName.memberCardNoLoginDesc;
+        desc =
+        '${DateUtil.fromMillisecondsSinceEpoch('yyyy-MM-dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
       } else if (controller.memberStatusInfo == null ||
           controller.memberStatusInfo?.expired == true) {
         desc = StringName.memberCardNoVipDesc;
@@ -521,7 +561,7 @@ class MemberPage extends BasePage<MemberController> {
         desc = StringName.memberCardPermanentVipDesc;
       } else {
         desc =
-            '${DateUtil.fromMillisecondsSinceEpoch('yyyy.MM.dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
+            '${DateUtil.fromMillisecondsSinceEpoch('yyyy-MM-dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
       }
       return Text(desc,
           style: TextStyle(fontSize: 11.sp, color: ColorName.black50,fontWeight: FontWeight.w400));