周睿 2 tahun lalu
induk
melakukan
b03bc9e60d
36 mengubah file dengan 641 tambahan dan 19 penghapusan
  1. 1 1
      app_business/android/app/src/main/AndroidManifest.xml
  2. TEMPAT SAMPAH
      app_business/android/app/src/main/ic_launcher-playstore.png
  3. 5 0
      app_business/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  4. 5 0
      app_business/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  5. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  6. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  7. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  8. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  9. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  10. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  11. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  12. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  13. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  14. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  15. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  16. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  17. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  18. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  19. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  20. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  21. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  22. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  23. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  24. TEMPAT SAMPAH
      app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  25. 4 0
      app_business/android/app/src/main/res/values/ic_launcher_background.xml
  26. 2 1
      app_business/lib/service/api.dart
  27. 37 0
      app_business/lib/view/home/data_detail.dart
  28. 11 0
      app_business/lib/view/home/home.dart
  29. 62 0
      app_business/lib/view/home/settings.dart
  30. 1 1
      libs/common_pub
  31. 10 6
      libs/track_common/lib/service/map_watch.dart
  32. 293 0
      libs/track_common/lib/view/home/data_detail/data_detail.dart
  33. 33 0
      libs/track_common/lib/view/home/data_detail/data_detail_bar_charts.dart
  34. 116 0
      libs/track_common/lib/view/home/data_detail/data_detail_controller.dart
  35. 48 0
      libs/track_common/lib/view/home/data_detail/data_detail_cp.dart
  36. 13 10
      libs/track_common/lib/view/home/personal_rank.dart

+ 1 - 1
app_business/android/app/src/main/AndroidManifest.xml

@@ -1,6 +1,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <application
-        android:label="app_business"
+        android:label="彩图奔跑场控"
         android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher">
         <activity

TEMPAT SAMPAH
app_business/android/app/src/main/ic_launcher-playstore.png


+ 5 - 0
app_business/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

+ 5 - 0
app_business/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp


TEMPAT SAMPAH
app_business/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 4 - 0
app_business/android/app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#FD860D</color>
+</resources>

+ 2 - 1
app_business/lib/service/api.dart

@@ -14,13 +14,14 @@ import 'package:track_common/track_common.dart';
 
 import 'abase.dart';
 
+export 'package:app_business/generated/base.pb.dart'
+    show DefaultRequest, IdRequest;
 export 'package:app_business/generated/google/protobuf/timestamp.pb.dart';
 export 'package:app_business/generated/track_offical.pbgrpc.dart'
     show ToMatchRegusterAddRequest, ToMatchRegusterListRequest;
 export 'package:fixnum/fixnum.dart' show Int64;
 
 typedef SmsType = pb.SmsType;
-typedef IdRequest = pb.IdRequest;
 
 extension _ImageExt on pb.NetImage {
   NetImage toModel() {

+ 37 - 0
app_business/lib/view/home/data_detail.dart

@@ -0,0 +1,37 @@
+import 'package:app_business/service/abase.dart';
+import 'package:app_business/service/api.dart';
+import 'package:track_common/view/home/data_detail/data_detail_controller.dart';
+
+export 'package:track_common/view/home/data_detail/data_detail.dart';
+export 'package:track_common/view/home/data_detail/data_detail_controller.dart';
+
+class DataDetailControllerImpl extends DataDetailController {
+  ApiService get api => Get.find();
+
+  @override
+  Future<List<int>> getHistoryDetail(int gameId) async {
+    final r =
+        await api.stub.toHistoryGameDetail(IdRequest()..id = Int64(gameId));
+    return r.writeToBuffer();
+  }
+
+  @override
+  Future<List<UserDetail>> getUserList(int mapId) async {
+    final r =
+        await api.stub.toHistoryUserListQuery(IdRequest()..id = Int64(mapId));
+
+    return r.list
+        .map((e) => UserDetail()
+          ..id = e.oId
+          ..name = e.oName
+          ..history = e.list
+              .map((e2) => UserHistorySimple()
+                ..gameId = e2.gameId
+                ..routeName = e2.courseName
+                ..eventName = e2.actName
+                ..duration = e2.duration.toModel()
+                ..startAt = e2.startAt.toModel())
+              .toList())
+        .toList();
+  }
+}

+ 11 - 0
app_business/lib/view/home/home.dart

@@ -1,5 +1,7 @@
+import 'package:app_business/view/home/data_detail.dart';
 import 'package:app_business/view/home/event_manage/event_manage_controller.dart';
 import 'package:app_business/view/home/personal_rank.dart';
+import 'package:app_business/view/home/settings.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
 import 'package:track_common/view.dart';
@@ -47,6 +49,15 @@ class HomeControllerImpl extends HomeController {
                     return const PersonalRankPage();
                   },
                 )),
+        HomeTab(
+            '数据详情',
+            () => _TabBuilder<DataDetailController>(
+                  init: () => DataDetailControllerImpl(),
+                  builder: (c) {
+                    return const DataDetailPage();
+                  },
+                )),
+        HomeTab('设置', () => const SettingsPage())
       ];
 }
 

+ 62 - 0
app_business/lib/view/home/settings.dart

@@ -0,0 +1,62 @@
+import 'package:app_business/service/abase.dart';
+import 'package:app_business/service/api.dart';
+import 'package:flutter/material.dart';
+import 'package:track_common/service/map_watch.dart';
+import 'package:track_common/view.dart';
+
+class SettingsPage extends StatelessWidget {
+  const SettingsPage({super.key});
+
+  ApiService get api => Get.find();
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+        child: SizedBox(
+            width: 320,
+            child: Card(
+              margin: const EdgeInsets.symmetric(vertical: 24),
+              child: ListView(
+                children: [_SettingsElem(title: '退出登录', onTap: onSignOut)],
+              ),
+            )));
+  }
+
+  Widget divider() {
+    return const Divider(color: Colors.grey);
+  }
+
+  Future<void> onSignOut() async {
+    final mapWatchService = Get.find<MapWatchService>();
+    mapWatchService.setMap(null);
+    api.signOut();
+    LoginView.to(
+        canBack: false, thenToPageCall: () => Get.offAllNamed(HomeView.name));
+  }
+}
+
+class _SettingsElem extends StatelessWidget {
+  const _SettingsElem({required this.onTap, required this.title, this.tail});
+
+  final VoidCallback onTap;
+  final String title;
+  final Widget? tail;
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+        onTap: onTap,
+        child: Container(
+          height: 60,
+          color: Colors.transparent,
+          padding: const EdgeInsets.symmetric(horizontal: 24),
+          child: Row(
+            children: [
+              Text(title),
+              const Spacer(),
+              tail ?? const Icon(Icons.chevron_right)
+            ],
+          ),
+        ));
+  }
+}

+ 1 - 1
libs/common_pub

@@ -1 +1 @@
-Subproject commit e8f4bfab5527ea8bf24d5f284987bfc9982df8fb
+Subproject commit 8e26060692e76ada8cf2cad3ab2b5da88123d3b9

+ 10 - 6
libs/track_common/lib/service/map_watch.dart

@@ -122,13 +122,17 @@ abstract class MapWatchService extends GetxService {
   @protected
   Future<MapWatch> newInstanceByMap(MapInfo info);
 
-  Future<void> setMap(MapInfo mapInfo) async {
-    final thisInstance = await newInstanceByMap(mapInfo);
-    thisInstance.addPlugs([thisInstance.plugMap]);
+  Future<void> setMap(MapInfo? mapInfo) async {
     _instance.value?.close();
-    _instance.value = thisInstance;
-    thisInstance.init();
-    thisInstance.workFlushData();
+    if (mapInfo != null) {
+      final thisInstance = await newInstanceByMap(mapInfo);
+      thisInstance.addPlugs([thisInstance.plugMap]);
+      _instance.value = thisInstance;
+      thisInstance.init();
+      thisInstance.workFlushData();
+    } else {
+      _instance.value = null;
+    }
   }
 }
 

+ 293 - 0
libs/track_common/lib/view/home/data_detail/data_detail.dart

@@ -0,0 +1,293 @@
+import 'package:common_pub/ui/history_detail/trace_bar.dart';
+import 'package:common_pub/ui/map_view/map_view.dart';
+import 'package:common_pub/ui/map_view/view_map_cp.dart';
+import 'package:common_pub/ui/map_view/view_map_image.dart';
+import 'package:common_pub/ui/map_view/view_map_touch.dart';
+import 'package:common_pub/ui/map_view/view_map_trace.dart';
+import 'package:common_pub/ui/map_view/view_plug_loading.dart';
+import 'package:track_common/service/map_watch.dart';
+import 'package:track_common/widget/prelude.dart';
+
+import 'data_detail_bar_charts.dart';
+import 'data_detail_controller.dart';
+import 'data_detail_cp.dart';
+
+class DataDetailPage extends GetView<DataDetailController> {
+  const DataDetailPage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final mapWatch = controller.mapWatch;
+
+    return Container(
+        height: double.infinity,
+        width: double.infinity,
+        color: const Color(0xffc9c0c0),
+        alignment: Alignment.center,
+        child: mapWatch != null ? content(context, mapWatch) : noData());
+  }
+
+  Widget noData() {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Image.asset(Assets.imagesIcNoData, height: 64, package: package),
+          const SizedBox(height: 25),
+          const Text('没有数据, 请选择地图',
+              style: TextStyle(color: Color(0xff707070), fontSize: 18.5)),
+        ],
+      ),
+    );
+  }
+
+  static const cpColor = Color(0xffcc00ff);
+
+  Widget content(BuildContext context, MapWatch map) {
+    return Obx(() {
+      return Row(children: [
+        Expanded(
+            child: Column(
+          children: [
+            Expanded(
+                child: ViewMapStack(
+                    plug: map.plugMap,
+                    children: [SizedBox.expand(child: _MapContent())])),
+            _traceBarView()
+          ],
+        )),
+        _UserListView()
+      ]);
+    });
+  }
+
+  Widget _traceBarView() {
+    final detail = controller.selectedDetail.value;
+    final children = <Widget>[];
+
+    if (detail != null) {
+      final data =
+          detail.traceList.map((e) => TraceBarData(e.pace)..ts = e.ts).toList();
+
+      children.addAll([
+        const SizedBox(height: 8),
+        const Text('配速(按时间)'),
+        TraceBar(
+          data,
+          controller: controller.traceBarController,
+          direction: Axis.horizontal,
+          trackWidth: 10,
+          paddingStart: 100,
+          paddingEnd: 100,
+          mask: true,
+          isShowCp: true,
+          cpList: detail.controlPoints,
+        ),
+      ]);
+    }
+
+    return Container(
+      decoration: const BoxDecoration(color: Colors.white),
+      height: 73,
+      width: double.infinity,
+      child: Column(children: children),
+    );
+  }
+}
+
+class _MapContent extends GetView<DataDetailController> {
+  static const cpColor = Color(0xffcc00ff);
+  @override
+  Widget build(BuildContext context) {
+    return Obx(() {
+      final map = controller.mapWatch!;
+
+      final children = <Widget>[
+        ViewPlugLoading(map.plugMap),
+        ViewMapImage(map.plugMap),
+      ];
+      final data = controller.selectedDetail.value;
+      if (data != null) {
+        children.add(
+          Container(
+            color: Colors.white.withAlpha(120),
+          ),
+        );
+
+        if (controller.selectedLoading.value) {
+          children.add(const Center(child: CircularProgressIndicator()));
+        } else {
+          children.add(ViewMapCP(
+            key: UniqueKey(),
+            map.plugMap,
+            cpWantAndHistoryList: data.controlPoints,
+            isHideRouteBeforeStart: false,
+            isShowPath: false,
+            cpTheme: ViewMapCPTheme()
+              ..cpJumpColor = cpColor
+              ..cpPunchedColor = cpColor,
+          ));
+
+          children.add(ViewMapTrace(
+              key: UniqueKey(),
+              map.plugMap,
+              data.traceList,
+              controller: controller.viewMapTraceController));
+        }
+      }
+
+      children.add(ViewMapTouch(map.plugMap));
+
+      return Stack(alignment: Alignment.topLeft, children: children);
+    });
+  }
+}
+
+class _UserListView extends GetView<DataDetailController> {
+  @override
+  Widget build(BuildContext context) {
+    return Obx(() {
+      return Container(
+          width: 263,
+          height: double.infinity,
+          decoration: const BoxDecoration(color: Colors.white, boxShadow: [
+            BoxShadow(color: Color(0x33000000), blurRadius: 4.3)
+          ]),
+          padding: const EdgeInsets.all(20),
+          child: Column(
+            children: [
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  const Text('用户列表'),
+                  IconButton(
+                      onPressed: controller.selectedDetail.value != null
+                          ? () => controller.showDetail.value =
+                              !controller.showDetail.value
+                          : null,
+                      icon: const Icon(Icons.more_horiz)),
+                ],
+              ),
+              Expanded(
+                  child: ListView(
+                children: controller.showDetail.value
+                    ? _detailView()
+                    : controller.userList
+                        .map((element) => _userElem(element))
+                        .toList(),
+              ))
+            ],
+          ));
+    });
+  }
+
+  List<Widget> _detailView() {
+    final detail = controller.selectedDetail.value!;
+
+    return <Widget>[
+      DataDetailCP(cpList: detail.controlPoints),
+      const SizedBox(height: 8),
+      const DataDetailBarCharts(),
+    ];
+  }
+
+  Widget _userElem(UserInfo data) {
+    return Obx(() {
+      final children = <Widget>[
+        Container(
+          width: double.infinity,
+          height: 42,
+          margin: const EdgeInsets.only(top: 8),
+          padding: const EdgeInsets.only(left: 4),
+          decoration: BoxDecoration(
+              color: Colors.white,
+              boxShadow: const [
+                BoxShadow(color: Color(0x29000000), blurRadius: 3)
+              ],
+              borderRadius: BorderRadius.circular(3.56)),
+          child: Row(
+            children: [
+              Container(
+                height: double.infinity,
+                width: 3.56,
+                margin: const EdgeInsets.only(top: 8, bottom: 8, right: 3.55),
+                decoration: BoxDecoration(
+                    color: data.data.id == controller.selectedUserId.value
+                        ? Colors.orange
+                        : Colors.transparent,
+                    borderRadius: BorderRadius.circular(2.1)),
+              ),
+              Text(data.data.name),
+              const Spacer(),
+              IconButton(
+                  onPressed: () {
+                    data.isExpand.value = !data.isExpand.value;
+                  },
+                  icon: Icon(data.isExpand.value
+                      ? Icons.arrow_drop_up
+                      : Icons.arrow_drop_down))
+            ],
+          ),
+        )
+      ];
+      if (data.isExpand.value) {
+        children.add(Column(
+          children: data.data.history
+              .map((element) => GestureDetector(
+                  onTap: () => controller.selectDetail(element, data),
+                  child: detailElem(element)))
+              .toList(),
+        ));
+      }
+
+      return Column(
+        children: children,
+      );
+    });
+  }
+
+  Widget detailElem(UserHistorySimple detail) {
+    return Container(
+      margin: const EdgeInsets.only(left: 12, top: 2, bottom: 4),
+      width: double.infinity,
+      height: 58,
+      padding: const EdgeInsets.fromLTRB(4, 6, 4, 6),
+      decoration: BoxDecoration(
+          color: Colors.white,
+          borderRadius: BorderRadius.circular(3.5),
+          boxShadow: const [
+            BoxShadow(color: Color(0x33000000), blurRadius: 1.3)
+          ]),
+      child: Row(
+        children: [
+          Container(
+            height: double.infinity,
+            width: 3.56,
+            margin: const EdgeInsets.only(right: 3.55),
+            decoration: BoxDecoration(
+                color: detail.gameId ==
+                        controller.selectedDetailSimple.value.gameId
+                    ? Colors.orange
+                    : Colors.transparent,
+                borderRadius: BorderRadius.circular(2.1)),
+          ),
+          Expanded(
+              child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(detail.routeName,
+                  maxLines: 1, overflow: TextOverflow.ellipsis),
+              Text(
+                detail.eventName,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+            ],
+          )),
+          const SizedBox(width: 12),
+          Text(detail.isComplete ? '完赛' : '未完赛')
+        ],
+      ),
+    );
+  }
+}

+ 33 - 0
libs/track_common/lib/view/home/data_detail/data_detail_bar_charts.dart

@@ -0,0 +1,33 @@
+import 'package:common_pub/ui/history_detail/sum_bar_chart.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import 'data_detail_controller.dart';
+
+class DataDetailBarCharts extends GetView<DataDetailController> {
+  const DataDetailBarCharts({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final detail = controller.selectedDetail.value!;
+
+    return Container(
+      width: double.infinity,
+      padding: const EdgeInsets.all(2.8),
+      decoration: BoxDecoration(
+          color: const Color(0xffe0e0e0),
+          borderRadius: BorderRadius.circular(3.56)),
+      child: Column(children: [
+        BarChartHrPRange(hrPercentRangeList: detail.hrPercentRangeList),
+        const SizedBox(height: 2),
+        BarChartPaceDistance(
+            paceDistancePercentRangeList: detail.paceDistancePercentRangeList,
+            distance: detail.distance),
+        const SizedBox(height: 2),
+        BarChartPaceTime(
+            paceTimePercentRangeList: detail.paceTimePercentRangeList,
+            duration: detail.duration)
+      ]),
+    );
+  }
+}

+ 116 - 0
libs/track_common/lib/view/home/data_detail/data_detail_controller.dart

@@ -0,0 +1,116 @@
+import 'dart:async';
+
+import 'package:common_pub/logger.dart';
+import 'package:common_pub/model/history_detail.dart';
+import 'package:common_pub/ui/history_detail/trace_bar.dart';
+import 'package:common_pub/ui/map_view/view_map_cp.dart';
+import 'package:common_pub/ui/map_view/view_map_trace.dart';
+import 'package:get/get.dart';
+
+import '../../../service/map_watch.dart';
+
+class UserDetail {
+  var id = 0;
+  var name = '';
+  var history = <UserHistorySimple>[];
+}
+
+class UserHistorySimple {
+  var gameId = 0;
+  var routeName = '';
+  var eventName = '';
+  var isComplete = false;
+  var startAt = DateTime.now();
+  var duration = 0.seconds;
+}
+
+class UserInfo {
+  var data = UserDetail();
+  final isExpand = false.obs;
+}
+
+abstract class DataDetailController extends GetxController {
+  MapWatch? get mapWatch => Get.find<MapWatchService>().instance;
+  final userMap = <int, UserInfo>{}.obs;
+  Iterable<UserInfo> get userList => userMap.values;
+  final selectedDetailSimple = UserHistorySimple().obs;
+  final selectedUserId = (-9).obs;
+  final Rx<HistoryDetail?> selectedDetail = Rx(null);
+  final showDetail = false.obs;
+  final isCPExpand = true.obs;
+  final traceBarController = TraceBarController();
+  final viewMapTraceController = ViewMapTraceController();
+  StreamSubscription<int?>? _subscriptionTrace;
+  final selectedLoading = false.obs;
+  final viewCPController = ViewMapCPController();
+
+  @override
+  void onInit() {
+    super.onInit();
+    _subscriptionTrace = traceBarController.cursorState.listen((p0) {
+      viewMapTraceController.cursor = p0;
+    });
+    workFlush();
+  }
+
+  Future<List<UserDetail>> getUserList(int mapId);
+
+  /// GameDetailV2Reply bytes
+  Future<List<int>> getHistoryDetail(int gameId);
+
+  Future<void> workFlush() async {
+    while (!isClosed) {
+      await flushData();
+      await 1.seconds.delay();
+    }
+  }
+
+  Future<void> flushData() async {
+    final map = mapWatch;
+    if (map == null) {
+      return;
+    }
+
+    try {
+      final list = await getUserList(map.id);
+      for (final one in list) {
+        final updated = userMap[one.id] ?? UserInfo();
+        updated.data = one;
+        userMap[one.id] = updated;
+      }
+    } catch (e) {
+      error(e);
+    }
+  }
+
+  void selectDetail(UserHistorySimple d, UserInfo user) async {
+    selectedDetailSimple.value = d;
+    selectedUserId.value = user.data.id;
+    selectedLoading.value = true;
+    try {
+      final t = DateTime.now();
+      final detailBytes = await getHistoryDetail(d.gameId);
+      final data = GameDetailV2Reply.fromBuffer(detailBytes);
+      final detail = HistoryDetail(data);
+
+      final cost = DateTime.now().difference(t);
+      info("api cost $cost");
+
+      await detail.init(mapWatch!.plugMap.gameMap);
+      final old = selectedDetail.value;
+      selectedDetail.value = detail;
+      debug('select: ${detail.data.gameId}');
+      if (old != null) {
+        old.dispose();
+      }
+    } finally {
+      selectedLoading.value = false;
+    }
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+    _subscriptionTrace?.cancel();
+  }
+}

+ 48 - 0
libs/track_common/lib/view/home/data_detail/data_detail_cp.dart

@@ -0,0 +1,48 @@
+import 'package:common_pub/model/control_point.dart';
+import 'package:common_pub/ui/control_point_history_view.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import 'data_detail_controller.dart';
+
+class DataDetailCP extends GetView<DataDetailController> {
+  final List<ControlPoint> cpList;
+
+  const DataDetailCP({super.key, required this.cpList});
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+        width: double.infinity,
+        padding: const EdgeInsets.all(2.9),
+        decoration: BoxDecoration(
+            color: const Color(0xffe0e0e0),
+            borderRadius: BorderRadius.circular(3.56)),
+        child: Column(children: [
+          GestureDetector(
+            onTap: () {
+              controller.isCPExpand.value = !controller.isCPExpand.value;
+            },
+            child: Obx(() => Container(
+                  padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
+                  margin: EdgeInsets.only(
+                      bottom: controller.isCPExpand.value ? 4 : 0),
+                  decoration: BoxDecoration(
+                      color: Colors.white,
+                      borderRadius: BorderRadius.circular(3.56)),
+                  child: Row(
+                    children: [
+                      const Text('控制点'),
+                      const Spacer(),
+                      Icon(controller.isCPExpand.value
+                          ? Icons.arrow_drop_up
+                          : Icons.arrow_drop_down)
+                    ],
+                  ),
+                )),
+          ),
+          Obx(() => controller.isCPExpand.value
+              ? ControlPointHistoryView(cpList: cpList)
+              : const SizedBox())
+        ]));
+  }
+}

+ 13 - 10
libs/track_common/lib/view/home/personal_rank.dart

@@ -119,13 +119,16 @@ class PersonalRankPage extends GetView<PersonalRankController> {
             Padding(padding: const EdgeInsets.all(8), child: titlePoint()),
             Text('活动列表',
                 style: context.textTheme.titleLarge
-                    ?.copyWith(color: Colors.white)),
+                    ?.copyWith(color: Colors.white, fontSize: 14.22)),
             const Spacer(),
-            Container(
-                decoration: BoxDecoration(
-                    border: Border.all(color: const Color(0xffe3e3e3))),
-                child: TextButton(
-                    onPressed: () => _pickDate(context, c),
+            GestureDetector(
+                onTap: () => _pickDate(context, c),
+                child: Container(
+                    decoration: BoxDecoration(
+                        border: Border.all(color: const Color(0xffe3e3e3))),
+                    height: 22.04,
+                    alignment: Alignment.center,
+                    padding: const EdgeInsets.symmetric(horizontal: 8),
                     child: Text(TimeOfDay.fromDateTime(c.filterStartAt.value)
                         .format(context))))
           ],
@@ -190,13 +193,13 @@ class PersonalRankPage extends GetView<PersonalRankController> {
       children: [
         Row(
           children: [
-            const Padding(padding: EdgeInsets.all(8), child: TitlePoint()),
+            Padding(padding: const EdgeInsets.all(8), child: titlePoint()),
             Text('个人排名',
                 style: context.textTheme.titleLarge
-                    ?.copyWith(color: Colors.white)),
+                    ?.copyWith(color: Colors.white, fontSize: 14.22)),
             Text(' (${active.name})',
-                style: context.textTheme.titleLarge
-                    ?.copyWith(color: const Color(0xffffcb00))),
+                style: context.textTheme.titleLarge?.copyWith(
+                    color: const Color(0xffffcb00), fontSize: 14.22)),
           ],
         ),
         Expanded(