ソースを参照

[new]增加轨迹记录列表展示

zk 5 ヶ月 前
コミット
5e2abf9470
32 ファイル変更989 行追加176 行削除
  1. BIN
      assets/images/icon_net_mobile.webp
  2. BIN
      assets/images/icon_net_wifi.webp
  3. BIN
      assets/images/icon_track_detail_time_base_arrow.webp
  4. BIN
      assets/images/icon_track_error.webp
  5. BIN
      assets/images/icon_track_moving.webp
  6. BIN
      assets/images/icon_track_stay.webp
  7. BIN
      assets/images/icon_track_unlock.webp
  8. BIN
      assets/images/icon_track_unlock_no_permission.webp
  9. BIN
      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. 18 0
      lib/data/api/response/location_track_days_response.dart
  13. 21 0
      lib/data/api/response/location_track_days_response.g.dart
  14. 16 0
      lib/data/api/response/track_daily_response.dart
  15. 44 0
      lib/data/bean/track_daily_bean.dart
  16. 31 0
      lib/data/bean/track_daily_bean.g.dart
  17. 22 0
      lib/data/bean/track_days.dart
  18. 19 0
      lib/data/bean/track_days.g.dart
  19. 3 0
      lib/data/consts/constants.dart
  20. 49 1
      lib/data/repositories/track_repository.dart
  21. 21 27
      lib/module/track/track_controller.dart
  22. 3 0
      lib/module/track/track_day_detail/time_proportion/time_proportion_controller.dart
  23. 31 0
      lib/module/track/track_day_detail/time_proportion/time_proportion_view.dart
  24. 323 0
      lib/module/track/track_day_detail/track_daily_item.dart
  25. 70 0
      lib/module/track/track_day_detail/track_day_detail_controller.dart
  26. 134 0
      lib/module/track/track_day_detail/track_day_detail_view.dart
  27. 95 146
      lib/module/track/track_page.dart
  28. 6 0
      lib/module/track/track_status.dart
  29. 14 0
      lib/module/track/track_util.dart
  30. 1 1
      lib/socket/atmob_location_client.dart
  31. 44 0
      lib/widget/fixed_size_tab_indicator.dart
  32. 6 0
      pubspec.yaml

BIN
assets/images/icon_net_mobile.webp


BIN
assets/images/icon_net_wifi.webp


BIN
assets/images/icon_track_detail_time_base_arrow.webp


BIN
assets/images/icon_track_error.webp


BIN
assets/images/icon_track_moving.webp


BIN
assets/images/icon_track_stay.webp


BIN
assets/images/icon_track_unlock.webp


BIN
assets/images/icon_track_unlock_no_permission.webp


BIN
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);
 }

+ 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);
+}

+ 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;
+    });
+  }
 }

+ 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';
+    }
+  }
 }

+ 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);
+  }
+}

+ 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