Browse Source

[new]会员页增加商品等信息显示

zk 8 tháng trước cách đây
mục cha
commit
5dc71c62ff
41 tập tin đã thay đổi với 1361 bổ sung45 xóa
  1. BIN
      assets/fonts/price.ttf
  2. BIN
      assets/images/bg_member_header.webp
  3. BIN
      assets/images/icon_evaluate_1.webp
  4. BIN
      assets/images/icon_evaluate_2.webp
  5. BIN
      assets/images/icon_evaluate_3.webp
  6. BIN
      assets/images/icon_evaluate_4.webp
  7. BIN
      assets/images/icon_evaluate_5.webp
  8. BIN
      assets/images/icon_member_avatar.webp
  9. BIN
      assets/images/icon_member_fun5.webp
  10. BIN
      assets/images/icon_member_fun_1.webp
  11. BIN
      assets/images/icon_member_fun_2.webp
  12. BIN
      assets/images/icon_member_fun_3.webp
  13. BIN
      assets/images/icon_member_fun_4.webp
  14. BIN
      assets/images/icon_member_fun_6.webp
  15. 17 0
      assets/string/base/string.xml
  16. 5 0
      lib/data/api/atmob_api.dart
  17. 38 0
      lib/data/api/atmob_api.g.dart
  18. 20 0
      lib/data/api/response/item_list_response.dart
  19. 23 0
      lib/data/api/response/item_list_response.g.dart
  20. 57 0
      lib/data/bean/goods_bean.dart
  21. 37 0
      lib/data/bean/goods_bean.g.dart
  22. 5 6
      lib/data/bean/member_status_info.dart
  23. 27 0
      lib/data/bean/pay_item_bean.dart
  24. 24 0
      lib/data/bean/pay_item_bean.g.dart
  25. 8 0
      lib/data/repositories/member_repository.dart
  26. 4 1
      lib/di/get_it.config.dart
  27. 205 1
      lib/module/member/member_controller.dart
  28. 12 0
      lib/module/member/member_evaluate_bean.dart
  29. 11 0
      lib/module/member/member_fun_bean.dart
  30. 487 2
      lib/module/member/member_page.dart
  31. 5 0
      lib/module/mine/mine_controller.dart
  32. 34 31
      lib/module/mine/mine_page.dart
  33. 0 1
      lib/module/permission/permission_setting_controller.dart
  34. 65 0
      lib/resource/assets.gen.dart
  35. 15 0
      lib/resource/fonts.gen.dart
  36. 30 0
      lib/resource/string.gen.dart
  37. 5 3
      lib/utils/async_util.dart
  38. 11 0
      lib/utils/project_expand.dart
  39. 123 0
      lib/widget/animated_switcher_widget.dart
  40. 87 0
      lib/widget/auto_scroll_list_view.dart
  41. 6 0
      pubspec.yaml

BIN
assets/fonts/price.ttf


BIN
assets/images/bg_member_header.webp


BIN
assets/images/icon_evaluate_1.webp


BIN
assets/images/icon_evaluate_2.webp


BIN
assets/images/icon_evaluate_3.webp


BIN
assets/images/icon_evaluate_4.webp


BIN
assets/images/icon_evaluate_5.webp


BIN
assets/images/icon_member_avatar.webp


BIN
assets/images/icon_member_fun5.webp


BIN
assets/images/icon_member_fun_1.webp


BIN
assets/images/icon_member_fun_2.webp


BIN
assets/images/icon_member_fun_3.webp


BIN
assets/images/icon_member_fun_4.webp


BIN
assets/images/icon_member_fun_6.webp


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

@@ -253,4 +253,21 @@
     <string name="permission_setting_success">设置成功</string>
     <string name="member_free_code_error_toast">每位用户只能领取一次试用</string>
     <string name="member_free_code_is_member">您已经是会员了</string>
+    <string name="member_equity_introduction">会员尊享以下特权</string>
+    <string name="member_fun_name_1">实时定位</string>
+    <string name="member_fun_name_1_desc">定位实时共享</string>
+    <string name="member_fun_name_2">情侣守护</string>
+    <string name="member_fun_name_2_desc">远程定位关心</string>
+    <string name="member_fun_name_3">定位打卡</string>
+    <string name="member_fun_name_3_desc">定位实时共享</string>
+    <string name="member_fun_name_4">打卡查岗</string>
+    <string name="member_fun_name_4_desc">真实时间水印</string>
+    <string name="member_fun_name_5">历史轨迹</string>
+    <string name="member_fun_name_5_desc">追踪轨迹回放</string>
+    <string name="member_fun_name_6">紧急求助</string>
+    <string name="member_fun_name_6_desc">一对一服务</string>
+    <string name="member_user_evaluate">用户评价</string>
+    <string name="member_tips">
+        本应用功能仅限于家庭成员和亲人朋友之间使用,根据相关法规和隐私协议规定,共享位置功能需要对方下载得到好友授权同意才能正常使用。
+    </string>
 </resources>

+ 5 - 0
lib/data/api/atmob_api.dart

@@ -16,6 +16,7 @@ import 'package:location/data/api/response/configs_response.dart';
 import 'package:location/data/api/response/contact_list_response.dart';
 import 'package:location/data/api/response/contact_may_day_all_response.dart';
 import 'package:location/data/api/response/friends_list_response.dart';
+import 'package:location/data/api/response/item_list_response.dart';
 import 'package:location/data/api/response/login_response.dart';
 import 'package:location/data/api/response/member_status_response.dart';
 import 'package:location/data/api/response/member_trial_response.dart';
@@ -139,4 +140,8 @@ abstract class AtmobApi {
   @POST("/s/v1/member/trial")
   Future<BaseResponse<MemberTrialResponse>> memberTrial(
       @Body() AppBaseRequest request);
+
+  @POST("/s/v1/item/list")
+  Future<BaseResponse<ItemListResponse>> getMemberList(
+      @Body() AppBaseRequest request);
 }

+ 38 - 0
lib/data/api/atmob_api.g.dart

@@ -1092,6 +1092,44 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<ItemListResponse>> getMemberList(
+      AppBaseRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<ItemListResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/item/list',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<ItemListResponse> _value;
+    try {
+      _value = BaseResponse<ItemListResponse>.fromJson(
+        _result.data!,
+        (json) => ItemListResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
     if (T != dynamic &&
         !(requestOptions.responseType == ResponseType.bytes ||

+ 20 - 0
lib/data/api/response/item_list_response.dart

@@ -0,0 +1,20 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/data/bean/goods_bean.dart';
+
+import '../../bean/pay_item_bean.dart';
+
+part 'item_list_response.g.dart';
+
+@JsonSerializable()
+class ItemListResponse {
+  @JsonKey(name: "list")
+  List<GoodsBean>? goodsList;
+
+  @JsonKey(name: "payOptions")
+  List<PayItemBean>? payInfoList;
+
+  ItemListResponse(this.goodsList, this.payInfoList);
+
+  factory ItemListResponse.fromJson(Map<String, dynamic> json) =>
+      _$ItemListResponseFromJson(json);
+}

+ 23 - 0
lib/data/api/response/item_list_response.g.dart

@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'item_list_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ItemListResponse _$ItemListResponseFromJson(Map<String, dynamic> json) =>
+    ItemListResponse(
+      (json['list'] as List<dynamic>?)
+          ?.map((e) => GoodsBean.fromJson(e as Map<String, dynamic>))
+          .toList(),
+      (json['payOptions'] as List<dynamic>?)
+          ?.map((e) => PayItemBean.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$ItemListResponseToJson(ItemListResponse instance) =>
+    <String, dynamic>{
+      'list': instance.goodsList,
+      'payOptions': instance.payInfoList,
+    };

+ 57 - 0
lib/data/bean/goods_bean.dart

@@ -0,0 +1,57 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'goods_bean.g.dart';
+
+@JsonSerializable()
+class GoodsBean {
+  @JsonKey(name: 'id')
+  int id;
+
+  @JsonKey(name: 'sort')
+  int? sort;
+
+  @JsonKey(name: 'name')
+  String name;
+
+  @JsonKey(name: 'level')
+  int level;
+
+  @JsonKey(name: 'description')
+  String? description;
+
+  @JsonKey(name: 'originalAmount')
+  int originalAmount;
+
+  @JsonKey(name: 'amount')
+  int amount;
+
+  @JsonKey(name: 'subscriptionMillis')
+  int subscriptionMillis;
+
+  @JsonKey(name: 'popular')
+  bool popular;
+
+  @JsonKey(name: 'newcomer')
+  bool newcomer;
+
+  @JsonKey(name: 'tag')
+  String? tag;
+
+  @JsonKey(name: 'appleGoodsId')
+  String? appleGoodsId;
+
+  GoodsBean(
+      this.id,
+      this.name,
+      this.level,
+      this.originalAmount,
+      this.amount,
+      this.subscriptionMillis,
+      this.popular,
+      this.newcomer,
+      this.tag,
+      this.appleGoodsId);
+
+  factory GoodsBean.fromJson(Map<String, dynamic> json) =>
+      _$GoodsBeanFromJson(json);
+}

+ 37 - 0
lib/data/bean/goods_bean.g.dart

@@ -0,0 +1,37 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'goods_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+GoodsBean _$GoodsBeanFromJson(Map<String, dynamic> json) => GoodsBean(
+      (json['id'] as num).toInt(),
+      json['name'] as String,
+      (json['level'] as num).toInt(),
+      (json['originalAmount'] as num).toInt(),
+      (json['amount'] as num).toInt(),
+      (json['subscriptionMillis'] as num).toInt(),
+      json['popular'] as bool,
+      json['newcomer'] as bool,
+      json['tag'] as String?,
+      json['appleGoodsId'] as String?,
+    )
+      ..sort = (json['sort'] as num?)?.toInt()
+      ..description = json['description'] as String?;
+
+Map<String, dynamic> _$GoodsBeanToJson(GoodsBean instance) => <String, dynamic>{
+      'id': instance.id,
+      'sort': instance.sort,
+      'name': instance.name,
+      'level': instance.level,
+      'description': instance.description,
+      'originalAmount': instance.originalAmount,
+      'amount': instance.amount,
+      'subscriptionMillis': instance.subscriptionMillis,
+      'popular': instance.popular,
+      'newcomer': instance.newcomer,
+      'tag': instance.tag,
+      'appleGoodsId': instance.appleGoodsId,
+    };

+ 5 - 6
lib/data/bean/member_status_info.dart

@@ -15,16 +15,15 @@ class MemberStatusInfo {
   });
 
   /// 获取会员等级描述
-  String getLevelDesc() {
-    if (expired) {
-      return '未开通会员';
+  static String getLevelDesc(MemberStatusInfo? info) {
+    if (info == null || info.expired == true) {
+      return '未开通';
     }
-    if (level > 0 && level < 100) {
+    if (info.level > 0 && info.level < 100) {
       return '试用会员';
     }
-
     // 根据等级返回描述
-    switch (level) {
+    switch (info.level) {
       case 0:
         return '未开通';
       case 100:

+ 27 - 0
lib/data/bean/pay_item_bean.dart

@@ -0,0 +1,27 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'pay_item_bean.g.dart';
+
+@JsonSerializable()
+class PayItemBean {
+  @JsonKey(name: 'id')
+  int id;
+
+  @JsonKey(name: 'payMethod')
+  int payMethod;
+
+  @JsonKey(name: 'payPlatform')
+  int payPlatform;
+
+  @JsonKey(name: 'title')
+  String title;
+
+  @JsonKey(name: 'isDefaultCheck')
+  bool? isDefaultCheck;
+
+  PayItemBean(this.id, this.payMethod, this.payPlatform, this.title,
+      this.isDefaultCheck);
+
+  factory PayItemBean.fromJson(Map<String, dynamic> json) =>
+      _$PayItemBeanFromJson(json);
+}

+ 24 - 0
lib/data/bean/pay_item_bean.g.dart

@@ -0,0 +1,24 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'pay_item_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PayItemBean _$PayItemBeanFromJson(Map<String, dynamic> json) => PayItemBean(
+      (json['id'] as num).toInt(),
+      (json['payMethod'] as num).toInt(),
+      (json['payPlatform'] as num).toInt(),
+      json['title'] as String,
+      json['isDefaultCheck'] as bool?,
+    );
+
+Map<String, dynamic> _$PayItemBeanToJson(PayItemBean instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'payMethod': instance.payMethod,
+      'payPlatform': instance.payPlatform,
+      'title': instance.title,
+      'isDefaultCheck': instance.isDefaultCheck,
+    };

+ 8 - 0
lib/data/repositories/member_repository.dart

@@ -3,6 +3,8 @@ import 'package:location/base/app_base_request.dart';
 import 'package:location/data/api/atmob_api.dart';
 import 'package:location/utils/http_handler.dart';
 
+import '../api/response/item_list_response.dart';
+
 @lazySingleton
 class MemberRepository {
   final AtmobApi atmobApi;
@@ -15,4 +17,10 @@ class MemberRepository {
         .then(HttpHandler.handle(false))
         .then((response) => response.freeMemberMinutes);
   }
+
+  Future<ItemListResponse> getMemberList() {
+    return atmobApi
+        .getMemberList(AppBaseRequest())
+        .then(HttpHandler.handle(true));
+  }
 }

+ 4 - 1
lib/di/get_it.config.dart

@@ -56,7 +56,6 @@ extension GetItInjectableX on _i174.GetIt {
     final networkModule = _$NetworkModule();
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
     gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
-    gh.factory<_i269.MemberController>(() => _i269.MemberController());
     gh.factory<_i973.SplashController>(() => _i973.SplashController());
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
     gh.factory<_i108.PermissionSettingController>(
@@ -82,6 +81,10 @@ extension GetItInjectableX on _i174.GetIt {
         () => _i814.MemberRepository(gh<_i243.AtmobApi>()));
     gh.factory<_i1008.LoginController>(
         () => _i1008.LoginController(gh<_i20.AccountRepository>()));
+    gh.factory<_i269.MemberController>(() => _i269.MemberController(
+          gh<_i20.AccountRepository>(),
+          gh<_i814.MemberRepository>(),
+        ));
     gh.factory<_i489.NewsController>(
         () => _i489.NewsController(gh<_i791.MessageRepository>()));
     gh.lazySingleton<_i825.ConfigRepository>(() => _i825.ConfigRepository(

+ 205 - 1
lib/module/member/member_controller.dart

@@ -1,5 +1,209 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.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/pay_item_bean.dart';
+import 'package:location/data/repositories/account_repository.dart';
+import 'package:location/data/repositories/member_repository.dart';
+import 'package:location/handler/error_handler.dart';
+import 'package:location/module/login/login_page.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/utils/async_util.dart';
+import 'package:location/utils/common_expand.dart';
+import '../../data/bean/member_status_info.dart';
+import '../../data/consts/web_url.dart';
+import '../../resource/string.gen.dart';
+import '../../widget/animated_switcher_widget.dart';
+import '../browser/browser_view.dart';
+import 'member_evaluate_bean.dart';
+import 'member_fun_bean.dart';
 
 @injectable
-class MemberController extends BaseController {}
+class MemberController extends BaseController {
+  final AccountRepository accountRepository;
+  final MemberRepository memberRepository;
+
+  final switcherController = SwitcherController();
+
+  final ScrollController scrollController = ScrollController();
+
+  StreamController? _changeStreamController;
+  final random = Random();
+
+  final RxDouble _toolBarOpacity = 0.0.obs;
+
+  double get toolBarOpacity => _toolBarOpacity.value;
+
+  final List<String> _storeTypes = ['终身会员', '年度会员', '月度会员'];
+
+  String? get phone => accountRepository.loginPhoneNum.value;
+
+  bool get isLogin => accountRepository.isLogin.value;
+
+  MemberStatusInfo? get memberStatusInfo =>
+      accountRepository.memberStatusInfo.value;
+
+  final RxList<GoodsBean> goodsList = <GoodsBean>[].obs;
+
+  final Rxn<GoodsBean> _selectedGoods = Rxn<GoodsBean>();
+
+  GoodsBean? get selectedGoods => _selectedGoods.value;
+
+  final RxList<PayItemBean> payItemList = <PayItemBean>[].obs;
+
+  CancelableFuture? _memberDataFuture;
+
+  final List<MemberFunBean> funList = [
+    MemberFunBean(1, Assets.images.iconMemberFun1.path,
+        StringName.memberFunName1, StringName.memberFunName1Desc),
+    MemberFunBean(2, Assets.images.iconMemberFun2.path,
+        StringName.memberFunName2, StringName.memberFunName2Desc),
+    MemberFunBean(3, Assets.images.iconMemberFun3.path,
+        StringName.memberFunName3, StringName.memberFunName3Desc),
+    // MemberFunBean(4, Assets.images.iconMemberFun4.path, StringName.memberFunName4, StringName.memberFunName4Desc), //该功能还未开发
+    MemberFunBean(5, Assets.images.iconMemberFun5.path,
+        StringName.memberFunName5, StringName.memberFunName5Desc),
+    MemberFunBean(6, Assets.images.iconMemberFun6.path,
+        StringName.memberFunName6, StringName.memberFunName6Desc),
+  ];
+
+  final List<MemberEvaluateBean> evaluateList = [
+    MemberEvaluateBean(1, Assets.images.iconEvaluate1.path, '用户189****7913',
+        "上班没时间,远程遛娃,非常方便很好用。"),
+    MemberEvaluateBean(2, Assets.images.iconEvaluate2.path, '用户177****2345',
+        "这个功能太棒了!尤其是夜间出行时,一键报警让我感觉特别安心。"),
+    MemberEvaluateBean(3, Assets.images.iconEvaluate3.path, '用户138****6789',
+        "强烈推荐!我和家人经常用这个功能来共享位置,尤其是旅游时,走散了也不怕。"),
+    MemberEvaluateBean(4, Assets.images.iconEvaluate4.path, '用户159****3456',
+        "实时定位非常精准,用来监控孩子的行踪特别方便,再也不用担心他们乱跑了。"),
+    MemberEvaluateBean(5, Assets.images.iconEvaluate5.path, '用户182****9012',
+        "用来遛狗也很方便,再也不用担心狗狗跑丢了,真是个好工具!"),
+  ];
+
+  MemberController(this.accountRepository, this.memberRepository);
+
+  @override
+  void onReady() async {
+    super.onReady();
+    _startAnimationSwitcher();
+    scrollController.addListener(() {
+      double offset = scrollController.offset;
+      double opacity = offset / 100;
+      if (opacity > 1) {
+        opacity = 1;
+      } else if (opacity < 0) {
+        opacity = 0;
+      }
+      _toolBarOpacity.value = opacity;
+    });
+    refreshMemberData();
+  }
+
+  void _startAnimationSwitcher() {
+    _changeStreamController = AsyncUtil.interval(
+        (time) => changeSwitcherContent(), Duration(seconds: 3), -1);
+  }
+
+  Future<void> changeSwitcherContent() async {
+    int userId = random.nextInt(10000);
+    String userIdStr = userId.toString();
+    int padLength = 4 - userIdStr.length;
+    for (int i = 0; i < padLength; i++) {
+      userIdStr = random.nextInt(10).toString() + userIdStr;
+    }
+    bool isHour = random.nextBool();
+    String secondsOrHour;
+    if (isHour) {
+      secondsOrHour = "${1 + random.nextInt(8)}小时";
+    } else {
+      secondsOrHour = "${1 + random.nextInt(59)}分钟";
+    }
+
+    int index = random.nextInt(_storeTypes.length);
+    switcherController.updateWidget(Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        RichText(
+            text: TextSpan(
+                style: TextStyle(fontSize: 12.sp, color: Colors.white),
+                children: [
+              TextSpan(text: userIdStr),
+              TextSpan(text: '用户 '),
+              TextSpan(text: secondsOrHour),
+              TextSpan(text: '前购买了'),
+              TextSpan(
+                  text: _storeTypes[index],
+                  style: TextStyle(color: '#FFC95D'.color)),
+            ]))
+      ],
+    ));
+  }
+
+  String getUserName(String phone) {
+    if (phone.length > 4) {
+      phone = phone.substring(phone.length - 4);
+    }
+    return '${StringName.mineAccountLoggedDesc}$phone';
+  }
+
+  void back() {
+    Get.back();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _changeStreamController?.close();
+    _memberDataFuture?.cancel();
+    scrollController.dispose();
+  }
+
+  void onLoginClick() {
+    if (accountRepository.isLogin.value) {
+      return;
+    }
+    LoginPage.start();
+  }
+
+  void refreshMemberData() {
+    _memberDataFuture?.cancel();
+    _memberDataFuture =
+        AsyncUtil.retryWithExponentialBackoff(() => _requestMemberData(), 4);
+    _memberDataFuture?.catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  Future<void> _requestMemberData() {
+    return memberRepository.getMemberList().then((response) {
+      goodsList.clear();
+      payItemList.clear();
+      _selectedGoods.value = null;
+      if (response.goodsList?.isNotEmpty == true) {
+        goodsList.addAll(response.goodsList!);
+        _selectedGoods.value = goodsList.first;
+      }
+      if (response.payInfoList?.isNotEmpty == true) {
+        payItemList.addAll(response.payInfoList!);
+      }
+    });
+  }
+
+  void onGoodsItemClick(GoodsBean item) {
+    _selectedGoods.value = item;
+  }
+
+  void onPrivacyPolicyClick() {
+    BrowserPage.start(WebUrl.privacyPolicy);
+  }
+
+  void onTermOfServiceClick() {
+    BrowserPage.start(WebUrl.userAgreement);
+  }
+}

+ 12 - 0
lib/module/member/member_evaluate_bean.dart

@@ -0,0 +1,12 @@
+class MemberEvaluateBean {
+  final int id;
+
+  final String avatarPath;
+
+  final String userName;
+
+  final String userEvaluate;
+
+  MemberEvaluateBean(
+      this.id, this.avatarPath, this.userName, this.userEvaluate);
+}

+ 11 - 0
lib/module/member/member_fun_bean.dart

@@ -0,0 +1,11 @@
+class MemberFunBean {
+  final int id;
+
+  final String iconPath;
+
+  final String funName;
+
+  final String funDesc;
+
+  MemberFunBean(this.id, this.iconPath, this.funName, this.funDesc);
+}

+ 487 - 2
lib/module/member/member_page.dart

@@ -1,11 +1,24 @@
 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/module/member/member_controller.dart';
-
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/utils/project_expand.dart';
+import 'package:location/widget/auto_scroll_list_view.dart';
+import '../../data/bean/member_status_info.dart';
+import '../../resource/fonts.gen.dart';
+import '../../resource/string.gen.dart';
 import '../../router/app_pages.dart';
+import '../../utils/date_util.dart';
+import '../../widget/animated_switcher_widget.dart';
+import 'member_evaluate_bean.dart';
 
 class MemberPage extends BasePage<MemberController> {
   const MemberPage({super.key});
@@ -15,7 +28,479 @@ class MemberPage extends BasePage<MemberController> {
   }
 
   @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  bool statusBarDarkFont() {
+    return false;
+  }
+
+  @override
   Widget buildBody(BuildContext context) {
-    return Text('这是会员页');
+    return Stack(
+      children: [
+        SingleChildScrollView(
+          controller: controller.scrollController,
+          child: Stack(
+            children: [
+              Assets.images.bgMemberHeader.image(width: double.infinity),
+              SafeArea(
+                child: Column(
+                  children: [
+                    SizedBox(height: 62.w),
+                    buildUserInfoView(),
+                    SizedBox(height: 26.w),
+                    Container(
+                      width: double.infinity,
+                      decoration: BoxDecoration(
+                          borderRadius: BorderRadius.only(
+                              topLeft: Radius.circular(14.w),
+                              topRight: Radius.circular(14.w)),
+                          gradient: LinearGradient(
+                              begin: Alignment.topCenter,
+                              end: Alignment.bottomCenter,
+                              stops: [0.0, 0.1],
+                              colors: ['#EFE9FF'.color, Colors.white])),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          SizedBox(height: 23.w),
+                          buildGoodsList(),
+                          SizedBox(height: 12.w),
+                          buildPrivacyPolicyView(),
+                          SizedBox(height: 30.w),
+                          Padding(
+                            padding: EdgeInsets.only(left: 12.w),
+                            child: Text(StringName.memberEquityIntroduction,
+                                style: TextStyle(
+                                    fontSize: 16.sp,
+                                    color: ColorName.black90,
+                                    fontWeight: FontWeight.bold)),
+                          ),
+                          SizedBox(height: 19.w),
+                          buildFunctionList(),
+                          SizedBox(height: 40.w),
+                          Padding(
+                            padding: EdgeInsets.only(left: 12.w),
+                            child: Text(StringName.memberUserEvaluate,
+                                style: TextStyle(
+                                    fontSize: 16.sp,
+                                    color: ColorName.black90,
+                                    fontWeight: FontWeight.bold)),
+                          ),
+                          SizedBox(height: 8.w),
+                          buildUserEvaluateList(),
+                          SizedBox(height: 20.w),
+                          Container(
+                            padding: EdgeInsets.all(12.w),
+                            decoration: BoxDecoration(
+                                color: '#F7F7F7'.color,
+                                borderRadius: BorderRadius.circular(6.w)),
+                            margin: EdgeInsets.symmetric(horizontal: 12.w),
+                            child: Text(StringName.memberTips,
+                                style: TextStyle(
+                                    fontSize: 12.sp, color: '#A7A7A7'.color)),
+                          ),
+                          SizedBox(height: 100.w)
+                        ],
+                      ),
+                    )
+                  ],
+                ),
+              )
+            ],
+          ),
+        ),
+        buildHeadBar(),
+        buildMemberBottomView()
+      ],
+    );
+  }
+
+  Widget buildGoodsList() {
+    return Obx(() {
+      return SizedBox(
+        height: 123.w,
+        child: ListView.builder(
+            padding: EdgeInsets.only(left: 12.w),
+            physics: const BouncingScrollPhysics(
+                parent: AlwaysScrollableScrollPhysics()),
+            scrollDirection: Axis.horizontal,
+            itemBuilder: (BuildContext ctx, int index) {
+              return Obx(() {
+                final item = controller.goodsList[index];
+                bool isSelected = controller.selectedGoods?.id == item.id;
+                return GestureDetector(
+                  behavior: HitTestBehavior.translucent,
+                  onTap: () => controller.onGoodsItemClick(item),
+                  child: Container(
+                      margin: EdgeInsets.only(right: 10.w),
+                      width: 138.w,
+                      height: 123.w,
+                      child: Stack(
+                        children: [
+                          Container(
+                            width: double.infinity,
+                            height: double.infinity,
+                            decoration: BoxDecoration(
+                                color: isSelected
+                                    ? '#6BC4BAFF'.color
+                                    : ColorName.white20,
+                                borderRadius: BorderRadius.circular(18.w),
+                                border: Border.all(
+                                    color: isSelected
+                                        ? '#C4BAFF'.color
+                                        : '#E8E8E8'.color,
+                                    width: 3.w)),
+                            padding: EdgeInsets.only(left: 10.w),
+                            child: Column(
+                              crossAxisAlignment: CrossAxisAlignment.start,
+                              children: [
+                                Spacer(flex: 2),
+                                Text(
+                                  item.name,
+                                  style: TextStyle(
+                                      fontSize: 14.sp,
+                                      color: ColorName.black90,
+                                      fontWeight: FontWeight.bold),
+                                ),
+                                SizedBox(height: 3.w),
+                                Text('¥${item.originalAmount.divideBy100()}',
+                                    style: TextStyle(
+                                        decoration: TextDecoration.lineThrough,
+                                        fontSize: 10.sp,
+                                        color: ColorName.black60)),
+                                Spacer(flex: 1),
+                                RichText(
+                                    text: TextSpan(
+                                        style: TextStyle(
+                                            color: isSelected
+                                                ? '#EA1231'.color
+                                                : ColorName.black80,
+                                            fontWeight: FontWeight.bold),
+                                        children: [
+                                      TextSpan(
+                                          text: '¥',
+                                          style: TextStyle(
+                                              fontSize: 20.sp, height: 1)),
+                                      TextSpan(
+                                          text: item.amount.divideBy100(),
+                                          style: TextStyle(
+                                              fontSize: 34.sp,
+                                              height: 1,
+                                              fontFamily: FontFamily.oppoSans))
+                                    ])),
+                                Padding(
+                                  padding: EdgeInsets.only(left: 7.w),
+                                  child: Text(item.description ?? '',
+                                      style: TextStyle(
+                                          fontSize: 12.sp,
+                                          color: ColorName.black40)),
+                                ),
+                                Spacer(
+                                  flex: 1,
+                                )
+                              ],
+                            ),
+                          ),
+                          Visibility(
+                              visible: item.tag?.isNotEmpty == true,
+                              child: Positioned(
+                                top: 0,
+                                right: 0,
+                                child: Container(
+                                  decoration: BoxDecoration(
+                                      gradient: LinearGradient(colors: [
+                                        '#A26CFF'.color,
+                                        '#FF7CD8'.color,
+                                        '#898BFF'.color
+                                      ]),
+                                      borderRadius: BorderRadius.only(
+                                          topRight: Radius.circular(11.w),
+                                          bottomLeft: Radius.circular(11.w))),
+                                  padding: EdgeInsets.symmetric(
+                                      horizontal: 10.w, vertical: 4.w),
+                                  child: Text(item.tag ?? '',
+                                      style: TextStyle(
+                                          fontSize: 12.sp,
+                                          color: Colors.white)),
+                                ),
+                              ))
+                        ],
+                      )),
+                );
+              });
+            },
+            itemCount: controller.goodsList.length),
+      );
+    });
+  }
+
+  Widget buildMemberBottomView() {
+    return Align(
+      alignment: Alignment.bottomCenter,
+      child: Container(
+        padding:
+            EdgeInsets.only(left: 12.w, right: 12.w, top: 13.w, bottom: 20.w),
+        decoration: BoxDecoration(
+          color: Colors.white,
+          boxShadow: [
+            BoxShadow(
+                color: Colors.black.withOpacity(0.05),
+                offset: Offset(0, -2),
+                blurRadius: 4)
+          ],
+          borderRadius: BorderRadius.only(
+              topLeft: Radius.circular(16.w), topRight: Radius.circular(16.w)),
+        ),
+        child: Row(
+          children: [
+            Text('¥',
+                style: TextStyle(
+                    fontSize: 14.sp,
+                    color: '#EA1231'.color,
+                    fontWeight: FontWeight.bold)),
+            Text(
+              '168',
+              style: TextStyle(
+                  fontSize: 24.sp,
+                  color: '#EA1231'.color,
+                  fontWeight: FontWeight.bold),
+            ),
+            Text(
+              ' / 永久会员',
+              style: TextStyle(fontSize: 12.sp, color: '#000000'.color),
+            ),
+            Spacer(),
+            Container(
+              decoration: BoxDecoration(
+                  color: ColorName.colorPrimary,
+                  borderRadius: BorderRadius.circular(100.w)),
+              width: 164.w,
+              height: 44.w,
+              child: Center(
+                child: Text(
+                  '立即解锁',
+                  style: TextStyle(
+                      fontSize: 15.sp,
+                      color: Colors.white,
+                      fontWeight: FontWeight.bold),
+                ),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildUserInfoView() {
+    return Row(
+      children: [
+        SizedBox(width: 20.w),
+        Assets.images.iconMemberAvatar.image(width: 40.w, height: 40.w),
+        SizedBox(width: 10.w),
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Obx(() {
+              return GestureDetector(
+                onTap: controller.onLoginClick,
+                child: Text(
+                    controller.phone?.isNotEmpty == true
+                        ? controller.getUserName(controller.phone!)
+                        : StringName.mineAccountGoLogin,
+                    style: TextStyle(
+                        fontSize: 16.sp,
+                        color: Colors.white,
+                        fontWeight: FontWeight.bold)),
+              );
+            }),
+            buildMemberCardVipDesc()
+          ],
+        ),
+        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)
+      ],
+    );
+  }
+
+  Widget buildMemberCardVipDesc() {
+    return Obx(() {
+      String desc = '';
+      if (!controller.isLogin) {
+        desc = StringName.memberCardNoLoginDesc;
+      } else if (controller.memberStatusInfo == null ||
+          controller.memberStatusInfo?.expired == true) {
+        desc = StringName.memberCardNoVipDesc;
+      } else if (controller.memberStatusInfo?.expired == false &&
+          controller.memberStatusInfo?.permanent == true) {
+        desc = StringName.memberCardPermanentVipDesc;
+      } else {
+        desc =
+            '${DateUtil.fromMillisecondsSinceEpoch('yyyy.MM.dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
+      }
+      return Text(desc,
+          style: TextStyle(fontSize: 12.sp, color: Colors.white60));
+    });
+  }
+
+  Widget buildHeadBar() {
+    return Obx(() {
+      return Container(
+        color: ColorName.colorPrimary.withOpacity(controller.toolBarOpacity),
+        child: SafeArea(
+          child: SizedBox(
+            width: double.infinity,
+            height: 56.w,
+            child: Stack(alignment: Alignment.center, children: [
+              Positioned(
+                  left: 12.w,
+                  child: GestureDetector(
+                    onTap: controller.back,
+                    child: Assets.images.iconWhiteBack
+                        .image(width: 24.w, height: 24.w),
+                  )),
+              Container(child: buildVerticalSlideshowWidget())
+            ]),
+          ),
+        ),
+      );
+    });
+  }
+
+  Widget buildVerticalSlideshowWidget() {
+    return Container(
+      width: 220.w,
+      height: 24.w,
+      decoration: BoxDecoration(
+        color: '#1F000000'.color,
+        borderRadius: BorderRadius.circular(100.w),
+      ),
+      child: Center(
+          child: AnimatedSwitcherWidget(
+              controller: controller.switcherController)),
+    );
+  }
+
+  Widget buildPrivacyPolicyView() {
+    return Padding(
+      padding: EdgeInsets.only(left: 12.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)),
+          ])),
+    );
+  }
+
+  Widget buildFunctionList() {
+    return SizedBox(
+        height: 80.w,
+        child: AutoScrollListView(
+            padding: EdgeInsets.only(left: 12.w),
+            itemBuilder: (ctx, index) {
+              final item = controller.funList[index];
+              return Padding(
+                padding: EdgeInsets.only(right: 20.w),
+                child: Column(
+                  children: [
+                    Image.asset(item.iconPath, width: 36.w, height: 36.w),
+                    Spacer(flex: 3),
+                    Text(item.funName,
+                        style: TextStyle(
+                            fontSize: 12.8.sp,
+                            color: ColorName.black90,
+                            fontWeight: FontWeight.bold)),
+                    Spacer(flex: 2),
+                    Text(item.funDesc,
+                        style: TextStyle(
+                          fontSize: 10.6.sp,
+                          color: ColorName.black50,
+                        )),
+                  ],
+                ),
+              );
+            },
+            itemCount: controller.funList.length));
+  }
+
+  Widget buildUserEvaluateList() {
+    return Column(
+      children: [
+        for (int index = 0; index < controller.evaluateList.length; index++)
+          buildUserEvaluateItem(controller.evaluateList[index],
+              index == controller.evaluateList.length - 1)
+      ],
+    );
+  }
+
+  Widget buildUserEvaluateItem(MemberEvaluateBean item, bool isLast) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        SizedBox(height: 20.w),
+        Row(
+          children: [
+            SizedBox(width: 12.w),
+            Image.asset(item.avatarPath, width: 24.w, height: 24.w),
+            SizedBox(width: 8.w),
+            Text(
+              item.userName,
+              style: TextStyle(fontSize: 14.sp, color: Colors.black),
+            )
+          ],
+        ),
+        SizedBox(height: 1.w),
+        Padding(
+            padding: EdgeInsets.only(left: 44.w, right: 12.w),
+            child: Text(
+              item.userEvaluate,
+              style: TextStyle(fontSize: 14.sp, color: '#BF000000'.color),
+            )),
+        SizedBox(height: 20.w),
+        Visibility(
+          child: Container(
+              margin: EdgeInsets.only(left: 26.w),
+              width: 288.w,
+              color: '#21000000'.color,
+              height: 1.w),
+        )
+      ],
+    );
   }
 }

+ 5 - 0
lib/module/mine/mine_controller.dart

@@ -8,6 +8,7 @@ import 'package:location/data/repositories/config_repository.dart';
 import 'package:location/handler/error_handler.dart';
 import 'package:location/module/feedback/feed_back_page.dart';
 import 'package:location/module/login/login_page.dart';
+import 'package:location/module/member/member_page.dart';
 import 'package:location/module/urgent_contact/urgent_contact_page.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/http_handler.dart';
@@ -120,4 +121,8 @@ class MineController extends BaseController {
       }
     });
   }
+
+  void onMemberCardClick() {
+    MemberPage.start();
+  }
 }

+ 34 - 31
lib/module/mine/mine_page.dart

@@ -241,36 +241,39 @@ class MinePage extends BasePage<MineController> {
   }
 
   Widget buildMemberCard() {
-    return AspectRatio(
-      aspectRatio: 332 / 75,
-      child: Container(
-        margin: EdgeInsets.symmetric(horizontal: 14.w),
-        decoration: BoxDecoration(
-            image: DecorationImage(
-                image: Assets.images.bgMineMemberCard.provider(),
-                fit: BoxFit.fill)),
-        child: Row(
-          children: [
-            SizedBox(width: 14.w),
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: [
-                Row(
-                  children: [
-                    Assets.images.iconMineUnlockVip.image(width: 68.w),
-                    SizedBox(width: 6.5.w),
-                    Assets.images.iconMineSmallVip
-                        .image(width: 21.6.w, height: 21.6.w),
-                  ],
-                ),
-                SizedBox(height: 6.w),
-                buildMemberCardVipDesc()
-              ],
-            ),
-            Spacer(),
-            buildBuyMemberCardBtn()
-          ],
+    return GestureDetector(
+      onTap: controller.onMemberCardClick,
+      child: AspectRatio(
+        aspectRatio: 332 / 75,
+        child: Container(
+          margin: EdgeInsets.symmetric(horizontal: 14.w),
+          decoration: BoxDecoration(
+              image: DecorationImage(
+                  image: Assets.images.bgMineMemberCard.provider(),
+                  fit: BoxFit.fill)),
+          child: Row(
+            children: [
+              SizedBox(width: 14.w),
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Row(
+                    children: [
+                      Assets.images.iconMineUnlockVip.image(width: 68.w),
+                      SizedBox(width: 6.5.w),
+                      Assets.images.iconMineSmallVip
+                          .image(width: 21.6.w, height: 21.6.w),
+                    ],
+                  ),
+                  SizedBox(height: 6.w),
+                  buildMemberCardVipDesc()
+                ],
+              ),
+              Spacer(),
+              buildBuyMemberCardBtn()
+            ],
+          ),
         ),
       ),
     );
@@ -289,7 +292,7 @@ class MinePage extends BasePage<MineController> {
         desc = StringName.memberCardPermanentVipDesc;
       } else {
         desc =
-            '${StringName.memberCardExpirationDesc} ${DateUtil.fromMillisecondsSinceEpoch('yyyy.MM.dd', controller.memberStatusInfo?.endTimestamp ?? 0)}';
+            '${DateUtil.fromMillisecondsSinceEpoch('yyyy.MM.dd', controller.memberStatusInfo?.endTimestamp ?? 0)} ${StringName.memberCardExpirationDesc}';
       }
       return Text(desc,
           style: TextStyle(fontSize: 12.sp, color: ColorName.white80));

+ 0 - 1
lib/module/permission/permission_setting_controller.dart

@@ -6,7 +6,6 @@ import 'package:location/base/base_controller.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/app_info_util.dart';
 import 'package:location/utils/toast_util.dart';
-import 'package:url_launcher/url_launcher.dart';
 
 import '../../utils/permission_util.dart';
 

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

@@ -32,6 +32,10 @@ class $AssetsImagesGen {
   AssetGenImage get bgLoginHeadContainer =>
       const AssetGenImage('assets/images/bg_login_head_container.webp');
 
+  /// File path: assets/images/bg_member_header.webp
+  AssetGenImage get bgMemberHeader =>
+      const AssetGenImage('assets/images/bg_member_header.webp');
+
   /// File path: assets/images/bg_mine_member_card.webp
   AssetGenImage get bgMineMemberCard =>
       const AssetGenImage('assets/images/bg_mine_member_card.webp');
@@ -92,6 +96,26 @@ class $AssetsImagesGen {
   AssetGenImage get iconDialogClose =>
       const AssetGenImage('assets/images/icon_dialog_close.webp');
 
+  /// File path: assets/images/icon_evaluate_1.webp
+  AssetGenImage get iconEvaluate1 =>
+      const AssetGenImage('assets/images/icon_evaluate_1.webp');
+
+  /// File path: assets/images/icon_evaluate_2.webp
+  AssetGenImage get iconEvaluate2 =>
+      const AssetGenImage('assets/images/icon_evaluate_2.webp');
+
+  /// File path: assets/images/icon_evaluate_3.webp
+  AssetGenImage get iconEvaluate3 =>
+      const AssetGenImage('assets/images/icon_evaluate_3.webp');
+
+  /// File path: assets/images/icon_evaluate_4.webp
+  AssetGenImage get iconEvaluate4 =>
+      const AssetGenImage('assets/images/icon_evaluate_4.webp');
+
+  /// File path: assets/images/icon_evaluate_5.webp
+  AssetGenImage get iconEvaluate5 =>
+      const AssetGenImage('assets/images/icon_evaluate_5.webp');
+
   /// File path: assets/images/icon_experiment.webp
   AssetGenImage get iconExperiment =>
       const AssetGenImage('assets/images/icon_experiment.webp');
@@ -172,6 +196,34 @@ class $AssetsImagesGen {
   AssetGenImage get iconMainRefreshMineLocation =>
       const AssetGenImage('assets/images/icon_main_refresh_mine_location.webp');
 
+  /// File path: assets/images/icon_member_avatar.webp
+  AssetGenImage get iconMemberAvatar =>
+      const AssetGenImage('assets/images/icon_member_avatar.webp');
+
+  /// File path: assets/images/icon_member_fun5.webp
+  AssetGenImage get iconMemberFun5 =>
+      const AssetGenImage('assets/images/icon_member_fun5.webp');
+
+  /// File path: assets/images/icon_member_fun_1.webp
+  AssetGenImage get iconMemberFun1 =>
+      const AssetGenImage('assets/images/icon_member_fun_1.webp');
+
+  /// File path: assets/images/icon_member_fun_2.webp
+  AssetGenImage get iconMemberFun2 =>
+      const AssetGenImage('assets/images/icon_member_fun_2.webp');
+
+  /// File path: assets/images/icon_member_fun_3.webp
+  AssetGenImage get iconMemberFun3 =>
+      const AssetGenImage('assets/images/icon_member_fun_3.webp');
+
+  /// File path: assets/images/icon_member_fun_4.webp
+  AssetGenImage get iconMemberFun4 =>
+      const AssetGenImage('assets/images/icon_member_fun_4.webp');
+
+  /// File path: assets/images/icon_member_fun_6.webp
+  AssetGenImage get iconMemberFun6 =>
+      const AssetGenImage('assets/images/icon_member_fun_6.webp');
+
   /// File path: assets/images/icon_member_vip_receive_arrow.webp
   AssetGenImage get iconMemberVipReceiveArrow =>
       const AssetGenImage('assets/images/icon_member_vip_receive_arrow.webp');
@@ -295,6 +347,7 @@ class $AssetsImagesGen {
         bgDialogLocationPermissionIos,
         bgFriendItem,
         bgLoginHeadContainer,
+        bgMemberHeader,
         bgMineMemberCard,
         bgPageBackground,
         bgTrackLocationTie,
@@ -310,6 +363,11 @@ class $AssetsImagesGen {
         iconDefaultMineAvatar,
         iconDialogAddFriend,
         iconDialogClose,
+        iconEvaluate1,
+        iconEvaluate2,
+        iconEvaluate3,
+        iconEvaluate4,
+        iconEvaluate5,
         iconExperiment,
         iconFriendEdit,
         iconFriendEditArrow,
@@ -330,6 +388,13 @@ class $AssetsImagesGen {
         iconMainNews,
         iconMainRefreshFriendLocation,
         iconMainRefreshMineLocation,
+        iconMemberAvatar,
+        iconMemberFun5,
+        iconMemberFun1,
+        iconMemberFun2,
+        iconMemberFun3,
+        iconMemberFun4,
+        iconMemberFun6,
         iconMemberVipReceiveArrow,
         iconMessageFriendHelp,
         iconMineFunAbout,

+ 15 - 0
lib/resource/fonts.gen.dart

@@ -0,0 +1,15 @@
+/// GENERATED CODE - DO NOT MODIFY BY HAND
+/// *****************************************************
+///  FlutterGen
+/// *****************************************************
+
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
+
+class FontFamily {
+  FontFamily._();
+
+  /// Font family: OppoSans
+  static const String oppoSans = 'OppoSans';
+}

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

@@ -205,6 +205,21 @@ class StringName {
   static final String permissionSettingSuccess = 'permission_setting_success'.tr; // 设置成功
   static final String memberFreeCodeErrorToast = 'member_free_code_error_toast'.tr; // 每位用户只能领取一次试用
   static final String memberFreeCodeIsmember = 'member_free_code_is_member'.tr; // 您已经是会员了
+  static final String memberEquityIntroduction = 'member_equity_introduction'.tr; // 会员尊享以下特权
+  static final String memberFunName1 = 'member_fun_name_1'.tr; // 实时定位
+  static final String memberFunName1Desc = 'member_fun_name_1_desc'.tr; // 定位实时共享
+  static final String memberFunName2 = 'member_fun_name_2'.tr; // 情侣守护
+  static final String memberFunName2Desc = 'member_fun_name_2_desc'.tr; // 远程定位关心
+  static final String memberFunName3 = 'member_fun_name_3'.tr; // 定位打卡
+  static final String memberFunName3Desc = 'member_fun_name_3_desc'.tr; // 定位实时共享
+  static final String memberFunName4 = 'member_fun_name_4'.tr; // 打卡查岗
+  static final String memberFunName4Desc = 'member_fun_name_4_desc'.tr; // 真实时间水印
+  static final String memberFunName5 = 'member_fun_name_5'.tr; // 历史轨迹
+  static final String memberFunName5Desc = 'member_fun_name_5_desc'.tr; // 追踪轨迹回放
+  static final String memberFunName6 = 'member_fun_name_6'.tr; // 紧急求助
+  static final String memberFunName6Desc = 'member_fun_name_6_desc'.tr; // 一对一服务
+  static final String memberUserEvaluate = 'member_user_evaluate'.tr; // 用户评价
+  static final String memberTips = 'member_tips'.tr; // 本应用功能仅限于家庭成员和亲人朋友之间使用,根据相关法规和隐私协议规定,共享位置功能需要对方下载得到好友授权同意才能正常使用。
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -413,6 +428,21 @@ class StringMultiSource {
       'permission_setting_success': '设置成功',
       'member_free_code_error_toast': '每位用户只能领取一次试用',
       'member_free_code_is_member': '您已经是会员了',
+      'member_equity_introduction': '会员尊享以下特权',
+      'member_fun_name_1': '实时定位',
+      'member_fun_name_1_desc': '定位实时共享',
+      'member_fun_name_2': '情侣守护',
+      'member_fun_name_2_desc': '远程定位关心',
+      'member_fun_name_3': '定位打卡',
+      'member_fun_name_3_desc': '定位实时共享',
+      'member_fun_name_4': '打卡查岗',
+      'member_fun_name_4_desc': '真实时间水印',
+      'member_fun_name_5': '历史轨迹',
+      'member_fun_name_5_desc': '追踪轨迹回放',
+      'member_fun_name_6': '紧急求助',
+      'member_fun_name_6_desc': '一对一服务',
+      'member_user_evaluate': '用户评价',
+      'member_tips': '本应用功能仅限于家庭成员和亲人朋友之间使用,根据相关法规和隐私协议规定,共享位置功能需要对方下载得到好友授权同意才能正常使用。',
     },
   };
 }

+ 5 - 3
lib/utils/async_util.dart

@@ -118,14 +118,16 @@ class AsyncUtil {
       callback(count).then((value) {
         controller.add(value);
         count++;
-        if (count < times) {
+        if (times == -1 || count < times) {
           timer = Timer(interval, tick);
         } else {
           controller.close();
         }
       }).catchError((error) {
-        controller.addError(error);
-        controller.close();
+        if (!controller.isClosed) {
+          controller.addError(error);
+          controller.close();
+        }
       });
     }
 

+ 11 - 0
lib/utils/project_expand.dart

@@ -0,0 +1,11 @@
+extension IntExtension on int {
+  String divideBy100() {
+    double result = this / 100;
+    String resultStr = result.toString();
+    if (resultStr.contains('.')) {
+      resultStr = resultStr.replaceAll(RegExp(r'0*$'), '');
+      resultStr = resultStr.replaceAll(RegExp(r'\.$'), '');
+    }
+    return resultStr;
+  }
+}

+ 123 - 0
lib/widget/animated_switcher_widget.dart

@@ -0,0 +1,123 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class AnimatedSwitcherWidget extends StatefulWidget {
+  final SwitcherController controller;
+  final Duration duration;
+
+  const AnimatedSwitcherWidget({
+    super.key,
+    required this.controller,
+    this.duration = const Duration(milliseconds: 1500),
+  });
+
+  @override
+  State<AnimatedSwitcherWidget> createState() => _AnimatedSwitcherWidgetState();
+}
+
+class _AnimatedSwitcherWidgetState extends State<AnimatedSwitcherWidget> {
+  Widget? _currentWidget;
+
+  @override
+  void initState() {
+    super.initState();
+    // 注册更新回调
+    widget.controller._registerOnUpdate(() => setState(() {
+          _currentWidget = widget.controller._currentWidget;
+        }));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ClipRRect(
+      child: AnimatedSwitcher(
+        duration: widget.duration,
+        transitionBuilder: (Widget child, Animation<double> animation) {
+          return SlideTransitionX(
+            direction: AxisDirection.down,
+            position: animation,
+            child: child,
+          );
+        },
+        child: Center(key: ValueKey(_currentWidget), child: _currentWidget),
+      ),
+    );
+  }
+}
+
+class SwitcherController {
+  // 保存当前组件和回调函数
+  Widget? _currentWidget;
+  VoidCallback? _onUpdate;
+
+  // 更新组件并触发回调
+  void updateWidget(Widget newWidget) {
+    _currentWidget = newWidget;
+    _onUpdate?.call();
+  }
+
+  // 注册更新回调(供State内部调用)
+  void _registerOnUpdate(VoidCallback onUpdate) {
+    _onUpdate = onUpdate;
+  }
+}
+
+class SlideTransitionX extends AnimatedWidget {
+  SlideTransitionX({
+    super.key,
+    required Animation<double> position,
+    this.transformHitTests = true,
+    this.direction = AxisDirection.down,
+    required this.child,
+  }) : super(listenable: position) {
+    switch (direction) {
+      case AxisDirection.up:
+        _tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));
+        break;
+      case AxisDirection.right:
+        _tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
+        break;
+      case AxisDirection.down:
+        _tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));
+        break;
+      case AxisDirection.left:
+        _tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));
+        break;
+    }
+  }
+
+  final bool transformHitTests;
+
+  final Widget child;
+
+  final AxisDirection direction;
+
+  late final Tween<Offset> _tween;
+
+  @override
+  Widget build(BuildContext context) {
+    final position = listenable as Animation<double>;
+    Offset offset = _tween.evaluate(position);
+    if (position.status == AnimationStatus.reverse) {
+      switch (direction) {
+        case AxisDirection.up:
+          offset = Offset(offset.dx, -offset.dy);
+          break;
+        case AxisDirection.right:
+          offset = Offset(-offset.dx, offset.dy);
+          break;
+        case AxisDirection.down:
+          offset = Offset(offset.dx, -offset.dy);
+          break;
+        case AxisDirection.left:
+          offset = Offset(-offset.dx, offset.dy);
+          break;
+      }
+    }
+    return FractionalTranslation(
+      translation: offset,
+      transformHitTests: transformHitTests,
+      child: child,
+    );
+  }
+}

+ 87 - 0
lib/widget/auto_scroll_list_view.dart

@@ -0,0 +1,87 @@
+import 'dart:async';
+
+import 'package:flutter/cupertino.dart';
+
+class AutoScrollListView extends StatefulWidget {
+  final NullableIndexedWidgetBuilder itemBuilder;
+
+  final int itemCount;
+
+  final Axis scrollDirection;
+
+  final EdgeInsetsGeometry? padding;
+
+  final bool isAutoScrolling;
+
+  const AutoScrollListView(
+      {super.key,
+      required this.itemBuilder,
+      required this.itemCount,
+      this.padding,
+      this.isAutoScrolling = true,
+      this.scrollDirection = Axis.horizontal});
+
+  @override
+  State<AutoScrollListView> createState() => _AutoScrollListViewState();
+}
+
+class _AutoScrollListViewState extends State<AutoScrollListView> {
+  final ScrollController scrollController = ScrollController();
+  Timer? _timer;
+  bool _isUserScrolling = true;
+
+  @override
+  void initState() {
+    super.initState();
+    _isUserScrolling = widget.isAutoScrolling;
+    _startAutoScroll();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    scrollController.dispose();
+    _timer?.cancel();
+  }
+
+  void _startAutoScroll() {
+    _timer = Timer.periodic(Duration(milliseconds: 50), (timer) {
+      if (_isUserScrolling) {
+        scrollController.animateTo(
+          scrollController.position.pixels + 1,
+          duration: Duration(milliseconds: 50),
+          curve: Curves.linear,
+        );
+      }
+    });
+  }
+
+  void _onUserScroll() {
+    if (!_isUserScrolling) {
+      return;
+    }
+    _timer?.cancel();
+    _timer = Timer(Duration(seconds: 1), () {
+      setState(() {
+        _isUserScrolling = widget.isAutoScrolling;
+      });
+      _startAutoScroll();
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onPanDown: (_) => _onUserScroll(),
+      onPanUpdate: (_) => _onUserScroll(),
+      child: ListView.builder(
+          padding: widget.padding,
+          scrollDirection: widget.scrollDirection,
+          itemCount: 1000000,
+          itemBuilder: (context, index) {
+            return widget.itemBuilder(context, index % widget.itemCount);
+          },
+          controller: scrollController),
+    );
+  }
+}

+ 6 - 0
pubspec.yaml

@@ -168,6 +168,7 @@ dev_dependencies:
 #----------gen配置---------------
 flutter_gen:
   output: lib/resource/
+  fonts:
   colors:
     inputs:
       - assets/color/common_color.xml
@@ -201,6 +202,11 @@ flutter:
   assets:
     - assets/images/
 
+  fonts:
+    - family: OppoSans
+      fonts:
+        - asset: assets/fonts/price.ttf
+
 
 wechat_kit:
   ios: no_pay # 默认 pay