Bladeren bron

[new]增加登录界面

zk 9 maanden geleden
bovenliggende
commit
c07886c638
66 gewijzigde bestanden met toevoegingen van 2301 en 97 verwijderingen
  1. 1 1
      android/build.gradle.kts
  2. BIN
      assets/images/bg_login_head_container.webp
  3. BIN
      assets/images/bg_mine_member_card.webp
  4. 0 0
      assets/images/bg_page_background.webp
  5. BIN
      assets/images/icon_black_back.webp
  6. BIN
      assets/images/icon_checkbox_selected.webp
  7. BIN
      assets/images/icon_checkbox_un_select.webp
  8. BIN
      assets/images/icon_experiment.webp
  9. BIN
      assets/images/icon_member_vip_receive_arrow.webp
  10. BIN
      assets/images/icon_mine_fun_about.webp
  11. BIN
      assets/images/icon_mine_fun_account_feedback.webp
  12. BIN
      assets/images/icon_mine_fun_arrow.webp
  13. BIN
      assets/images/icon_mine_fun_customer_service.webp
  14. BIN
      assets/images/icon_mine_fun_exit_account.webp
  15. BIN
      assets/images/icon_mine_fun_logout_account.webp
  16. BIN
      assets/images/icon_mine_fun_permission_setting.webp
  17. BIN
      assets/images/icon_mine_fun_share.webp
  18. BIN
      assets/images/icon_mine_logged.webp
  19. BIN
      assets/images/icon_mine_no_login.webp
  20. BIN
      assets/images/icon_mine_small_vip.webp
  21. BIN
      assets/images/icon_mine_unlock_vip.webp
  22. BIN
      assets/images/icon_vip.webp
  23. BIN
      assets/images/icon_white_back.webp
  24. 59 0
      assets/string/base/string.xml
  25. 14 0
      lib/base/app_base_request.dart
  26. 66 0
      lib/base/app_base_request.g.dart
  27. 2 1
      lib/base/base_request.dart
  28. 12 0
      lib/data/api/atmob_api.dart
  29. 76 0
      lib/data/api/atmob_api.g.dart
  30. 19 0
      lib/data/api/request/login_request.dart
  31. 70 0
      lib/data/api/request/login_request.g.dart
  32. 6 2
      lib/data/api/request/send_code_request.dart
  33. 3 1
      lib/data/api/request/send_code_request.g.dart
  34. 14 0
      lib/data/api/response/login_response.dart
  35. 17 0
      lib/data/api/response/login_response.g.dart
  36. 41 0
      lib/data/api/response/member_status_response.dart
  37. 31 0
      lib/data/api/response/member_status_response.g.dart
  38. 46 0
      lib/data/bean/member_status_info.dart
  39. 10 3
      lib/data/consts/constants.dart
  40. 37 0
      lib/data/consts/error_code.dart
  41. 93 2
      lib/data/repositories/account_repository.dart
  42. 4 0
      lib/device/platform_android_info.dart
  43. 12 3
      lib/di/get_it.config.dart
  44. 24 0
      lib/handler/error_handler.dart
  45. 112 33
      lib/main.dart
  46. 0 15
      lib/module/add_friend/add_friend_dialog_controller.dart
  47. 63 0
      lib/module/browser/browser_controller.dart
  48. 55 0
      lib/module/browser/browser_view.dart
  49. 136 0
      lib/module/login/login_controller.dart
  50. 289 0
      lib/module/login/login_page.dart
  51. 8 0
      lib/module/main/main_controller.dart
  52. 30 25
      lib/module/main/main_page.dart
  53. 56 0
      lib/module/mine/mine_controller.dart
  54. 354 0
      lib/module/mine/mine_page.dart
  55. 1 1
      lib/module/splash/splash_page.dart
  56. 110 5
      lib/resource/assets.gen.dart
  57. 121 0
      lib/resource/string.gen.dart
  58. 15 0
      lib/router/app_pages.dart
  59. 3 3
      lib/utils/app_info_util.dart
  60. 206 0
      lib/utils/async_util.dart
  61. 1 0
      lib/utils/common_util.dart
  62. 10 0
      lib/utils/date_util.dart
  63. 3 0
      lib/utils/toast_util.dart
  64. 16 0
      lib/widget/common_view.dart
  65. 42 1
      pubspec.lock
  66. 13 1
      pubspec.yaml

+ 1 - 1
android/build.gradle.kts

@@ -1,7 +1,7 @@
 allprojects {
     extra.apply {
         set("compileSdkVersion", 34)
-        set("applicationId", "com.atmob.location")
+        set("applicationId", "com.manbu.shouji.android")
         set("minSdkVersion", 23)
         set("targetSdkVersion", 34)
         set("ndkVersion", "27.0.12077973")

BIN
assets/images/bg_login_head_container.webp


BIN
assets/images/bg_mine_member_card.webp


assets/images/bg_splash.webp → assets/images/bg_page_background.webp


BIN
assets/images/icon_black_back.webp


BIN
assets/images/icon_checkbox_selected.webp


BIN
assets/images/icon_checkbox_un_select.webp


BIN
assets/images/icon_experiment.webp


BIN
assets/images/icon_member_vip_receive_arrow.webp


BIN
assets/images/icon_mine_fun_about.webp


BIN
assets/images/icon_mine_fun_account_feedback.webp


BIN
assets/images/icon_mine_fun_arrow.webp


BIN
assets/images/icon_mine_fun_customer_service.webp


BIN
assets/images/icon_mine_fun_exit_account.webp


BIN
assets/images/icon_mine_fun_logout_account.webp


BIN
assets/images/icon_mine_fun_permission_setting.webp


BIN
assets/images/icon_mine_fun_share.webp


BIN
assets/images/icon_mine_logged.webp


BIN
assets/images/icon_mine_no_login.webp


BIN
assets/images/icon_mine_small_vip.webp


BIN
assets/images/icon_mine_unlock_vip.webp


BIN
assets/images/icon_vip.webp


BIN
assets/images/icon_white_back.webp


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

@@ -13,6 +13,11 @@
     <string name="load_no_data">没有更多数据了</string>
     <string name="load_failed">加载失败</string>
 
+    <string name="network_error">网络异常</string>
+
+    <string name="privacy_policy">《隐私权政策》</string>
+    <string name="term_of_service">《服务条款》</string>
+
     <string name="friend_add_title">添加好友</string>
     <string name="friend_add_desc">查看实时定位,开启轨迹守护</string>
     <string name="friend_add_phone_et_hint">请输入手机号</string>
@@ -25,4 +30,58 @@
     <string name="main_help_tab">一键求助</string>
     <string name="main_mine_tab">个人中心</string>
 
+    <string name="mine_account_go_login">点击登录</string>
+    <string name="mine_account_logged_desc">用户</string>
+    <string name="mine_not_login_desc">登录后可体验更多服务</string>
+    <string name="mine_open_vip">开通VIP可体验更多服务</string>
+    <string name="mine_vip">您好,尊贵的VIP用户</string>
+
+    <string name="mine_member_permanent">您已是尊贵的永久会员</string>
+
+    <string name="member_level_0">未开通</string>
+    <string name="member_level_100">日卡会员</string>
+    <string name="member_level_700">周卡会员</string>
+    <string name="member_level_3100">月度会员</string>
+    <string name="member_level_9200">季度会员</string>
+    <string name="member_level_36600">年度会员</string>
+    <string name="member_level_3660000">终身会员</string>
+    <string name="member_level_undefined">未知</string>
+
+    <string name="member_try_out">会员试用</string>
+
+    <string name="member_card_no_login_desc">升级VIP会员,享受更多权益</string>
+    <string name="member_card_no_vip_desc">开通VIP会员,享受更多权益</string>
+    <string name="member_card_expiration_desc">到期</string>
+    <string name="member_card_permanent_vip_desc">您已是尊贵的永久会员</string>
+    <string name="member_vip_unlock">立即解锁</string>
+    <string name="member_vip_renew">立即续费</string>
+    <string name="member_vip_permanent">永久会员</string>
+    <string name="member_experience_vip">免费VIP体验礼包</string>
+    <string name="member_experience_vip_receive">去领取</string>
+
+    <string name="mine_fun_share">邀请好友</string>
+    <string name="mine_fun_customer_service">专属客服</string>
+    <string name="mine_fun_permission_setting">权限设置</string>
+    <string name="mine_fun_account_feedback">用户反馈</string>
+    <string name="mine_fun_about">关于我们</string>
+    <string name="mine_fun_logout_account">注销账号</string>
+    <string name="mine_fun_exit_account">退出账号</string>
+
+    <string name="login">登录</string>
+    <string name="login_send_verification_code">发送验证码</string>
+    <string name="login_retransmission_code">s后重发</string>
+    <string name="login_et_phone_hint">请输入11位手机号码</string>
+    <string name="login_et_privacy_read">已阅读并同意</string>
+    <string name="login_et_privacy_and">和</string>
+
+    <string name="login_print_verification_code">请输入验证码</string>
+    <string name="login_print_phone_verification">请输入正确格式的手机号码</string>
+    <string name="login_agree_privacy">请先阅读并同意《隐私权政策》和《服务条款》</string>
+    <string name="login_verification_code_error_toast">验证码输入错误,请重新输入</string>
+    <string name="account_no_login">账号未登录</string>
+    <string name="login_request_code_frequently_toast">请求过于频繁,请稍后再试</string>
+    <string name="login_verification_code_request_failed_toast">验证码发送失败,请重试</string>
+    <string name="login_success">登录成功</string>
+    <string name="login_too_often_toast">登录过于频繁,请稍后再试</string>
+    <string name="login_failed_toast">登录失败</string>
 </resources>

+ 14 - 0
lib/base/app_base_request.dart

@@ -0,0 +1,14 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/data/repositories/account_repository.dart';
+import 'base_request.dart';
+
+part 'app_base_request.g.dart';
+
+@JsonSerializable()
+class AppBaseRequest extends BaseRequest {
+  @JsonKey(name: 'authToken')
+  String? authToken = AccountRepository.token;
+
+  @override
+  Map<String, dynamic> toJson() => _$AppBaseRequestToJson(this);
+}

+ 66 - 0
lib/base/app_base_request.g.dart

@@ -0,0 +1,66 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'app_base_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+AppBaseRequest _$AppBaseRequestFromJson(Map<String, dynamic> json) =>
+    AppBaseRequest()
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$AppBaseRequestToJson(AppBaseRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+    };

+ 2 - 1
lib/base/base_request.dart

@@ -109,7 +109,8 @@ class BaseRequest {
   }
 
   void initPackageInfo() {
-    packageName = appInfoUtil.packageName;
+    // packageName = appInfoUtil.packageName;
+    packageName = 'com.manbu.shouji';
     appVersionName = appInfoUtil.appVersionName;
     appVersionCode = appInfoUtil.appVersionCode;
   }

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

@@ -1,6 +1,10 @@
 import 'package:dio/dio.dart';
+import 'package:location/base/app_base_request.dart';
 import 'package:location/base/base_response.dart';
+import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
+import 'package:location/data/api/response/login_response.dart';
+import 'package:location/data/api/response/member_status_response.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
 
@@ -13,4 +17,12 @@ abstract class AtmobApi {
 
   @POST("/s/v1/user/code")
   Future<BaseResponse> loginSendCode(@Body() SendCodeRequest request);
+
+  @POST("/s/v1/user/login")
+  Future<BaseResponse<LoginResponse>> loginUserLogin(
+      @Body() LoginRequest request);
+
+  @POST("/s/v1/user/member")
+  Future<BaseResponse<MemberStatusResponse>> getMemberStatus(
+      @Body() AppBaseRequest request);
 }

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

@@ -58,6 +58,82 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<LoginResponse>> loginUserLogin(
+      LoginRequest 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<LoginResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/user/login',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<LoginResponse> _value;
+    try {
+      _value = BaseResponse<LoginResponse>.fromJson(
+        _result.data!,
+        (json) => LoginResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<MemberStatusResponse>> getMemberStatus(
+      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<MemberStatusResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/user/member',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<MemberStatusResponse> _value;
+    try {
+      _value = BaseResponse<MemberStatusResponse>.fromJson(
+        _result.data!,
+        (json) => MemberStatusResponse.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 ||

+ 19 - 0
lib/data/api/request/login_request.dart

@@ -0,0 +1,19 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+import 'package:location/base/base_request.dart';
+
+part 'login_request.g.dart';
+
+@JsonSerializable()
+class LoginRequest extends AppBaseRequest {
+  @JsonKey(name: "phone")
+  String phoneNum;
+
+  @JsonKey(name: "code")
+  String verificationCode;
+
+  LoginRequest(this.phoneNum, this.verificationCode);
+
+  @override
+  Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
+}

+ 70 - 0
lib/data/api/request/login_request.g.dart

@@ -0,0 +1,70 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'login_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) => LoginRequest(
+      json['phone'] as String,
+      json['code'] as String,
+    )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$LoginRequestToJson(LoginRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'phone': instance.phoneNum,
+      'code': instance.verificationCode,
+    };

+ 6 - 2
lib/data/api/request/send_code_request.dart

@@ -1,12 +1,16 @@
 import 'package:json_annotation/json_annotation.dart';
-import 'package:location/base/base_request.dart';
+
+import '../../../base/app_base_request.dart';
 
 part 'send_code_request.g.dart';
 
 @JsonSerializable()
-class SendCodeRequest extends BaseRequest {
+class SendCodeRequest extends AppBaseRequest {
   @JsonKey(name: "phone")
   final String phoneNum;
 
   SendCodeRequest(this.phoneNum);
+
+  @override
+  Map<String, dynamic> toJson() => _$SendCodeRequestToJson(this);
 }

+ 3 - 1
lib/data/api/request/send_code_request.g.dart

@@ -34,7 +34,8 @@ SendCodeRequest _$SendCodeRequestFromJson(Map<String, dynamic> json) =>
       ..wifiName = json['wifiName'] as String?
       ..region = json['region'] as String?
       ..locLng = (json['locLng'] as num?)?.toDouble()
-      ..locLat = (json['locLat'] as num?)?.toDouble();
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
 
 Map<String, dynamic> _$SendCodeRequestToJson(SendCodeRequest instance) =>
     <String, dynamic>{
@@ -63,5 +64,6 @@ Map<String, dynamic> _$SendCodeRequestToJson(SendCodeRequest instance) =>
       'region': instance.region,
       'locLng': instance.locLng,
       'locLat': instance.locLat,
+      'authToken': instance.authToken,
       'phone': instance.phoneNum,
     };

+ 14 - 0
lib/data/api/response/login_response.dart

@@ -0,0 +1,14 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'login_response.g.dart';
+
+@JsonSerializable()
+class LoginResponse {
+  @JsonKey(name: "authToken")
+  String authToken;
+
+  LoginResponse(this.authToken);
+
+  factory LoginResponse.fromJson(Map<String, dynamic> json) =>
+      _$LoginResponseFromJson(json);
+}

+ 17 - 0
lib/data/api/response/login_response.g.dart

@@ -0,0 +1,17 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'login_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) =>
+    LoginResponse(
+      json['authToken'] as String,
+    );
+
+Map<String, dynamic> _$LoginResponseToJson(LoginResponse instance) =>
+    <String, dynamic>{
+      'authToken': instance.authToken,
+    };

+ 41 - 0
lib/data/api/response/member_status_response.dart

@@ -0,0 +1,41 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'member_status_response.g.dart'; // 自动生成的代码文件
+
+@JsonSerializable()
+class MemberStatusResponse {
+  @JsonKey(name: 'userId')
+  final String userId;
+
+  @JsonKey(name: 'level')
+  final int level;
+
+  @JsonKey(name: 'startTimestamp')
+  final int startTimestamp;
+
+  @JsonKey(name: 'endTimestamp')
+  final int endTimestamp;
+
+  @JsonKey(name: 'serverTimestamp')
+  final int serverTimestamp;
+
+  @JsonKey(name: 'expired')
+  final bool expired;
+
+  @JsonKey(name: 'permanent')
+  final bool permanent;
+
+  MemberStatusResponse({
+    required this.userId,
+    required this.level,
+    required this.startTimestamp,
+    required this.endTimestamp,
+    required this.serverTimestamp,
+    required this.expired,
+    required this.permanent,
+  });
+
+  // 反序列化:从 JSON 到 Dart 对象
+  factory MemberStatusResponse.fromJson(Map<String, dynamic> json) =>
+      _$MemberStatusResponseFromJson(json);
+}

+ 31 - 0
lib/data/api/response/member_status_response.g.dart

@@ -0,0 +1,31 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'member_status_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+MemberStatusResponse _$MemberStatusResponseFromJson(
+        Map<String, dynamic> json) =>
+    MemberStatusResponse(
+      userId: json['userId'] as String,
+      level: (json['level'] as num).toInt(),
+      startTimestamp: (json['startTimestamp'] as num).toInt(),
+      endTimestamp: (json['endTimestamp'] as num).toInt(),
+      serverTimestamp: (json['serverTimestamp'] as num).toInt(),
+      expired: json['expired'] as bool,
+      permanent: json['permanent'] as bool,
+    );
+
+Map<String, dynamic> _$MemberStatusResponseToJson(
+        MemberStatusResponse instance) =>
+    <String, dynamic>{
+      'userId': instance.userId,
+      'level': instance.level,
+      'startTimestamp': instance.startTimestamp,
+      'endTimestamp': instance.endTimestamp,
+      'serverTimestamp': instance.serverTimestamp,
+      'expired': instance.expired,
+      'permanent': instance.permanent,
+    };

+ 46 - 0
lib/data/bean/member_status_info.dart

@@ -0,0 +1,46 @@
+import 'package:intl/intl.dart';
+import 'package:location/resource/string.gen.dart';
+
+class MemberStatusInfo {
+  final int level;
+  final int endTimestamp;
+  final bool expired;
+  final bool permanent;
+
+  MemberStatusInfo({
+    required this.level,
+    required this.endTimestamp,
+    required this.expired,
+    required this.permanent,
+  });
+
+  /// 获取会员等级描述
+  String getLevelDesc() {
+    if (expired) {
+      return '未开通会员';
+    }
+    if (level > 0 && level < 100) {
+      return '试用会员';
+    }
+
+    // 根据等级返回描述
+    switch (level) {
+      case 0:
+        return '未开通';
+      case 100:
+        return '日卡会员';
+      case 700:
+        return '周卡会员';
+      case 3100:
+        return '月度会员';
+      case 9200:
+        return '季度会员';
+      case 36600:
+        return '年度会员';
+      case 3660000:
+        return '终身会员';
+      default:
+        return '未知会员等级';
+    }
+  }
+}

+ 10 - 3
lib/data/consts/constants.dart

@@ -11,11 +11,11 @@ class Constants {
 
   static const String envProd = 'prod';
 
-  static const String _devBaseUrl = "http://192.168.10.230:8880";
+  static const String _devBaseUrl = "http://192.168.10.68:56389";
 
-  static const String _testBaseUrl = "http://42.193.245.11";
+  static const String _testBaseUrl = "http://loc-api.v8dashen.com";
 
-  static const String _prodBaseUrl = "https://project-api.atmob.com";
+  static const String _prodBaseUrl = "http://loc-api.v8dashen.com";
 
   static const String privacyPolicy =
       "https://doc.v8dashen.com/doc/298eb75d38dc2c4a";
@@ -30,6 +30,13 @@ class Constants {
   static bool isProdEnv() {
     return Constants.env == Constants.envProd;
   }
+
+  static const String appDefaultChannel = "Android";
+  static const int appDefaultAppId = 0;
+  static const int appDefaultTgPlatformId = 0;
+  static const String appChanelName = "app_channel_name";
+  static const String appChannelId = "app_channel_id";
+  static const String appTgPlatformId = "app_tg_platform_id";
 }
 
 String getBaseUrl() {

+ 37 - 0
lib/data/consts/error_code.dart

@@ -0,0 +1,37 @@
+import 'package:location/resource/string.gen.dart';
+
+class ErrorCode {
+  /// 登录相关错误码
+  static const int verificationCodeError = 1005;
+  static const int noLoginError = 1006;
+  static const int noMember = 1007;
+
+  /// 好友关系相关错误码
+  static const int friendNotRegistered = 1100;
+  static const int friendRequestSent = 1101;
+  static const int alreadyInFriendList = 1102;
+  static const int cannotAddSelf = 1103;
+
+  /// 紧急联系人相关错误码
+  static const int maxContactsReached = 1200;
+  static const int contactAlreadyAdded = 1201;
+  static const int smsSendFailed = 1202;
+
+  /// 会员服务相关错误码
+  static const int getMemberFree = 1300;
+  static const int isMember = 1301;
+}
+
+/// 错误码扩展方法
+extension ErrorDescription on int {
+  String get description {
+    switch (this) {
+      case ErrorCode.verificationCodeError:
+        return StringName.loginVerificationCodeErrorToast;
+      case ErrorCode.noLoginError:
+        return StringName.accountNoLogin;
+      default:
+        return 'UNKNOWN_ERROR';
+    }
+  }
+}

+ 93 - 2
lib/data/repositories/account_repository.dart

@@ -1,17 +1,108 @@
+import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/data/api/atmob_api.dart';
+import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/send_code_request.dart';
+import 'package:location/data/bean/member_status_info.dart';
+import 'package:location/data/consts/error_code.dart';
+import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/http_handler.dart';
+import 'package:location/utils/mmkv_util.dart';
+
+import '../api/response/login_response.dart';
 
 @lazySingleton
 class AccountRepository {
   final AtmobApi atmobApi;
 
-  AccountRepository(this.atmobApi);
+  final String keyAccountLoginPhoneNum = 'key_account_login_phone_num';
+  final String keyAccountLoginToken = 'key_account_login_token';
+
+  RxnString loginPhoneNum = RxnString();
+  RxBool isLogin = RxBool(false);
+  Rxn<MemberStatusInfo> memberStatusInfo = Rxn<MemberStatusInfo>();
+
+  int? _lastRequestCodeTime;
+  int _errorCodeTimes = 0;
+
+  static String? token;
+
+  AccountRepository(this.atmobApi) {
+    token = KVUtil.getString(keyAccountLoginToken, null);
+    isLogin.bindStream(
+      loginPhoneNum.map((value) {
+        return value?.isNotEmpty == true;
+      }),
+    );
+    loginPhoneNum.value = KVUtil.getString(keyAccountLoginPhoneNum, null);
+  }
 
   Future<void> loginSendCode(String phoneNum) {
+    final currentTime = DateTime.now().millisecondsSinceEpoch;
+
+    // 检查是否在 60 秒内重复请求
+    if (currentTime - (_lastRequestCodeTime ?? 0) < 60 * 1000) {
+      throw RequestCodeTooOftenException();
+    }
     return atmobApi
         .loginSendCode(SendCodeRequest(phoneNum))
-        .then(HttpHandler.handle(true));
+        .then(HttpHandler.handle(true))
+        .then((value) {
+      _lastRequestCodeTime = currentTime;
+      _errorCodeTimes = 0;
+    });
+  }
+
+  Future<LoginResponse> loginUserLogin(
+      String phoneNum, String verificationCode) {
+    if (_errorCodeTimes >= 5) {
+      throw LoginTooOftenException();
+    }
+    return atmobApi
+        .loginUserLogin(LoginRequest(phoneNum, verificationCode))
+        .then(HttpHandler.handle(true))
+        .then((response) {
+      _errorCodeTimes = 0;
+      onLoginSuccess(phoneNum, response.authToken);
+      return response;
+    }).catchError((error) {
+      if (error is ServerErrorException &&
+          error.code == ErrorCode.verificationCodeError) {
+        _errorCodeTimes++;
+      }
+      throw error;
+    });
   }
+
+  void onLoginSuccess(String phoneNum, String authToken) {
+    AccountRepository.token = token;
+    loginPhoneNum.value = phoneNum;
+
+    KVUtil.putString(keyAccountLoginPhoneNum, phoneNum);
+    KVUtil.putString(keyAccountLoginToken, authToken);
+
+    refreshMemberStatus();
+  }
+
+  void refreshMemberStatus() {}
+}
+
+class RequestCodeTooOftenException implements Exception {
+  final String message;
+
+  /// 可选的构造函数,支持自定义错误信息
+  RequestCodeTooOftenException([this.message = '请求验证码过于频繁']);
+
+  @override
+  String toString() => message;
+}
+
+class LoginTooOftenException implements Exception {
+  final String message;
+
+  /// 可选的构造函数,支持自定义错误信息
+  LoginTooOftenException([this.message = '登录过于频繁']);
+
+  @override
+  String toString() => message;
 }

+ 4 - 0
lib/device/platform_android_info.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:android_id/android_id.dart';
+import 'package:oaid/oaid_kit.dart';
 import 'atmob_platform_info.dart';
 
 class PlatformAndroidInfo {
@@ -14,6 +15,9 @@ class PlatformAndroidInfo {
           .setAndroidId(deviceId)
           .setBrand(androidInfo.brand)
           .setModel(androidInfo.model);
+
+      String? oaid = await Oaid.getOaid();
+      atmobPlatformInfo.setOaid(oaid);
     }
   }
 }

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

@@ -15,7 +15,10 @@ import 'package:injectable/injectable.dart' as _i526;
 import '../data/api/atmob_api.dart' as _i243;
 import '../data/repositories/account_repository.dart' as _i20;
 import '../module/add_friend/add_friend_dialog_controller.dart' as _i897;
+import '../module/browser/browser_controller.dart' as _i923;
+import '../module/login/login_controller.dart' as _i1008;
 import '../module/main/main_controller.dart' as _i731;
+import '../module/mine/mine_controller.dart' as _i732;
 import '../module/splash/splash_controller.dart' as _i973;
 import 'network_module.dart' as _i567;
 
@@ -31,15 +34,21 @@ extension GetItInjectableX on _i174.GetIt {
       environmentFilter,
     );
     final networkModule = _$NetworkModule();
-    gh.factory<_i731.MainController>(() => _i731.MainController());
+    gh.factory<_i897.AddFriendDialogController>(
+        () => _i897.AddFriendDialogController());
+    gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
     gh.factory<_i973.SplashController>(() => _i973.SplashController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.singleton<_i243.AtmobApi>(
         () => networkModule.provideAtmobApi(gh<_i361.Dio>()));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
-    gh.factory<_i897.AddFriendDialogController>(
-        () => _i897.AddFriendDialogController(gh<_i20.AccountRepository>()));
+    gh.factory<_i1008.LoginController>(
+        () => _i1008.LoginController(gh<_i20.AccountRepository>()));
+    gh.factory<_i731.MainController>(
+        () => _i731.MainController(gh<_i20.AccountRepository>()));
+    gh.factory<_i732.MineController>(
+        () => _i732.MineController(gh<_i20.AccountRepository>()));
     return this;
   }
 }

+ 24 - 0
lib/handler/error_handler.dart

@@ -0,0 +1,24 @@
+import 'package:location/data/consts/error_code.dart';
+import 'package:location/utils/toast_util.dart';
+
+import '../resource/string.gen.dart';
+import '../utils/http_handler.dart';
+
+class ErrorHandler {
+  ErrorHandler._();
+
+  static void toastError(dynamic error, {String? message}) {
+    String toastMessage = (error is ServerErrorException)
+        ? _getToastMessageFromError(error)
+        : _getDefaultToastMessage(message);
+    ToastUtil.show(toastMessage);
+  }
+
+  static String _getToastMessageFromError(ServerErrorException error) {
+    return error.code?.description ?? error.message ?? StringName.networkError;
+  }
+
+  static String _getDefaultToastMessage(String? message) {
+    return message ?? StringName.networkError;
+  }
+}

+ 112 - 33
lib/main.dart

@@ -1,7 +1,11 @@
+import 'dart:io';
+
+import 'package:atmob_channel_reader/atmob_channel_reader.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
 import 'package:get/get_navigation/src/root/get_material_app.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
@@ -12,6 +16,7 @@ import 'package:location/utils/mmkv_util.dart';
 import 'package:location/utils/toast_util.dart';
 import 'package:pull_to_refresh/pull_to_refresh.dart';
 
+import 'device/atmob_platform_info.dart';
 import 'di/get_it.dart';
 import 'data/consts/build_config.dart';
 import 'data/consts/constants.dart';
@@ -44,6 +49,36 @@ Future<void> initRequired() async {
 void initCommon() {
   //全局配置smartDialog
   smartConfig();
+  //渠道(仅Android)
+  _initChannel();
+}
+
+_initChannel() async {
+  await AtmobChannelReader.default4Test(Constants.appDefaultChannel,
+      Constants.appDefaultAppId, Constants.appDefaultTgPlatformId);
+
+  String? channel = KVUtil.getString(
+      Constants.appChanelName, await AtmobChannelReader.getChannel());
+  KVUtil.putString(Constants.appChanelName, channel);
+
+  int? channelId = KVUtil.getInt(Constants.appChannelId, -1);
+  if (channelId == -1) {
+    channelId = await AtmobChannelReader.getAppId();
+  }
+  if (channelId != null) {
+    KVUtil.putInt(Constants.appChannelId, channelId);
+  }
+
+  int? appTgPlatformId = KVUtil.getInt(Constants.appTgPlatformId, -1);
+  if (appTgPlatformId == -1) {
+    appTgPlatformId = await AtmobChannelReader.getTgPlatformId();
+  }
+  if (appTgPlatformId != null) {
+    KVUtil.putInt(Constants.appTgPlatformId, appTgPlatformId);
+  }
+  atmobPlatformInfo.setChannelName(channel);
+  atmobPlatformInfo.setAppId(channelId);
+  atmobPlatformInfo.setTgPlatform(appTgPlatformId);
 }
 
 void smartConfig() {
@@ -75,15 +110,16 @@ class MyApp extends StatelessWidget {
     return ScreenUtilInit(
       designSize: const Size(360, 640),
       builder: (_, child) {
-        return _buildMaterialApp();
+        return buildApp();
       },
     );
   }
 
-  _buildMaterialApp() {
+  buildApp() {
     return RefreshConfiguration(
-      headerBuilder: () =>
-          const MaterialClassicHeader(color: ColorName.colorPrimary),
+      headerBuilder: () => Platform.isAndroid
+          ? const MaterialClassicHeader(color: ColorName.colorPrimary)
+          : const ClassicHeader(),
       footerBuilder: () => ClassicFooter(
         canLoadingText: StringName.loadingMore,
         idleText: StringName.loadPullUp,
@@ -91,37 +127,80 @@ class MyApp extends StatelessWidget {
         noDataText: StringName.loadNoData,
         failedText: StringName.loadFailed,
       ),
-      child: GetMaterialApp(
-        onGenerateTitle: (_) => StringName.appName,
-        getPages: AppPage.pages,
-        initialRoute: RoutePath.splash,
-        initialBinding: AppBinding(),
-        theme: ThemeData(
-          useMaterial3: true,
-          textSelectionTheme: const TextSelectionThemeData(
-            cursorColor: ColorName.colorPrimary, // 设置默认光标颜色
-            selectionHandleColor: ColorName.colorPrimary, // 设置光标下面水滴的颜色
-          ),
+      child: Platform.isAndroid ? buildMaterialApp() : buildIosApp(),
+    );
+  }
+
+  Widget buildIosApp() {
+    return GetCupertinoApp(
+      onGenerateTitle: AppCommonConfig.appName,
+      getPages: AppCommonConfig.getPages,
+      initialRoute: AppCommonConfig.initialRoute,
+      initialBinding: AppCommonConfig.initialBinding,
+      navigatorObservers: AppCommonConfig.navigatorObservers,
+      builder: AppCommonConfig.builder,
+      translations: AppCommonConfig.translations,
+      localizationsDelegates: AppCommonConfig.localizations.delegates,
+      supportedLocales: AppCommonConfig.localizations.supportedLocales,
+      locale: AppCommonConfig.localizations.locale,
+      fallbackLocale: AppCommonConfig.localizations.fallbackLocale,
+    );
+  }
+
+  Widget buildMaterialApp() {
+    return GetMaterialApp(
+      onGenerateTitle: AppCommonConfig.appName,
+      getPages: AppCommonConfig.getPages,
+      initialRoute: AppCommonConfig.initialRoute,
+      initialBinding: AppCommonConfig.initialBinding,
+      theme: ThemeData(
+        useMaterial3: true,
+        textSelectionTheme: const TextSelectionThemeData(
+          cursorColor: ColorName.colorPrimary, // 设置默认光标颜色
+          selectionHandleColor: ColorName.colorPrimary, // 设置光标下面水滴的颜色
         ),
-        navigatorObservers: [FlutterSmartDialog.observer],
-        builder: FlutterSmartDialog.init(),
-        translations: StringResource(),
-        localizationsDelegates: const [
-          GlobalMaterialLocalizations.delegate,
-          //是Flutter的一个本地化委托,用于提供Material组件库的本地化支持
-          GlobalWidgetsLocalizations.delegate,
-          //用于提供通用部件(Widgets)的本地化支持
-          GlobalCupertinoLocalizations.delegate,
-          //用于提供Cupertino风格的组件的本地化支持
-        ],
-        supportedLocales: const [
-          Locale('zh', 'CN'), // 支持的语言和地区
-        ],
-        // 你的翻译
-        locale: const Locale('zh', 'CN'),
-        // 将会按照此处指定的语言翻译 添加一个回调语言选项,以备上面指定的语言翻译不存在
-        fallbackLocale: const Locale('zh', 'CN'),
       ),
+      navigatorObservers: AppCommonConfig.navigatorObservers,
+      builder: AppCommonConfig.builder,
+      translations: AppCommonConfig.translations,
+      localizationsDelegates: AppCommonConfig.localizations.delegates,
+      supportedLocales: AppCommonConfig.localizations.supportedLocales,
+      locale: AppCommonConfig.localizations.locale,
+      fallbackLocale: AppCommonConfig.localizations.fallbackLocale,
     );
   }
 }
+
+class AppCommonConfig {
+  static GenerateAppTitle? appName = (_) => StringName.appName;
+
+  // 路由配置
+  static List<GetPage>? getPages = AppPage.pages;
+  static const initialRoute = RoutePath.splash;
+
+  // 初始化绑定
+  static Bindings initialBinding = AppBinding();
+
+  // 导航观察者
+  static List<NavigatorObserver> navigatorObservers = [
+    FlutterSmartDialog.observer
+  ];
+
+  // 弹窗初始化
+  static final builder = FlutterSmartDialog.init();
+
+  // 本地化配置
+  static const localizations = (
+    delegates: [
+      GlobalMaterialLocalizations.delegate,
+      GlobalWidgetsLocalizations.delegate,
+      GlobalCupertinoLocalizations.delegate,
+    ],
+    supportedLocales: [Locale('zh', 'CN')],
+    locale: Locale('zh', 'CN'),
+    fallbackLocale: Locale('zh', 'CN'),
+  );
+
+  // 多语言配置
+  static Translations translations = StringResource();
+}

+ 0 - 15
lib/module/add_friend/add_friend_dialog_controller.dart

@@ -1,25 +1,10 @@
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
-import 'package:location/data/repositories/account_repository.dart';
 import '../../../base/base_controller.dart';
-import '../../utils/atmob_log.dart';
 
 @injectable
 class AddFriendDialogController extends BaseController {
-  final AccountRepository accountRepository;
 
   final title = ''.obs;
 
-  AddFriendDialogController(this.accountRepository) {
-    AtmobLog.d("zk", 'AddFriendDialogController .. constructor');
-  }
-
-  @override
-  void onInit() {
-    accountRepository.loginSendCode('123').then((data) {
-      AtmobLog.d("zk", '1');
-    }).catchError((error) {
-      AtmobLog.d("zk", '$error');
-    });
-  }
 }

+ 63 - 0
lib/module/browser/browser_controller.dart

@@ -0,0 +1,63 @@
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+import '../../base/base_controller.dart';
+import '../../resource/colors.gen.dart';
+
+@injectable
+class BrowserController extends BaseController {
+  String url = (Get.arguments is String) ? (Get.arguments as String) : '';
+
+  final WebViewController webViewController = WebViewController();
+
+  final title = ''.obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initWebSetting();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+    _loadUrl();
+  }
+
+  void _initWebSetting() {
+    webViewController.setJavaScriptMode(JavaScriptMode.unrestricted);
+    webViewController.setBackgroundColor(ColorName.white);
+    webViewController.setNavigationDelegate(
+      NavigationDelegate(
+        onPageFinished: (String url) {
+          _getTitle();
+        },
+      ),
+    );
+  }
+
+  void _loadUrl() {
+    if (url.isEmpty) {
+      return;
+    }
+    webViewController.loadRequest(Uri.parse(url));
+  }
+
+  Future<bool> handleBack() async {
+    if (await webViewController.canGoBack()) {
+      webViewController.goBack();
+      return false;
+    }
+    return true;
+  }
+
+  void _getTitle() async {
+    await Future.delayed(const Duration(milliseconds: 500));
+    webViewController.getTitle().then((title) {
+      if (title != null) {
+        this.title.value = title;
+      }
+    });
+  }
+}

+ 55 - 0
lib/module/browser/browser_view.dart

@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.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:webview_flutter/webview_flutter.dart';
+
+import '../../base/base_page.dart';
+import '../../resource/assets.gen.dart';
+import '../../resource/colors.gen.dart';
+import '../../router/app_pages.dart';
+import 'browser_controller.dart';
+
+class BrowserPage extends BasePage<BrowserController> {
+  const BrowserPage({super.key});
+
+  static start(String url) {
+    Get.toNamed(RoutePath.browser, arguments: url);
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return WillPopScope(
+      onWillPop: () async {
+        return await controller.handleBack();
+      },
+      child: Scaffold(
+        backgroundColor: Colors.transparent,
+        appBar: AppBar(
+          systemOverlayStyle: SystemUiOverlayStyle.dark,
+          backgroundColor: Colors.transparent,
+          title: Obx(() => Text(controller.title.value,
+              style: TextStyle(
+                  fontSize: 17.sp, color: ColorName.primaryTextColor))),
+          leading: IconButton(
+            icon: SizedBox(
+                width: 24.w,
+                height: 24.w,
+                child: Assets.images.iconBlackBack.image()),
+            // Custom icon
+            onPressed: () {
+              Get.back();
+            },
+          ),
+        ),
+        body: _buildContentView(),
+      ),
+    );
+  }
+
+  Widget _buildContentView() {
+    return WebViewWidget(controller: controller.webViewController);
+  }
+}

+ 136 - 0
lib/module/login/login_controller.dart

@@ -0,0 +1,136 @@
+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/consts/error_code.dart';
+import 'package:location/handler/error_handler.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/http_handler.dart';
+import 'package:location/utils/toast_util.dart';
+
+import '../../data/repositories/account_repository.dart';
+
+@injectable
+class LoginController extends BaseController {
+  final RxString _phone = ''.obs;
+  final RxString _code = ''.obs;
+
+  String get phone => _phone.value;
+
+  String get code => _code.value;
+
+  final int _countDownTime = 60;
+  final RxnInt _countDown = RxnInt();
+
+  int? get countDown => _countDown.value;
+
+  final RxBool _isAgreePrivacy = false.obs;
+
+  bool get isAgreePrivacy => _isAgreePrivacy.value;
+
+  final AccountRepository accountRepository;
+
+  LoginController(this.accountRepository);
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  void onPhoneChanged(String value) {
+    _phone.value = value;
+  }
+
+  void onCodeChanged(String value) {
+    _code.value = value;
+  }
+
+  void onBackClick() {
+    Get.back();
+  }
+
+  void onSendVerificationCode() {
+    if (_countDown.value != null) {
+      return;
+    }
+    if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
+      ToastUtil.show(StringName.loginPrintPhoneVerification);
+      return;
+    }
+    if (!isAgreePrivacy) {
+      ToastUtil.show(StringName.loginAgreePrivacy);
+      return;
+    }
+    accountRepository.loginSendCode(phone).then((value) {
+      _countDown.value = _countDownTime;
+      _startCountDown();
+    }).catchError((error) {
+      if (error is RequestCodeTooOftenException) {
+        ToastUtil.show(StringName.loginRequestCodeFrequentlyToast);
+        return;
+      }
+      if (error is ServerErrorException) {
+        ToastUtil.show(error.message);
+      } else {
+        ToastUtil.show(StringName.loginVerificationCodeRequestFailedToast);
+      }
+    });
+  }
+
+  void _startCountDown() {
+    Future.delayed(Duration(seconds: 1), () {
+      int? time = _countDown.value;
+      if (time != null) {
+        _countDown.value = time - 1;
+        if (time > 0) {
+          _startCountDown();
+        } else {
+          _countDown.value = null;
+        }
+      }
+    });
+  }
+
+  void onPrivacyClick() {
+    _isAgreePrivacy.value = !_isAgreePrivacy.value;
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _countDown.value = null;
+  }
+
+  void onLoginClick() {
+    if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
+      ToastUtil.show(StringName.loginPrintPhoneVerification);
+      return;
+    }
+    if (!isAgreePrivacy) {
+      ToastUtil.show(StringName.loginAgreePrivacy);
+      return;
+    }
+    if (code.isEmpty) {
+      ToastUtil.show(StringName.loginPrintVerificationCode);
+      return;
+    }
+    accountRepository.loginUserLogin(phone, code).then((data) {
+      Get.back();
+      ToastUtil.show(StringName.loginSuccess);
+    }).catchError((error) {
+      if (error is LoginTooOftenException) {
+        ToastUtil.show(StringName.loginTooOftenToast);
+        return;
+      }
+      if (error is ServerErrorException) {
+        if (error.code == ErrorCode.verificationCodeError) {
+          ToastUtil.show(StringName.loginVerificationCodeErrorToast);
+        } else {
+          ToastUtil.show(error.message);
+        }
+      } else {
+        ToastUtil.show(StringName.loginFailedToast);
+      }
+    });
+  }
+}

+ 289 - 0
lib/module/login/login_page.dart

@@ -0,0 +1,289 @@
+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/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/router/app_pages.dart';
+import 'package:location/utils/expand.dart';
+import '../../data/consts/constants.dart';
+import '../browser/browser_view.dart';
+import 'login_controller.dart';
+
+class LoginPage extends BasePage<LoginController> {
+  const LoginPage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.login);
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        AspectRatio(
+            aspectRatio: 360 / 285,
+            child: Assets.images.bgLoginHeadContainer
+                .image(width: double.infinity)),
+        buildLoginHeader(),
+        SafeArea(
+          child: Column(
+            children: [
+              SizedBox(height: 150.5.w),
+              Expanded(
+                child: Container(
+                  width: double.infinity,
+                  height: double.infinity,
+                  decoration: BoxDecoration(
+                    color: ColorName.white,
+                    borderRadius: BorderRadius.only(
+                      topLeft: Radius.circular(18.w),
+                      topRight: Radius.circular(18.w),
+                    ),
+                  ),
+                  child: Column(
+                    children: [
+                      SizedBox(height: 27.w),
+                      buildPhoneTextFiled(),
+                      SizedBox(height: 12.w),
+                      buildCodeTextFiled(),
+                      SizedBox(height: 10.w),
+                      buildPrivacyTxt(),
+                      SizedBox(height: 148.h),
+                      buildLoginBtn(),
+                    ],
+                  ),
+                ),
+              )
+            ],
+          ),
+        )
+      ],
+    );
+  }
+
+  Widget buildPrivacyTxt() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 24.w),
+      child: GestureDetector(
+        onTap: controller.onPrivacyClick,
+        child: Row(
+          children: [
+            Obx(() {
+              return Image(
+                  image: controller.isAgreePrivacy
+                      ? Assets.images.iconCheckboxSelected.provider()
+                      : Assets.images.iconCheckboxUnSelect.provider(),
+                  width: 20.w,
+                  height: 20.w);
+            }),
+            RichText(
+                text: TextSpan(
+                    style: TextStyle(
+                        color: '#A7A7A7'.color,
+                        fontSize: 12.sp,
+                        decoration: TextDecoration.none),
+                    children: [
+                  TextSpan(text: StringName.loginEtPrivacyRead),
+                  buildLinkText(
+                      StringName.privacyPolicy, Constants.privacyPolicy),
+                  TextSpan(text: StringName.loginEtPrivacyAnd),
+                  buildLinkText(
+                      StringName.termOfService, Constants.userAgreement),
+                ]))
+          ],
+        ),
+      ),
+    );
+  }
+
+  TextSpan buildLinkText(String text, String url) {
+    return TextSpan(
+      text: text,
+      style: TextStyle(color: '#2F79FF'.color),
+      recognizer: TapGestureRecognizer()
+        ..onTap = () {
+          BrowserPage.start(url);
+        },
+    );
+  }
+
+  Widget buildCodeTextFiled() {
+    return Container(
+      height: 50.w,
+      margin: EdgeInsets.symmetric(horizontal: 24.w),
+      padding: EdgeInsets.symmetric(horizontal: 12.w),
+      decoration: BoxDecoration(
+          color: '#FAFAFA'.color, borderRadius: BorderRadius.circular(6.w)),
+      child: Row(
+        children: [
+          Expanded(
+            child: TextField(
+              cursorHeight: 20.w,
+              style: TextStyle(
+                  fontSize: 16.sp,
+                  color: ColorName.primaryTextColor,
+                  fontWeight: FontWeight.bold),
+              maxLines: 1,
+              maxLength: 4,
+              keyboardType: TextInputType.phone,
+              textAlignVertical: TextAlignVertical.center,
+              textInputAction: TextInputAction.next,
+              decoration: InputDecoration(
+                hintText: StringName.loginPrintVerificationCode,
+                counterText: '',
+                hintStyle: TextStyle(
+                    fontSize: 16.sp,
+                    color: "#A7A7A7".toColor(),
+                    fontWeight: FontWeight.normal),
+                labelStyle: TextStyle(
+                  fontSize: 16.sp,
+                  color: ColorName.primaryTextColor,
+                ),
+                contentPadding: const EdgeInsets.all(0),
+                border: const OutlineInputBorder(borderSide: BorderSide.none),
+                enabled: true,
+              ),
+              onChanged: controller.onCodeChanged,
+            ),
+          ),
+          buildVerificationCodeSendBtn()
+        ],
+      ),
+    );
+  }
+
+  Widget buildVerificationCodeSendBtn() {
+    return Obx(() {
+      return GestureDetector(
+        onTap: controller.onSendVerificationCode,
+        child: Container(
+            margin: EdgeInsets.only(left: 12.w),
+            decoration: BoxDecoration(
+                color: controller.phone.length == 11 &&
+                        controller.countDown == null
+                    ? '#7B7DFF'.color
+                    : '#337B7DFF'.color,
+                borderRadius: BorderRadius.circular(4.w)),
+            padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.w),
+            child: Obx(() {
+              String txt = "";
+              if (controller.countDown != null) {
+                txt =
+                    '${controller.countDown}${StringName.loginRetransmissionCode}';
+              } else {
+                txt = StringName.loginSendVerificationCode;
+              }
+              return Text(txt,
+                  style: TextStyle(fontSize: 14.sp, color: ColorName.white));
+            })),
+      );
+    });
+  }
+
+  Widget buildPhoneTextFiled() {
+    return Container(
+      height: 50.w,
+      margin: EdgeInsets.symmetric(horizontal: 24.w),
+      padding: EdgeInsets.symmetric(horizontal: 12.w),
+      decoration: BoxDecoration(
+          color: '#FAFAFA'.color, borderRadius: BorderRadius.circular(6.w)),
+      child: Row(
+        children: [
+          Text('+86',
+              style: TextStyle(
+                  fontSize: 16.sp,
+                  color: '#202020'.color,
+                  fontWeight: FontWeight.bold)),
+          SizedBox(width: 8.w),
+          Container(width: 1.w, height: 20.w, color: '#E2E2E2'.color),
+          SizedBox(width: 25.w),
+          Expanded(
+            child: TextField(
+              cursorHeight: 20.w,
+              style: TextStyle(
+                  fontSize: 16.sp,
+                  color: ColorName.primaryTextColor,
+                  fontWeight: FontWeight.bold),
+              maxLines: 1,
+              maxLength: 11,
+              keyboardType: TextInputType.phone,
+              textAlignVertical: TextAlignVertical.center,
+              textInputAction: TextInputAction.next,
+              decoration: InputDecoration(
+                hintText: StringName.loginEtPhoneHint,
+                counterText: '',
+                hintStyle: TextStyle(
+                    fontSize: 16.sp,
+                    color: "#A7A7A7".toColor(),
+                    fontWeight: FontWeight.normal),
+                labelStyle: TextStyle(
+                  fontSize: 16.sp,
+                  color: ColorName.primaryTextColor,
+                ),
+                contentPadding: const EdgeInsets.all(0),
+                border: const OutlineInputBorder(borderSide: BorderSide.none),
+                enabled: true,
+              ),
+              onChanged: controller.onPhoneChanged,
+            ),
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget buildLoginHeader() {
+    return SafeArea(
+      child: Container(
+          margin: EdgeInsets.only(top: 23.w, left: 12.w),
+          child: Row(
+            children: [
+              GestureDetector(
+                  onTap: controller.onBackClick,
+                  child: Assets.images.iconWhiteBack
+                      .image(width: 25.w, height: 25.w)),
+              SizedBox(width: 4.w),
+              Text(StringName.login,
+                  style: TextStyle(fontSize: 17.sp, color: ColorName.white))
+            ],
+          )),
+    );
+  }
+
+  Widget buildLoginBtn() {
+    return Obx(() {
+      return GestureDetector(
+        onTap: controller.onLoginClick,
+        child: Container(
+          decoration: BoxDecoration(
+              color: controller.phone.length == 11 &&
+                      controller.code.isNotEmpty &&
+                      controller.isAgreePrivacy
+                  ? '#7B7DFF'.color
+                  : '#337B7DFF'.color,
+              borderRadius: BorderRadius.circular(30.w)),
+          width: 280.w,
+          height: 44.w,
+          child: Center(
+            child: Text(StringName.login,
+                style: TextStyle(
+                    fontSize: 16.sp,
+                    color: ColorName.white,
+                    fontWeight: FontWeight.bold)),
+          ),
+        ),
+      );
+    });
+  }
+}

+ 8 - 0
lib/module/main/main_controller.dart

@@ -1,11 +1,19 @@
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
+import 'package:location/data/repositories/account_repository.dart';
 
 import '../add_friend/add_friend_view.dart';
+import '../mine/mine_page.dart';
 
 @injectable
 class MainController extends BaseController {
+  MainController(AccountRepository accountRepository);
+
   void onAddFriendClick() {
     AddFriendView.show();
   }
+
+  void onMineClick() {
+    MinePage.start();
+  }
 }

+ 30 - 25
lib/module/main/main_page.dart

@@ -6,6 +6,7 @@ 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/main/main_controller.dart';
+import 'package:location/module/mine/mine_page.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
@@ -150,39 +151,43 @@ class MainPage extends BasePage<MainController> {
                 StringName.mainHelpTab, () {})),
         Expanded(
             child: buildFunItem(Assets.images.iconMainMine.provider(),
-                StringName.mainMineTab, () {}))
+                StringName.mainMineTab, () => controller.onMineClick()))
       ],
     );
   }
 
   Widget buildFunItem(ImageProvider imgProvider, String title, Function() onTap,
       {bool? isShowDot}) {
-    return Column(
-      children: [
-        Stack(children: [
-          SizedBox(width: 44.w, height: 44.w, child: Image(image: imgProvider)),
-          Visibility(
-            visible: isShowDot ?? false,
-            child: Positioned(
-              top: 6.w,
-              right: 6.w,
-              child: Container(
-                width: 10.w,
-                height: 10.w,
-                decoration: BoxDecoration(
-                  shape: BoxShape.circle,
-                  color: '#FF333D'.color, // 背景颜色
+    return GestureDetector(
+      onTap: onTap,
+      child: Column(
+        children: [
+          Stack(children: [
+            SizedBox(
+                width: 44.w, height: 44.w, child: Image(image: imgProvider)),
+            Visibility(
+              visible: isShowDot ?? false,
+              child: Positioned(
+                top: 6.w,
+                right: 6.w,
+                child: Container(
+                  width: 10.w,
+                  height: 10.w,
+                  decoration: BoxDecoration(
+                    shape: BoxShape.circle,
+                    color: '#FF333D'.color, // 背景颜色
+                  ),
                 ),
               ),
-            ),
-          )
-        ]),
-        Text(title,
-            style: TextStyle(
-                fontSize: 12.sp,
-                color: ColorName.black70,
-                fontWeight: FontWeight.bold))
-      ],
+            )
+          ]),
+          Text(title,
+              style: TextStyle(
+                  fontSize: 12.sp,
+                  color: ColorName.black70,
+                  fontWeight: FontWeight.bold))
+        ],
+      ),
     );
   }
 

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

@@ -0,0 +1,56 @@
+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/bean/member_status_info.dart';
+import 'package:location/module/login/login_page.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/toast_util.dart';
+
+import '../../data/repositories/account_repository.dart';
+
+@injectable
+class MineController extends BaseController {
+  final AccountRepository accountRepository;
+
+  MineController(this.accountRepository);
+
+  bool get isLogin => accountRepository.isLogin.value;
+
+  MemberStatusInfo? get memberStatusInfo =>
+      accountRepository.memberStatusInfo.value;
+
+  String? get phone => accountRepository.loginPhoneNum.value;
+
+  void onBack() {
+    Get.back();
+  }
+
+  String getUserName(String phone) {
+    if (phone.length > 4) {
+      phone = phone.substring(phone.length - 4);
+    }
+    return '${StringName.mineAccountLoggedDesc}$phone';
+  }
+
+  onShareClick() {}
+
+  onCustomerServiceClick() {}
+
+  onPermissionSettingClick() {}
+
+  onAccountFeedbackClick() {}
+
+  onAboutClick() {}
+
+  onLogoutAccountClick() {}
+
+  onFunExitAccountClick() {}
+
+  onLoginClick() {
+    if (isLogin) {
+      return;
+    }
+    LoginPage.start();
+  }
+}

+ 354 - 0
lib/module/mine/mine_page.dart

@@ -0,0 +1,354 @@
+import 'package:flutter/cupertino.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/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/expand.dart';
+import '../../router/app_pages.dart';
+import '../../utils/date_util.dart';
+import '../../widget/common_view.dart';
+import 'mine_controller.dart';
+
+class MinePage extends BasePage<MineController> {
+  const MinePage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.mine);
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Color backgroundColor() {
+    return '#FAFAFA'.color;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        Assets.images.bgPageBackground.image(width: double.infinity),
+        SafeArea(
+          child: SingleChildScrollView(
+            child: Column(children: [
+              SizedBox(height: 70.w),
+              Row(
+                children: [
+                  SizedBox(width: 12.w),
+                  Obx(() {
+                    return controller.isLogin
+                        ? Assets.images.iconMineLogged
+                            .image(width: 54.w, height: 54.w)
+                        : Assets.images.iconMineNoLogin
+                            .image(width: 54.w, height: 54.w);
+                  }),
+                  SizedBox(width: 10.w),
+                  buildLoginInfo(),
+                  Spacer(),
+                  buildMemberTryOutView()
+                ],
+              ),
+              SizedBox(height: 20.w),
+              buildExperienceContent(),
+              SizedBox(height: 16.w),
+              buildFunList()
+            ]),
+          ),
+        ),
+        buildBackBtn(),
+      ],
+    );
+  }
+
+  Widget buildMineFunItem(
+      ImageProvider icon, String funName, VoidCallback onTap) {
+    return Container(
+      padding: EdgeInsets.symmetric(vertical: 15.w, horizontal: 12.w),
+      child: Row(
+        children: [
+          Image(image: icon, width: 24.w, height: 24.w),
+          SizedBox(width: 6.w),
+          Text(funName,
+              style: TextStyle(fontSize: 15.sp, color: '#202020'.color)),
+          Spacer(),
+          Assets.images.iconMineFunArrow.image(width: 20.w, height: 20.w)
+        ],
+      ),
+    );
+  }
+
+  Widget buildExperienceContent() {
+    return Stack(
+      children: [
+        Column(
+          children: [
+            AspectRatio(
+                aspectRatio: 332 / 57, child: SizedBox(width: double.infinity)),
+            Stack(
+              children: [
+                Container(
+                  margin: EdgeInsets.symmetric(horizontal: 14.w),
+                  width: double.infinity,
+                  height: 50.w,
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.only(
+                        bottomLeft: Radius.circular(8.w),
+                        bottomRight: Radius.circular(8.w)),
+                    gradient: LinearGradient(
+                        begin: Alignment.centerLeft,
+                        end: Alignment.centerRight,
+                        colors: ['#FFF8DA'.color, '#FFF1BA'.color]),
+                  ),
+                ),
+                Positioned(
+                  bottom: 0,
+                  left: 0,
+                  right: 0,
+                  child: Container(
+                    margin: EdgeInsets.symmetric(horizontal: 14.w),
+                    height: 32.w,
+                    child: Row(
+                      children: [
+                        SizedBox(width: 15.w),
+                        Assets.images.iconExperiment
+                            .image(width: 16.w, height: 16.w),
+                        SizedBox(width: 4.w),
+                        Text(StringName.memberExperienceVip,
+                            style: TextStyle(
+                                fontSize: 13.sp, color: '#8A5F03'.color)),
+                        Spacer(),
+                        Text(StringName.memberExperienceVipReceive,
+                            style: TextStyle(
+                                fontSize: 13.sp, color: '#8A5F03'.color)),
+                        Assets.images.iconMemberVipReceiveArrow
+                            .image(width: 16.w, height: 16.w),
+                        SizedBox(width: 13.w),
+                      ],
+                    ),
+                  ),
+                )
+              ],
+            )
+          ],
+        ),
+        buildMemberCard()
+      ],
+    );
+  }
+
+  Widget buildLoginInfo() {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTap: () => controller.onLoginClick(),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            children: [
+              Obx(() {
+                String desc = "";
+                if (controller.isLogin &&
+                    controller.phone?.isNotEmpty == true) {
+                  desc = controller.getUserName(controller.phone!);
+                } else {
+                  desc = StringName.mineAccountGoLogin;
+                }
+                return Text(desc,
+                    style: TextStyle(
+                        fontSize: 16.sp,
+                        color: '#333333'.color,
+                        fontWeight: FontWeight.bold));
+              }),
+              SizedBox(width: 6.w),
+              Obx(() {
+                return Visibility(
+                    visible: controller.isLogin &&
+                        controller.memberStatusInfo != null &&
+                        controller.memberStatusInfo?.expired == false,
+                    child: Assets.images.iconVip.image(width: 28.w));
+              })
+            ],
+          ),
+          SizedBox(height: 6.w),
+          buildLoginDesc(),
+        ],
+      ),
+    );
+  }
+
+  Container buildMemberTryOutView() {
+    return Container(
+      margin: EdgeInsets.only(right: 16.w),
+      decoration: BoxDecoration(
+          color: '#267B7DFF'.color, borderRadius: BorderRadius.circular(26.w)),
+      padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.w),
+      child: Text(StringName.memberTryOut,
+          style: TextStyle(fontSize: 12.sp, color: '#8163FF'.color)),
+    );
+  }
+
+  Widget buildBackBtn() {
+    return SafeArea(
+      child: GestureDetector(
+          onTap: controller.onBack,
+          child: Container(
+              margin: EdgeInsets.only(top: 16.w, left: 14.w),
+              child: CommonView.getBackBtnView())),
+    );
+  }
+
+  Widget buildLoginDesc() {
+    return Obx(() {
+      String txt = '';
+      if (!controller.isLogin) {
+        txt = StringName.mineNotLoginDesc;
+      } else if (controller.memberStatusInfo != null &&
+          controller.memberStatusInfo?.expired == false) {
+        txt = StringName.mineVip;
+      } else {
+        txt = StringName.mineOpenVip;
+      }
+      return Text(txt,
+          style: TextStyle(fontSize: 13.sp, color: '#727272'.color));
+    });
+  }
+
+  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()
+          ],
+        ),
+      ),
+    );
+  }
+
+  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 =
+            '${StringName.memberCardExpirationDesc} ${DateUtil.fromMillisecondsSinceEpoch('yyyy.MM.dd', controller.memberStatusInfo?.endTimestamp ?? 0)}';
+      }
+      return Text(desc,
+          style: TextStyle(fontSize: 12.sp, color: ColorName.white80));
+    });
+  }
+
+  Widget buildBuyMemberCardBtn() {
+    return Obx(() {
+      String txt = "";
+      if (!controller.isLogin ||
+          controller.memberStatusInfo == null ||
+          controller.memberStatusInfo?.expired == true) {
+        txt = StringName.memberVipUnlock;
+      } else if (controller.memberStatusInfo?.expired == false &&
+          controller.memberStatusInfo?.permanent == true) {
+        txt = StringName.mineMemberPermanent;
+      } else {
+        txt = StringName.memberVipRenew;
+      }
+      return Container(
+        margin: EdgeInsets.only(right: 20.w),
+        decoration: BoxDecoration(
+            color: ColorName.white, borderRadius: BorderRadius.circular(26.w)),
+        padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w),
+        child: Text(txt,
+            style: TextStyle(
+                fontSize: 12.sp,
+                color: '#5558FC'.color,
+                fontWeight: FontWeight.bold)),
+      );
+    });
+  }
+
+  Widget buildFunList() {
+    return Container(
+      decoration: BoxDecoration(
+          color: ColorName.white, borderRadius: BorderRadius.circular(12.w)),
+      margin: EdgeInsets.symmetric(horizontal: 12.w),
+      padding: EdgeInsets.symmetric(vertical: 5.w),
+      child: Column(
+        children: [
+          buildMineFunItem(Assets.images.iconMineFunShare.provider(),
+              StringName.mineFunShare, () => controller.onShareClick()),
+          buildMineFunItem(
+              Assets.images.iconMineFunCustomerService.provider(),
+              StringName.mineFunCustomerService,
+              () => controller.onCustomerServiceClick()),
+          buildMineFunItem(
+              Assets.images.iconMineFunPermissionSetting.provider(),
+              StringName.mineFunPermissionSetting,
+              () => controller.onPermissionSettingClick()),
+          buildMineFunItem(
+              Assets.images.iconMineFunAccountFeedback.provider(),
+              StringName.mineFunAccountFeedback,
+              () => controller.onAccountFeedbackClick()),
+          buildMineFunItem(Assets.images.iconMineFunAbout.provider(),
+              StringName.mineFunAbout, () => controller.onAboutClick()),
+          Obx(() {
+            return Visibility(
+              visible: controller.isLogin,
+              child: buildMineFunItem(
+                  Assets.images.iconMineFunLogoutAccount.provider(),
+                  StringName.mineFunLogoutAccount,
+                  () => controller.onLogoutAccountClick()),
+            );
+          }),
+          Obx(() {
+            return Visibility(
+              visible: controller.isLogin,
+              child: buildMineFunItem(
+                  Assets.images.iconMineFunExitAccount.provider(),
+                  StringName.mineFunExitAccount,
+                  () => controller.onFunExitAccountClick()),
+            );
+          }),
+        ],
+      ),
+    );
+  }
+}

+ 1 - 1
lib/module/splash/splash_page.dart

@@ -24,7 +24,7 @@ class SplashPage extends BasePage<SplashController> {
 
     return Stack(
       children: [
-        Assets.images.bgSplash.image(width: double.infinity),
+        Assets.images.bgPageBackground.image(width: double.infinity),
         Column(
           children: [
             SizedBox(height: 170.h),

+ 110 - 5
lib/resource/assets.gen.dart

@@ -16,9 +16,33 @@ class $AssetsImagesGen {
   AssetGenImage get bgAddFriendDialog =>
       const AssetGenImage('assets/images/bg_add_friend_dialog.webp');
 
-  /// File path: assets/images/bg_splash.webp
-  AssetGenImage get bgSplash =>
-      const AssetGenImage('assets/images/bg_splash.webp');
+  /// File path: assets/images/bg_login_head_container.webp
+  AssetGenImage get bgLoginHeadContainer =>
+      const AssetGenImage('assets/images/bg_login_head_container.webp');
+
+  /// File path: assets/images/bg_mine_member_card.webp
+  AssetGenImage get bgMineMemberCard =>
+      const AssetGenImage('assets/images/bg_mine_member_card.webp');
+
+  /// File path: assets/images/bg_page_background.webp
+  AssetGenImage get bgPageBackground =>
+      const AssetGenImage('assets/images/bg_page_background.webp');
+
+  /// File path: assets/images/icon_black_back.webp
+  AssetGenImage get iconBlackBack =>
+      const AssetGenImage('assets/images/icon_black_back.webp');
+
+  /// File path: assets/images/icon_checkbox_selected.webp
+  AssetGenImage get iconCheckboxSelected =>
+      const AssetGenImage('assets/images/icon_checkbox_selected.webp');
+
+  /// File path: assets/images/icon_checkbox_un_select.webp
+  AssetGenImage get iconCheckboxUnSelect =>
+      const AssetGenImage('assets/images/icon_checkbox_un_select.webp');
+
+  /// File path: assets/images/icon_experiment.webp
+  AssetGenImage get iconExperiment =>
+      const AssetGenImage('assets/images/icon_experiment.webp');
 
   /// File path: assets/images/icon_login_address_book.webp
   AssetGenImage get iconLoginAddressBook =>
@@ -76,14 +100,80 @@ class $AssetsImagesGen {
   AssetGenImage get iconMainRefreshMineLocation =>
       const AssetGenImage('assets/images/icon_main_refresh_mine_location.webp');
 
+  /// File path: assets/images/icon_member_vip_receive_arrow.webp
+  AssetGenImage get iconMemberVipReceiveArrow =>
+      const AssetGenImage('assets/images/icon_member_vip_receive_arrow.webp');
+
+  /// File path: assets/images/icon_mine_fun_about.webp
+  AssetGenImage get iconMineFunAbout =>
+      const AssetGenImage('assets/images/icon_mine_fun_about.webp');
+
+  /// File path: assets/images/icon_mine_fun_account_feedback.webp
+  AssetGenImage get iconMineFunAccountFeedback =>
+      const AssetGenImage('assets/images/icon_mine_fun_account_feedback.webp');
+
+  /// File path: assets/images/icon_mine_fun_arrow.webp
+  AssetGenImage get iconMineFunArrow =>
+      const AssetGenImage('assets/images/icon_mine_fun_arrow.webp');
+
+  /// File path: assets/images/icon_mine_fun_customer_service.webp
+  AssetGenImage get iconMineFunCustomerService =>
+      const AssetGenImage('assets/images/icon_mine_fun_customer_service.webp');
+
+  /// File path: assets/images/icon_mine_fun_exit_account.webp
+  AssetGenImage get iconMineFunExitAccount =>
+      const AssetGenImage('assets/images/icon_mine_fun_exit_account.webp');
+
+  /// File path: assets/images/icon_mine_fun_logout_account.webp
+  AssetGenImage get iconMineFunLogoutAccount =>
+      const AssetGenImage('assets/images/icon_mine_fun_logout_account.webp');
+
+  /// File path: assets/images/icon_mine_fun_permission_setting.webp
+  AssetGenImage get iconMineFunPermissionSetting => const AssetGenImage(
+      'assets/images/icon_mine_fun_permission_setting.webp');
+
+  /// File path: assets/images/icon_mine_fun_share.webp
+  AssetGenImage get iconMineFunShare =>
+      const AssetGenImage('assets/images/icon_mine_fun_share.webp');
+
+  /// File path: assets/images/icon_mine_logged.webp
+  AssetGenImage get iconMineLogged =>
+      const AssetGenImage('assets/images/icon_mine_logged.webp');
+
+  /// File path: assets/images/icon_mine_no_login.webp
+  AssetGenImage get iconMineNoLogin =>
+      const AssetGenImage('assets/images/icon_mine_no_login.webp');
+
+  /// File path: assets/images/icon_mine_small_vip.webp
+  AssetGenImage get iconMineSmallVip =>
+      const AssetGenImage('assets/images/icon_mine_small_vip.webp');
+
+  /// File path: assets/images/icon_mine_unlock_vip.webp
+  AssetGenImage get iconMineUnlockVip =>
+      const AssetGenImage('assets/images/icon_mine_unlock_vip.webp');
+
   /// File path: assets/images/icon_splash_title.webp
   AssetGenImage get iconSplashTitle =>
       const AssetGenImage('assets/images/icon_splash_title.webp');
 
+  /// File path: assets/images/icon_vip.webp
+  AssetGenImage get iconVip =>
+      const AssetGenImage('assets/images/icon_vip.webp');
+
+  /// File path: assets/images/icon_white_back.webp
+  AssetGenImage get iconWhiteBack =>
+      const AssetGenImage('assets/images/icon_white_back.webp');
+
   /// List of all assets
   List<AssetGenImage> get values => [
         bgAddFriendDialog,
-        bgSplash,
+        bgLoginHeadContainer,
+        bgMineMemberCard,
+        bgPageBackground,
+        iconBlackBack,
+        iconCheckboxSelected,
+        iconCheckboxUnSelect,
+        iconExperiment,
         iconLoginAddressBook,
         iconLoginClose,
         iconLoginGoWxArrow,
@@ -98,7 +188,22 @@ class $AssetsImagesGen {
         iconMainNews,
         iconMainRefreshFriendLocation,
         iconMainRefreshMineLocation,
-        iconSplashTitle
+        iconMemberVipReceiveArrow,
+        iconMineFunAbout,
+        iconMineFunAccountFeedback,
+        iconMineFunArrow,
+        iconMineFunCustomerService,
+        iconMineFunExitAccount,
+        iconMineFunLogoutAccount,
+        iconMineFunPermissionSetting,
+        iconMineFunShare,
+        iconMineLogged,
+        iconMineNoLogin,
+        iconMineSmallVip,
+        iconMineUnlockVip,
+        iconSplashTitle,
+        iconVip,
+        iconWhiteBack
       ];
 }
 

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

@@ -12,6 +12,9 @@ class StringName {
   static final String loadingTxt = 'loading_txt'.tr; // 正在加载
   static final String loadNoData = 'load_no_data'.tr; // 没有更多数据了
   static final String loadFailed = 'load_failed'.tr; // 加载失败
+  static final String networkError = 'network_error'.tr; // 网络异常
+  static final String privacyPolicy = 'privacy_policy'.tr; // 《隐私权政策》
+  static final String termOfService = 'term_of_service'.tr; // 《服务条款》
   static final String friendAddTitle = 'friend_add_title'.tr; // 添加好友
   static final String friendAddDesc = 'friend_add_desc'.tr; // 查看实时定位,开启轨迹守护
   static final String friendAddPhoneEtHint =
@@ -24,6 +27,74 @@ class StringName {
   static final String mainNewsTab = 'main_news_tab'.tr; // 消息中心
   static final String mainHelpTab = 'main_help_tab'.tr; // 一键求助
   static final String mainMineTab = 'main_mine_tab'.tr; // 个人中心
+  static final String mineAccountGoLogin = 'mine_account_go_login'.tr; // 点击登录
+  static final String mineAccountLoggedDesc =
+      'mine_account_logged_desc'.tr; // 用户
+  static final String mineNotLoginDesc = 'mine_not_login_desc'.tr; // 登录后可体验更多服务
+  static final String mineOpenVip = 'mine_open_vip'.tr; // 开通VIP可体验更多服务
+  static final String mineVip = 'mine_vip'.tr; // 您好,尊贵的VIP用户
+  static final String mineMemberPermanent =
+      'mine_member_permanent'.tr; // 您已是尊贵的永久会员
+  static final String memberLevel0 = 'member_level_0'.tr; // 未开通
+  static final String memberLevel100 = 'member_level_100'.tr; // 日卡会员
+  static final String memberLevel700 = 'member_level_700'.tr; // 周卡会员
+  static final String memberLevel3100 = 'member_level_3100'.tr; // 月度会员
+  static final String memberLevel9200 = 'member_level_9200'.tr; // 季度会员
+  static final String memberLevel36600 = 'member_level_36600'.tr; // 年度会员
+  static final String memberLevel3660000 = 'member_level_3660000'.tr; // 终身会员
+  static final String memberLevelUndefined = 'member_level_undefined'.tr; // 未知
+  static final String memberTryOut = 'member_try_out'.tr; // 会员试用
+  static final String memberCardNoLoginDesc =
+      'member_card_no_login_desc'.tr; // 升级VIP会员,享受更多权益
+  static final String memberCardNoVipDesc =
+      'member_card_no_vip_desc'.tr; // 开通VIP会员,享受更多权益
+  static final String memberCardExpirationDesc =
+      'member_card_expiration_desc'.tr; // 到期
+  static final String memberCardPermanentVipDesc =
+      'member_card_permanent_vip_desc'.tr; // 您已是尊贵的永久会员
+  static final String memberVipUnlock = 'member_vip_unlock'.tr; // 立即解锁
+  static final String memberVipRenew = 'member_vip_renew'.tr; // 立即续费
+  static final String memberVipPermanent = 'member_vip_permanent'.tr; // 永久会员
+  static final String memberExperienceVip =
+      'member_experience_vip'.tr; // 免费VIP体验礼包
+  static final String memberExperienceVipReceive =
+      'member_experience_vip_receive'.tr; // 去领取
+  static final String mineFunShare = 'mine_fun_share'.tr; // 邀请好友
+  static final String mineFunCustomerService =
+      'mine_fun_customer_service'.tr; // 专属客服
+  static final String mineFunPermissionSetting =
+      'mine_fun_permission_setting'.tr; // 权限设置
+  static final String mineFunAccountFeedback =
+      'mine_fun_account_feedback'.tr; // 用户反馈
+  static final String mineFunAbout = 'mine_fun_about'.tr; // 关于我们
+  static final String mineFunLogoutAccount =
+      'mine_fun_logout_account'.tr; // 注销账号
+  static final String mineFunExitAccount = 'mine_fun_exit_account'.tr; // 退出账号
+  static final String login = 'login'.tr; // 登录
+  static final String loginSendVerificationCode =
+      'login_send_verification_code'.tr; // 发送验证码
+  static final String loginRetransmissionCode =
+      'login_retransmission_code'.tr; // s后重发
+  static final String loginEtPhoneHint = 'login_et_phone_hint'.tr; // 请输入11位手机号码
+  static final String loginEtPrivacyRead = 'login_et_privacy_read'.tr; // 已阅读并同意
+  static final String loginEtPrivacyAnd = 'login_et_privacy_and'.tr; // 和
+  static final String loginPrintVerificationCode =
+      'login_print_verification_code'.tr; // 请输入验证码
+  static final String loginPrintPhoneVerification =
+      'login_print_phone_verification'.tr; // 请输入正确格式的手机号码
+  static final String loginAgreePrivacy =
+      'login_agree_privacy'.tr; // 请先阅读并同意《隐私权政策》和《服务条款》
+  static final String loginVerificationCodeErrorToast =
+      'login_verification_code_error_toast'.tr; // 验证码输入错误,请重新输入
+  static final String accountNoLogin = 'account_no_login'.tr; // 账号未登录
+  static final String loginRequestCodeFrequentlyToast =
+      'login_request_code_frequently_toast'.tr; // 请求过于频繁,请稍后再试
+  static final String loginVerificationCodeRequestFailedToast =
+      'login_verification_code_request_failed_toast'.tr; // 验证码发送失败,请重试
+  static final String loginSuccess = 'login_success'.tr; // 登录成功
+  static final String loginTooOftenToast =
+      'login_too_often_toast'.tr; // 登录过于频繁,请稍后再试
+  static final String loginFailedToast = 'login_failed_toast'.tr; // 登录失败
 }
 
 class StringMultiSource {
@@ -39,6 +110,9 @@ class StringMultiSource {
       'loading_txt': '正在加载',
       'load_no_data': '没有更多数据了',
       'load_failed': '加载失败',
+      'network_error': '网络异常',
+      'privacy_policy': '《隐私权政策》',
+      'term_of_service': '《服务条款》',
       'friend_add_title': '添加好友',
       'friend_add_desc': '查看实时定位,开启轨迹守护',
       'friend_add_phone_et_hint': '请输入手机号',
@@ -49,6 +123,53 @@ class StringMultiSource {
       'main_news_tab': '消息中心',
       'main_help_tab': '一键求助',
       'main_mine_tab': '个人中心',
+      'mine_account_go_login': '点击登录',
+      'mine_account_logged_desc': '用户',
+      'mine_not_login_desc': '登录后可体验更多服务',
+      'mine_open_vip': '开通VIP可体验更多服务',
+      'mine_vip': '您好,尊贵的VIP用户',
+      'mine_member_permanent': '您已是尊贵的永久会员',
+      'member_level_0': '未开通',
+      'member_level_100': '日卡会员',
+      'member_level_700': '周卡会员',
+      'member_level_3100': '月度会员',
+      'member_level_9200': '季度会员',
+      'member_level_36600': '年度会员',
+      'member_level_3660000': '终身会员',
+      'member_level_undefined': '未知',
+      'member_try_out': '会员试用',
+      'member_card_no_login_desc': '升级VIP会员,享受更多权益',
+      'member_card_no_vip_desc': '开通VIP会员,享受更多权益',
+      'member_card_expiration_desc': '到期',
+      'member_card_permanent_vip_desc': '您已是尊贵的永久会员',
+      'member_vip_unlock': '立即解锁',
+      'member_vip_renew': '立即续费',
+      'member_vip_permanent': '永久会员',
+      'member_experience_vip': '免费VIP体验礼包',
+      'member_experience_vip_receive': '去领取',
+      'mine_fun_share': '邀请好友',
+      'mine_fun_customer_service': '专属客服',
+      'mine_fun_permission_setting': '权限设置',
+      'mine_fun_account_feedback': '用户反馈',
+      'mine_fun_about': '关于我们',
+      'mine_fun_logout_account': '注销账号',
+      'mine_fun_exit_account': '退出账号',
+      'login': '登录',
+      'login_send_verification_code': '发送验证码',
+      'login_retransmission_code': 's后重发',
+      'login_et_phone_hint': '请输入11位手机号码',
+      'login_et_privacy_read': '已阅读并同意',
+      'login_et_privacy_and': '和',
+      'login_print_verification_code': '请输入验证码',
+      'login_print_phone_verification': '请输入正确格式的手机号码',
+      'login_agree_privacy': '请先阅读并同意《隐私权政策》和《服务条款》',
+      'login_verification_code_error_toast': '验证码输入错误,请重新输入',
+      'account_no_login': '账号未登录',
+      'login_request_code_frequently_toast': '请求过于频繁,请稍后再试',
+      'login_verification_code_request_failed_toast': '验证码发送失败,请重试',
+      'login_success': '登录成功',
+      'login_too_often_toast': '登录过于频繁,请稍后再试',
+      'login_failed_toast': '登录失败',
     },
   };
 }

+ 15 - 0
lib/router/app_pages.dart

@@ -1,8 +1,14 @@
 import 'package:get/get.dart';
 import 'package:location/di/get_it.dart';
+import 'package:location/module/browser/browser_controller.dart';
+import 'package:location/module/browser/browser_view.dart';
+import 'package:location/module/login/login_controller.dart';
 import 'package:location/module/main/main_page.dart';
+import 'package:location/module/mine/mine_page.dart';
 import '../module/add_friend/add_friend_dialog_controller.dart';
+import '../module/login/login_page.dart';
 import '../module/main/main_controller.dart';
+import '../module/mine/mine_controller.dart';
 import '../module/splash/splash_controller.dart';
 import '../module/splash/splash_page.dart';
 
@@ -14,6 +20,9 @@ abstract class RoutePath {
   static const splash = '/';
 
   static const mainTab = '/mainTab';
+  static const login = '/login';
+  static const mine = '/mine';
+  static const browser = '/browser';
 }
 
 class AppBinding extends Bindings {
@@ -22,6 +31,9 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<SplashController>());
     lazyPut(() => getIt.get<MainController>());
     lazyPut(() => getIt.get<AddFriendDialogController>());
+    lazyPut(() => getIt.get<LoginController>());
+    lazyPut(() => getIt.get<MineController>());
+    lazyPut(() => getIt.get<BrowserController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -32,4 +44,7 @@ class AppBinding extends Bindings {
 final generalPages = [
   GetPage(name: RoutePath.splash, page: () => SplashPage()),
   GetPage(name: RoutePath.mainTab, page: () => MainPage()),
+  GetPage(name: RoutePath.login, page: () => LoginPage()),
+  GetPage(name: RoutePath.mine, page: () => MinePage()),
+  GetPage(name: RoutePath.browser, page: () => BrowserPage()),
 ];

+ 3 - 3
lib/utils/app_info_util.dart

@@ -13,11 +13,11 @@ class AppInfoUtil {
 
   String? get appName => _packageInfo?.appName;
 
-  String? get packageName => "com.manbu.shouji";
+  String? get packageName => _packageInfo?.packageName;
 
-  String? get appVersionName => '3.2.1';
+  String? get appVersionName => _packageInfo?.version;
 
-  int? get appVersionCode => 321;
+  int? get appVersionCode => int.tryParse(_packageInfo?.buildNumber ?? '');
 }
 
 final appInfoUtil = AppInfoUtil._();

+ 206 - 0
lib/utils/async_util.dart

@@ -0,0 +1,206 @@
+import 'dart:async';
+
+typedef FutureCallback<T> = Future<T> Function();
+
+typedef IntervalCallback<T> = Future<T> Function(int times);
+
+typedef CancelCallback<T> = void Function();
+
+typedef FutureCompleter<T> = void Function(Completer<T> completer);
+
+typedef Predicate<T> = bool Function(T? value);
+
+class AsyncUtil {
+  AsyncUtil._();
+
+  static CancelableFuture<T> retryWithExponentialBackoff<T>(
+      FutureCallback<T> callback, int maxRetry,
+      {Predicate<dynamic>? predicate}) {
+    const Duration initialInterval = Duration(seconds: 1);
+    int retryCount = 0;
+    Timer? timer;
+
+    void attempt(Completer<T> completer) {
+      callback().then((value) {
+        if (!completer.isCompleted) {
+          completer.complete(value);
+        }
+      }).catchError((error) {
+        if (retryCount < maxRetry && (predicate == null || predicate(error))) {
+          retryCount++;
+          Duration nextInterval = initialInterval * (1 << (retryCount - 1));
+          timer = Timer(nextInterval, () => attempt(completer));
+        } else {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        }
+      });
+    }
+
+    return CancelableFuture<T>((completer) {
+      attempt(completer);
+    }, () {
+      timer?.cancel();
+    });
+  }
+
+  static CancelableFuture<T> retry<T>(
+      FutureCallback<T> callback, Duration interval,
+      {Duration? timeout, int? maxRetry, Predicate<dynamic>? predicate}) {
+    int retryCount = 0;
+    Timer? timer;
+    Timer? timeoutTimer;
+
+    void attempt(Completer<T> completer) {
+      callback().then((value) {
+        if (!completer.isCompleted) {
+          completer.complete(value);
+        }
+      }).catchError((error) {
+        if ((maxRetry == null || maxRetry <= 0 || retryCount < maxRetry) &&
+            (predicate == null || predicate(error))) {
+          retryCount++;
+          timer = Timer(interval, () => attempt(completer));
+        } else {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        }
+      });
+    }
+
+    return CancelableFuture<T>((completer) {
+      if (timeout != null) {
+        timeoutTimer = Timer(timeout, () {
+          if (!completer.isCompleted) {
+            completer.completeError(TimeoutException('Operation timed out'));
+          }
+        });
+      }
+      attempt(completer);
+    }, () {
+      timer?.cancel();
+      timeoutTimer?.cancel();
+    });
+  }
+
+  static CancelableFuture<T> delay<T>(
+      FutureCallback<T> callback, Duration interval) {
+    Timer? timer;
+
+    return CancelableFuture<T>((completer) {
+      timer = Timer(interval, () {
+        callback().then((value) {
+          if (!completer.isCompleted) {
+            completer.complete(value);
+          }
+        }).catchError((error) {
+          if (!completer.isCompleted) {
+            completer.completeError(error);
+          }
+        });
+      });
+    }, () {
+      timer?.cancel();
+    });
+  }
+
+  static StreamController<T> interval<T>(
+      IntervalCallback<T> callback, Duration interval, int times,
+      {Duration? delay}) {
+    Timer? timer;
+    StreamController<T> controller = StreamController<T>(onCancel: () {
+      timer?.cancel();
+    });
+    int count = 0;
+    void tick() {
+      callback(count).then((value) {
+        controller.add(value);
+        count++;
+        if (count < times) {
+          timer = Timer(interval, tick);
+        } else {
+          controller.close();
+        }
+      }).catchError((error) {
+        controller.addError(error);
+        controller.close();
+      });
+    }
+
+    if (delay != null && delay > Duration.zero) {
+      timer = Timer(delay, tick);
+    } else {
+      tick();
+    }
+    return controller;
+  }
+}
+
+abstract interface class Cancelable {
+  void cancel();
+}
+
+class CancelableFuture<T> implements Future<T>, Cancelable {
+  final Completer<T> _completer = Completer<T>();
+
+  final CancelCallback? _cancelable;
+
+  CancelableFuture(FutureCompleter<T> completer, this._cancelable) {
+    completer.call(_completer);
+  }
+
+  @override
+  void cancel() {
+    _cancelable?.call();
+    if (!_completer.isCompleted) {
+      _completer.completeError(CancelledError());
+    }
+  }
+
+  @override
+  Stream<T> asStream() => _completer.future.asStream();
+
+  @override
+  Future<T> catchError(Function onError, {bool Function(Object error)? test}) =>
+      _completer.future.catchError(onError, test: test);
+
+  @override
+  Future<R> then<R>(FutureOr<R> Function(T value) onValue,
+          {Function? onError}) =>
+      _completer.future.then(onValue, onError: onError);
+
+  @override
+  Future<T> timeout(Duration timeLimit, {FutureOr<T> Function()? onTimeout}) =>
+      _completer.future.timeout(timeLimit, onTimeout: onTimeout);
+
+  @override
+  Future<T> whenComplete(FutureOr<void> Function() action) =>
+      _completer.future.whenComplete(action);
+}
+
+class CancelledError extends Error {
+  @override
+  String toString() {
+    return 'Operation was cancelled';
+  }
+}
+
+extension CancelableFutureExtension<T> on Future<T> {
+  CancelableFuture<T> asCancelable(
+      FutureCompleter completer, CancelCallback? cancelable) {
+    CancelableFuture<T> cancelableFuture =
+        CancelableFuture(completer, cancelable);
+    then((value) {
+      if (!cancelableFuture._completer.isCompleted) {
+        cancelableFuture._completer.complete(value);
+      }
+    }).catchError((error) {
+      if (!cancelableFuture._completer.isCompleted) {
+        cancelableFuture._completer.completeError(error);
+      }
+    });
+    return cancelableFuture;
+  }
+}

+ 1 - 0
lib/utils/common_util.dart

@@ -0,0 +1 @@
+

+ 10 - 0
lib/utils/date_util.dart

@@ -0,0 +1,10 @@
+import 'package:intl/intl.dart';
+
+class DateUtil {
+  DateUtil._();
+
+  static String fromMillisecondsSinceEpoch(String format, int endTimestamp) {
+    final date = DateTime.fromMillisecondsSinceEpoch(endTimestamp);
+    return DateFormat(format).format(date);
+  }
+}

+ 3 - 0
lib/utils/toast_util.dart

@@ -4,6 +4,9 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
 class ToastUtil {
   ToastUtil._();
 
+  static int lengthShort = 2000;
+  static int lengthLong = 3500;
+
   static void show(String? msg,
       {Duration? displayTime,
       SmartToastType? displayType = SmartToastType.normal,

+ 16 - 0
lib/widget/common_view.dart

@@ -0,0 +1,16 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../resource/assets.gen.dart';
+
+class CommonView {
+  static Widget getBackBtnView() {
+    return Container(
+        padding: EdgeInsets.all(3.w),
+        decoration: BoxDecoration(
+            color: Colors.white,
+            borderRadius: BorderRadius.all(Radius.circular(10.w))),
+        child: Assets.images.iconBlackBack.image(width: 24.w, height: 24.w));
+  }
+}

+ 42 - 1
pubspec.lock

@@ -451,7 +451,7 @@ packages:
     source: hosted
     version: "2.6.2"
   intl:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: intl
       sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
@@ -610,6 +610,15 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.0"
+  oaid:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: "v0.0.1"
+      resolved-ref: "22c60c77cdbc3aa91277d83a711c659b74d24227"
+      url: "http://git.atmob.com:28999/Atmob-Flutter/Oaid.git"
+    source: git
+    version: "0.0.1"
   package_config:
     dependency: transitive
     description:
@@ -1031,6 +1040,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.0.2"
+  webview_flutter:
+    dependency: "direct main"
+    description:
+      name: webview_flutter
+      sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.10.0"
+  webview_flutter_android:
+    dependency: transitive
+    description:
+      name: webview_flutter_android
+      sha256: "512c26ccc5b8a571fd5d13ec994b7509f142ff6faf85835e243dde3538fdc713"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.2"
+  webview_flutter_platform_interface:
+    dependency: transitive
+    description:
+      name: webview_flutter_platform_interface
+      sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.10.0"
+  webview_flutter_wkwebview:
+    dependency: transitive
+    description:
+      name: webview_flutter_wkwebview
+      sha256: d7403ef4f042714c9ee2b26eaac4cadae7394cb0d4e608b1dd850c3ff96bd893
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.18.2"
   win32:
     dependency: transitive
     description:

+ 13 - 1
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.0.0+1
+version: 3.2.1+321
 
 environment:
   sdk: ^3.5.0
@@ -72,6 +72,12 @@ dependencies:
   #依赖注入
   injectable: 2.5.0
 
+  #日期格式化等
+  intl: 0.19.0
+
+  #网页跳转
+  webview_flutter: 4.10.0
+
   #日志打印
   atmob_logging:
     version: ^0.0.5
@@ -86,6 +92,12 @@ dependencies:
       name: atmob_channel_reader
       url: http://pub.v8dashen.com/
 
+  #oaid
+  oaid:
+    git:
+      url: http://git.atmob.com:28999/Atmob-Flutter/Oaid.git
+      ref: v0.0.1
+
 dev_dependencies:
   flutter_test:
     sdk: flutter