Browse Source

[new]增加会员活动流程

zk 8 tháng trước cách đây
mục cha
commit
6b29435bf1
33 tập tin đã thay đổi với 1949 bổ sung95 xóa
  1. BIN
      assets/images/bg_member_activity.webp
  2. BIN
      assets/images/bg_member_activity_container.webp
  3. BIN
      assets/images/bg_member_activity_main.webp
  4. BIN
      assets/images/icon_member_activity_close.webp
  5. BIN
      assets/images/icon_member_activity_countdown.webp
  6. BIN
      assets/images/icon_member_activity_coupon.webp
  7. BIN
      assets/images/img_member_activity_banner_1.webp
  8. BIN
      assets/images/img_member_activity_banner_2.webp
  9. BIN
      assets/images/img_member_activity_banner_3.webp
  10. BIN
      assets/images/img_member_activity_banner_4.webp
  11. BIN
      assets/images/img_member_activity_favourable_txt.webp
  12. BIN
      assets/images/img_member_btn_shadow.webp
  13. 6 0
      assets/string/base/string.xml
  14. 3 0
      lib/data/consts/web_url.dart
  15. 68 18
      lib/data/repositories/member_repository.dart
  16. 14 0
      lib/di/get_it.config.dart
  17. 280 0
      lib/helper/member_pay_helper.dart
  18. 23 2
      lib/module/main/main_controller.dart
  19. 239 57
      lib/module/main/main_page.dart
  20. 119 0
      lib/module/member/activity/member_activity_banner_widget.dart
  21. 129 0
      lib/module/member/activity/member_activity_controller.dart
  22. 624 0
      lib/module/member/activity/member_activity_page.dart
  23. 6 0
      lib/module/member/member_controller.dart
  24. 33 17
      lib/module/member/member_page.dart
  25. 1 1
      lib/module/mine/mine_controller.dart
  26. 60 0
      lib/resource/assets.gen.dart
  27. 16 0
      lib/resource/string.gen.dart
  28. 5 0
      lib/router/app_pages.dart
  29. 5 0
      lib/utils/payment_status_manager.dart
  30. 100 0
      lib/widget/activity_countdown_txt_view.dart
  31. 110 0
      lib/widget/activity_countdown_view.dart
  32. 92 0
      lib/widget/shimmer_effect.dart
  33. 16 0
      pubspec.lock

BIN
assets/images/bg_member_activity.webp


BIN
assets/images/bg_member_activity_container.webp


BIN
assets/images/bg_member_activity_main.webp


BIN
assets/images/icon_member_activity_close.webp


BIN
assets/images/icon_member_activity_countdown.webp


BIN
assets/images/icon_member_activity_coupon.webp


BIN
assets/images/img_member_activity_banner_1.webp


BIN
assets/images/img_member_activity_banner_2.webp


BIN
assets/images/img_member_activity_banner_3.webp


BIN
assets/images/img_member_activity_banner_4.webp


BIN
assets/images/img_member_activity_favourable_txt.webp


BIN
assets/images/img_member_btn_shadow.webp


+ 6 - 0
assets/string/base/string.xml

@@ -386,4 +386,10 @@
     <string name="track_stay_longest_place">停留最长地点</string>
     <string name="track_stay_share_logo_desc">为你重要的朋友保驾护航</string>
     <string name="track_stay_share_analysis">loca分析中,请稍等..</string>
+    <string name="member_activity_title">超值优惠</string>
+    <string name="member_activity_no_payway">暂无支付方式</string>
+    <string name="member_activity_fun_title">功能介绍</string>
+    <string name="member_activity_countdown">优惠活动倒计时</string>
+    <string name="member_activity_specially_preferential">限时特惠</string>
+    <string name="member_activity_to_buy">去使用</string>
 </resources>

+ 3 - 0
lib/data/consts/web_url.dart

@@ -21,6 +21,9 @@ class WebUrl {
 
   static const String _customUrl = "https://qiyu-kefu.atmob.com";
 
+  static String renewalAgreement =
+      Platform.isIOS ? "https://doc.v8dashen.com/doc/4e69dec06586d0f1" : '';
+
   static String get privacyPolicy => Platform.isIOS? _privacyPolicyIos : _privacyPolicy;
 
   static String get userAgreement => Platform.isIOS? _userAgreementIos : _userAgreement;

+ 68 - 18
lib/data/repositories/member_repository.dart

@@ -1,8 +1,14 @@
+import 'dart:async';
+
+import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/app_base_request.dart';
 import 'package:location/data/api/atmob_api.dart';
 import 'package:location/data/api/request/subscription_check_request.dart';
+import 'package:location/data/bean/goods_bean.dart';
+import 'package:location/data/bean/pay_item_bean.dart';
 import 'package:location/data/repositories/account_repository.dart';
+import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/http_handler.dart';
 
 import '../../di/get_it.dart';
@@ -22,12 +28,50 @@ class MemberRepository {
   final AtmobApi atmobApi;
   final AccountRepository accountRepository;
 
+  //活动时长
+  final Rxn<Duration> activityDuration = Rxn<Duration>();
+
+  //最后选中的商品
+  final Rxn<GoodsBean> lastSelectedGoods = Rxn<GoodsBean>();
+
+  //最后选中的支付方式
+  PayItemBean? lastSelectedPayItem;
+  Timer? _timer;
+
   MemberRepository(this.atmobApi, this.accountRepository);
 
   static MemberRepository getInstance() {
     return getIt.get<MemberRepository>();
   }
 
+  void setLastSelectedMember(GoodsBean goodsBean, PayItemBean payItemBean) {
+    lastSelectedGoods.value = goodsBean;
+    lastSelectedPayItem = payItemBean;
+  }
+
+  void startActivityCountdown() {
+    _timer?.cancel();
+    activityDuration.value = Duration(minutes: 15);
+    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
+      final time = activityDuration.value;
+      if (time != null) {
+        if (time.inSeconds <= 1) {
+          _timer?.cancel();
+          clearLastSelectedMember();
+        } else {
+          activityDuration.value = Duration(seconds: time.inSeconds - 1);
+        }
+      }
+    });
+  }
+
+  void clearLastSelectedMember() {
+    lastSelectedGoods.value = null;
+    lastSelectedPayItem = null;
+    activityDuration.value = null;
+    _timer?.cancel();
+  }
+
   Future<void> memberTrial() {
     return atmobApi
         .memberTrial(AppBaseRequest())
@@ -63,32 +107,37 @@ class MemberRepository {
   }
 
   //查询订阅状态
-  Future<SubscriptionCheckResponse>subscriptionCheck(int payMethod,String receiptData) {
-    return atmobApi.subscriptionCheck(SubscriptionCheckRequest(payMethod, receiptData))
-    .then(HttpHandler.handle(false))
-    .then((SubscriptionCheckResponse data){
-      return  data;
+  Future<SubscriptionCheckResponse> subscriptionCheck(
+      int payMethod, String receiptData) {
+    return atmobApi
+        .subscriptionCheck(SubscriptionCheckRequest(payMethod, receiptData))
+        .then(HttpHandler.handle(false))
+        .then((SubscriptionCheckResponse data) {
+      return data;
     });
   }
 
   //恢复订阅
-  Future<void>subscriptionResume(int payMethod,String receiptData) {
-    return atmobApi.subscriptionresume(SubscriptionResumeRequest(payMethod, receiptData))
+  Future<void> subscriptionResume(int payMethod, String receiptData) {
+    return atmobApi
+        .subscriptionresume(SubscriptionResumeRequest(payMethod, receiptData))
         .then(HttpHandler.handle(true));
   }
 
   //试用结束查看试用信息
-  Future<MemberTrialInfoResponse>memberTrailInfo() {
-    return atmobApi.memberTrailInfo(AppBaseRequest())
+  Future<MemberTrialInfoResponse> memberTrailInfo() {
+    return atmobApi
+        .memberTrailInfo(AppBaseRequest())
         .then(HttpHandler.handle(true))
         .then((MemberTrialInfoResponse trialInfoResponse) {
-          return trialInfoResponse;
+      return trialInfoResponse;
     });
   }
 
   //试用结束查看试用信息
-  Future<OrderFirstCheckResponse>orderFirstCheck() {
-    return atmobApi.orderFirstCheck(AppBaseRequest())
+  Future<OrderFirstCheckResponse> orderFirstCheck() {
+    return atmobApi
+        .orderFirstCheck(AppBaseRequest())
         .then(HttpHandler.handle(true))
         .then((OrderFirstCheckResponse firstCheckResponse) {
       return firstCheckResponse;
@@ -96,12 +145,13 @@ class MemberRepository {
   }
 
   ///好评领会员-中台
-  Future<void>memberEvaluate() {
-    return atmobApi.memberEvaluate(AppBaseRequest())
-        .then(HttpHandler.handle(true)).then((_) {
-          //刷新会员状态
+  Future<void> memberEvaluate() {
+    return atmobApi
+        .memberEvaluate(AppBaseRequest())
+        .then(HttpHandler.handle(true))
+        .then((_) {
+      //刷新会员状态
       accountRepository.refreshMemberStatus();
-    }).catchError((erro) {
-    });
+    }).catchError((erro) {});
   }
 }

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

@@ -24,6 +24,7 @@ import '../data/repositories/phone_event_repository.dart' as _i274;
 import '../data/repositories/track_repository.dart' as _i240;
 import '../data/repositories/urgent_contact_repository.dart' as _i983;
 import '../helper/internet_connection_helper.dart' as _i772;
+import '../helper/member_pay_helper.dart' as _i235;
 import '../module/about/about_controller.dart' as _i256;
 import '../module/add_friend/add_friend_dialog_controller.dart' as _i897;
 import '../module/analyse/location_analyse_controller.dart' as _i783;
@@ -34,6 +35,7 @@ import '../module/friend/setting/friend_setting_controller.dart' as _i492;
 import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
 import '../module/main/today_track_helper.dart' as _i683;
+import '../module/member/activity/member_activity_controller.dart' as _i952;
 import '../module/member/member_controller.dart' as _i269;
 import '../module/mine/mine_controller.dart' as _i732;
 import '../module/news/news_controller.dart' as _i489;
@@ -156,11 +158,23 @@ extension GetItInjectableX on _i174.GetIt {
           gh<_i814.MemberRepository>(),
           gh<_i779.PaymentStatusManager>(),
         ));
+    gh.lazySingleton<_i235.MemberPayHelper>(() => _i235.MemberPayHelper(
+          gh<_i779.PaymentStatusManager>(),
+          gh<_i814.MemberRepository>(),
+        ));
+    gh.factory<_i952.MemberActivityController>(
+        () => _i952.MemberActivityController(
+              gh<_i20.AccountRepository>(),
+              gh<_i814.MemberRepository>(),
+              gh<_i235.MemberPayHelper>(),
+            ));
     gh.factory<_i731.MainController>(() => _i731.MainController(
           gh<_i1053.FriendsRepository>(),
           gh<_i20.AccountRepository>(),
+          gh<_i814.MemberRepository>(),
           gh<_i791.MessageRepository>(),
           gh<_i683.TodayTrackHelper>(),
+          gh<_i235.MemberPayHelper>(),
           gh<_i220.AtmobLocationClient>(),
           gh<_i983.UrgentContactRepository>(),
           gh<_i825.ConfigRepository>(),

+ 280 - 0
lib/helper/member_pay_helper.dart

@@ -0,0 +1,280 @@
+import 'dart:io';
+
+import 'package:agile_pay/flutter_pay.dart';
+import 'package:apple_pay/apple_pay.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/data/bean/goods_bean.dart';
+import 'package:location/data/bean/pay_item_bean.dart';
+import 'package:location/data/repositories/member_repository.dart';
+import 'package:location/utils/common_expand.dart';
+
+import '../data/api/response/request_pay_response.dart';
+import '../data/bean/wechat_payment_sign_bean.dart';
+import '../data/consts/channel_id.dart';
+import '../data/consts/error_code.dart';
+import '../data/consts/payment_type.dart';
+import '../data/repositories/account_repository.dart';
+import '../device/atmob_platform_info.dart';
+import '../dialog/alipay_qr_code_dialog.dart';
+import '../dialog/common_confirm_dialog.dart';
+import '../dialog/loading_dialog.dart';
+import '../dialog/wechat_qr_code_dialog.dart';
+import '../module/login/login_page.dart';
+import '../resource/string.gen.dart';
+import '../utils/http_handler.dart';
+import '../utils/payment_status_manager.dart';
+import '../utils/toast_util.dart';
+
+@lazySingleton
+class MemberPayHelper implements PaymentStatusCallback {
+  final PaymentStatusManager paymentStatusManager;
+  final MemberRepository memberRepository;
+
+  MemberPayHelper(this.paymentStatusManager, this.memberRepository);
+
+  Future<void> launchPay(GoodsBean? bean, PayItemBean? payWay) async {
+    if (bean == null) {
+      ToastUtil.show(StringName.memberPleaseChoiceGoods);
+      return;
+    }
+    if (payWay == null && !Platform.isIOS) {
+      ToastUtil.show(StringName.memberPleaseChoicePayment);
+      return;
+    }
+    //增加渠道登录判断
+    if (atmobPlatformInfo.tgPlatform == ChannelId.sd) {
+      if (!AccountRepository.getInstance().isLogin.value) {
+        ToastUtil.show(StringName.accountNoLogin);
+        final isLogin = await LoginPage.start();
+        if (!isLogin) {
+          return;
+        }
+      }
+    }
+
+    int goodsId = bean.id;
+    int payPlatform = payWay!.payPlatform;
+    int payMethod = payWay.payMethod;
+
+    LoadingDialog.show(StringName.payLoading);
+    memberRepository.setLastSelectedMember(bean, payWay);
+    try {
+      final RequestPayResponse response = await MemberRepository.getInstance()
+          .submitAndRequestPay(
+              goodsId: goodsId, payPlatform: payPlatform, payMethod: payMethod);
+      int payWayType =
+          getPayWayType(payMethod: payMethod, payPlatform: payPlatform);
+      if (payWayType == PayWayType.paymentWayWechat) {
+        _onWeChatPay(response.outTradeNo, response.wechatPayPrepayJson!,
+            payMethod, bean, payWay);
+      } else if (payWayType == PayWayType.paymentWayWechatScan) {
+        _onWechatScanPay(
+            response.outTradeNo, response.wechatPayQrcodeUrl!, payWay, bean);
+      } else if (payWayType == PayWayType.paymentWayAlipay) {
+        _onAliPay(response.outTradeNo, response.alipayOrderString!, payMethod,
+            bean, payWay);
+      } else if (payWayType == PayWayType.paymentWayAlipayScan) {
+        _onAliScanPay(
+            response.outTradeNo, response.alipayQrcodeHtml!, payWay, bean);
+      } else if (payWayType == PayWayType.paymentWayApple) {
+        _onApplePay(
+            response.outTradeNo, response.appAccountToken ?? "", payWay, bean);
+      } else {
+        LoadingDialog.hide();
+        ToastUtil.show(StringName.payNotSupport);
+      }
+    } catch (error) {
+      LoadingDialog.hide();
+      if (error is ServerErrorException) {
+        if (error.code == ErrorCode.payOrderError) {
+          ToastUtil.show(error.message);
+        } else if (error.code == ErrorCode.noLoginError) {
+          ToastUtil.show(StringName.accountNoLogin);
+          LoginPage.start();
+        } else {
+          ToastUtil.show(error.message);
+        }
+      } else {
+        ToastUtil.show(StringName.memberPaymentFailed);
+      }
+      rethrow;
+    }
+  }
+
+  Future<void> _onApplePay(
+    String outTradeNo,
+    String appAccountToken,
+    PayItemBean payWayInfo,
+    GoodsBean goodsInfo,
+  ) async {
+    final result = await ApplePay().purchase(
+      productId: goodsInfo.appleGoodsId ?? "",
+      appAccountToken: appAccountToken,
+    );
+    if (result["success"] == true) {
+      var receipt = result['receipt'];
+      print('购买成功: ${result['receipt']}');
+      checkPaymentStatus(
+        outTradeNo,
+        payWayInfo,
+        goodsInfo,
+        receiptData: receipt,
+      );
+    } else {
+      LoadingDialog.hide();
+      ToastUtil.show("支付失败,请稍后重试");
+      print('购买失败: ${result['error']}');
+    }
+  }
+
+  void _onAliScanPay(String outTradeNo, String qrHtml, PayItemBean paymentWay,
+      GoodsBean goodsBean) {
+    LoadingDialog.hide();
+    AlipayQrCodeDialog.show(
+        qrCodeHtml: qrHtml,
+        loadSuccessCallback: () {
+          checkPaymentStatus(outTradeNo, paymentWay, goodsBean);
+        },
+        onCloseCallback: () async {
+          //关闭后再持续查询几秒
+          CustomLoadingDialog.show();
+          await Future.delayed(Duration(seconds: 4));
+          paymentStatusManager.removePollingSubscription(outTradeNo);
+          CustomLoadingDialog.hide();
+        });
+  }
+
+  void _onAliPay(String outTradeNo, String payJson, int payMethod,
+      GoodsBean buyGoods, PayItemBean buyPayWay) {
+    final payInfo = AliPayInfo(payJson);
+    requestSdkPay(payInfo, outTradeNo, payMethod, buyGoods, buyPayWay);
+  }
+
+  void _onWeChatPay(String outTradeNo, String payJson, int payMethod,
+      GoodsBean buyGoods, PayItemBean buyPayWay) {
+    final bean = WechatPaymentSignBean.stringToBean(payJson);
+    final payInfo = WechatPayInfo(
+        appId: bean.appId,
+        partnerId: bean.partnerId,
+        prepayId: bean.prepayId,
+        package: bean.package,
+        noncestr: bean.nonceStr,
+        timestamp: bean.timeStamp,
+        sign: bean.sign);
+    requestSdkPay(payInfo, outTradeNo, payMethod, buyGoods, buyPayWay);
+  }
+
+  void _onWechatScanPay(String outTradeNo, String qrCodeUrl,
+      PayItemBean paymentWay, GoodsBean goodsBean) {
+    LoadingDialog.hide();
+    WechatQrCodeDialog.show(
+        qrCodeUrl: qrCodeUrl,
+        loadSuccessCallback: () {
+          checkPaymentStatus(outTradeNo, paymentWay, goodsBean);
+        },
+        onCloseCallback: () async {
+          //关闭后再持续查询几秒
+          CustomLoadingDialog.show();
+          await Future.delayed(Duration(seconds: 4));
+          paymentStatusManager.removePollingSubscription(outTradeNo);
+          CustomLoadingDialog.hide();
+        });
+  }
+
+  void requestSdkPay(dynamic payInfo, String outTradeNo, int payMethod,
+      GoodsBean buyGoods, PayItemBean buyPayWay) {
+    AgilePay.startPay(payInfo, success: (String? result) {
+      checkPaymentStatus(outTradeNo, buyPayWay, buyGoods, receiptData: result);
+    }, payError: (int errno, String? errorMessage) {
+      LoadingDialog.hide();
+      debugPrint('MemberPayHelper---payError: $errno, $errorMessage');
+      errorPayToast(errno);
+    }, error: (int errno, String? error) {
+      LoadingDialog.hide();
+      debugPrint('MemberPayHelper---error: $errno, $error');
+      errorPayToast(errno);
+    });
+  }
+
+  void checkPaymentStatus(
+      String orderNo, PayItemBean paymentWay, GoodsBean goodsBean,
+      {String? receiptData}) {
+    LoadingDialog.show(StringName.payQuerypayState);
+    paymentStatusManager.registerPaymentSuccessCallback(orderNo, this);
+    paymentStatusManager.checkPaymentStatus(orderNo, paymentWay, goodsBean,
+        receiptData: receiptData);
+  }
+
+  void errorPayToast(int errno) {
+    if (errno == AgilePayCode.payCodeNotSupport) {
+      ToastUtil.show(StringName.payNotSupport);
+    } else if (errno == AgilePayCode.payCodeCancelError) {
+      ToastUtil.show(StringName.payUserCancel);
+    } else if (errno == AgilePayCode.payCodeWxEnvError) {
+      ToastUtil.show(StringName.payWxEvnError);
+    } else if (errno == AgilePayCode.payCodeNotConnectStore) {
+      ToastUtil.show(StringName.payNotConnectStore);
+    } else {
+      ToastUtil.show(StringName.payError);
+    }
+  }
+
+  @override
+  void onPaymentFail() {
+    LoadingDialog.hide();
+  }
+
+  @override
+  void onPaymentSuccess(
+      String orderNo, PayItemBean paymentWay, GoodsBean storeItemBean) {
+    ///购买成功消失
+    LoadingDialog.hide();
+    memberRepository.clearLastSelectedMember();
+
+    ///购买成功之后弹出
+    onPaySucessShow();
+  }
+
+  void onPaySucessShow() {
+    try {
+      WechatQrCodeDialog.dismiss();
+      AlipayQrCodeDialog.dismiss();
+      CustomLoadingDialog.hide();
+      LoadingDialog.hide();
+    } catch (e) {
+      debugPrint('zk---onPaymentSuccess error: $e');
+    }
+    showPaymentSuccessDialog(onConfirm: () {
+      if (Get.key.currentState?.canPop() ?? false) {
+        Get.back();
+      }
+    }, onCancel: () {
+      if (Get.key.currentState?.canPop() ?? false) {
+        Get.back();
+      }
+    });
+  }
+
+  void showPaymentSuccessDialog(
+      {required VoidCallback onConfirm, required VoidCallback onCancel}) {
+    CommonConfirmDialog.show(
+        backDismiss: false,
+        clickMaskDismiss: false,
+        titleWidget: Text(StringName.paySuccessTitle,
+            style: TextStyle(
+                fontSize: 17.sp,
+                color: '#333333'.color,
+                fontWeight: FontWeight.bold)),
+        descWidget: Text(
+          StringName.paySuccessDesc,
+          style: TextStyle(fontSize: 14.sp, color: '#404040'.color),
+        ),
+        confirmText: StringName.dialogSure,
+        cancelOnTap: onCancel,
+        confirmOnTap: onConfirm);
+  }
+}

+ 23 - 2
lib/module/main/main_controller.dart

@@ -6,13 +6,16 @@ import 'package:flutter_tool_android/flutter_tool_android.dart';
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
+import 'package:location/data/bean/goods_bean.dart';
 import 'package:location/data/bean/location_info.dart';
 import 'package:location/data/bean/user_info.dart';
 import 'package:location/data/consts/constants.dart';
 import 'package:location/data/repositories/account_repository.dart';
 import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/data/repositories/member_repository.dart';
 import 'package:location/data/repositories/message_repository.dart';
 import 'package:location/handler/error_handler.dart';
+import 'package:location/helper/member_pay_helper.dart';
 import 'package:location/module/friend/friend_page.dart';
 import 'package:location/module/login/login_page.dart';
 import 'package:location/module/main/today_track_helper.dart';
@@ -25,6 +28,7 @@ import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/mmkv_util.dart';
 import 'package:location/utils/toast_util.dart';
 import 'package:permission_handler/permission_handler.dart';
+import 'package:sliding_sheet2/sliding_sheet2.dart';
 import '../../data/bean/member_status_info.dart';
 import '../../data/repositories/config_repository.dart';
 import '../../data/repositories/track_repository.dart';
@@ -37,7 +41,6 @@ import '../../dialog/location_permission_dialog.dart';
 import '../../helper/internet_connection_helper.dart';
 import '../../sdk/wechat/wechat_share_util.dart';
 import '../../socket/atmob_location_client.dart';
-import '../../utils/de_bounce.dart';
 import '../../utils/location_convert_marker_util.dart';
 import '../../utils/permission_util.dart';
 import '../add_friend/add_friend_page.dart';
@@ -73,12 +76,20 @@ class MainController extends BaseController {
   bool isFirstShowMineLocation = true;
   DateTime _lastRefreshTime = DateTime.fromMillisecondsSinceEpoch(0);
 
+  Duration? get activityDuration => memberRepository.activityDuration.value;
+
+  GoodsBean? get lastSelectedGoods => memberRepository.lastSelectedGoods.value;
+
+  final SheetController sheetController = SheetController();
+
   final FriendsRepository friendsRepository;
   final AccountRepository accountRepository;
   final MessageRepository messageRepository;
   final UrgentContactRepository urgentContactRepository;
   final TrackRepository trackRepository;
+  final MemberRepository memberRepository;
   final TodayTrackHelper todayTrackHelper;
+  final MemberPayHelper memberPayHelper;
 
   bool get hasUnreadMessage => messageRepository.hasUnreadMessage.value;
 
@@ -100,8 +111,10 @@ class MainController extends BaseController {
   MainController(
       this.friendsRepository,
       this.accountRepository,
+      this.memberRepository,
       this.messageRepository,
       this.todayTrackHelper,
+      this.memberPayHelper,
       AtmobLocationClient atmobLocationClient,
       this.urgentContactRepository,
       ConfigRepository configRepository,
@@ -167,7 +180,6 @@ class MainController extends BaseController {
     if (memberInfo == null) {
       return;
     }
-    AtmobLog.d("zk", 'isLogin:${accountRepository.isLogin.value}');
     if (memberInfo.expired && isFirstShowMemberPage == true ||
         !accountRepository.isLogin.value) {
       isFirstShowMemberPage = false;
@@ -547,6 +559,15 @@ class MainController extends BaseController {
     MemberPage.start();
   }
 
+  void onBuyMemberActivityClick() {
+    if (lastSelectedGoods == null &&
+        memberRepository.lastSelectedPayItem == null) {
+      return;
+    }
+    memberPayHelper.launchPay(
+        lastSelectedGoods!, memberRepository.lastSelectedPayItem!);
+  }
+
   @override
   void onClose() {
     mineLocationSubscription?.cancel();

+ 239 - 57
lib/module/main/main_page.dart

@@ -21,6 +21,7 @@ import '../../data/consts/constants.dart';
 import '../../router/app_pages.dart';
 import '../../utils/common_style.dart';
 import '../../utils/common_util.dart';
+import '../../widget/activity_countdown_txt_view.dart';
 import '../../widget/marquee_text.dart';
 import '../../widget/relative_time_text.dart';
 import 'main_friend_item.dart';
@@ -79,10 +80,8 @@ class MainPage extends BasePage<MainController> {
 
   Widget buildMainBottomView() {
     return SlidingSheet(
-      color: '#F9F9F9'.color,
-      elevation: 10,
-      shadowColor: Colors.black.withOpacity(0.1),
-      cornerRadius: 18.w,
+      controller: controller.sheetController,
+      color: ColorName.transparent,
       snapSpec: SnapSpec(
         initialSnap: SnapSpec.headerSnap,
         // Enable snapping. This is true by default.
@@ -105,19 +104,23 @@ class MainPage extends BasePage<MainController> {
         final visibleFraction = info.visibleFraction;
         controller.onFriendVisibleFraction(visibleFraction);
       },
-      child: AspectRatio(
-        aspectRatio: 336 / 134,
-        child: Container(
-            margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.w),
-            decoration: BoxDecoration(
-                color: Colors.white, borderRadius: BorderRadius.circular(20.r)),
-            child: Obx(() {
-              final todayTrack = controller.selectedFriend?.id == null
-                  ? null
-                  : controller
-                      .todayTrackReportMap[controller.selectedFriend?.id];
-              return buildTodayTrackView(todayTrack);
-            })),
+      child: Container(
+        color: '#F9F9F9'.color,
+        child: AspectRatio(
+          aspectRatio: 336 / 134,
+          child: Container(
+              margin: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.w),
+              decoration: BoxDecoration(
+                  color: Colors.white,
+                  borderRadius: BorderRadius.circular(20.r)),
+              child: Obx(() {
+                final todayTrack = controller.selectedFriend?.id == null
+                    ? null
+                    : controller
+                        .todayTrackReportMap[controller.selectedFriend?.id];
+                return buildTodayTrackView(todayTrack);
+              })),
+        ),
       ),
     );
   }
@@ -139,7 +142,8 @@ class MainPage extends BasePage<MainController> {
   Widget buildNoMemberView() {
     return GestureDetector(
         onTap: controller.onTrackNoMemberClick,
-        child: Assets.images.imgTrackNoMemberTips.image(fit: BoxFit.fill));
+        child: Assets.images.imgTrackNoMemberTips
+            .image(width: double.infinity, fit: BoxFit.fill));
   }
 
   Widget buildTodayTrackLoadingView() {
@@ -326,30 +330,32 @@ class MainPage extends BasePage<MainController> {
   }
 
   Widget buildMapFunView() {
-    return Positioned(
-      right: 0.w,
-      bottom: 140.w,
-      child: Column(
-        children: [
-          GestureDetector(
-            onTap: controller.onRefreshFriendLocationClick,
-            child: Container(
-                margin: EdgeInsets.only(right: 12.w),
-                child: Assets.images.iconMainRefreshFriendLocation
-                    .image(width: 42.w, height: 42.w)),
-          ),
-          SizedBox(height: 14.w),
-          GestureDetector(
-            onTap: controller.onCurrentLocationClick,
-            child: Container(
-                margin: EdgeInsets.only(right: 12.w),
-                child: Assets.images.iconMainRefreshMineLocation
-                    .image(width: 42.w, height: 42.w)),
-          ),
-          SizedBox(height: 20.w)
-        ],
-      ),
-    );
+    return Obx(() {
+      return Positioned(
+        right: 0.w,
+        bottom: controller.lastSelectedGoods == null ? 140.w : 180.w,
+        child: Column(
+          children: [
+            GestureDetector(
+              onTap: controller.onRefreshFriendLocationClick,
+              child: Container(
+                  margin: EdgeInsets.only(right: 12.w),
+                  child: Assets.images.iconMainRefreshFriendLocation
+                      .image(width: 42.w, height: 42.w)),
+            ),
+            SizedBox(height: 14.w),
+            GestureDetector(
+              onTap: controller.onCurrentLocationClick,
+              child: Container(
+                  margin: EdgeInsets.only(right: 12.w),
+                  child: Assets.images.iconMainRefreshMineLocation
+                      .image(width: 42.w, height: 42.w)),
+            ),
+            SizedBox(height: 20.w)
+          ],
+        ),
+      );
+    });
   }
 
   Container buildTabContainer() {
@@ -475,21 +481,197 @@ class MainPage extends BasePage<MainController> {
   }
 
   Widget buildHeaderBuilder(BuildContext context, SheetState state) {
-    return IntrinsicHeight(
-      child: Column(
-        children: [
-          SizedBox(height: 5.w),
-          Container(
-            width: 32.w,
-            height: 3.w,
-            decoration: BoxDecoration(
-                color: '#D9D9D9'.color,
-                borderRadius: BorderRadius.all(Radius.circular(49.w))),
-          ),
-          SizedBox(height: 12.w),
-          buildSelectFriendInfoView(),
-          SizedBox(height: 13.w)
-        ],
+    return Obx(() {
+      return IntrinsicHeight(
+        child: Stack(
+          alignment: Alignment.topCenter,
+          children: [
+            Visibility(
+              visible: controller.lastSelectedGoods != null,
+              child: Assets.images.bgMemberActivityMain
+                  .image(width: 336.w, height: 67.w),
+            ),
+            Visibility(
+                visible: controller.lastSelectedGoods != null,
+                child: buildActivityMemberView()),
+            Column(
+              children: [
+                // Visibility(
+                //     visible: controller.lastSelectedGoods != null,
+                //     child: SizedBox(height: 58.w)),
+                SizedBox(height: 58.w),
+                Container(
+                  width: double.infinity,
+                  decoration: BoxDecoration(
+                    color: '#F9F9F9'.color,
+                    borderRadius: BorderRadius.only(
+                      topLeft: Radius.circular(20.w),
+                      topRight: Radius.circular(20.w),
+                    ),
+                    boxShadow: [
+                      BoxShadow(
+                        color: ColorName.black.withOpacity(0.01),
+                        blurRadius: 10,
+                        offset:
+                            const Offset(0, -2), // changes position of shadow
+                      ),
+                    ],
+                  ),
+                  child: Column(
+                    children: [
+                      SizedBox(height: 5.w),
+                      Container(
+                        width: 32.w,
+                        height: 3.w,
+                        decoration: BoxDecoration(
+                            color: '#D9D9D9'.color,
+                            borderRadius:
+                                BorderRadius.all(Radius.circular(49.w))),
+                      ),
+                      SizedBox(height: 12.w),
+                      buildSelectFriendInfoView(),
+                      SizedBox(height: 13.w)
+                    ],
+                  ),
+                )
+              ],
+            )
+          ],
+        ),
+      );
+    });
+  }
+
+  Widget buildActivityMemberView() {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTap: controller.onBuyMemberActivityClick,
+      child: Container(
+          height: 58.w,
+          width: double.infinity,
+          margin: EdgeInsets.only(left: 12.w, right: 12.w),
+          child: Stack(
+            children: [
+              Row(
+                children: [
+                  SizedBox(width: 12.w),
+                  Assets.images.iconMemberActivityCoupon
+                      .image(width: 40.w, height: 40.w),
+                  SizedBox(width: 8.w),
+                  Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Spacer(flex: 1),
+                      Row(
+                        children: [
+                          Text('您有一订单未支付专属优惠券',
+                              style: TextStyle(
+                                  fontSize: 10.sp,
+                                  color: ColorName.white87,
+                                  fontWeight: FontWeight.bold)),
+                          Obx(() {
+                            return Text(
+                                ' -¥${(((controller.lastSelectedGoods?.originalAmount ?? 0) - (controller.lastSelectedGoods?.amount ?? 0)) / 100).toInt()}',
+                                style: TextStyle(
+                                    fontSize: 20.sp,
+                                    color: '#E7DBA7'.color,
+                                    fontWeight: FontWeight.bold));
+                          })
+                        ],
+                      ),
+                      Row(
+                        children: [
+                          Obx(() {
+                            return ActivityCountdownTextView(
+                                timeItemHeight: 15.w,
+                                contentPadding: EdgeInsets.zero,
+                                timeItemWidth: 16.w,
+                                textStyle: TextStyle(
+                                    fontSize: 10.sp, color: '#322C54'.color),
+                                duration: controller.activityDuration ??
+                                    Duration(seconds: 0),
+                                separator: buildCountdownSeparator(),
+                                timeBgBoxDecoration: BoxDecoration(
+                                  gradient: LinearGradient(
+                                      colors: [
+                                        ColorName.white,
+                                        ColorName.white87
+                                      ],
+                                      begin: Alignment.topCenter,
+                                      end: Alignment.bottomCenter),
+                                  borderRadius: BorderRadius.circular(3.w),
+                                ));
+                          }),
+                          SizedBox(width: 4.w),
+                          Text(
+                            StringName.memberActivitySpeciallyPreferential,
+                            style: TextStyle(
+                                fontSize: 10.sp,
+                                color: ColorName.white87,
+                                fontWeight: FontWeight.bold),
+                          )
+                        ],
+                      ),
+                      Spacer(flex: 4),
+                    ],
+                  ),
+                ],
+              ),
+              Positioned(
+                top: 15.w,
+                bottom: 14.w,
+                right: 10.w,
+                child: Container(
+                    decoration: BoxDecoration(
+                      borderRadius: BorderRadius.circular(100.w),
+                      gradient: LinearGradient(
+                          colors: [
+                            '#FFFEF3'.color,
+                            '#FFE5A3'.color,
+                          ],
+                          begin: Alignment.topCenter,
+                          end: Alignment.bottomCenter),
+                    ),
+                    padding: EdgeInsets.symmetric(horizontal: 12.w),
+                    child: Center(
+                      child: Text(StringName.memberActivityToBuy,
+                          style: TextStyle(
+                              fontSize: 14.sp,
+                              color: '#40338B'.color,
+                              height: 1,
+                              fontWeight: FontWeight.bold)),
+                    )),
+              )
+            ],
+          )),
+    );
+  }
+
+  Widget buildCountdownSeparator() {
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 2.w),
+      child: IntrinsicHeight(
+        child: Column(
+          children: [
+            Container(
+              width: 2.w,
+              height: 2.w,
+              decoration: BoxDecoration(
+                color: ColorName.white87,
+                shape: BoxShape.circle,
+              ),
+            ),
+            SizedBox(height: 3.w),
+            Container(
+              width: 2.w,
+              height: 2.w,
+              decoration: BoxDecoration(
+                color: ColorName.white87,
+                shape: BoxShape.circle,
+              ),
+            )
+          ],
+        ),
       ),
     );
   }

+ 119 - 0
lib/module/member/activity/member_activity_banner_widget.dart

@@ -0,0 +1,119 @@
+import 'package:flutter/cupertino.dart';
+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 'package:smooth_page_indicator/smooth_page_indicator.dart';
+
+class InfiniteScrollController extends PageController {
+  final int itemCount;
+
+  InfiniteScrollController({
+    required this.itemCount,
+    int initialPage = 0,
+    super.viewportFraction,
+  }) : super(
+          initialPage: initialPage * 1000,
+        );
+
+  int getRealIndex(int page) {
+    if (itemCount == 0) return 0;
+    return page % itemCount;
+  }
+}
+
+class MemberActivityBannerWidget extends StatefulWidget {
+  final int itemCount;
+  final IndexedWidgetBuilder itemBuilder;
+  final double bannerHeight;
+  final double viewportFraction;
+
+  const MemberActivityBannerWidget({
+    super.key,
+    required this.itemCount,
+    required this.itemBuilder,
+    this.bannerHeight = 100.0,
+    this.viewportFraction = 1,
+  });
+
+  @override
+  _MemberActivityBannerWidgetState createState() =>
+      _MemberActivityBannerWidgetState();
+}
+
+class _MemberActivityBannerWidgetState
+    extends State<MemberActivityBannerWidget> {
+  late InfiniteScrollController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = InfiniteScrollController(
+        initialPage: widget.itemCount * 500,
+        itemCount: widget.itemCount,
+        viewportFraction: widget.viewportFraction);
+
+    _startAutoPlay();
+  }
+
+  void _startAutoPlay() {
+    Future.delayed(const Duration(seconds: 5), () {
+      if (_controller.hasClients) {
+        _controller.nextPage(
+          duration: const Duration(milliseconds: 500),
+          curve: Curves.ease,
+        );
+        _startAutoPlay();
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return IntrinsicHeight(
+      child: Column(
+        children: [
+          SizedBox(
+            height: widget.bannerHeight,
+            child: PageView.builder(
+              controller: _controller,
+              itemBuilder: (context, index) {
+                final realIndex = _controller.getRealIndex(index);
+                return widget.itemBuilder(context, realIndex);
+              },
+            ),
+          ),
+          Container(
+            alignment: Alignment.center,
+            padding: EdgeInsets.only(top: 16.w),
+            child: SmoothPageIndicator(
+              controller: _controller,
+              count: widget.itemCount,
+              effect: ExpandingDotsEffect(
+                expansionFactor: 2,
+                dotWidth: 5.0,
+                dotHeight: 5.w,
+                spacing: 4.0,
+                dotColor: '#D7D7E8'.color,
+                activeDotColor: ColorName.colorPrimary,
+              ),
+              onDotClicked: (index) {
+                _controller.animateToPage(
+                  index + _controller.initialPage,
+                  duration: const Duration(milliseconds: 500),
+                  curve: Curves.ease,
+                );
+              },
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 129 - 0
lib/module/member/activity/member_activity_controller.dart

@@ -0,0 +1,129 @@
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/base/base_controller.dart';
+import 'package:location/data/repositories/account_repository.dart';
+import 'package:location/data/repositories/member_repository.dart';
+
+import '../../../data/bean/goods_bean.dart';
+import '../../../data/bean/member_status_info.dart';
+import '../../../data/bean/pay_item_bean.dart';
+import '../../../data/consts/web_url.dart';
+import '../../../handler/error_handler.dart';
+import '../../../helper/member_pay_helper.dart';
+import '../../../resource/assets.gen.dart';
+import '../../../resource/string.gen.dart';
+import '../../../utils/async_util.dart';
+import '../../../utils/toast_util.dart';
+import '../../browser/browser_view.dart';
+
+@injectable
+class MemberActivityController extends BaseController {
+  final Rxn<GoodsBean> _selectedGoods = Rxn<GoodsBean>();
+
+  GoodsBean? get selectedGoods => _selectedGoods.value;
+
+  MemberStatusInfo? get memberStatusInfo =>
+      accountRepository.memberStatusInfo.value;
+
+  final List<ImageProvider> funImages = [
+    Assets.images.imgMemberActivityBanner1.provider(),
+    Assets.images.imgMemberActivityBanner2.provider(),
+    Assets.images.imgMemberActivityBanner3.provider(),
+    Assets.images.imgMemberActivityBanner4.provider(),
+  ];
+
+  final RxList<GoodsBean> goodsList = <GoodsBean>[].obs;
+  final List<PayItemBean> payItemList = <PayItemBean>[];
+  CancelableFuture? _memberDataFuture;
+
+  final AccountRepository accountRepository;
+  final MemberRepository memberRepository;
+  final MemberPayHelper memberPayHelper;
+
+  Duration? get activityDuration => memberRepository.activityDuration.value;
+
+  MemberActivityController(
+      this.accountRepository, this.memberRepository, this.memberPayHelper);
+
+  @override
+  void onReady() {
+    super.onReady();
+    refreshMemberData();
+    memberRepository.startActivityCountdown();
+    precacheImage(
+        Assets.images.iconMemberSpecialProductsSelect.provider(), Get.context!);
+    precacheImage(
+        Assets.images.iconMemberSpecialProductsNormal.provider(), Get.context!);
+  }
+
+  void refreshMemberData() {
+    _memberDataFuture?.cancel();
+    _memberDataFuture =
+        AsyncUtil.retryWithExponentialBackoff(() => _requestMemberData(), 4);
+    _memberDataFuture?.catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  Future<void> _requestMemberData() {
+    return memberRepository
+        .getMemberList(itemListType: Platform.isIOS ? 2 : 0)
+        .then((response) {
+      goodsList.clear();
+      payItemList.clear();
+      _selectedGoods.value = null;
+      if (response.payInfoList?.isNotEmpty == true) {
+        payItemList.addAll(response.payInfoList!);
+      }
+      if (response.goodsList?.isNotEmpty == true) {
+        goodsList.addAll(response.goodsList!);
+        _selectedGoods.value = goodsList.first;
+      }
+      if (goodsList.isNotEmpty && payItemList.isNotEmpty) {
+        memberRepository.setLastSelectedMember(
+            goodsList.first, payItemList.first);
+      }
+    });
+  }
+
+  void onBack() {
+    Get.back();
+  }
+
+  void onBuyClick() {
+    if (payItemList.isEmpty) {
+      ToastUtil.show(StringName.memberActivityNoPayway);
+      return;
+    }
+    memberPayHelper.launchPay(selectedGoods, payItemList.first);
+  }
+
+  void onGoodsItemClick(GoodsBean goodsInfo) {
+    _selectedGoods.value = goodsInfo;
+    onBuyClick();
+  }
+
+  void onPrivacyPolicyClick() {
+    BrowserPage.start(WebUrl.privacyPolicy);
+  }
+
+  void onTermOfServiceClick() {
+    BrowserPage.start(WebUrl.userAgreement);
+  }
+
+  void onRenewalAgreementClick() {
+    BrowserPage.start(WebUrl.renewalAgreement);
+  }
+
+  void onRecoverClick() {}
+
+  @override
+  void onClose() {
+    super.onClose();
+    _memberDataFuture?.cancel();
+  }
+}

+ 624 - 0
lib/module/member/activity/member_activity_page.dart

@@ -0,0 +1,624 @@
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/base/base_page.dart';
+import 'package:location/data/bean/goods_bean.dart';
+import 'package:location/module/member/activity/member_activity_banner_widget.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/utils/project_expand.dart';
+import '../../../resource/fonts.gen.dart';
+import '../../../router/app_pages.dart';
+import '../../../widget/activity_countdown_txt_view.dart';
+import '../../../widget/activity_countdown_view.dart';
+import '../../../widget/shimmer_effect.dart';
+import 'member_activity_controller.dart';
+
+class MemberActivityPage extends BasePage<MemberActivityController> {
+  const MemberActivityPage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.memberActivity);
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        buildActivityContent(),
+        buildToolbar(),
+        buildMemberBottomView()
+      ],
+    );
+  }
+
+  Widget buildToolbar() {
+    return SafeArea(
+      child: SizedBox(
+        width: double.infinity,
+        height: 56.w,
+        child: Stack(
+          children: [
+            Positioned(
+              left: 10,
+              top: 0,
+              bottom: 0,
+              child: GestureDetector(
+                onTap: controller.onBack,
+                child: Container(
+                    padding: EdgeInsets.only(
+                        left: 10.w, right: 12.w, top: 10.w, bottom: 10.w),
+                    child: Assets.images.iconMemberActivityClose
+                        .image(width: 8.w)),
+              ),
+            ),
+            Center(
+              child: Text(
+                StringName.memberActivityTitle,
+                style: TextStyle(
+                    fontSize: 18.sp,
+                    color: Colors.white,
+                    fontWeight: FontWeight.bold),
+              ),
+            ),
+            Positioned(
+              right: 10.w,
+              top: 0,
+              bottom: 0,
+              child: GestureDetector(
+                onTap: controller.onRecoverClick,
+                child: Row(
+                  children: [
+                    Assets.images.iconAppleRecoverSubscribe
+                        .image(width: 14.w, height: 14.w),
+                    SizedBox(width: 1.w),
+                    Text(StringName.appleRecoverSubscribeTxt,
+                        style: TextStyle(
+                            fontSize: 11.sp,
+                            color: Colors.white,
+                            fontWeight: FontWeight.bold))
+                  ],
+                ),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildActivityContent() {
+    return SizedBox(
+      width: double.infinity,
+      height: double.infinity,
+      child: SingleChildScrollView(
+        child: Stack(
+          children: [
+            Assets.images.bgMemberActivity
+                .image(width: double.infinity, fit: BoxFit.cover),
+            SafeArea(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  SizedBox(height: 83.w),
+                  Container(
+                      margin: EdgeInsets.only(left: 16.w),
+                      child: Assets.images.iconMemberActivityCountdown
+                          .image(height: 22.w)),
+                  SizedBox(height: 10.w),
+                  Container(
+                    margin: EdgeInsets.only(left: 16.w, bottom: 16.w),
+                    child: Assets.images.imgMemberActivityFavourableTxt
+                        .image(height: 20.w),
+                  ),
+                  buildGoodsContainer(),
+                ],
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildGoodsContainer() {
+    return Container(
+      width: double.infinity,
+      decoration: BoxDecoration(
+        gradient: LinearGradient(
+            stops: const [0.0, 0.16],
+            colors: ['#E4E4FF'.color, Colors.white],
+            begin: Alignment.topCenter,
+            end: Alignment.bottomCenter),
+        borderRadius: BorderRadius.only(
+            topLeft: Radius.circular(20.w), topRight: Radius.circular(20.w)),
+      ),
+      child: Stack(
+        children: [
+          Assets.images.bgMemberActivityContainer
+              .image(width: double.infinity, fit: BoxFit.fill),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              SizedBox(height: 14.w),
+              buildFunTitleView(),
+              buildMemberActivityFunctionIntroduction(),
+              SizedBox(height: 8.w),
+              buildGoodsListView(),
+              buildPrivacyPolicyView(),
+              SizedBox(height: 150.w),
+            ],
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget buildFunTitleView() {
+    return Container(
+      margin: EdgeInsets.only(left: 16.w, bottom: 18.w),
+      child: Stack(
+        children: [
+          Container(
+            width: 117.w,
+            height: 8.w,
+            decoration: BoxDecoration(
+                gradient: LinearGradient(
+                  colors: ['#AA70FE'.color, '#006A2DC4'.color],
+                  begin: Alignment.centerLeft,
+                  end: Alignment.centerRight,
+                ),
+                borderRadius: BorderRadius.only(
+                    topLeft: Radius.circular(10.w),
+                    bottomLeft: Radius.circular(10.w))),
+            margin: EdgeInsets.only(top: 15.5.w),
+          ),
+          Container(
+            margin: EdgeInsets.only(left: 4.w),
+            child: Text(
+              StringName.memberActivityFunTitle,
+              style: TextStyle(
+                  fontSize: 15.sp,
+                  color: '#202020'.color,
+                  fontWeight: FontWeight.bold),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget buildPrivacyPolicyView() {
+    return Padding(
+      padding: EdgeInsets.only(left: 15.w),
+      child: RichText(
+          text: TextSpan(
+              style: TextStyle(fontSize: 12.sp, color: ColorName.black40),
+              children: [
+            TextSpan(text: '购买前请先阅读'),
+            TextSpan(
+                recognizer: TapGestureRecognizer()
+                  ..onTap = () {
+                    controller.onPrivacyPolicyClick();
+                  },
+                text: '隐私政策',
+                style: TextStyle(
+                    color: ColorName.black60,
+                    decoration: TextDecoration.underline)),
+            TextSpan(text: '&'),
+            TextSpan(
+                recognizer: TapGestureRecognizer()
+                  ..onTap = () {
+                    controller.onTermOfServiceClick();
+                  },
+                text: '服务条款',
+                style: TextStyle(
+                    color: ColorName.black60,
+                    decoration: TextDecoration.underline)),
+            if (Platform.isIOS) TextSpan(text: '&'),
+            if (Platform.isIOS)
+              TextSpan(
+                  recognizer: TapGestureRecognizer()
+                    ..onTap = () {
+                      controller.onRenewalAgreementClick();
+                    },
+                  text: '续费协议',
+                  style: TextStyle(
+                      color: ColorName.black60,
+                      decoration: TextDecoration.underline)),
+          ])),
+    );
+  }
+
+  Widget buildCountdownSeparator() {
+    return Container(
+      margin: EdgeInsets.symmetric(horizontal: 2.w),
+      child: IntrinsicHeight(
+        child: Column(
+          children: [
+            Container(
+              width: 2.w,
+              height: 2.w,
+              decoration: BoxDecoration(
+                color: '#FF5656'.color,
+                shape: BoxShape.circle,
+              ),
+            ),
+            SizedBox(height: 3.w),
+            Container(
+              width: 2.w,
+              height: 2.w,
+              decoration: BoxDecoration(
+                color: '#FF5656'.color,
+                shape: BoxShape.circle,
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildMemberBottomView() {
+    return Obx(() {
+      if (controller.memberStatusInfo != null &&
+          controller.memberStatusInfo?.permanent == true) {
+        return SizedBox(); // 不显示
+      }
+
+      return Align(
+          alignment: Alignment.bottomCenter,
+          child: Container(
+            margin: EdgeInsets.only(bottom: 15.w, left: 12.w, right: 12.w),
+            child: Stack(
+              children: [
+                Container(
+                  width: 336.w,
+                  height: 47.w,
+                  decoration: BoxDecoration(
+                      borderRadius: BorderRadius.only(
+                        topLeft: Radius.circular(30.w),
+                        topRight: Radius.circular(30.w),
+                      ),
+                      color: '#FFFED8'.color),
+                  child: Align(
+                    alignment: Alignment(0.0, -0.75),
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          StringName.memberActivityCountdown,
+                          style: TextStyle(
+                              fontSize: 11.sp, color: '#FF5656'.color),
+                        ),
+                        SizedBox(width: 4.w),
+                        Obx(() {
+                          return ActivityCountdownTextView(
+                              timeItemHeight: 15.w,
+                              contentPadding: EdgeInsets.zero,
+                              timeItemWidth: 16.w,
+                              textStyle: TextStyle(
+                                  fontSize: 10.sp, color: Colors.white),
+                              duration: controller.activityDuration ??
+                                  Duration(seconds: 0),
+                              separator: buildCountdownSeparator(),
+                              timeBgBoxDecoration: BoxDecoration(
+                                color: '#FF5656'.color,
+                                borderRadius: BorderRadius.circular(3.w),
+                              ));
+                        }),
+                        SizedBox(width: 4.w),
+                        Text(
+                          StringName.memberActivitySpeciallyPreferential,
+                          style: TextStyle(
+                              fontSize: 10.sp, color: '#FF5656'.color),
+                        )
+                      ],
+                    ),
+                  ),
+                ),
+                GestureDetector(
+                  onTap: controller.onBuyClick,
+                  child: Container(
+                    margin: EdgeInsets.only(top: 24.w),
+                    height: 50.w,
+                    width: 336.w,
+                    child: ShimmerEffect(
+                      image: Assets.images.imgMemberBtnShadow.provider(),
+                      shimmerWidthFactor: 0.244047619047619,
+                      duration: Duration(milliseconds: 3000),
+                      delay: Duration(milliseconds: 800),
+                      child: Container(
+                        height: 50.w,
+                        width: 336.w,
+                        alignment: Alignment.center,
+                        decoration: BoxDecoration(
+                            image: DecorationImage(
+                                image: Assets.images.iconMemberSettlementBg
+                                    .provider(),
+                                fit: BoxFit.fill)),
+                        child: Text(
+                          StringName.memberVipUnlock,
+                          style: TextStyle(
+                              fontSize: 18.sp,
+                              color: '#FFF8EF'.color,
+                              fontWeight: FontWeight.bold),
+                        ),
+                      ),
+                    ),
+                  ),
+                )
+              ],
+            ),
+          )
+
+          // Container(
+          //   color: ColorName.white,
+          //   padding:
+          //       EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.w, top: 8.w),
+          //   child: Container(
+          //     width: double.infinity,
+          //     height: 50.w,
+          //     padding: EdgeInsets.only(left: 20.w),
+          //     decoration: BoxDecoration(
+          //       image: DecorationImage(
+          //         image: Assets.images.iconMemberSettlementBg.provider(),
+          //         fit: BoxFit.fill,
+          //       ),
+          //     ),
+          //     child: Stack(
+          //       children: [
+          //         Assets.images.imgMemberBtnShadow.image(height: double.infinity)
+          //       ],
+          //     ),
+          //   ),
+          // ),
+          );
+    });
+  }
+
+  Widget buildMemberActivityFunctionIntroduction() {
+    return MemberActivityBannerWidget(
+        bannerHeight: 145.w,
+        viewportFraction: 0.88,
+        itemCount: controller.funImages.length,
+        itemBuilder: (context, index) {
+          final image = controller.funImages[index];
+          return Container(
+            margin: EdgeInsets.symmetric(horizontal: 5.w),
+            child: Image(
+                image: image,
+                width: double.infinity,
+                height: double.infinity,
+                fit: BoxFit.fill),
+          );
+        });
+  }
+
+  Widget buildGoodsListView() {
+    return IntrinsicHeight(
+      child: Obx(() {
+        return Column(
+          children: [
+            for (int i = 0; i < controller.goodsList.length; i++)
+              if (i == 0)
+                buildFavourableGoodsView(controller.goodsList[i])
+              else
+                buildNormalGoodsView(controller.goodsList[i])
+          ],
+        );
+      }),
+    );
+  }
+
+  Widget buildFavourableGoodsView(GoodsBean goodsInfo) {
+    bool isSelected = controller.selectedGoods?.id == goodsInfo.id;
+    return GestureDetector(
+      onTap: () {
+        controller.onGoodsItemClick(goodsInfo);
+      },
+      child: Container(
+        height: 96.w,
+        width: double.infinity,
+        margin: EdgeInsets.only(left: 12.w, right: 16.w, bottom: 12.w),
+        decoration: BoxDecoration(
+          image: DecorationImage(
+            image: isSelected
+                ? Assets.images.iconMemberSpecialProductsSelect.provider()
+                : Assets.images.iconMemberSpecialProductsNormal.provider(),
+            fit: BoxFit.fill,
+          ),
+        ),
+        child: Stack(
+          children: [
+            Positioned(
+              top: 10.w,
+              bottom: 0,
+              left: 24.w,
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  RichText(
+                      text: TextSpan(
+                          style: TextStyle(
+                              color: isSelected
+                                  ? '#FF5656'.color
+                                  : "#323133".color,
+                              fontWeight: FontWeight.bold),
+                          children: [
+                        TextSpan(
+                            text: '¥',
+                            style: TextStyle(fontSize: 16.sp, height: 1)),
+                        TextSpan(
+                            text: goodsInfo.amount.divideBy100(),
+                            style: TextStyle(
+                              fontSize: 28.sp,
+                              height: 1,
+                              //fontFamily: FontFamily.oppoSans
+                            ))
+                      ])),
+                  Text('¥${goodsInfo.originalAmount.divideBy100()}',
+                      style: TextStyle(
+                          decoration: TextDecoration.lineThrough,
+                          decorationColor: ColorName.black40,
+                          decorationThickness: 1.0,
+                          fontSize: 12.sp,
+                          color: ColorName.black40))
+                ],
+              ),
+            ),
+            Positioned(
+              top: 10.w,
+              bottom: 0,
+              left: 105.w,
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Text(
+                    goodsInfo.name,
+                    style: TextStyle(
+                        fontSize: 17.sp,
+                        color: "#333333".color,
+                        fontWeight: FontWeight.bold),
+                  ),
+                  SizedBox(
+                    height: 3.w,
+                  ),
+                  Text(
+                    goodsInfo.description ?? "",
+                    style: TextStyle(
+                        fontSize: 11.sp,
+                        color: "#9191BA".color,
+                        fontWeight: FontWeight.bold),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildNormalGoodsView(GoodsBean goodsInfo) {
+    bool isSelected = controller.selectedGoods?.id == goodsInfo.id;
+    return GestureDetector(
+      onTap: () {
+        controller.onGoodsItemClick(goodsInfo);
+      },
+      child: Container(
+        width: double.infinity,
+        height: 72.w,
+        margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 12.w),
+        child: Stack(
+          children: [
+            Container(
+                width: double.infinity,
+                height: double.infinity,
+                decoration: BoxDecoration(
+                    gradient: isSelected
+                        ? LinearGradient(
+                            colors: ['#FFFFFF'.color, '#F3EAFF'.color],
+                            begin: Alignment.topLeft,
+                            end: Alignment.bottomRight,
+                            stops: const [0.4, 1.0])
+                        : null,
+                    borderRadius: BorderRadius.circular(12.w),
+                    border: Border.all(
+                      color: isSelected ? '#7A13C6'.color : '#DDDDDD'.color,
+                      width: isSelected ? 2.5.w : 1.w,
+                    ))),
+            Stack(
+              children: [
+                Positioned(
+                  top: 4.w,
+                  bottom: 0,
+                  left: 20.w,
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      RichText(
+                          text: TextSpan(
+                              style: TextStyle(
+                                  fontFamily: FontFamily.oppoSans,
+                                  color: isSelected
+                                      ? '#FF5656'.color
+                                      : "#323133".color,
+                                  fontWeight: FontWeight.bold),
+                              children: [
+                            TextSpan(
+                                text: '¥',
+                                style: TextStyle(fontSize: 16.sp, height: 1)),
+                            TextSpan(
+                                text: goodsInfo.amount.divideBy100(),
+                                style: TextStyle(
+                                  fontSize: 28.sp,
+                                  height: 1,
+                                  //fontFamily: FontFamily.oppoSans
+                                ))
+                          ])),
+                      Text('¥${goodsInfo.originalAmount.divideBy100()}',
+                          style: TextStyle(
+                              decoration: TextDecoration.lineThrough,
+                              decorationColor: ColorName.black40,
+                              decorationThickness: 1.0,
+                              fontSize: 12.sp,
+                              color: ColorName.black40))
+                    ],
+                  ),
+                ),
+                Positioned(
+                  top: 0.w,
+                  bottom: 0,
+                  left: 101.w,
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Text(
+                        goodsInfo.name,
+                        style: TextStyle(
+                            fontSize: 17.sp,
+                            color: "#333333".color,
+                            fontWeight: FontWeight.bold),
+                      ),
+                      SizedBox(
+                        height: 3.w,
+                      ),
+                      Text(
+                        goodsInfo.description ?? "",
+                        style: TextStyle(
+                            fontSize: 11.sp,
+                            color: "#9191BA".color,
+                            fontWeight: FontWeight.bold),
+                      ),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

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

@@ -278,6 +278,10 @@ class MemberController extends BaseController implements PaymentStatusCallback {
     BrowserPage.start(WebUrl.userAgreement);
   }
 
+  void onRenewalAgreementClick() {
+    BrowserPage.start(WebUrl.renewalAgreement);
+  }
+
   void onPayWayItemClick(PayItemBean item) {
     _selectedPayWay.value = item;
   }
@@ -347,6 +351,7 @@ class MemberController extends BaseController implements PaymentStatusCallback {
         getPayWayType(payMethod: payMethod, payPlatform: payPlatform);
 
     LoadingDialog.show(StringName.payLoading);
+    memberRepository.setLastSelectedMember(buyGoods, buyPayWay);
     memberRepository
         .submitAndRequestPay(
             goodsId: goodsId, payPlatform: payPlatform, payMethod: payMethod)
@@ -615,6 +620,7 @@ class MemberController extends BaseController implements PaymentStatusCallback {
       String orderNo, PayItemBean paymentWay, GoodsBean storeItemBean) {
     ///购买成功消失
     LoadingDialog.hide();
+    memberRepository.clearLastSelectedMember();
 
     ///购买成功之后弹出
     afterTheFirstPurchasePromptSharingBoxPops();

+ 33 - 17
lib/module/member/member_page.dart

@@ -9,6 +9,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_page.dart';
+import 'package:location/module/member/activity/member_activity_page.dart';
 import 'package:location/module/member/member_controller.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
@@ -48,7 +49,11 @@ class MemberPage extends BasePage<MemberController> {
 
   static void start(
       {MemberPageType? enterTyp = MemberPageType.universalAccessEnter}) {
-    Get.toNamed(RoutePath.member, arguments: enterTyp);
+    if (enterTyp == MemberPageType.activity) {
+      MemberActivityPage.start();
+    } else {
+      Get.toNamed(RoutePath.member, arguments: enterTyp);
+    }
   }
 
   @override
@@ -244,15 +249,15 @@ class MemberPage extends BasePage<MemberController> {
                                     : "#323133".color,
                                 fontWeight: FontWeight.bold),
                             children: [
-                              TextSpan(
-                                  text: '¥',
+                          TextSpan(
+                              text: '¥',
                               style: TextStyle(fontSize: 16.sp, height: 1)),
                           TextSpan(
-                                  text: goodsInfo.amount.divideBy100(),
-                                  style: TextStyle(
-                                    fontSize: 28.sp,
-                                    height: 1,
-                                    //fontFamily: FontFamily.oppoSans
+                              text: goodsInfo.amount.divideBy100(),
+                              style: TextStyle(
+                                fontSize: 28.sp,
+                                height: 1,
+                                //fontFamily: FontFamily.oppoSans
                               ))
                         ])),
                     Text('¥${goodsInfo.originalAmount.divideBy100()}',
@@ -539,13 +544,13 @@ class MemberPage extends BasePage<MemberController> {
         controller.isLogin
             ? ClipOval(
                 child: Container(
-            width: 32.w,
-            height: 32.w,
-            child: CachedNetworkImage(
-              imageUrl: controller.memberStatusInfo?.avatar ?? "",
-              fit: BoxFit.cover,
-            ),
-          ),
+                  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),
@@ -610,7 +615,7 @@ class MemberPage extends BasePage<MemberController> {
       if (!controller.isLogin) {
         if ((controller.memberStatusInfo?.endTimestamp ?? 0) > 0) {
           desc =
-          '${DateUtil.fromMillisecondsSinceEpoch('yyyy-MM-dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
+              '${DateUtil.fromMillisecondsSinceEpoch('yyyy-MM-dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
         } else {
           desc = StringName.memberCardNoLoginDesc;
         }
@@ -638,7 +643,7 @@ class MemberPage extends BasePage<MemberController> {
         SizedBox(
           height: MediaQuery.of(Get.context!).padding.top,
         ),
-        Container(
+        SizedBox(
           width: double.infinity,
           height: 56.w,
           child: Row(
@@ -754,6 +759,17 @@ class MemberPage extends BasePage<MemberController> {
                 style: TextStyle(
                     color: ColorName.black60,
                     decoration: TextDecoration.underline)),
+            if (Platform.isIOS) TextSpan(text: '&'),
+            if (Platform.isIOS)
+              TextSpan(
+                  recognizer: TapGestureRecognizer()
+                    ..onTap = () {
+                      controller.onRenewalAgreementClick();
+                    },
+                  text: '续费协议',
+                  style: TextStyle(
+                      color: ColorName.black60,
+                      decoration: TextDecoration.underline)),
           ])),
     );
   }

+ 1 - 1
lib/module/mine/mine_controller.dart

@@ -122,7 +122,7 @@ class MineController extends BaseController {
   void onMineDescClick() {
     if (isLogin) {
       if (accountRepository.memberIsExpired()) {
-        MemberPage.start(enterTyp: MemberPageType.activity);
+        MemberPage.start();
       }
       return;
     }

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

@@ -71,6 +71,18 @@ class $AssetsImagesGen {
   AssetGenImage get bgLoginHeadContainer =>
       const AssetGenImage('assets/images/bg_login_head_container.webp');
 
+  /// File path: assets/images/bg_member_activity.webp
+  AssetGenImage get bgMemberActivity =>
+      const AssetGenImage('assets/images/bg_member_activity.webp');
+
+  /// File path: assets/images/bg_member_activity_container.webp
+  AssetGenImage get bgMemberActivityContainer =>
+      const AssetGenImage('assets/images/bg_member_activity_container.webp');
+
+  /// File path: assets/images/bg_member_activity_main.webp
+  AssetGenImage get bgMemberActivityMain =>
+      const AssetGenImage('assets/images/bg_member_activity_main.webp');
+
   /// File path: assets/images/bg_member_header.webp
   AssetGenImage get bgMemberHeader =>
       const AssetGenImage('assets/images/bg_member_header.webp');
@@ -323,6 +335,18 @@ class $AssetsImagesGen {
   AssetGenImage get iconMainTrackArrow =>
       const AssetGenImage('assets/images/icon_main_track_arrow.webp');
 
+  /// File path: assets/images/icon_member_activity_close.webp
+  AssetGenImage get iconMemberActivityClose =>
+      const AssetGenImage('assets/images/icon_member_activity_close.webp');
+
+  /// File path: assets/images/icon_member_activity_countdown.webp
+  AssetGenImage get iconMemberActivityCountdown =>
+      const AssetGenImage('assets/images/icon_member_activity_countdown.webp');
+
+  /// File path: assets/images/icon_member_activity_coupon.webp
+  AssetGenImage get iconMemberActivityCoupon =>
+      const AssetGenImage('assets/images/icon_member_activity_coupon.webp');
+
   /// File path: assets/images/icon_member_avatar.webp
   AssetGenImage get iconMemberAvatar =>
       const AssetGenImage('assets/images/icon_member_avatar.webp');
@@ -663,6 +687,30 @@ class $AssetsImagesGen {
   AssetGenImage get imgDialogLocationAlwaysTip3 => const AssetGenImage(
       'assets/images/img_dialog_location_always_tip_3.webp');
 
+  /// File path: assets/images/img_member_activity_banner_1.webp
+  AssetGenImage get imgMemberActivityBanner1 =>
+      const AssetGenImage('assets/images/img_member_activity_banner_1.webp');
+
+  /// File path: assets/images/img_member_activity_banner_2.webp
+  AssetGenImage get imgMemberActivityBanner2 =>
+      const AssetGenImage('assets/images/img_member_activity_banner_2.webp');
+
+  /// File path: assets/images/img_member_activity_banner_3.webp
+  AssetGenImage get imgMemberActivityBanner3 =>
+      const AssetGenImage('assets/images/img_member_activity_banner_3.webp');
+
+  /// File path: assets/images/img_member_activity_banner_4.webp
+  AssetGenImage get imgMemberActivityBanner4 =>
+      const AssetGenImage('assets/images/img_member_activity_banner_4.webp');
+
+  /// File path: assets/images/img_member_activity_favourable_txt.webp
+  AssetGenImage get imgMemberActivityFavourableTxt => const AssetGenImage(
+      'assets/images/img_member_activity_favourable_txt.webp');
+
+  /// File path: assets/images/img_member_btn_shadow.webp
+  AssetGenImage get imgMemberBtnShadow =>
+      const AssetGenImage('assets/images/img_member_btn_shadow.webp');
+
   /// File path: assets/images/img_member_first_week_discount_container.webp
   AssetGenImage get imgMemberFirstWeekDiscountContainer => const AssetGenImage(
       'assets/images/img_member_first_week_discount_container.webp');
@@ -721,6 +769,9 @@ class $AssetsImagesGen {
         bgLocationAnalyse,
         bgLocationAnalyseAi,
         bgLoginHeadContainer,
+        bgMemberActivity,
+        bgMemberActivityContainer,
+        bgMemberActivityMain,
         bgMemberHeader,
         bgMineMemberCard,
         bgPageBackground,
@@ -784,6 +835,9 @@ class $AssetsImagesGen {
         iconMainRefreshFriendLocation,
         iconMainRefreshMineLocation,
         iconMainTrackArrow,
+        iconMemberActivityClose,
+        iconMemberActivityCountdown,
+        iconMemberActivityCoupon,
         iconMemberAvatar,
         iconMemberCommentVerySatisfied,
         iconMemberContactClickHelp,
@@ -869,6 +923,12 @@ class $AssetsImagesGen {
         imgDialogLocationAlwaysTip1,
         imgDialogLocationAlwaysTip2,
         imgDialogLocationAlwaysTip3,
+        imgMemberActivityBanner1,
+        imgMemberActivityBanner2,
+        imgMemberActivityBanner3,
+        imgMemberActivityBanner4,
+        imgMemberActivityFavourableTxt,
+        imgMemberBtnShadow,
         imgMemberFirstWeekDiscountContainer,
         imgMemberHeaderAd1,
         imgMemberHeaderAd2,

+ 16 - 0
lib/resource/string.gen.dart

@@ -320,6 +320,16 @@ class StringName {
   static String get trackStayLongestPlace => 'track_stay_longest_place'.tr; // 停留最长地点
   static String get trackStayShareLogoDesc => 'track_stay_share_logo_desc'.tr; // 为你重要的朋友保驾护航
   static String get trackStayShareAnalysis => 'track_stay_share_analysis'.tr; // loca分析中,请稍等..
+  static String get memberActivityTitle => 'member_activity_title'.tr; // 超值优惠
+  static String get memberActivityNoPayway =>
+      'member_activity_no_payway'.tr; // 暂无支付方式
+  static String get memberActivityFunTitle =>
+      'member_activity_fun_title'.tr; // 功能介绍
+  static String get memberActivityCountdown =>
+      'member_activity_countdown'.tr; // 优惠活动倒计时
+  static String get memberActivitySpeciallyPreferential =>
+      'member_activity_specially_preferential'.tr; // 限时特惠
+  static String get memberActivityToBuy => 'member_activity_to_buy'.tr; // 去使用
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -643,6 +653,12 @@ class StringMultiSource {
       'track_stay_longest_place': '停留最长地点',
       'track_stay_share_logo_desc': '为你重要的朋友保驾护航',
       'track_stay_share_analysis': 'loca分析中,请稍等..',
+      'member_activity_title': '超值优惠',
+      'member_activity_no_payway': '暂无支付方式',
+      'member_activity_fun_title': '功能介绍',
+      'member_activity_countdown': '优惠活动倒计时',
+      'member_activity_specially_preferential': '限时特惠',
+      'member_activity_to_buy': '去使用',
     },
   };
 }

+ 5 - 0
lib/router/app_pages.dart

@@ -13,6 +13,7 @@ import 'package:location/module/friend/setting/friend_setting_controller.dart';
 import 'package:location/module/friend/setting/friend_setting_page.dart';
 import 'package:location/module/login/login_controller.dart';
 import 'package:location/module/main/main_page.dart';
+import 'package:location/module/member/activity/member_activity_controller.dart';
 import 'package:location/module/member/member_controller.dart';
 import 'package:location/module/member/member_page.dart';
 import 'package:location/module/mine/mine_page.dart';
@@ -30,6 +31,7 @@ import '../module/add_friend/add_friend_dialog_controller.dart';
 import '../module/analyse/location_analyse_controller.dart';
 import '../module/login/login_page.dart';
 import '../module/main/main_controller.dart';
+import '../module/member/activity/member_activity_page.dart';
 import '../module/mine/mine_controller.dart';
 import '../module/news/news_controller.dart';
 import '../module/news/news_report/news_report_controller.dart';
@@ -52,6 +54,7 @@ abstract class RoutePath {
   static const friend = '/friend';
   static const friendSetting = '/friendSetting';
   static const member = '/member';
+  static const memberActivity = '/memberActivity';
   static const track = '/track';
   static const trackDetail = '/trackDetail';
   static const news = '/news';
@@ -86,6 +89,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<PermissionSettingController>());
     lazyPut(() => getIt.get<TrackDetailController>());
     lazyPut(() => getIt.get<LocationAnalyseController>());
+    lazyPut(() => getIt.get<MemberActivityController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -113,4 +117,5 @@ final generalPages = [
   GetPage(
       name: RoutePath.permissionSetting, page: () => PermissionSettingPage()),
   GetPage(name: RoutePath.locationAnalyse, page: () => LocationAnalysePage()),
+  GetPage(name: RoutePath.memberActivity, page: () => MemberActivityPage()),
 ];

+ 5 - 0
lib/utils/payment_status_manager.dart

@@ -6,6 +6,7 @@ import 'package:location/data/repositories/member_repository.dart';
 import 'package:synchronized/synchronized.dart';
 
 import '../data/api/response/order_first_check_response.dart';
+import '../di/get_it.dart';
 import '../handler/event_handler.dart';
 import 'async_util.dart';
 
@@ -31,6 +32,10 @@ class PaymentStatusManager {
 
   PaymentStatusManager(this.memberRepository);
 
+  static PaymentStatusManager getInstance() {
+    return getIt.get<PaymentStatusManager>();
+  }
+
   final Map<String, PaymentStatusCallback> callbackMap = {};
   final Map<String, CancelableFuture> pollingSubscriptionMap = {};
   final _lock = Lock();

+ 100 - 0
lib/widget/activity_countdown_txt_view.dart

@@ -0,0 +1,100 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+
+class ActivityCountdownTextView extends StatefulWidget {
+  final Duration duration;
+  final TextStyle? textStyle;
+  final BoxDecoration? timeBgBoxDecoration;
+  final double? timeItemWidth;
+  final double? timeItemHeight;
+  final BoxFit bgFit;
+  final EdgeInsetsGeometry contentPadding;
+  final Widget? separator;
+
+  const ActivityCountdownTextView({
+    super.key,
+    required this.duration,
+    this.timeBgBoxDecoration,
+    this.textStyle,
+    this.timeItemWidth,
+    this.timeItemHeight,
+    this.bgFit = BoxFit.contain,
+    this.contentPadding = const EdgeInsets.all(4),
+    this.separator,
+  });
+
+  @override
+  State<ActivityCountdownTextView> createState() =>
+      _ActivityCountdownTxtViewState();
+}
+
+class _ActivityCountdownTxtViewState extends State<ActivityCountdownTextView> {
+  late Duration _remaining;
+
+  @override
+  void didUpdateWidget(covariant ActivityCountdownTextView oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.duration != widget.duration) {
+      setState(() {
+        _remaining = widget.duration;
+      });
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _remaining = widget.duration;
+  }
+
+  String _twoDigits(int n) => n.toString().padLeft(2, '0');
+
+  @override
+  Widget build(BuildContext context) {
+    final hours = _twoDigits(_remaining.inHours);
+    final minutes = _twoDigits(_remaining.inMinutes.remainder(60));
+    final seconds = _twoDigits(_remaining.inSeconds.remainder(60));
+
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        _buildTimeBox(hours),
+        _buildSeparator(),
+        _buildTimeBox(minutes),
+        _buildSeparator(),
+        _buildTimeBox(seconds),
+      ],
+    );
+  }
+
+  /// 自定义时间数字块,2位合并显示
+  Widget _buildTimeBox(String text) {
+    return Container(
+      width: widget.timeItemWidth,
+      height: widget.timeItemHeight,
+      alignment: Alignment.center,
+      padding: widget.contentPadding,
+      decoration: widget.timeBgBoxDecoration,
+      child: Text(
+        text,
+        maxLines: 1,
+        style: widget.textStyle ??
+            const TextStyle(
+                fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white),
+      ),
+    );
+  }
+
+  /// 自定义分隔符(冒号),如果未设置就给默认间距
+  Widget _buildSeparator() {
+    return widget.separator ??
+        const SizedBox(
+          width: 4,
+        );
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+  }
+}

+ 110 - 0
lib/widget/activity_countdown_view.dart

@@ -0,0 +1,110 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+
+class ActivityCountdownView extends StatefulWidget {
+  final Duration duration;
+  final TextStyle? textStyle;
+  final BoxDecoration? timeBgBoxDecoration;
+  final double? timeItemWidth;
+  final double? timeItemHeight;
+  final BoxFit bgFit;
+  final EdgeInsetsGeometry contentPadding;
+  final Widget? separator;
+  final VoidCallback? onFinish;
+
+  const ActivityCountdownView({
+    super.key,
+    required this.duration,
+    this.timeBgBoxDecoration,
+    this.textStyle,
+    this.timeItemWidth,
+    this.timeItemHeight,
+    this.bgFit = BoxFit.contain,
+    this.contentPadding = const EdgeInsets.all(4),
+    this.separator,
+    this.onFinish,
+  });
+
+  @override
+  State<ActivityCountdownView> createState() => _ActivityCountdownViewState();
+}
+
+class _ActivityCountdownViewState extends State<ActivityCountdownView> {
+  late Duration _remaining;
+  Timer? _timer;
+
+  @override
+  void initState() {
+    super.initState();
+    _remaining = widget.duration;
+    _startTimer();
+  }
+
+  void _startTimer() {
+    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
+      if (_remaining.inSeconds <= 1) {
+        _timer?.cancel();
+        widget.onFinish?.call();
+        setState(() {
+          _remaining = const Duration(seconds: 0);
+        });
+      } else {
+        setState(() {
+          _remaining -= const Duration(seconds: 1);
+        });
+      }
+    });
+  }
+
+  String _twoDigits(int n) => n.toString().padLeft(2, '0');
+
+  @override
+  Widget build(BuildContext context) {
+    final hours = _twoDigits(_remaining.inHours);
+    final minutes = _twoDigits(_remaining.inMinutes.remainder(60));
+    final seconds = _twoDigits(_remaining.inSeconds.remainder(60));
+
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        _buildTimeBox(hours),
+        _buildSeparator(),
+        _buildTimeBox(minutes),
+        _buildSeparator(),
+        _buildTimeBox(seconds),
+      ],
+    );
+  }
+
+  /// 自定义时间数字块,2位合并显示
+  Widget _buildTimeBox(String text) {
+    return Container(
+      width: widget.timeItemWidth,
+      height: widget.timeItemHeight,
+      alignment: Alignment.center,
+      padding: widget.contentPadding,
+      decoration: widget.timeBgBoxDecoration,
+      child: Text(
+        text,
+        maxLines: 1,
+        style: widget.textStyle ??
+            const TextStyle(
+                fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white),
+      ),
+    );
+  }
+
+  /// 自定义分隔符(冒号),如果未设置就给默认间距
+  Widget _buildSeparator() {
+    return widget.separator ??
+        const SizedBox(
+          width: 4,
+        );
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    super.dispose();
+  }
+}

+ 92 - 0
lib/widget/shimmer_effect.dart

@@ -0,0 +1,92 @@
+import 'package:flutter/material.dart';
+
+class ShimmerEffect extends StatefulWidget {
+  final Widget child;
+  final Duration duration; // 光效滑动的时间
+  final Duration delay; // 每轮动画结束后的延迟
+  final Alignment begin; // 滑动起点
+  final Alignment end; // 滑动终点
+  final ImageProvider image; // 外部传入的光效图片
+  final double shimmerWidthFactor; // 光效图片相对于 child 的宽度
+
+  const ShimmerEffect({
+    super.key,
+    required this.child,
+    required this.image,
+    this.duration = const Duration(seconds: 2),
+    this.delay = const Duration(seconds: 1),
+    this.begin = const Alignment(-1, 0),
+    this.end = const Alignment(1, 0),
+    this.shimmerWidthFactor = 0.2,
+  });
+
+  @override
+  State<ShimmerEffect> createState() => _ShimmerEffectState();
+}
+
+class _ShimmerEffectState extends State<ShimmerEffect>
+    with SingleTickerProviderStateMixin {
+  late final AnimationController _controller;
+  late final Animation<double> _animation;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _controller = AnimationController(
+      vsync: this,
+      duration: widget.duration,
+    );
+
+    _animation = Tween<double>(begin: -1.0, end: 1.0).animate(_controller);
+
+    _startLoop();
+  }
+
+  void _startLoop() async {
+    while (mounted) {
+      await _controller.forward();
+      _controller.reset();
+      await Future.delayed(widget.delay);
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(builder: (context, constraints) {
+      final width = constraints.maxWidth;
+      final shimmerWidth = width * widget.shimmerWidthFactor;
+
+      return Stack(
+        children: [
+          widget.child,
+          AnimatedBuilder(
+            animation: _controller,
+            builder: (context, _) {
+              final dx = _animation.value * (width + shimmerWidth);
+
+              return Positioned(
+                left: dx,
+                top: 0,
+                width: shimmerWidth,
+                height: constraints.maxHeight,
+                child: IgnorePointer(
+                  child: Image(
+                    image: widget.image,
+                    fit: BoxFit.fill,
+                  ),
+                ),
+              );
+            },
+          )
+        ],
+      );
+    });
+  }
+}

+ 16 - 0
pubspec.lock

@@ -420,6 +420,22 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.0.0"
+  flip_board:
+    dependency: "direct main"
+    description:
+      name: flip_board
+      sha256: "5b3bb7cebc7daa3b950be773a910cca40ff1516972dfb7dd4c5c5d58c94b4416"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
+  flip_panel_plus:
+    dependency: "direct main"
+    description:
+      name: flip_panel_plus
+      sha256: f4f31b7c1ecd4bcb3c2cc297c908b44a620c29dd57c9a3b7d5c82a48d2ae6018
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0+3"
   flutter:
     dependency: "direct main"
     description: flutter