Jelajahi Sumber

[new]增加轨迹详情显示&轨迹搜索功能

zk 8 bulan lalu
induk
melakukan
a65c780d8f

TEMPAT SAMPAH
assets/images/icon_track_location.webp


TEMPAT SAMPAH
assets/images/icon_track_search.webp


TEMPAT SAMPAH
assets/images/icon_track_search_clear.webp


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

@@ -288,4 +288,6 @@
     <string name="member_payment_failed">开通失败,请稍后重试</string>
     <string name="exit_app_tip">再按一次退出应用</string>
     <string name="trace_detail">轨迹详情</string>
+    <string name="trace_detail_search_hint">查找地址</string>
+    <string name="trace_detail_title">Ta的线路轨迹</string>
 </resources>

+ 2 - 0
lib/data/bean/atmob_track_point.dart

@@ -28,6 +28,8 @@ class AtmobTrackPoint {
   @JsonKey(name: "id")
   int? id;
 
+  int? traceType;
+
   AtmobTrackPoint(
       {required this.longitude,
       required this.latitude,

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

@@ -1,31 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'atmob_track_point.dart';
-
-// **************************************************************************
-// JsonSerializableGenerator
-// **************************************************************************
-
-AtmobTrackPoint _$AtmobTrackPointFromJson(Map<String, dynamic> json) =>
-    AtmobTrackPoint(
-      longitude: (json['lng'] as num).toDouble(),
-      latitude: (json['lat'] as num).toDouble(),
-      time: (json['ts'] as num).toInt(),
-      speed: (json['speed'] as num?)?.toDouble(),
-      bearing: (json['bearing'] as num?)?.toDouble(),
-      addr: json['addr'] as String?,
-      photo: json['photo'] as String?,
-      id: (json['id'] as num?)?.toInt(),
-    );
-
-Map<String, dynamic> _$AtmobTrackPointToJson(AtmobTrackPoint instance) =>
-    <String, dynamic>{
-      'lng': instance.longitude,
-      'lat': instance.latitude,
-      'ts': instance.time,
-      'speed': instance.speed,
-      'bearing': instance.bearing,
-      'addr': instance.addr,
-      'photo': instance.photo,
-      'id': instance.id,
-    };

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

@@ -36,6 +36,7 @@ import '../module/news/pending_list/news_pending_list_controller.dart' as _i433;
 import '../module/permission/permission_setting_controller.dart' as _i108;
 import '../module/splash/splash_controller.dart' as _i973;
 import '../module/track/track_controller.dart' as _i518;
+import '../module/track/track_detail/track_detail_controller.dart' as _i756;
 import '../module/urgent_contact/add_contact/add_urgent_contact_controller.dart'
     as _i955;
 import '../module/urgent_contact/urgent_contact_controller.dart' as _i720;
@@ -61,6 +62,8 @@ extension GetItInjectableX on _i174.GetIt {
     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());

+ 11 - 1
lib/module/track/track_controller.dart

@@ -15,6 +15,7 @@ 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/member/member_page.dart';
+import 'package:location/module/track/track_detail/track_detail_page.dart';
 import 'package:location/module/track/track_util.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/atmob_log.dart';
@@ -67,6 +68,7 @@ class TrackController extends BaseController
   LocationInfo? get currentLocation => _currentLocation.value;
 
   List<LatLng>? points;
+  List<AtmobTrackPoint>? originPoints;
 
   final RxBool _isShowTraceDetailBtn = false.obs;
 
@@ -200,6 +202,8 @@ class TrackController extends BaseController
     LoadingDialog.show(StringName.trackLoadingTxt);
     _startAddress.value = '';
     _endAddress.value = '';
+    originPoints = null;
+    points = null;
     Future.value().then((_) {
       if (userInfo?.virtual == true) {
         return trackRepository.queryVirtualTrack();
@@ -242,6 +246,7 @@ class TrackController extends BaseController
       }
       return Pair(pointsList, convertList);
     }).then((pair) {
+      originPoints = pair.first;
       points = pair.second;
       _showTrack();
       _setStartAndEndAddress(start: pair.first.first, end: pair.first.last);
@@ -328,10 +333,15 @@ class TrackController extends BaseController
   }
 
   void onTraceDetailClick() {
-    if (points == null || points!.length < 2) {
+    // if (accountRepository.memberIsExpired()) {
+    //   MemberPage.start();
+    //   return;
+    // }
+    if (originPoints == null || originPoints!.length < 2) {
       showTraceNoDataDialog(onConfirm: () {});
       return;
     }
+    TrackDetailPage.start(originPoints!);
   }
 }
 

+ 72 - 0
lib/module/track/track_detail/track_detail_controller.dart

@@ -0,0 +1,72 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/base/base_controller.dart';
+import '../../../data/bean/atmob_track_point.dart';
+
+@injectable
+class TrackDetailController extends BaseController {
+  static const int lineStart = 1;
+  static const int lineEnd = 2;
+
+  final List<AtmobTrackPoint> originPoints = [];
+  final RxList<AtmobTrackPoint> pointList = RxList();
+
+  final TextEditingController searchController = TextEditingController();
+  final RxString _searchTxt = ''.obs;
+
+  String get searchTxt => _searchTxt.value;
+
+  @override
+  void onInit() {
+    super.onInit();
+    final argument = Get.arguments;
+    if (argument != null && argument is List<AtmobTrackPoint>) {
+      if (argument.length >= 2) {
+        argument.first.traceType = lineStart;
+        argument.last.traceType = lineEnd;
+      }
+      originPoints.addAll(argument);
+    }
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+    _dealSearchList();
+  }
+
+  void back() {
+    Get.back();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    searchController.dispose();
+  }
+
+  void onSearch(String txt) {
+    _searchTxt.value = txt;
+    _dealSearchList();
+  }
+
+  void _dealSearchList() {
+    if (_searchTxt.value.isEmpty) {
+      pointList.clear();
+      pointList.addAll(originPoints);
+    } else {
+      pointList.clear();
+      pointList.addAll(originPoints.where((element) {
+        return element.addr?.contains(_searchTxt.value) == true;
+      }));
+    }
+  }
+
+  void onClearSearchTxt() {
+    searchController.clear();
+    _searchTxt.value = '';
+    _dealSearchList();
+  }
+}

+ 199 - 0
lib/module/track/track_detail/track_detail_page.dart

@@ -0,0 +1,199 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/base/base_page.dart';
+import 'package:location/module/track/track_detail/track_detail_controller.dart';
+import 'package:location/resource/assets.gen.dart';
+import 'package:location/resource/colors.gen.dart';
+import 'package:location/resource/string.gen.dart';
+import 'package:location/router/app_pages.dart';
+import 'package:location/utils/common_expand.dart';
+import 'package:location/utils/date_util.dart';
+import 'package:location/widget/common_view.dart';
+
+import '../../../data/bean/atmob_track_point.dart';
+import '../../../utils/dashed_line_painter.dart';
+
+class TrackDetailPage extends BasePage<TrackDetailController> {
+  const TrackDetailPage({super.key});
+
+  static void start(List<AtmobTrackPoint>? points) {
+    Get.toNamed(RoutePath.trackDetail, arguments: points);
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Column(
+      children: [
+        CommonView.buildAppBar(StringName.traceDetail,
+            titleCenter: false, backOnTap: controller.back),
+        SizedBox(height: 12.w),
+        buildSearchView(),
+        Expanded(
+            child: CustomScrollView(slivers: [
+          SliverToBoxAdapter(
+            child: Container(
+              padding: EdgeInsets.only(left: 24.w, top: 32.w, bottom: 13.w),
+              child: Text(
+                StringName.traceDetailTitle,
+                style: TextStyle(
+                    fontSize: 15.sp,
+                    color: '#202020'.color,
+                    fontWeight: FontWeight.bold),
+              ),
+            ),
+          ),
+          Obx(() {
+            return SliverList.builder(
+                itemBuilder: buildTrackItem,
+                itemCount: controller.pointList.length);
+          })
+        ]))
+      ],
+    );
+  }
+
+  Widget buildSearchView() {
+    return Container(
+      width: double.infinity,
+      height: 52.w,
+      margin: EdgeInsets.symmetric(horizontal: 12.w),
+      decoration: BoxDecoration(
+        color: ColorName.white,
+        borderRadius: BorderRadius.circular(12.w),
+        border: Border.all(color: '#333738'.color, width: 1.w),
+      ),
+      child: Row(
+        children: [
+          SizedBox(width: 16.w),
+          Assets.images.iconTrackSearch.image(width: 22.w, height: 22.w),
+          SizedBox(width: 6.w),
+          Expanded(
+            child: TextField(
+                onChanged: (txt) {
+                  controller.onSearch(txt);
+                },
+                style: TextStyle(fontSize: 15.sp, color: ColorName.black90),
+                controller: controller.searchController,
+                maxLines: 1,
+                maxLength: 30,
+                keyboardType: TextInputType.text,
+                textAlignVertical: TextAlignVertical.center,
+                textInputAction: TextInputAction.next,
+                decoration: InputDecoration(
+                    hintText: StringName.traceDetailSearchHint,
+                    counterText: '',
+                    hintStyle:
+                        TextStyle(fontSize: 15.sp, color: "#A7A7A7".toColor()),
+                    contentPadding: const EdgeInsets.all(0),
+                    border:
+                        const OutlineInputBorder(borderSide: BorderSide.none))),
+          ),
+          SizedBox(width: 16.w),
+          Obx(() {
+            return GestureDetector(
+              onTap: controller.onClearSearchTxt,
+              child: Visibility(
+                  visible: controller.searchTxt.isNotEmpty,
+                  child: Assets.images.iconTrackSearchClear
+                      .image(width: 20.w, height: 20.w)),
+            );
+          }),
+          SizedBox(width: 16.w),
+        ],
+      ),
+    );
+  }
+
+  Widget buildTrackItem(BuildContext context, int index) {
+    final item = controller.pointList[index];
+    bool isFirst = index == 0;
+    bool isLast = index == controller.pointList.length - 1;
+    bool isFirstPoint = item.traceType == TrackDetailController.lineStart;
+    bool isLastPoint = item.traceType == TrackDetailController.lineEnd;
+    return Row(
+      children: [
+        SizedBox(width: 24.w),
+        Column(
+          children: [
+            Visibility(
+                maintainSize: true,
+                maintainAnimation: true,
+                maintainState: true,
+                visible: !isFirst,
+                child: Padding(
+                  padding: EdgeInsets.only(top: 1.w),
+                  child: VerticalDashedLine(
+                      dashSpace: 2.w,
+                      color: '#F0F0F0'.color,
+                      height: 35.w,
+                      dashLength: 3.w,
+                      strokeWidth: 1.w),
+                )),
+            SizedBox(height: 8.w),
+            SizedBox(
+              width: 20.w,
+              height: 20.w,
+              child: Builder(builder: (context) {
+                if (isFirstPoint) {
+                  return Container(
+                      margin: EdgeInsets.all(3.5.w),
+                      decoration: BoxDecoration(
+                          border:
+                              Border.all(color: '#12C172'.color, width: 2.w),
+                          shape: BoxShape.circle));
+                } else if (isLastPoint) {
+                  return Container(
+                      margin: EdgeInsets.all(3.5.w),
+                      decoration: BoxDecoration(
+                          border:
+                              Border.all(color: '#F3353A'.color, width: 2.w),
+                          shape: BoxShape.circle));
+                } else {
+                  return Assets.images.iconTrackLocation
+                      .image(width: double.infinity, height: double.infinity);
+                }
+              }),
+            ),
+            SizedBox(height: 8.w),
+            Visibility(
+                maintainSize: true,
+                maintainAnimation: true,
+                maintainState: true,
+                visible: !isLastPoint,
+                child: VerticalDashedLine(
+                    dashSpace: 2.w,
+                    dashLength: 3.w,
+                    color: '#F0F0F0'.color,
+                    height: 35.w,
+                    strokeWidth: 1.w)),
+          ],
+        ),
+        SizedBox(width: 8.w),
+        Expanded(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                item.addr ?? '',
+                style: TextStyle(
+                    fontSize: 16.sp,
+                    color: '#404040'.color,
+                    fontWeight: FontWeight.bold),
+              ),
+              SizedBox(height: 4.w),
+              Text(
+                  DateUtil.fromMillisecondsSinceEpoch(
+                      "yyyy年MM月dd日 HH:mm", item.time),
+                  style: TextStyle(fontSize: 13.sp, color: '#A7A7A7'.color)),
+            ],
+          ),
+        ),
+        SizedBox(width: 12.w),
+      ],
+    );
+  }
+}

+ 48 - 54
lib/module/track/track_page.dart

@@ -104,60 +104,54 @@ class TrackPage extends BasePage<TrackController> {
   }
 
   Widget buildOperationBtn() {
-    return GestureDetector(
-      onTap: controller.onTrackQueryClick,
-      child: 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,
+    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: 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),
-                      ),
+                  child: Center(
+                    child: Text(
+                      maxLines: 1,
+                      StringName.traceDetail,
+                      style: TextStyle(
+                          fontSize: 14.sp, color: ColorName.colorPrimary),
                     ),
                   ),
                 ),
-              );
-            }),
-            // Obx(() {
-            //   return Visibility(
-            //       visible: controller.currentIndex == 0 &&
-            //           controller.isShowTraceDetailBtn,
-            //       child: SizedBox(width: 18.w));
-            // }),
-            Obx(() {
-              double width = 152.w;
-              if (controller.currentIndex == 1) {
-                width = 322.w;
-              } else if (controller.isShowTraceDetailBtn) {
-                width = 152.w;
-              } else {
-                width = 322.w;
-              }
-              return AnimatedContainer(
+              ),
+            );
+          }),
+          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
@@ -177,10 +171,10 @@ class TrackPage extends BasePage<TrackController> {
                     );
                   }),
                 ),
-              );
-            })
-          ],
-        ),
+              ),
+            );
+          })
+        ],
       ),
     );
   }

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

@@ -320,10 +320,22 @@ class $AssetsImagesGen {
   AssetGenImage get iconSplashTitle =>
       const AssetGenImage('assets/images/icon_splash_title.webp');
 
+  /// File path: assets/images/icon_track_location.webp
+  AssetGenImage get iconTrackLocation =>
+      const AssetGenImage('assets/images/icon_track_location.webp');
+
   /// File path: assets/images/icon_track_location_now.webp
   AssetGenImage get iconTrackLocationNow =>
       const AssetGenImage('assets/images/icon_track_location_now.webp');
 
+  /// File path: assets/images/icon_track_search.webp
+  AssetGenImage get iconTrackSearch =>
+      const AssetGenImage('assets/images/icon_track_search.webp');
+
+  /// File path: assets/images/icon_track_search_clear.webp
+  AssetGenImage get iconTrackSearchClear =>
+      const AssetGenImage('assets/images/icon_track_search_clear.webp');
+
   /// File path: assets/images/icon_track_select_time_arrow.webp
   AssetGenImage get iconTrackSelectTimeArrow =>
       const AssetGenImage('assets/images/icon_track_select_time_arrow.webp');
@@ -455,7 +467,10 @@ class $AssetsImagesGen {
         iconNews,
         iconNewsItem,
         iconSplashTitle,
+        iconTrackLocation,
         iconTrackLocationNow,
+        iconTrackSearch,
+        iconTrackSearchClear,
         iconTrackSelectTimeArrow,
         iconUrgentAdd,
         iconUrgentContactAdd,

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

@@ -243,6 +243,9 @@ class StringName {
       'member_payment_failed'.tr; // 开通失败,请稍后重试
   static final String exitAppTip = 'exit_app_tip'.tr; // 再按一次退出应用
   static final String traceDetail = 'trace_detail'.tr; // 轨迹详情
+  static final String traceDetailSearchHint =
+      'trace_detail_search_hint'.tr; // 查找地址
+  static final String traceDetailTitle = 'trace_detail_title'.tr; // Ta的线路轨迹
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -483,6 +486,8 @@ class StringMultiSource {
       'member_payment_failed': '开通失败,请稍后重试',
       'exit_app_tip': '再按一次退出应用',
       'trace_detail': '轨迹详情',
+      'trace_detail_search_hint': '查找地址',
+      'trace_detail_title': 'Ta的线路轨迹',
     },
   };
 }

+ 5 - 0
lib/router/app_pages.dart

@@ -20,6 +20,8 @@ import 'package:location/module/news/pending_list/news_pending_list_controller.d
 import 'package:location/module/news/pending_list/news_pending_list_page.dart';
 import 'package:location/module/permission/permission_setting_controller.dart';
 import 'package:location/module/permission/permission_setting_page.dart';
+import 'package:location/module/track/track_detail/track_detail_controller.dart';
+import 'package:location/module/track/track_detail/track_detail_page.dart';
 import 'package:location/module/urgent_contact/add_contact/add_urgent_contact_controller.dart';
 import 'package:location/module/urgent_contact/urgent_contact_controller.dart';
 import 'package:location/module/urgent_contact/urgent_contact_page.dart';
@@ -47,6 +49,7 @@ abstract class RoutePath {
   static const friendSetting = '/friendSetting';
   static const member = '/member';
   static const track = '/track';
+  static const trackDetail = '/trackDetail';
   static const news = '/news';
   static const newsPendingList = '/newsPendingList';
   static const urgentContact = '/urgentContact';
@@ -74,6 +77,7 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<FeedBackController>());
     lazyPut(() => getIt.get<AboutController>());
     lazyPut(() => getIt.get<PermissionSettingController>());
+    lazyPut(() => getIt.get<TrackDetailController>());
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -96,6 +100,7 @@ final generalPages = [
   GetPage(name: RoutePath.urgentContact, page: () => UrgentContactPage()),
   GetPage(name: RoutePath.feedback, page: () => FeedBackPage()),
   GetPage(name: RoutePath.about, page: () => AboutPage()),
+  GetPage(name: RoutePath.trackDetail, page: () => TrackDetailPage()),
   GetPage(
       name: RoutePath.permissionSetting, page: () => PermissionSettingPage()),
 ];

+ 67 - 0
lib/utils/dashed_line_painter.dart

@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+
+class VerticalDashedLine extends StatelessWidget {
+  final double height; // 虚线总高度
+  final Color color; // 颜色
+  final double strokeWidth; // 线条粗细
+  final double dashLength; // 虚线线段长度
+  final double dashSpace; // 虚线间隔长度
+
+  const VerticalDashedLine({
+    this.height = 100,
+    this.color = Colors.grey,
+    this.strokeWidth = 1,
+    this.dashLength = 5,
+    this.dashSpace = 3,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return CustomPaint(
+      size: Size(strokeWidth, height), // 宽度为线条粗细,高度为总高度
+      painter: _VerticalDashedLinePainter(
+        color: color,
+        strokeWidth: strokeWidth,
+        dashLength: dashLength,
+        dashSpace: dashSpace,
+      ),
+    );
+  }
+}
+
+class _VerticalDashedLinePainter extends CustomPainter {
+  final Color color;
+  final double strokeWidth;
+  final double dashLength;
+  final double dashSpace;
+
+  _VerticalDashedLinePainter({
+    required this.color,
+    required this.strokeWidth,
+    required this.dashLength,
+    required this.dashSpace,
+  });
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final paint = Paint()
+      ..color = color
+      ..strokeWidth = strokeWidth
+      ..style = PaintingStyle.stroke;
+
+    double startY = 0;
+    while (startY < size.height) {
+      // 绘制垂直方向的虚线线段
+      canvas.drawLine(
+        Offset(0, startY), // 起点 (x=0, y=startY)
+        Offset(0, startY + dashLength), // 终点 (x=0, y=startY+dashLength)
+        paint,
+      );
+      startY += dashLength + dashSpace;
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}

+ 0 - 3
pubspec.yaml

@@ -97,9 +97,6 @@ dependencies:
   #抽屉
   sliding_sheet2: ^2.0.1
 
-  #时间轴
-  #  timelines_plus: 1.0.6
-
   #时间滚轴选择器
   flutter_cupertino_datetime_picker: ^3.0.0