Jelajahi Sumber

Merge branch 'v1.1.0' of git.atmob.com:Atmob-Flutter/location2025 into v1.1.0

zhoukun 9 bulan lalu
induk
melakukan
e806f0a99c
61 mengubah file dengan 1892 tambahan dan 372 penghapusan
  1. TEMPAT SAMPAH
      assets/images/icon_net_mobile.webp
  2. TEMPAT SAMPAH
      assets/images/icon_net_wifi.webp
  3. TEMPAT SAMPAH
      assets/images/icon_track_detail_time_base_arrow.webp
  4. TEMPAT SAMPAH
      assets/images/icon_track_error.webp
  5. TEMPAT SAMPAH
      assets/images/icon_track_moving.webp
  6. TEMPAT SAMPAH
      assets/images/icon_track_stay.webp
  7. TEMPAT SAMPAH
      assets/images/icon_track_unlock.webp
  8. TEMPAT SAMPAH
      assets/images/icon_track_unlock_no_permission.webp
  9. TEMPAT SAMPAH
      assets/images/img_track_ai_analyse.webp
  10. 8 1
      assets/string/base/string.xml
  11. 10 0
      lib/data/api/atmob_api.dart
  12. 77 0
      lib/data/api/atmob_api.g.dart
  13. 18 0
      lib/data/api/response/location_track_days_response.dart
  14. 21 0
      lib/data/api/response/location_track_days_response.g.dart
  15. 16 0
      lib/data/api/response/track_daily_response.dart
  16. 19 0
      lib/data/api/response/track_daily_response.g.dart
  17. 44 0
      lib/data/bean/track_daily_bean.dart
  18. 31 0
      lib/data/bean/track_daily_bean.g.dart
  19. 22 0
      lib/data/bean/track_days.dart
  20. 19 0
      lib/data/bean/track_days.g.dart
  21. 3 0
      lib/data/consts/constants.dart
  22. 49 1
      lib/data/repositories/track_repository.dart
  23. 10 10
      lib/di/get_it.config.dart
  24. 21 27
      lib/module/track/track_controller.dart
  25. 3 0
      lib/module/track/track_day_detail/time_proportion/time_proportion_controller.dart
  26. 31 0
      lib/module/track/track_day_detail/time_proportion/time_proportion_view.dart
  27. 323 0
      lib/module/track/track_day_detail/track_daily_item.dart
  28. 70 0
      lib/module/track/track_day_detail/track_day_detail_controller.dart
  29. 134 0
      lib/module/track/track_day_detail/track_day_detail_view.dart
  30. 95 146
      lib/module/track/track_page.dart
  31. 6 0
      lib/module/track/track_status.dart
  32. 14 0
      lib/module/track/track_util.dart
  33. 45 0
      lib/resource/assets.gen.dart
  34. 18 2
      lib/resource/string.gen.dart
  35. 1 1
      lib/socket/atmob_location_client.dart
  36. 44 0
      lib/widget/fixed_size_tab_indicator.dart
  37. 33 0
      plugins/mobile_use_statistics/.gitignore
  38. 33 0
      plugins/mobile_use_statistics/.metadata
  39. 3 0
      plugins/mobile_use_statistics/CHANGELOG.md
  40. 1 0
      plugins/mobile_use_statistics/LICENSE
  41. 15 0
      plugins/mobile_use_statistics/README.md
  42. 4 0
      plugins/mobile_use_statistics/analysis_options.yaml
  43. 9 0
      plugins/mobile_use_statistics/android/.gitignore
  44. 52 0
      plugins/mobile_use_statistics/android/build.gradle
  45. 1 0
      plugins/mobile_use_statistics/android/settings.gradle
  46. 2 0
      plugins/mobile_use_statistics/android/src/main/AndroidManifest.xml
  47. 38 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java
  48. 29 0
      plugins/mobile_use_statistics/android/src/test/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPluginTest.java
  49. 38 0
      plugins/mobile_use_statistics/ios/.gitignore
  50. 0 0
      plugins/mobile_use_statistics/ios/Assets/.gitkeep
  51. 19 0
      plugins/mobile_use_statistics/ios/Classes/MobileUseStatisticsPlugin.swift
  52. 14 0
      plugins/mobile_use_statistics/ios/Resources/PrivacyInfo.xcprivacy
  53. 29 0
      plugins/mobile_use_statistics/ios/mobile_use_statistics.podspec
  54. 3 0
      plugins/mobile_use_statistics/lib/flutter_mobile_statistics.dart
  55. 29 0
      plugins/mobile_use_statistics/lib/src/event/event.dart
  56. 24 0
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics.dart
  57. 41 0
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics_method_channel.dart
  58. 45 0
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics_platform_interface.dart
  59. 72 0
      plugins/mobile_use_statistics/pubspec.yaml
  60. 200 184
      pubspec.lock
  61. 6 0
      pubspec.yaml

TEMPAT SAMPAH
assets/images/icon_net_mobile.webp


TEMPAT SAMPAH
assets/images/icon_net_wifi.webp


TEMPAT SAMPAH
assets/images/icon_track_detail_time_base_arrow.webp


TEMPAT SAMPAH
assets/images/icon_track_error.webp


TEMPAT SAMPAH
assets/images/icon_track_moving.webp


TEMPAT SAMPAH
assets/images/icon_track_stay.webp


TEMPAT SAMPAH
assets/images/icon_track_unlock.webp


TEMPAT SAMPAH
assets/images/icon_track_unlock_no_permission.webp


TEMPAT SAMPAH
assets/images/img_track_ai_analyse.webp


+ 8 - 1
assets/string/base/string.xml

@@ -96,7 +96,7 @@
     <string name="privacy_agree">同意</string>
 
     <string name="location_mine">我</string>
-    <string name="location_trace">轨迹</string>
+    <string name="location_trace">每日轨迹</string>
 
     <string name="dialog_cancel">取消</string>
     <string name="dialog_sure">确定</string>
@@ -318,4 +318,11 @@
     <string name="account_replace_btn_txt">我知道了</string>
     <string name="account_please_select_avatar">请选择头像</string>
     <string name="account_select_avatar_btn_txt">立即更换</string>
+    <string name="track_detail_expand">全部展开</string>
+    <string name="track_detail_fold">全部折叠</string>
+    <string name="track_detail_moving">对方正在移动中</string>
+    <string name="track_detail_no_authorize">未授权</string>
+    <string name="track_detail_mobile">移动网络</string>
+    <string name="track_detail_error">当前对方定位丢失</string>
+    <string name="track_detail_time_proportion">地点占比时长</string>
 </resources>

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

@@ -26,6 +26,7 @@ import 'package:location/data/api/response/contact_list_response.dart';
 import 'package:location/data/api/response/contact_may_day_all_response.dart';
 import 'package:location/data/api/response/friends_list_response.dart';
 import 'package:location/data/api/response/item_list_response.dart';
+import 'package:location/data/api/response/location_track_days_response.dart';
 import 'package:location/data/api/response/login_response.dart';
 import 'package:location/data/api/response/member_status_response.dart';
 import 'package:location/data/api/response/member_trial_info_response.dart';
@@ -37,6 +38,7 @@ import 'package:location/data/api/response/query_track_response.dart';
 import 'package:location/data/api/response/request_friend_list_response.dart';
 import 'package:location/data/api/response/request_pay_response.dart';
 import 'package:location/data/api/response/subscription_check_response.dart';
+import 'package:location/data/api/response/track_daily_response.dart';
 import 'package:location/data/api/response/user_avatar_response.dart';
 import 'package:retrofit/error_logger.dart';
 import 'package:retrofit/http.dart';
@@ -205,4 +207,12 @@ abstract class AtmobApi {
   @POST("/s/v1/member/evaluate")
   Future<BaseResponse> memberEvaluate(
       @Body() AppBaseRequest request);
+
+  @POST("/s/v1/location/track/days")
+  Future<BaseResponse<LocationTrackDaysResponse>> locationTrackDays(
+      @Body() AppBaseRequest request);
+
+  @POST("/s/v1/location/track/daily/query")
+  Future<BaseResponse<TrackDailyResponse>> trackDailyQuery(
+      @Body() QueryTrackRequest request, @DioOptions() RequestOptions options);
 }

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

@@ -1547,6 +1547,83 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<LocationTrackDaysResponse>> locationTrackDays(
+      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<LocationTrackDaysResponse>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+            .compose(
+              _dio.options,
+              '/s/v1/location/track/days',
+              queryParameters: queryParameters,
+              data: _data,
+            )
+            .copyWith(
+                baseUrl: _combineBaseUrls(
+              _dio.options.baseUrl,
+              baseUrl,
+            )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<LocationTrackDaysResponse> _value;
+    try {
+      _value = BaseResponse<LocationTrackDaysResponse>.fromJson(
+        _result.data!,
+        (json) =>
+            LocationTrackDaysResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<TrackDailyResponse>> trackDailyQuery(
+    QueryTrackRequest request,
+    RequestOptions options,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final newOptions = newRequestOptions(options);
+    newOptions.extra.addAll(_extra);
+    newOptions.headers.addAll(_dio.options.headers);
+    newOptions.headers.addAll(_headers);
+    final _options = newOptions.copyWith(
+      method: 'POST',
+      baseUrl: _combineBaseUrls(
+        _dio.options.baseUrl,
+        baseUrl,
+      ),
+      queryParameters: queryParameters,
+      path: '/s/v1/location/track/daily/query',
+    )..data = _data;
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<TrackDailyResponse> _value;
+    try {
+      _value = BaseResponse<TrackDailyResponse>.fromJson(
+        _result.data!,
+        (json) => TrackDailyResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions newRequestOptions(Object? options) {
     if (options is RequestOptions) {
       return options as RequestOptions;

+ 18 - 0
lib/data/api/response/location_track_days_response.dart

@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/track_days.dart';
+
+part 'location_track_days_response.g.dart';
+
+@JsonSerializable()
+class LocationTrackDaysResponse {
+  @JsonKey(name: 'days')
+  List<TrackDays> days;
+
+  LocationTrackDaysResponse({
+    required this.days,
+  });
+
+  factory LocationTrackDaysResponse.fromJson(Map<String, dynamic> json) =>
+      _$LocationTrackDaysResponseFromJson(json);
+}

+ 21 - 0
lib/data/api/response/location_track_days_response.g.dart

@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'location_track_days_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+LocationTrackDaysResponse _$LocationTrackDaysResponseFromJson(
+        Map<String, dynamic> json) =>
+    LocationTrackDaysResponse(
+      days: (json['days'] as List<dynamic>)
+          .map((e) => TrackDays.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$LocationTrackDaysResponseToJson(
+        LocationTrackDaysResponse instance) =>
+    <String, dynamic>{
+      'days': instance.days,
+    };

+ 16 - 0
lib/data/api/response/track_daily_response.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/track_daily_bean.dart';
+
+part 'track_daily_response.g.dart';
+
+@JsonSerializable()
+class TrackDailyResponse {
+  @JsonKey(name: "list")
+  List<TrackDailyBean>? trackDailyList;
+
+  TrackDailyResponse({this.trackDailyList});
+
+  factory TrackDailyResponse.fromJson(Map<String, dynamic> json) =>
+      _$TrackDailyResponseFromJson(json);
+}

+ 19 - 0
lib/data/api/response/track_daily_response.g.dart

@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'track_daily_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TrackDailyResponse _$TrackDailyResponseFromJson(Map<String, dynamic> json) =>
+    TrackDailyResponse(
+      trackDailyList: (json['list'] as List<dynamic>?)
+          ?.map((e) => TrackDailyBean.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    );
+
+Map<String, dynamic> _$TrackDailyResponseToJson(TrackDailyResponse instance) =>
+    <String, dynamic>{
+      'list': instance.trackDailyList,
+    };

+ 44 - 0
lib/data/bean/track_daily_bean.dart

@@ -0,0 +1,44 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'track_daily_bean.g.dart';
+
+@JsonSerializable()
+class TrackDailyBean {
+  @JsonKey(name: 'addr')
+  String? addr;
+
+  @JsonKey(name: 'start')
+  int start;
+
+  @JsonKey(name: 'end')
+  int end;
+
+  @JsonKey(name: 'duration')
+  int duration;
+
+  @JsonKey(name: 'network')
+  String? network;
+
+  @JsonKey(name: 'status')
+  int status;
+
+  @JsonKey(name: 'totalUnlock')
+  int? totalUnlock;
+
+  @JsonKey(name: 'highUnlock')
+  int? highUnlock;
+
+  TrackDailyBean({
+    this.addr,
+    required this.start,
+    required this.end,
+    required this.duration,
+    this.network,
+    required this.status,
+    this.totalUnlock,
+    this.highUnlock,
+  });
+
+  factory TrackDailyBean.fromJson(Map<String, dynamic> json) =>
+      _$TrackDailyBeanFromJson(json);
+}

+ 31 - 0
lib/data/bean/track_daily_bean.g.dart

@@ -0,0 +1,31 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'track_daily_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TrackDailyBean _$TrackDailyBeanFromJson(Map<String, dynamic> json) =>
+    TrackDailyBean(
+      addr: json['addr'] as String?,
+      start: (json['start'] as num).toInt(),
+      end: (json['end'] as num).toInt(),
+      duration: (json['duration'] as num).toInt(),
+      network: json['network'] as String?,
+      status: (json['status'] as num).toInt(),
+      totalUnlock: (json['totalUnlock'] as num?)?.toInt(),
+      highUnlock: (json['highUnlock'] as num?)?.toInt(),
+    );
+
+Map<String, dynamic> _$TrackDailyBeanToJson(TrackDailyBean instance) =>
+    <String, dynamic>{
+      'addr': instance.addr,
+      'start': instance.start,
+      'end': instance.end,
+      'duration': instance.duration,
+      'network': instance.network,
+      'status': instance.status,
+      'totalUnlock': instance.totalUnlock,
+      'highUnlock': instance.highUnlock,
+    };

+ 22 - 0
lib/data/bean/track_days.dart

@@ -0,0 +1,22 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'track_days.g.dart';
+
+@JsonSerializable()
+class TrackDays {
+  @JsonKey(name: 'day')
+  String day;
+  @JsonKey(name: 'start')
+  int start;
+  @JsonKey(name: 'end')
+  int end;
+
+  TrackDays({
+    required this.day,
+    required this.start,
+    required this.end,
+  });
+
+  factory TrackDays.fromJson(Map<String, dynamic> json) =>
+      _$TrackDaysFromJson(json);
+}

+ 19 - 0
lib/data/bean/track_days.g.dart

@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'track_days.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TrackDays _$TrackDaysFromJson(Map<String, dynamic> json) => TrackDays(
+      day: json['day'] as String,
+      start: (json['start'] as num).toInt(),
+      end: (json['end'] as num).toInt(),
+    );
+
+Map<String, dynamic> _$TrackDaysToJson(TrackDays instance) => <String, dynamic>{
+      'day': instance.day,
+      'start': instance.start,
+      'end': instance.end,
+    };

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

@@ -44,6 +44,9 @@ class Constants {
   static const double blurredY = 4.2;
 
   static const String keyLastSelectFriendId = 'key_last_select_friend_id';
+
+  //此字段用于标记当前网络状态为移动网络
+  static const String kMobileNetworkTag = "<MOBILE>";
 }
 
 String getBaseUrl() {

+ 49 - 1
lib/data/repositories/track_repository.dart

@@ -5,16 +5,27 @@ import 'package:injectable/injectable.dart';
 import 'package:location/base/app_base_request.dart';
 import 'package:location/data/api/atmob_api.dart';
 
+import '../../di/get_it.dart';
 import '../../utils/http_handler.dart';
 import '../api/request/query_track_request.dart';
+import '../api/response/location_track_days_response.dart';
 import '../api/response/query_track_response.dart';
+import '../api/response/track_daily_response.dart';
+import '../bean/track_daily_bean.dart';
+import '../bean/track_days.dart';
 
 @lazySingleton
 class TrackRepository {
   final AtmobApi atmobApi;
 
+  List<TrackDays>? days;
+
   TrackRepository(this.atmobApi);
 
+  static TrackRepository getInstance() {
+    return getIt.get<TrackRepository>();
+  }
+
   Future<QueryTrackResponse> queryVirtualTrack() {
     return atmobApi
         .queryVirtualTrack(AppBaseRequest())
@@ -37,7 +48,44 @@ class TrackRepository {
 
   ///试用期间上报查看轨迹次数
   Future<void> refreshMemberTrailTrack() {
-    return atmobApi.memberTrailTrack(AppBaseRequest()).then(HttpHandler.handle(true));
+    return atmobApi
+        .memberTrailTrack(AppBaseRequest())
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<List<TrackDays>> getLocationTrackDays() async {
+    if (days == null || days?.isEmpty == true) {
+      final response = await _locationTrackDays();
+      return response.days;
+    } else {
+      return days!;
+    }
+  }
+
+  Future<LocationTrackDaysResponse> _locationTrackDays() {
+    return atmobApi
+        .locationTrackDays(AppBaseRequest())
+        .then(HttpHandler.handle(false))
+        .then((response) {
+      days = response.days;
+      return response;
+    });
   }
 
+  Future<List<TrackDailyBean>?> trackDailyQuery(
+      {required int? startTime,
+      required int? endTime,
+      required String? userId}) {
+    return atmobApi
+        .trackDailyQuery(
+            QueryTrackRequest(
+                startTime: startTime, endTime: endTime, userId: userId),
+            RequestOptions(
+                receiveTimeout: Duration(seconds: 30),
+                connectTimeout: Duration(minutes: 2)))
+        .then(HttpHandler.handle(true))
+        .then((response) {
+      return response.trackDailyList;
+    });
+  }
 }

+ 10 - 10
lib/di/get_it.config.dart

@@ -56,29 +56,29 @@ extension GetItInjectableX on _i174.GetIt {
       environmentFilter,
     );
     final networkModule = _$NetworkModule();
-    gh.factory<_i973.SplashController>(() => _i973.SplashController());
-    gh.factory<_i756.TrackDetailController>(
-        () => _i756.TrackDetailController());
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
-    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i108.PermissionSettingController>(
         () => _i108.PermissionSettingController());
+    gh.factory<_i973.SplashController>(() => _i973.SplashController());
+    gh.factory<_i756.TrackDetailController>(
+        () => _i756.TrackDetailController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.lazySingleton<_i220.AtmobLocationClient>(
         () => _i220.AtmobLocationClient());
     gh.singleton<_i243.AtmobApi>(
         () => networkModule.provideAtmobApi(gh<_i361.Dio>()));
-    gh.lazySingleton<_i240.TrackRepository>(
-        () => _i240.TrackRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
-    gh.lazySingleton<_i1053.FriendsRepository>(
-        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i850.ContactRepository>(
         () => _i850.ContactRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i1053.FriendsRepository>(
+        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i791.MessageRepository>(
         () => _i791.MessageRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i240.TrackRepository>(
+        () => _i240.TrackRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i983.UrgentContactRepository>(
         () => _i983.UrgentContactRepository(gh<_i243.AtmobApi>()));
     gh.factory<_i1008.LoginController>(
@@ -121,10 +121,10 @@ extension GetItInjectableX on _i174.GetIt {
               gh<_i983.UrgentContactRepository>(),
               gh<_i20.AccountRepository>(),
             ));
-    gh.factory<_i492.FriendSettingController>(
-        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.factory<_i897.AddFriendDialogController>(
         () => _i897.AddFriendDialogController(gh<_i1053.FriendsRepository>()));
+    gh.factory<_i492.FriendSettingController>(
+        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.lazySingleton<_i814.MemberRepository>(() => _i814.MemberRepository(
           gh<_i243.AtmobApi>(),
           gh<_i20.AccountRepository>(),

+ 21 - 27
lib/module/track/track_controller.dart

@@ -7,7 +7,7 @@ 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/location_info.dart';
-import 'package:location/data/bean/member_status_info.dart';
+import 'package:location/data/bean/track_days.dart';
 import 'package:location/data/consts/constants.dart';
 import 'package:location/data/consts/error_code.dart';
 import 'package:location/data/repositories/account_repository.dart';
@@ -31,8 +31,7 @@ import '../../utils/date_util.dart';
 import '../../utils/pair.dart';
 
 @injectable
-class TrackController extends BaseController
-    with GetSingleTickerProviderStateMixin {
+class TrackController extends BaseController {
   final int errorQueryOriginalDataEmpty = 10; //查询原始数据集为空
   final int errorQueryOriginalTooFew = 11; //查询原始数据集少于2点
 
@@ -59,7 +58,6 @@ class TrackController extends BaseController
 
   final Duration maxDuration = Duration(days: 1);
   final String timeFormat = "yyyy年-MM月-dd日 HH时:mm分";
-  late TabController tabController;
   final RxInt _currentIndex = 0.obs;
 
   int get currentIndex => _currentIndex.value;
@@ -75,6 +73,8 @@ class TrackController extends BaseController
 
   bool get isShowTraceDetailBtn => _isShowTraceDetailBtn.value;
 
+  final RxList<TrackDays> daysList = RxList<TrackDays>();
+
   final TrackRepository trackRepository;
   final FriendsRepository friendsRepository;
   final AccountRepository accountRepository;
@@ -88,12 +88,9 @@ class TrackController extends BaseController
     if (param is UserInfo) {
       _userInfo.value = param;
     }
-    tabController = TabController(
-        length: 2, vsync: this, initialIndex: _currentIndex.value);
-    tabController.addListener(_handleTabChange);
     _initTime();
     _onCurrentLocationQuery(isShow: false);
-
+    _onRequestTrackDateList();
   }
 
   @override
@@ -103,23 +100,23 @@ class TrackController extends BaseController
     _recordNumberTrajectoryViewed();
   }
 
+  void _onRequestTrackDateList() {
+    trackRepository.getLocationTrackDays().then((list) {
+      daysList.assignAll(list);
+    }).catchError((error) {
+      ErrorHandler.toastError(error);
+    });
+  }
+
   ///记录查看轨迹的次数
   void _recordNumberTrajectoryViewed() {
-    print("_recordNumberTrajectoryViewedsfsdf--${accountRepository.memberStatusInfo?.value?.trialed }---${accountRepository.memberStatusInfo?.value?.level}");
-    if (accountRepository.memberStatusInfo?.value?.trialed == true && accountRepository.memberStatusInfo?.value?.level == 20) {
+    if (accountRepository.memberStatusInfo.value?.trialed == true &&
+        accountRepository.memberStatusInfo.value?.level == 20) {
       trackRepository.refreshMemberTrailTrack();
     }
   }
 
-  void _handleTabChange() {
-    if (tabController.indexIsChanging) return;
-    _currentIndex.value = tabController.index;
-    if (tabController.index == 0) {
-      _showTrack();
-    } else if (tabController.index == 1) {
-      _showCurrentLocation();
-    }
-  }
+  void _handleTabChange() {}
 
   void _initTime() {
     //开始时间往前推一天
@@ -280,11 +277,6 @@ class TrackController extends BaseController
     });
   }
 
-  @override
-  void onClose() {
-    super.onClose();
-    tabController.dispose();
-  }
 
   void _setStartAndEndAddress(
       {required AtmobTrackPoint start, required AtmobTrackPoint end}) {
@@ -337,9 +329,6 @@ class TrackController extends BaseController
           : MarkerType.traceEndFriendPoint,
       customAvatarUrl: userInfo?.avatar,
     ));
-    //显示起点标记
-    // drawMarker();
-    //显示终点标记
   }
 
   void showQueryErrorDialog() {
@@ -357,6 +346,11 @@ class TrackController extends BaseController
     }
     TrackDetailPage.start(originPoints!);
   }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
 }
 
 class TrackQueryException implements Exception {

+ 3 - 0
lib/module/track/track_day_detail/time_proportion/time_proportion_controller.dart

@@ -0,0 +1,3 @@
+import 'package:location/base/base_controller.dart';
+
+class TimeProportionController extends BaseController {}

+ 31 - 0
lib/module/track/track_day_detail/time_proportion/time_proportion_view.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:location/base/base_view.dart';
+import 'package:location/module/track/track_day_detail/time_proportion/time_proportion_controller.dart';
+import 'package:location/utils/common_expand.dart';
+
+import '../../../../resource/string.gen.dart';
+
+class TimeProportionView extends BaseView<TimeProportionController> {
+  const TimeProportionView({super.key});
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Container(
+      padding: EdgeInsets.all(12.w),
+      child: Column(
+        children: [
+          Align(
+            alignment: Alignment.centerLeft,
+            child: Text(StringName.trackDetailTimeProportion,
+                style: TextStyle(
+                    fontSize: 13.sp,
+                    color: '#333333'.color,
+                    fontWeight: FontWeight.bold)),
+          )
+        ],
+      ),
+    );
+  }
+}

+ 323 - 0
lib/module/track/track_day_detail/track_daily_item.dart

@@ -0,0 +1,323 @@
+import 'dart:math';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:location/data/bean/track_daily_bean.dart';
+import 'package:location/data/consts/constants.dart';
+import 'package:location/module/track/track_util.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/utils/date_util.dart';
+
+import '../track_status.dart';
+
+Widget buildTrackDailyItem(TrackDailyBean bean, bool isEnd) {
+  return Container(
+    padding: EdgeInsets.symmetric(horizontal: 12.w),
+    margin: EdgeInsets.only(bottom: 8.w),
+    child: Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Builder(builder: (context) {
+          if (bean.status == TrackStatus.moving) {
+            return _buildMovingTrackDailyItem(bean);
+          } else if (bean.status == TrackStatus.stay) {
+            return _buildStayTrackDailyItem(bean);
+          } else if (bean.status == TrackStatus.abnormal) {
+            return _buildAbnormalTrackDailyItem(bean);
+          } else {
+            return SizedBox(height: 50.w, child: Text('未知轨迹,请更新最新应用版本'));
+          }
+        }),
+        if (isEnd) _buildEndPoint(bean)
+      ],
+    ),
+  );
+}
+
+Widget _buildMovingTrackDailyItem(TrackDailyBean bean) {
+  return Column(
+    crossAxisAlignment: CrossAxisAlignment.start,
+    children: [
+      IntrinsicHeight(
+        child: Row(
+          children: [
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                _buildTimeText(bean.start),
+                SizedBox(height: 4.w),
+                _buildRingView(),
+                SizedBox(height: 4.w),
+                Expanded(
+                  child: Container(
+                    width: 1.w,
+                    decoration: BoxDecoration(
+                      color: '#F0F0F0'.color,
+                      borderRadius: BorderRadius.circular(100.r),
+                    ),
+                  ),
+                )
+              ],
+            ),
+            Expanded(
+                child: Container(
+              height: 50.w,
+              margin: EdgeInsets.only(top: 26.w),
+              decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(8.r),
+                  gradient: LinearGradient(colors: [
+                    '#F8F5FF'.color,
+                    ColorName.transparent,
+                  ])),
+              padding: EdgeInsets.symmetric(horizontal: 14.w),
+              child: Row(
+                children: [
+                  Assets.images.iconTrackMoving.image(width: 16.w),
+                  SizedBox(width: 5.w),
+                  Text(
+                    StringName.trackDetailMoving,
+                    style: TextStyle(
+                        fontSize: 12.sp,
+                        color: '#333333'.color,
+                        fontWeight: FontWeight.bold),
+                  )
+                ],
+              ),
+            ))
+          ],
+        ),
+      ),
+      SizedBox(height: 8.w),
+    ],
+  );
+}
+
+Widget _buildStayTrackDailyItem(TrackDailyBean bean) {
+  return Column(
+    crossAxisAlignment: CrossAxisAlignment.start,
+    children: [
+      IntrinsicHeight(
+        child: Row(
+          children: [
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                _buildTimeText(bean.start),
+                SizedBox(height: 4.w),
+                _buildRingView(),
+                SizedBox(height: 4.w),
+                Expanded(
+                  child: Container(
+                    width: 1.w,
+                    decoration: BoxDecoration(
+                      color: '#F0F0F0'.color,
+                      borderRadius: BorderRadius.circular(100.r),
+                    ),
+                  ),
+                )
+              ],
+            ),
+            Expanded(
+                child: Container(
+              padding: EdgeInsets.all(10.w),
+              margin: EdgeInsets.only(top: 20.w),
+              decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(8.r),
+                  gradient: LinearGradient(colors: [
+                    '#F8F5FF'.color,
+                    ColorName.transparent,
+                  ])),
+              child: ConstrainedBox(
+                constraints: BoxConstraints(minHeight: 60.w),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Expanded(
+                      child: Text(
+                        bean.addr ?? '',
+                        style: TextStyle(
+                            fontSize: 12.sp,
+                            color: '#333333'.color,
+                            fontWeight: FontWeight.bold),
+                      ),
+                    ),
+                    SizedBox(height: 11.w),
+                    Row(
+                      children: [
+                        _buildStayDesc(bean.duration),
+                        SizedBox(width: 18.w),
+                        _buildLockDesc(bean.highUnlock, bean.totalUnlock),
+                        SizedBox(width: 18.w),
+                        Expanded(child: _buildNetDesc(bean.network))
+                      ],
+                    )
+                  ],
+                ),
+              ),
+            ))
+          ],
+        ),
+      ),
+      SizedBox(height: 8.w),
+    ],
+  );
+}
+
+Widget _buildAbnormalTrackDailyItem(TrackDailyBean bean) {
+  return Column(
+    crossAxisAlignment: CrossAxisAlignment.start,
+    children: [
+      IntrinsicHeight(
+        child: Row(
+          children: [
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                _buildTimeText(bean.start),
+                SizedBox(height: 4.w),
+                _buildRingView(isError: true),
+                SizedBox(height: 4.w),
+                Expanded(
+                  child: Container(
+                    width: 1.w,
+                    decoration: BoxDecoration(
+                      color: '#F0F0F0'.color,
+                      borderRadius: BorderRadius.circular(100.r),
+                    ),
+                  ),
+                )
+              ],
+            ),
+            Expanded(
+                child: Container(
+              height: 50.w,
+              padding: EdgeInsets.all(10.w),
+              margin: EdgeInsets.only(top: 41.w, bottom: 7.w),
+              decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(8.r),
+                  gradient: LinearGradient(colors: [
+                    '#FFECEC'.color,
+                    ColorName.white,
+                  ])),
+              child: Row(
+                children: [
+                  Assets.images.iconTrackError.image(width: 19.4.w),
+                  SizedBox(width: 5.5.w),
+                  Text(StringName.trackDetailError,
+                      style: TextStyle(
+                          fontSize: 12.sp,
+                          color: '#333333'.color,
+                          fontWeight: FontWeight.bold)),
+                  SizedBox(width: 10.w),
+                  Spacer(),
+                  Assets.images.imgTrackAiAnalyse.image(width: 73.w),
+                  SizedBox(width: 6.w),
+                ],
+              ),
+            ))
+          ],
+        ),
+      ),
+      SizedBox(height: 8.w),
+    ],
+  );
+}
+
+Widget _buildRingView({bool isError = false}) {
+  return Container(
+    width: 12.w,
+    height: 12.w,
+    decoration: BoxDecoration(
+      shape: BoxShape.circle,
+      border: Border.all(
+        color: isError ? '#F24D4D'.color : '#66999999'.color,
+        width: isError ? 2.w : 1.w,
+      ),
+    ),
+  );
+}
+
+Widget _buildTimeText(int time) {
+  return Text(
+    DateUtil.fromMillisecondsSinceEpoch('HH:mm', time),
+    style: TextStyle(
+        fontSize: 12.sp, color: '#333333'.color, fontWeight: FontWeight.w500),
+  );
+}
+
+Widget _buildEndPoint(TrackDailyBean bean) {
+  return Column(
+    children: [
+      SizedBox(height: 4.w),
+      _buildRingView(),
+      SizedBox(height: 4.w),
+      _buildTimeText(bean.end),
+    ],
+  );
+}
+
+Widget _buildNetDesc(String? network) {
+  bool isMobile = network == Constants.kMobileNetworkTag;
+  return IntrinsicWidth(
+    child: Row(
+      children: [
+        isMobile
+            ? Assets.images.iconNetMobile.image(width: 14.w, height: 14.w)
+            : Assets.images.iconNetWifi.image(width: 14.w, height: 14.w),
+        SizedBox(width: 2.w),
+        Expanded(
+          child: Text(
+            isMobile ? StringName.trackDetailMobile : network ?? '',
+            style:
+                TextStyle(fontSize: 11.sp, color: '#666666'.color, height: 1),
+          ),
+        )
+      ],
+    ),
+  );
+}
+
+Widget _buildLockDesc(int? highUnlock, int? totalUnlock) {
+  if ((highUnlock == null && totalUnlock == null) ||
+      (highUnlock == 0 && totalUnlock == 0)) {
+    return Row(
+      children: [
+        Assets.images.iconTrackUnlockNoPermission
+            .image(width: 14.w, height: 14.w),
+        SizedBox(width: 2.w),
+        Text(StringName.trackDetailNoAuthorize,
+            style:
+                TextStyle(fontSize: 11.sp, color: '#919DBE'.color, height: 1))
+      ],
+    );
+  } else {
+    return Row(
+      children: [
+        Assets.images.iconTrackStay.image(width: 14.w, height: 14.w),
+        SizedBox(width: 2.w),
+        if (highUnlock != null && highUnlock == 0)
+          Text('高频解锁$highUnlock次,共${totalUnlock ?? 0}次',
+              style:
+                  TextStyle(fontSize: 11.sp, color: '#666666'.color, height: 1))
+        else
+          Text('解锁${totalUnlock ?? 0}次',
+              style:
+                  TextStyle(fontSize: 11.sp, color: '#666666'.color, height: 1))
+      ],
+    );
+  }
+}
+
+Widget _buildStayDesc(int duration) {
+  return Row(
+    children: [
+      Assets.images.iconTrackStay.image(width: 14.w, height: 14.w),
+      SizedBox(width: 2.w),
+      Text(TrackUtil.formatDurationFromMillis(duration),
+          style: TextStyle(fontSize: 11.sp, color: '#666666'.color, height: 1))
+    ],
+  );
+}

+ 70 - 0
lib/module/track/track_day_detail/track_day_detail_controller.dart

@@ -0,0 +1,70 @@
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/base/base_controller.dart';
+import 'package:location/data/bean/track_daily_bean.dart';
+import 'package:location/data/repositories/track_repository.dart';
+import 'package:location/dialog/loading_dialog.dart';
+import 'package:location/handler/error_handler.dart';
+import 'package:location/module/track/track_controller.dart';
+import 'package:location/resource/string.gen.dart';
+
+import '../../../data/bean/track_days.dart';
+
+class TrackDayDetailController extends BaseController {
+  final TrackDays days;
+
+  late TrackRepository trackRepository;
+  final RxBool _trackNoData = RxBool(false);
+  final RxList<TrackDailyBean> trackDailyList = RxList<TrackDailyBean>();
+
+  final RxBool _isExpanded = RxBool(false);
+  final RxBool _isRequested = RxBool(false);
+
+  bool get isRequested => _isRequested.value;
+
+  bool get isExpanded => _isExpanded.value;
+
+  bool get trackNoData => _trackNoData.value;
+
+  final TrackController trackController = Get.find<TrackController>();
+
+  TrackDayDetailController(this.days, bool isExpand) {
+    trackRepository = TrackRepository.getInstance();
+    _isExpanded.value = isExpand;
+  }
+
+  @override
+  void onInit() {
+    super.onInit();
+    _requestTrackDaily();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  void _requestTrackDaily() {
+    _isRequested.value = false;
+    CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt);
+    trackRepository
+        .trackDailyQuery(
+            startTime: days.start,
+            endTime: days.end,
+            userId: trackController.userInfo?.id)
+        .then((list) {
+      CustomLoadingDialog.hide();
+      _isRequested.value = true;
+      _trackNoData.value = (list == null || list.isEmpty == true);
+      trackDailyList.assignAll(list ?? []);
+    }).catchError((error) {
+      CustomLoadingDialog.hide();
+      _isRequested.value = false;
+      ErrorHandler.toastError(error);
+    });
+  }
+
+  void onTrackDetailFoldClick() {
+    _isExpanded.value = !_isExpanded.value;
+  }
+}

+ 134 - 0
lib/module/track/track_day_detail/track_day_detail_view.dart

@@ -0,0 +1,134 @@
+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_view.dart';
+import 'package:location/data/bean/track_days.dart';
+import 'package:location/module/track/track_day_detail/time_proportion/time_proportion_view.dart';
+import 'package:location/module/track/track_day_detail/track_daily_item.dart';
+import 'package:location/module/track/track_day_detail/track_day_detail_controller.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/utils/common_expand.dart';
+
+class TrackDayDetailView extends BaseView<TrackDayDetailController> {
+  late final String trackTag;
+
+  TrackDayDetailView(TrackDays days, {super.key, bool isExpand = false}) {
+    trackTag = days.day;
+    Get.lazyPut(() => TrackDayDetailController(days, isExpand),
+        tag: trackTag, fenix: true);
+  }
+
+  @override
+  TrackDayDetailController get controller =>
+      Get.find<TrackDayDetailController>(tag: trackTag);
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Obx(() {
+      return Visibility(
+        visible: controller.isRequested,
+        child: _buildTrackDetailView(),
+      );
+    });
+  }
+
+  Widget _buildTrackDetailView() {
+    return Obx(() {
+      if (controller.trackNoData) {
+        return _buildTrackNoData();
+      }
+      return _buildTrackListView();
+    });
+  }
+
+  Widget _buildTrackListView() {
+    return Stack(
+      children: [
+        CustomScrollView(
+          slivers: [
+            buildSliverHistoryTrack(),
+            SliverToBoxAdapter(
+                child: Container(
+              height: 8.w,
+              color: '#F8F5FF'.color,
+            )),
+            SliverToBoxAdapter(
+              child: buildProportionDurationView(),
+            )
+          ],
+        ),
+        buildFoldView(),
+      ],
+    );
+  }
+
+  Widget _buildTrackNoData() {
+    return Container(
+      child: Text('无数据'),
+    );
+  }
+
+  Widget buildFoldView() {
+    return Obx(() {
+      return Visibility(
+        visible: !controller.trackNoData,
+        child: Positioned(
+          top: 2.w,
+          right: 5.w,
+          child: GestureDetector(
+            behavior: HitTestBehavior.translucent,
+            onTap: controller.onTrackDetailFoldClick,
+            child: Container(
+              padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 8.w),
+              child: Obx(() {
+                return Row(
+                  crossAxisAlignment: controller.isExpanded
+                      ? CrossAxisAlignment.end
+                      : CrossAxisAlignment.center,
+                  children: [
+                    Text(
+                      controller.isExpanded
+                          ? StringName.trackDetailExpand
+                          : StringName.trackDetailFold,
+                      style: TextStyle(fontSize: 10.sp, color: '#666666'.color),
+                    ),
+                    SizedBox(width: 1.w),
+                    Transform.rotate(
+                      angle: controller.isExpanded ? 3.1416 : 0,
+                      child: Assets.images.iconTrackDetailTimeBaseArrow
+                          .image(width: 10.w, height: 10.w),
+                    )
+                  ],
+                );
+              }),
+            ),
+          ),
+        ),
+      );
+    });
+  }
+
+  Widget buildProportionDurationView() {
+    return TimeProportionView();
+  }
+
+  Widget buildSliverHistoryTrack() {
+    return Obx(() {
+      return SliverPadding(
+        padding: EdgeInsets.only(top: 20.w, bottom: 12.w),
+        sliver: SliverList.builder(
+            itemBuilder: buildHistoryTrackItem,
+            itemCount: controller.trackDailyList.length),
+      );
+    });
+  }
+
+  Widget buildHistoryTrackItem(BuildContext context, int index) {
+    return buildTrackDailyItem(controller.trackDailyList[index],
+        index == controller.trackDailyList.length - 1);
+  }
+}

+ 95 - 146
lib/module/track/track_page.dart

@@ -8,6 +8,7 @@ import 'package:get/get_core/src/get_main.dart';
 import 'package:location/base/base_page.dart';
 import 'package:location/data/bean/user_info.dart';
 import 'package:location/module/track/track_controller.dart';
+import 'package:location/module/track/track_day_detail/track_day_detail_view.dart';
 import 'package:location/resource/assets.gen.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/resource/string.gen.dart';
@@ -15,8 +16,8 @@ import 'package:location/utils/common_expand.dart';
 import 'package:location/utils/common_style.dart';
 import 'package:location/utils/date_util.dart';
 import 'package:sliding_sheet2/sliding_sheet2.dart';
-
 import '../../router/app_pages.dart';
+import '../../utils/fixed_size_tab_indicator.dart';
 import '../../widget/common_view.dart';
 import '../../widget/relative_time_text.dart';
 
@@ -43,13 +44,7 @@ class TrackPage extends BasePage<TrackController> {
             controller: controller.mapController,
           ),
         ),
-        SafeArea(
-          child: Container(
-            margin: EdgeInsets.only(top: 14.w, left: 12.w),
-            child: GestureDetector(
-                onTap: controller.back, child: CommonView.getBackBtnView()),
-          ),
-        ),
+        buildBackBtnView(),
         SlidingSheet(
           color: ColorName.white,
           controller: controller.sheetController,
@@ -57,136 +52,120 @@ class TrackPage extends BasePage<TrackController> {
           shadowColor: Colors.black.withOpacity(0.1),
           cornerRadius: 18.w,
           snapSpec: SnapSpec(
-            initialSnap: 1,
+            initialSnap: 0.45,
             // Enable snapping. This is true by default.
             snap: true,
             // Set custom snapping points.
-            snappings: [SnapSpec.headerFooterSnap, 1.0],
+            snappings: [SnapSpec.headerSnap, 0.45, 1.0],
             // Define to what the snappings relate to. In this case,
             // the total available space that the sheet can expand to.
             positioning: SnapPositioning.relativeToAvailableSpace,
           ),
-          footerBuilder: (context, state) {
-            return buildOperationBtn();
-          },
           headerBuilder: (context, state) {
-            return IntrinsicHeight(
-                child: Column(
-              children: [
-                SizedBox(height: 5.w),
-                Align(
-                  alignment: Alignment.center,
-                  child: Container(
-                    width: 32.w,
-                    height: 3.w,
-                    decoration: BoxDecoration(
-                      color: '#D9D9D9'.color,
-                      borderRadius: BorderRadius.circular(49.w),
-                    ),
-                  ),
-                ),
-                SizedBox(height: 25.w),
-                buildTrackHeaderView(),
-              ],
-            ));
+            return buildSheetHeadView();
           },
           builder: (context, state) {
-            return Column(
-              children: [
-                SizedBox(
-                  width: double.infinity,
-                  height: 220.w,
-                  child: TabBarView(
-                      controller: controller.tabController,
-                      children: [
-                        buildTrackHistoryContentView(),
-                        buildTrackNowContentView()
-                      ]),
-                )
-              ],
-            );
+            return buildSheetContentView();
           },
         )
       ],
     );
   }
 
-  Widget buildOperationBtn() {
-    return Container(
-      margin: EdgeInsets.only(bottom: 18.w, top: 9.w),
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Obx(() {
-            return GestureDetector(
-              onTap: controller.onTraceDetailClick,
-              child: AnimatedOpacity(
-                opacity: controller.currentIndex == 0 ? 1 : 0,
-                duration: Duration(milliseconds: 250),
-                child: AnimatedContainer(
-                  width: controller.currentIndex == 0 &&
-                          controller.isShowTraceDetailBtn
-                      ? 152.w
-                      : 0.w,
-                  height: 46.w,
-                  decoration: BoxDecoration(
-                    color: '#147B7DFF'.color,
-                    border:
-                        Border.all(color: ColorName.colorPrimary, width: 1.w),
-                    borderRadius: BorderRadius.circular(46.w),
-                  ),
-                  duration: Duration(milliseconds: 250),
-                  child: Center(
-                    child: Text(
-                      maxLines: 1,
-                      StringName.traceDetail,
-                      style: TextStyle(
-                          fontSize: 14.sp, color: ColorName.colorPrimary),
-                    ),
-                  ),
-                ),
-              ),
-            );
-          }),
-          Obx(() {
-            double width = 152.w;
-            if (controller.currentIndex == 1) {
-              width = 322.w;
-            } else if (controller.isShowTraceDetailBtn) {
-              width = 152.w;
-            } else {
-              width = 322.w;
-            }
-            return GestureDetector(
-              onTap: controller.onTrackQueryClick,
-              child: AnimatedContainer(
-                margin: EdgeInsets.only(
-                    left: controller.currentIndex == 0 &&
-                            controller.isShowTraceDetailBtn
-                        ? 18.w
-                        : 0),
-                duration: Duration(milliseconds: 250),
-                width: width,
-                height: 46.w,
-                decoration: getPrimaryBtnDecoration(46.w),
-                child: Center(
-                  child: Obx(() {
-                    return Text(
-                      controller.currentIndex == 0
-                          ? StringName.trackQueryPath
-                          : StringName.trackNowLocation,
-                      style: TextStyle(fontSize: 14.sp, color: Colors.white),
+  Widget buildBackBtnView() {
+    return SafeArea(
+      child: GestureDetector(
+        onTap: controller.back,
+        child: Container(
+          margin: EdgeInsets.only(top: 14.w, left: 12.w),
+          decoration: BoxDecoration(boxShadow: [
+            BoxShadow(
+              color: Colors.black.withOpacity(0.05),
+              blurRadius: 10.w,
+              offset: Offset(0, 2.w),
+            ),
+          ]),
+          child: CommonView.getBackBtnView(),
+        ),
+      ),
+    );
+  }
+
+  Widget buildSheetContentView() {
+    return SizedBox(
+      height: 0.77.sh,
+      width: double.infinity,
+      child: Obx(() {
+        return DefaultTabController(
+          length: controller.daysList.length,
+          child: Column(
+            children: [
+              SizedBox(
+                width: double.infinity,
+                child: TabBar(
+                  tabAlignment: TabAlignment.start,
+                  isScrollable: true,
+                  dividerHeight: 0,
+                  indicator: FixedSizeTabIndicator(
+                      width: 26.w,
+                      height: 3.w,
+                      radius: 0,
+                      color: ColorName.colorPrimary),
+                  unselectedLabelStyle:
+                      TextStyle(fontSize: 13.sp, color: '#666666'.color),
+                  labelStyle: TextStyle(
+                      fontSize: 13.sp,
+                      color: '#333333'.color,
+                      fontWeight: FontWeight.bold),
+                  tabs: controller.daysList.map((e) {
+                    return Tab(
+                      text: e.day,
                     );
-                  }),
+                  }).toList(),
                 ),
               ),
-            );
-          })
-        ],
-      ),
+              Container(
+                color: '#EEEEEE'.color,
+                height: 1.w,
+                width: double.infinity,
+              ),
+              Expanded(
+                  child: TabBarView(
+                children: List.generate(
+                    controller.daysList.length,
+                    (index) => TrackDayDetailView(controller.daysList[index],
+                        isExpand: index == 0)),
+              )),
+            ],
+          ),
+        );
+      }),
     );
   }
 
+  Widget buildSheetHeadView() {
+    return IntrinsicHeight(
+        child: Column(
+      children: [
+        SizedBox(height: 5.w),
+        Align(
+          alignment: Alignment.center,
+          child: Container(
+            width: 32.w,
+            height: 3.w,
+            decoration: BoxDecoration(
+              color: '#D9D9D9'.color,
+              borderRadius: BorderRadius.circular(49.w),
+            ),
+          ),
+        ),
+        SizedBox(height: 25.w),
+        buildTrackHeaderView(),
+        SizedBox(height: 20.w),
+      ],
+    ));
+  }
+
   Widget buildTrackHeaderView() {
     return Row(
       children: [
@@ -208,41 +187,11 @@ class TrackPage extends BasePage<TrackController> {
                 fontWeight: FontWeight.bold),
           ),
         ),
-        buildOperationTabBar(),
         SizedBox(width: 12.w),
       ],
     );
   }
 
-  Widget buildOperationTabBar() {
-    return IntrinsicWidth(
-      child: Container(
-        padding: EdgeInsets.all(2.w),
-        decoration: BoxDecoration(
-          color: '#F3F3F3'.color,
-          borderRadius: BorderRadius.circular(48.w),
-        ),
-        height: 32.w,
-        child: TabBar(
-          controller: controller.tabController,
-          indicator: BoxDecoration(
-            color: ColorName.colorPrimary,
-            borderRadius: BorderRadius.circular(48.w),
-          ),
-          dividerHeight: 0,
-          indicatorSize: TabBarIndicatorSize.tab,
-          unselectedLabelStyle:
-              TextStyle(fontSize: 14.sp, color: '#666666'.color),
-          labelStyle: TextStyle(fontSize: 14.sp, color: ColorName.white),
-          tabs: [
-            Tab(text: StringName.trackHistory),
-            Tab(text: StringName.trackNowLocation)
-          ],
-        ),
-      ),
-    );
-  }
-
   Widget buildTrackHistoryContentView() {
     return Column(
       children: [

+ 6 - 0
lib/module/track/track_status.dart

@@ -0,0 +1,6 @@
+///0:移动中 1:停留 2:异常
+abstract class TrackStatus {
+  static const int moving = 0;
+  static const int stay = 1;
+  static const int abnormal = 2;
+}

+ 14 - 0
lib/module/track/track_util.dart

@@ -30,4 +30,18 @@ class TrackUtil {
         .map((e) => LatLng(latitude: e.latitude, longitude: e.longitude))
         .toList();
   }
+
+  static String formatDurationFromMillis(int milliseconds) {
+    final totalMinutes = milliseconds ~/ 60000; // 毫秒转分钟
+    final hours = totalMinutes ~/ 60;
+    final minutes = totalMinutes % 60;
+
+    if (hours > 0 && minutes > 0) {
+      return '${hours}h${minutes}min';
+    } else if (hours > 0) {
+      return '${hours}h';
+    } else {
+      return '${minutes}min';
+    }
+  }
 }

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

@@ -392,6 +392,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconMineUrgentContact =>
       const AssetGenImage('assets/images/icon_mine_urgent_contact.webp');
 
+  /// File path: assets/images/icon_net_mobile.webp
+  AssetGenImage get iconNetMobile =>
+      const AssetGenImage('assets/images/icon_net_mobile.webp');
+
+  /// File path: assets/images/icon_net_wifi.webp
+  AssetGenImage get iconNetWifi =>
+      const AssetGenImage('assets/images/icon_net_wifi.webp');
+
   /// File path: assets/images/icon_news.webp
   AssetGenImage get iconNews =>
       const AssetGenImage('assets/images/icon_news.webp');
@@ -404,6 +412,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconSplashTitle =>
       const AssetGenImage('assets/images/icon_splash_title.webp');
 
+  /// File path: assets/images/icon_track_detail_time_base_arrow.webp
+  AssetGenImage get iconTrackDetailTimeBaseArrow => const AssetGenImage(
+      'assets/images/icon_track_detail_time_base_arrow.webp');
+
+  /// File path: assets/images/icon_track_error.webp
+  AssetGenImage get iconTrackError =>
+      const AssetGenImage('assets/images/icon_track_error.webp');
+
   /// File path: assets/images/icon_track_location.webp
   AssetGenImage get iconTrackLocation =>
       const AssetGenImage('assets/images/icon_track_location.webp');
@@ -412,6 +428,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconTrackLocationNow =>
       const AssetGenImage('assets/images/icon_track_location_now.webp');
 
+  /// File path: assets/images/icon_track_moving.webp
+  AssetGenImage get iconTrackMoving =>
+      const AssetGenImage('assets/images/icon_track_moving.webp');
+
   /// File path: assets/images/icon_track_search.webp
   AssetGenImage get iconTrackSearch =>
       const AssetGenImage('assets/images/icon_track_search.webp');
@@ -424,6 +444,18 @@ class $AssetsImagesGen {
   AssetGenImage get iconTrackSelectTimeArrow =>
       const AssetGenImage('assets/images/icon_track_select_time_arrow.webp');
 
+  /// File path: assets/images/icon_track_stay.webp
+  AssetGenImage get iconTrackStay =>
+      const AssetGenImage('assets/images/icon_track_stay.webp');
+
+  /// File path: assets/images/icon_track_unlock.webp
+  AssetGenImage get iconTrackUnlock =>
+      const AssetGenImage('assets/images/icon_track_unlock.webp');
+
+  /// File path: assets/images/icon_track_unlock_no_permission.webp
+  AssetGenImage get iconTrackUnlockNoPermission =>
+      const AssetGenImage('assets/images/icon_track_unlock_no_permission.webp');
+
   /// File path: assets/images/icon_urgent_add.webp
   AssetGenImage get iconUrgentAdd =>
       const AssetGenImage('assets/images/icon_urgent_add.webp');
@@ -484,6 +516,10 @@ class $AssetsImagesGen {
   AssetGenImage get imgMemberUserCancelsContainer => const AssetGenImage(
       'assets/images/img_member_user_cancels_container.webp');
 
+  /// File path: assets/images/img_track_ai_analyse.webp
+  AssetGenImage get imgTrackAiAnalyse =>
+      const AssetGenImage('assets/images/img_track_ai_analyse.webp');
+
   /// File path: assets/images/img_track_example.webp
   AssetGenImage get imgTrackExample =>
       const AssetGenImage('assets/images/img_track_example.webp');
@@ -585,14 +621,22 @@ class $AssetsImagesGen {
         iconMineTrialExpirationVip,
         iconMineUnlockVip,
         iconMineUrgentContact,
+        iconNetMobile,
+        iconNetWifi,
         iconNews,
         iconNewsItem,
         iconSplashTitle,
+        iconTrackDetailTimeBaseArrow,
+        iconTrackError,
         iconTrackLocation,
         iconTrackLocationNow,
+        iconTrackMoving,
         iconTrackSearch,
         iconTrackSearchClear,
         iconTrackSelectTimeArrow,
+        iconTrackStay,
+        iconTrackUnlock,
+        iconTrackUnlockNoPermission,
         iconUrgentAdd,
         iconUrgentContactAdd,
         iconUrgentContactDialPhone,
@@ -608,6 +652,7 @@ class $AssetsImagesGen {
         imgMemberFirstWeekDiscountContainer,
         imgMemberRetainContainer,
         imgMemberUserCancelsContainer,
+        imgTrackAiAnalyse,
         imgTrackExample
       ];
 }

+ 18 - 2
lib/resource/string.gen.dart

@@ -81,7 +81,7 @@ class StringName {
   static String get privacyDisagreeAndExit => 'privacy_disagree_and_exit'.tr; // 不同意并退出
   static String get privacyAgree => 'privacy_agree'.tr; // 同意
   static String get locationMine => 'location_mine'.tr; // 我
-  static String get locationTrace => 'location_trace'.tr; // 轨迹
+  static String get locationTrace => 'location_trace'.tr; // 每日轨迹
   static String get dialogCancel => 'dialog_cancel'.tr; // 取消
   static String get dialogSure => 'dialog_sure'.tr; // 确定
   static String get dialogExitAccountTitle => 'dialog_exit_account_title'.tr; // 提示
@@ -264,6 +264,15 @@ class StringName {
   static String get accountReplaceBtnTxt => 'account_replace_btn_txt'.tr; // 我知道了
   static String get accountPleaseSelectAvatar => 'account_please_select_avatar'.tr; // 请选择头像
   static String get accountSelectAvatarBtnTxt => 'account_select_avatar_btn_txt'.tr; // 立即更换
+  static String get trackDetailExpand => 'track_detail_expand'.tr; // 全部展开
+  static String get trackDetailFold => 'track_detail_fold'.tr; // 全部折叠
+  static String get trackDetailMoving => 'track_detail_moving'.tr; // 对方正在移动中
+  static String get trackDetailNoAuthorize =>
+      'track_detail_no_authorize'.tr; // 未授权
+  static String get trackDetailMobile => 'track_detail_mobile'.tr; // 移动网络
+  static String get trackDetailError => 'track_detail_error'.tr; // 当前对方定位丢失
+  static String get trackDetailTimeProportion =>
+      'track_detail_time_proportion'.tr; // 地点占比时长
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -348,7 +357,7 @@ class StringMultiSource {
       'privacy_disagree_and_exit': '不同意并退出',
       'privacy_agree': '同意',
       'location_mine': '我',
-      'location_trace': '轨迹',
+      'location_trace': '每日轨迹',
       'dialog_cancel': '取消',
       'dialog_sure': '确定',
       'dialog_exit_account_title': '提示',
@@ -531,6 +540,13 @@ class StringMultiSource {
       'account_replace_btn_txt': '我知道了',
       'account_please_select_avatar': '请选择头像',
       'account_select_avatar_btn_txt': '立即更换',
+      'track_detail_expand': '全部展开',
+      'track_detail_fold': '全部折叠',
+      'track_detail_moving': '对方正在移动中',
+      'track_detail_no_authorize': '未授权',
+      'track_detail_mobile': '移动网络',
+      'track_detail_error': '当前对方定位丢失',
+      'track_detail_time_proportion': '地点占比时长',
     },
   };
 }

+ 1 - 1
lib/socket/atmob_location_client.dart

@@ -209,7 +209,7 @@ class AtmobLocationClient {
     if (_webSocket == null) {
       return;
     }
-    _webSocket!.sink.add(msg);
+    // _webSocket!.sink.add(msg);
     AtmobLog.d(tag, 'send location: $msg');
   }
 

+ 44 - 0
lib/widget/fixed_size_tab_indicator.dart

@@ -0,0 +1,44 @@
+import 'package:flutter/cupertino.dart';
+
+class FixedSizeTabIndicator extends Decoration {
+  final double width; // Fixed width
+  final double height; // Indicator height
+  final double radius; // Corner radius
+  final Color color; // Indicator color
+
+  const FixedSizeTabIndicator({
+    required this.width,
+    required this.height,
+    required this.radius,
+    required this.color,
+  });
+
+  @override
+  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
+    return _CustomPainter(this, onChanged);
+  }
+}
+
+class _CustomPainter extends BoxPainter {
+  final FixedSizeTabIndicator decoration;
+
+  _CustomPainter(this.decoration, VoidCallback? onChanged) : super(onChanged);
+
+  @override
+  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
+    final Paint paint = Paint();
+    paint.color = decoration.color;
+    paint.style = PaintingStyle.fill;
+
+    final double xPos =
+        offset.dx + (configuration.size!.width / 2) - (decoration.width / 2);
+    final double yPos = configuration.size!.height - decoration.height;
+
+    final Rect rect =
+        Rect.fromLTWH(xPos, yPos, decoration.width, decoration.height);
+    final RRect rRect = RRect.fromRectAndRadius(
+        rect, Radius.circular(decoration.radius)); // Rounded corners
+
+    canvas.drawRRect(rRect, paint);
+  }
+}

+ 33 - 0
plugins/mobile_use_statistics/.gitignore

@@ -0,0 +1,33 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+build/

+ 33 - 0
plugins/mobile_use_statistics/.metadata

@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
+  channel: "[user-branch]"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+      base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+    - platform: android
+      create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+      base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+    - platform: ios
+      create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+      base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 3 - 0
plugins/mobile_use_statistics/CHANGELOG.md

@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.

+ 1 - 0
plugins/mobile_use_statistics/LICENSE

@@ -0,0 +1 @@
+TODO: Add your license here.

+ 15 - 0
plugins/mobile_use_statistics/README.md

@@ -0,0 +1,15 @@
+# mobile_use_statistics
+
+手机使用情况统计插件
+
+## Getting Started
+
+This project is a starting point for a Flutter
+[plug-in package](https://flutter.dev/to/develop-plugins),
+a specialized package that includes platform-specific implementation code for
+Android and/or iOS.
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
+

+ 4 - 0
plugins/mobile_use_statistics/analysis_options.yaml

@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 9 - 0
plugins/mobile_use_statistics/android/.gitignore

@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx

+ 52 - 0
plugins/mobile_use_statistics/android/build.gradle

@@ -0,0 +1,52 @@
+group = "com.atmob.mobile_use_statistics"
+version = "1.0"
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath("com.android.tools.build:gradle:8.7.0")
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+apply plugin: "com.android.library"
+
+android {
+    namespace = "com.atmob.mobile_use_statistics"
+
+    compileSdk = 35
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
+    }
+
+    defaultConfig {
+        minSdk = 21
+    }
+
+    dependencies {
+        testImplementation("junit:junit:4.13.2")
+        testImplementation("org.mockito:mockito-core:5.0.0")
+    }
+
+    testOptions {
+        unitTests.all {
+            testLogging {
+                events "passed", "skipped", "failed", "standardOut", "standardError"
+                outputs.upToDateWhen { false }
+                showStandardStreams = true
+            }
+        }
+    }
+}

+ 1 - 0
plugins/mobile_use_statistics/android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'mobile_use_statistics'

+ 2 - 0
plugins/mobile_use_statistics/android/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.mobile_use_statistics"></manifest>

+ 38 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java

@@ -0,0 +1,38 @@
+package com.atmob.mobile_use_statistics;
+
+import androidx.annotation.NonNull;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+
+/** MobileUseStatisticsPlugin */
+public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandler {
+  /// The MethodChannel that will the communication between Flutter and native Android
+  ///
+  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+  /// when the Flutter Engine is detached from the Activity
+  private MethodChannel channel;
+
+  @Override
+  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mobile_use_statistics");
+    channel.setMethodCallHandler(this);
+  }
+
+  @Override
+  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+    if (call.method.equals("getPlatformVersion")) {
+      result.success("Android " + android.os.Build.VERSION.RELEASE);
+    } else {
+      result.notImplemented();
+    }
+  }
+
+  @Override
+  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+    channel.setMethodCallHandler(null);
+  }
+}

+ 29 - 0
plugins/mobile_use_statistics/android/src/test/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPluginTest.java

@@ -0,0 +1,29 @@
+package com.atmob.mobile_use_statistics;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import org.junit.Test;
+
+/**
+ * This demonstrates a simple unit test of the Java portion of this plugin's implementation.
+ *
+ * Once you have built the plugin's example app, you can run these tests from the command
+ * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
+ * you can run them directly from IDEs that support JUnit such as Android Studio.
+ */
+
+public class MobileUseStatisticsPluginTest {
+  @Test
+  public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
+    MobileUseStatisticsPlugin plugin = new MobileUseStatisticsPlugin();
+
+    final MethodCall call = new MethodCall("getPlatformVersion", null);
+    MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    plugin.onMethodCall(call, mockResult);
+
+    verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
+  }
+}

+ 38 - 0
plugins/mobile_use_statistics/ios/.gitignore

@@ -0,0 +1,38 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/ephemeral/
+/Flutter/flutter_export_environment.sh

+ 0 - 0
plugins/mobile_use_statistics/ios/Assets/.gitkeep


+ 19 - 0
plugins/mobile_use_statistics/ios/Classes/MobileUseStatisticsPlugin.swift

@@ -0,0 +1,19 @@
+import Flutter
+import UIKit
+
+public class MobileUseStatisticsPlugin: NSObject, FlutterPlugin {
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let channel = FlutterMethodChannel(name: "mobile_use_statistics", binaryMessenger: registrar.messenger())
+    let instance = MobileUseStatisticsPlugin()
+    registrar.addMethodCallDelegate(instance, channel: channel)
+  }
+
+  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+    switch call.method {
+    case "getPlatformVersion":
+      result("iOS " + UIDevice.current.systemVersion)
+    default:
+      result(FlutterMethodNotImplemented)
+    }
+  }
+}

+ 14 - 0
plugins/mobile_use_statistics/ios/Resources/PrivacyInfo.xcprivacy

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSPrivacyTrackingDomains</key>
+	<array/>
+	<key>NSPrivacyAccessedAPITypes</key>
+	<array/>
+	<key>NSPrivacyCollectedDataTypes</key>
+	<array/>
+	<key>NSPrivacyTracking</key>
+	<false/>
+</dict>
+</plist>

+ 29 - 0
plugins/mobile_use_statistics/ios/mobile_use_statistics.podspec

@@ -0,0 +1,29 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint mobile_use_statistics.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'mobile_use_statistics'
+  s.version          = '0.0.1'
+  s.summary          = '手机使用情况统计插件'
+  s.description      = <<-DESC
+手机使用情况统计插件
+                       DESC
+  s.homepage         = 'http://example.com'
+  s.license          = { :file => '../LICENSE' }
+  s.author           = { 'Your Company' => 'email@example.com' }
+  s.source           = { :path => '.' }
+  s.source_files = 'Classes/**/*'
+  s.dependency 'Flutter'
+  s.platform = :ios, '12.0'
+
+  # Flutter.framework does not contain a i386 slice.
+  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+  s.swift_version = '5.0'
+
+  # If your plugin requires a privacy manifest, for example if it uses any
+  # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
+  # plugin's privacy impact, and then uncomment this line. For more information,
+  # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
+  # s.resource_bundles = {'mobile_use_statistics_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+end

+ 3 - 0
plugins/mobile_use_statistics/lib/flutter_mobile_statistics.dart

@@ -0,0 +1,3 @@
+library mobile_use_statistics;
+
+export 'package:mobile_use_statistics/src/mobile_use_statistics.dart';

+ 29 - 0
plugins/mobile_use_statistics/lib/src/event/event.dart

@@ -0,0 +1,29 @@
+//每一次的屏幕事件
+//startTime 解锁屏幕时间
+//endTime 锁屏时间,可能为0 ,0 说明当前在使用没有锁屏
+class Event {
+  int startTime;
+  int endTime;
+
+  Event({required this.startTime, required this.endTime});
+
+
+  @override
+  String toString() {
+    return 'Event{startTime: $startTime, endTime: $endTime}';
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'startTime': startTime,
+      'endTime': endTime,
+    };
+  }
+
+  factory Event.fromJson(Map<String, dynamic> json) {
+    return Event(
+      startTime: json['startTime'] as int,
+      endTime: json['endTime'] as int,
+    );
+  }
+}

+ 24 - 0
plugins/mobile_use_statistics/lib/src/mobile_use_statistics.dart

@@ -0,0 +1,24 @@
+import 'package:mobile_use_statistics/src/event/event.dart';
+
+import 'mobile_use_statistics_platform_interface.dart';
+
+class MobileUseStatistics {
+  Future<List<Event>?> getLockScreenStatistics({
+    required int startTime,
+    required int endTime,
+  }) {
+    return MobileUseStatisticsPlatform.instance.getLockScreenStatistics(
+      startTime: startTime,
+      endTime: endTime,
+    );
+  }
+
+  Future<bool> hasUseStatisticsPermission() {
+    return MobileUseStatisticsPlatform.instance.hasUseStatisticsPermission();
+  }
+
+  Future<bool> requestUseStatisticsPermission() {
+    return MobileUseStatisticsPlatform.instance
+        .requestUseStatisticsPermission();
+  }
+}

+ 41 - 0
plugins/mobile_use_statistics/lib/src/mobile_use_statistics_method_channel.dart

@@ -0,0 +1,41 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:mobile_use_statistics/src/event/event.dart';
+
+import 'mobile_use_statistics_platform_interface.dart';
+
+/// An implementation of [MobileUseStatisticsPlatform] that uses method channels.
+class MethodChannelMobileUseStatistics extends MobileUseStatisticsPlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  final methodChannel = const MethodChannel('mobile_use_statistics');
+
+  @override
+  Future<List<Event>?> getLockScreenStatistics({
+    required int startTime,
+    required int endTime,
+  }) async {
+    final result = await methodChannel.invokeMethod<List<dynamic>>(
+      'getLockScreenStatistics',
+      <String, dynamic>{'startTime': startTime, 'endTime': endTime},
+    );
+
+    return result?.map((e) => Event.fromJson(e as Map<String, dynamic>)).toList();
+  }
+
+  @override
+  Future<bool> hasUseStatisticsPermission() async {
+    final hasPermission = await methodChannel.invokeMethod<bool>(
+      'hasUseStatisticsPermission',
+    );
+    return hasPermission ?? false;
+  }
+
+  @override
+  Future<bool> requestUseStatisticsPermission() async {
+    final hasPermission = await methodChannel.invokeMethod<bool>(
+      'requestUseStatisticsPermission',
+    );
+    return hasPermission ?? false;
+  }
+}

+ 45 - 0
plugins/mobile_use_statistics/lib/src/mobile_use_statistics_platform_interface.dart

@@ -0,0 +1,45 @@
+import 'package:mobile_use_statistics/src/event/event.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'mobile_use_statistics_method_channel.dart';
+
+abstract class MobileUseStatisticsPlatform extends PlatformInterface {
+  /// Constructs a MobileUseStatisticsPlatform.
+  MobileUseStatisticsPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static MobileUseStatisticsPlatform _instance =
+      MethodChannelMobileUseStatistics();
+
+  /// The default instance of [MobileUseStatisticsPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelMobileUseStatistics].
+  static MobileUseStatisticsPlatform get instance => _instance;
+
+  /// Platform-specific implementations should set this with their own
+  /// platform-specific class that extends [MobileUseStatisticsPlatform] when
+  /// they register themselves.
+  static set instance(MobileUseStatisticsPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  Future<List<Event>?> getLockScreenStatistics({required int startTime, required int endTime}) async {
+    throw UnimplementedError(
+      'getLockScreenStatistics() has not been implemented.',
+    );
+  }
+
+  Future<bool> hasUseStatisticsPermission() async {
+    throw UnimplementedError(
+      'hasUseStatisticsPermission() has not been implemented.',
+    );
+  }
+
+  Future<bool> requestUseStatisticsPermission() async {
+    throw UnimplementedError(
+      'requestUseStatisticsPermission() has not been implemented.',
+    );
+  }
+}

+ 72 - 0
plugins/mobile_use_statistics/pubspec.yaml

@@ -0,0 +1,72 @@
+name: mobile_use_statistics
+description: "手机使用情况统计插件"
+version: 0.0.1
+homepage:
+
+environment:
+  sdk: ^3.7.2
+  flutter: '>=3.3.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.2
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^5.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+  # This section identifies this Flutter project as a plugin project.
+  # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
+  # which should be registered in the plugin registry. This is required for
+  # using method channels.
+  # The Android 'package' specifies package in which the registered class is.
+  # This is required for using method channels on Android.
+  # The 'ffiPlugin' specifies that native code should be built and bundled.
+  # This is required for using `dart:ffi`.
+  # All these are used by the tooling to maintain consistency when
+  # adding or updating assets for this project.
+  plugin:
+    platforms:
+      android:
+        package: com.atmob.mobile_use_statistics
+        pluginClass: MobileUseStatisticsPlugin
+      ios:
+        pluginClass: MobileUseStatisticsPlugin
+
+  # To add assets to your plugin package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/to/asset-from-package
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/to/resolution-aware-images
+
+  # To add custom fonts to your plugin package, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts in packages, see
+  # https://flutter.dev/to/font-from-package

File diff ditekan karena terlalu besar
+ 200 - 184
pubspec.lock


+ 6 - 0
pubspec.yaml

@@ -130,6 +130,12 @@ dependencies:
   #图片缓存
   cached_network_image: ^3.4.1
 
+  #检测组件是否进入/离开可视区域
+  visibility_detector: ^0.4.0+2
+
+  #饼图等
+  fl_chart: ^1.0.0
+
   #跳转评价
   in_app_review: ^2.0.10