statistics_page.dart 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/material.dart' as ui;
  4. import 'package:flutter/painting.dart';
  5. import 'package:sport/bean/jog/record.dart';
  6. import 'package:sport/bean/sport_index.dart';
  7. import 'package:sport/pages/home/consume_page.dart';
  8. import 'package:sport/pages/home/plan_page.dart';
  9. import 'package:sport/pages/run/run_detail_page.dart';
  10. import 'package:sport/pages/run/run_share_statistics.dart';
  11. import 'package:sport/provider/lib/provider_widget.dart';
  12. import 'package:sport/provider/lib/simple_model.dart';
  13. import 'package:sport/provider/lib/view_state_lifecycle.dart';
  14. import 'package:sport/router/navigator_util.dart';
  15. import 'package:sport/services/api/inject_api.dart';
  16. import 'package:sport/services/api/resp.dart';
  17. import 'package:sport/utils/DateFormat.dart';
  18. import 'package:sport/utils/date.dart';
  19. import 'package:sport/utils/sport_utils.dart';
  20. import 'package:sport/utils/toast.dart';
  21. import 'package:sport/widgets/appbar.dart';
  22. import 'package:sport/widgets/chart.dart';
  23. import 'package:sport/widgets/dialog/popupmenu.dart' as menu;
  24. import 'package:sport/widgets/dialog/request_dialog.dart';
  25. import 'package:sport/widgets/image.dart';
  26. import 'package:sport/widgets/loading.dart';
  27. import 'package:sport/widgets/misc.dart';
  28. import 'package:sport/widgets/popmenu_bg.dart';
  29. const Color _color = Color(0xffFFC400);
  30. const List<String> TABS = ["日", "周", "月", "年", "总"];
  31. class RunStatisticsPage extends StatefulWidget {
  32. @override
  33. State<StatefulWidget> createState() => _PageState();
  34. }
  35. class _PageState extends State<RunStatisticsPage> with TickerProviderStateMixin, InjectApi, InjectLoginApi {
  36. ValueNotifier<String> _tab = ValueNotifier<String>("周");
  37. ValueNotifier<JogRecord?> _valueNotifierSportDetail = ValueNotifier(null);
  38. ValueNotifier<DateTime> _valueNotifierDate = ValueNotifier(DateTime.now());
  39. late PageController _pageController;
  40. @override
  41. void initState() {
  42. super.initState();
  43. _pageController = PageController(initialPage: 0);
  44. changeTab();
  45. }
  46. @override
  47. void dispose() {
  48. _pageController.dispose();
  49. super.dispose();
  50. _tab.dispose();
  51. _valueNotifierSportDetail.dispose();
  52. _valueNotifierDate.dispose();
  53. }
  54. @override
  55. Widget build(BuildContext context) {
  56. return Scaffold(
  57. backgroundColor: Colors.white,
  58. appBar: buildAppBar(context, title: "跑步统计", actions: [
  59. IconButton(icon: Image.asset("lib/assets/img/bbs_icon_share.png"), onPressed: () => _share()),
  60. ]),
  61. body: Container(
  62. color: Colors.white,
  63. child: ValueListenableBuilder(
  64. valueListenable: _tab,
  65. builder: (BuildContext context, String value, Widget? child) {
  66. return Column(
  67. children: <Widget>[
  68. Padding(
  69. padding: const EdgeInsets.all(8.0),
  70. child: Row(
  71. mainAxisAlignment: MainAxisAlignment.center,
  72. children: ["周", "/", "月", "/", "年", "/", "总"]
  73. .map((e) => e == "/"
  74. ? Container(
  75. margin: const EdgeInsets.fromLTRB(5, 0, 1, 0),
  76. color: const Color(0xffdcdcdc),
  77. width: 0.5,
  78. height: 14,
  79. transform: Matrix4.rotationZ(0.35),
  80. )
  81. : InkWell(
  82. onTap: () {
  83. _tab.value = e;
  84. _valueNotifierDate.value = DateTime.now();
  85. _pageController.jumpToPage(0);
  86. // _pageController= PageController(initialPage: 0);
  87. changeTab();
  88. },
  89. child: Container(
  90. margin: EdgeInsets.symmetric(horizontal: 12.0),
  91. padding: EdgeInsets.all(8.0),
  92. decoration: value == e ? BoxDecoration(color: _color, shape: BoxShape.circle) : null,
  93. child: Text(
  94. "$e",
  95. style: value == e
  96. ? Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white)
  97. : Theme.of(context).textTheme.subtitle1!,
  98. ),
  99. )))
  100. .toList(),
  101. ),
  102. ),
  103. if (toType() != 4)
  104. Center(
  105. child: ValueListenableBuilder<DateTime>(
  106. valueListenable: _valueNotifierDate,
  107. builder: (_, time, ___) {
  108. int type = toType();
  109. String text = "";
  110. if (type == 0) {
  111. text = "${time.year}.${'${time.month}'.padLeft(2, '0')}.${'${time.day}'.padLeft(2, '0')} 6:00 - 24:00 ";
  112. } else if (type == 1) {
  113. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  114. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  115. print("$time ${time.weekday} == $start $end");
  116. text =
  117. "${start.year}.${'${start.month}'.padLeft(2, '0')}.${'${start.day}'.padLeft(2, '0')} 至 ${end.year}.${'${end.month}'.padLeft(2, '0')}.${'${end.day}'.padLeft(2, '0')}";
  118. } else if (type == 2) {
  119. text = ("${time.year}年${'${time.month}'.padLeft(2, '0')}月");
  120. } else if (type == 3) {
  121. text = ("${time.year}年");
  122. }
  123. return Row(
  124. mainAxisSize: MainAxisSize.min,
  125. children: <Widget>[
  126. GestureDetector(
  127. behavior: HitTestBehavior.opaque,
  128. onTap: () {
  129. _pageController.nextPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
  130. },
  131. child: Padding(
  132. padding: const EdgeInsets.all(18.0),
  133. child: arrowLeft(),
  134. ),
  135. ),
  136. Text(
  137. text,
  138. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Color(0xff333333)),
  139. strutStyle: fixedLine,
  140. ),
  141. GestureDetector(
  142. behavior: HitTestBehavior.opaque,
  143. onTap: () {
  144. if (_pageController.page == 0.0) {
  145. ToastUtil.show("没有数据了");
  146. return;
  147. }
  148. _pageController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
  149. },
  150. child: Padding(
  151. padding: const EdgeInsets.all(18.0),
  152. child: arrowRight(),
  153. ),
  154. ),
  155. ],
  156. );
  157. }),
  158. ),
  159. Expanded(
  160. child: Center(
  161. child: PageView.builder(
  162. key: ValueKey("${toType()}"),
  163. reverse: true,
  164. itemCount: toType() != 4 ? 10240 : 1,
  165. controller: _pageController,
  166. onPageChanged: (page) {
  167. rollDate(-page);
  168. },
  169. itemBuilder: (context, index) {
  170. int type = toType();
  171. DateTime time = offsetDate(type, -index);
  172. return _PageChild(type: type, time: time, future: createFuture(time));
  173. },
  174. ),
  175. ),
  176. ),
  177. ],
  178. );
  179. },
  180. ),
  181. ));
  182. }
  183. int toType() {
  184. return TABS.indexOf(_tab.value);
  185. }
  186. void changeTab() async {
  187. _valueNotifierSportDetail.value = null;
  188. try {
  189. RespData<JogRecord> data = await createFuture(_valueNotifierDate.value);
  190. _valueNotifierSportDetail.value = data.data;
  191. } catch (e) {
  192. print(e);
  193. }
  194. }
  195. Future<RespData<JogRecord>> createQueryFuture(int offset) {
  196. int type = toType();
  197. DateTime next = offsetDate(type, offset);
  198. return createFuture(next);
  199. }
  200. Future<RespData<JogRecord>> createFuture(DateTime time) {
  201. int type = toType();
  202. Future<RespData<JogRecord>> data;
  203. switch (type) {
  204. case 1:
  205. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  206. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  207. data = api.jogListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}',
  208. '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  209. break;
  210. case 2:
  211. DateTime start = DateTime(time.year, time.month, 1);
  212. DateTime end = DateTime(time.year, time.month + 1, 0);
  213. data = api.jogListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}',
  214. '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  215. break;
  216. case 3:
  217. data = api.jogListByMonth(time.year);
  218. break;
  219. case 4:
  220. data = api.jogListByYear();
  221. break;
  222. default:
  223. data = api.jogListByDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}',
  224. '${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}');
  225. break;
  226. }
  227. return data;
  228. }
  229. void rollDate(int offset) {
  230. if (_valueNotifierSportDetail.value == null) return;
  231. int type = toType();
  232. DateTime next = offsetDate(type, offset);
  233. _valueNotifierDate.value = next;
  234. changeTab();
  235. }
  236. _share() async {
  237. int type = toType();
  238. int index = _pageController.page?.toInt() ?? 0;
  239. DateTime time = offsetDate(type, -index);
  240. JogRecord? jogRecord = await request(context, () async {
  241. RespData<JogRecord> resp = await createFuture(time);
  242. if (resp.code == 0) return resp.data;
  243. return null;
  244. });
  245. if (jogRecord == null) return;
  246. NavigatorUtil.goPage(context, (context) => RunShareStatistics(record: jogRecord, type: type, time: time));
  247. }
  248. }
  249. class _PageChild extends StatefulWidget {
  250. final int type;
  251. final DateTime time;
  252. final Future<RespData<JogRecord>> future;
  253. const _PageChild({Key? key, required this.type, required this.time, required this.future}) : super(key: key);
  254. @override
  255. State<StatefulWidget> createState() {
  256. return _PageChildState();
  257. }
  258. }
  259. class _PageChildState extends ViewStateLifecycle<_PageChild, SimpleModel> {
  260. var contentPadding = const EdgeInsets.symmetric(horizontal: 0.0);
  261. var flexLeft = 7;
  262. var flexRight = 3;
  263. var _labelTextStyle = const TextStyle(fontSize: 16.0, color: Color(0xff333333));
  264. JogRecord? jogRecord;
  265. double _dx =0;
  266. double _dy = 0;
  267. @override
  268. SimpleModel createModel() => SimpleModel((page) async {
  269. RespData<JogRecord> resp = await widget.future;
  270. jogRecord = resp.data;
  271. return [];
  272. });
  273. int toType() {
  274. return widget.type;
  275. }
  276. String toKm(double km) {
  277. if (km > 20 && km < 25) {
  278. return "半程马拉松";
  279. } else if (km > 40 && km < 43) {
  280. return "全程马拉松";
  281. }
  282. return "${km.toInt()}公里";
  283. }
  284. Widget _label(String text, {Widget? right}) {
  285. return Container(
  286. color: Color(0xffF1F1F1),
  287. padding: EdgeInsets.only(left: 13.0),
  288. margin: EdgeInsets.only(top: 10.0, bottom: 6.0),
  289. child: Row(
  290. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  291. children: [
  292. Text(
  293. "$text",
  294. style: Theme.of(context).textTheme.bodyText2!,
  295. strutStyle: fixedLine,
  296. ),
  297. if (right != null) right,
  298. ],
  299. ),
  300. width: double.infinity,
  301. height: 25.0,
  302. );
  303. }
  304. Widget _row(BuildContext context, String label, String text, String unit, {Color? color}) {
  305. return Padding(
  306. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  307. child: Row(
  308. children: [
  309. Expanded(
  310. child: Text('$label', style: _labelTextStyle),
  311. flex: flexLeft,
  312. ),
  313. Expanded(
  314. child: RichText(
  315. text: TextSpan(style: DefaultTextStyle.of(context).style, children: <InlineSpan>[
  316. TextSpan(
  317. text: '$text',
  318. style: color == null
  319. ? Theme.of(context).textTheme.headline1!.copyWith(
  320. fontSize: 20.0,
  321. fontFamily: "DIN",
  322. )
  323. : Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 15.0, fontFamily: "DIN", color: color)),
  324. WidgetSpan(
  325. alignment: PlaceholderAlignment.middle,
  326. child: Text(' $unit',
  327. style: Theme.of(context).textTheme.subtitle1!.copyWith(
  328. fontSize: 12.0,
  329. )),
  330. ),
  331. ]),
  332. ),
  333. flex: flexRight,
  334. ),
  335. ],
  336. ),
  337. );
  338. }
  339. @override
  340. Widget build(BuildContext context) {
  341. String sportType = widget.type == 1 ? "周" : "周均";
  342. sportType = "周均";
  343. DateTime next = offsetDateNext(widget.time, widget.type, 1);
  344. int day = next.difference(widget.time).inDays;
  345. return ProviderWidget<SimpleModel>(
  346. model: model,
  347. onModelReady: (model) => model.initData(),
  348. builder: (_, model, __) {
  349. JogRecord? _value = jogRecord;
  350. if (_value == null) {
  351. return RequestLoadingWidget();
  352. }
  353. List<double> items = [1, 3, 5, 10, 15, 20, 21.09, 25, 30, 35, 40, 42.19];
  354. int best = _value.best?.kmDurationList?.length ?? 0;
  355. List<double> bestFilled = (best < 3 ? items.skip(best).take(3 - best) : items.skip(best).take(1)).toList();
  356. var _divider = const Divider(
  357. height: 32,
  358. );
  359. var _vDivider = Container(
  360. height: 42.0,
  361. width: .5,
  362. color: const Color(0xffDCDCDC),
  363. );
  364. return SingleChildScrollView(
  365. child: Column(
  366. children: <Widget>[
  367. SizedBox(
  368. height: 10.0,
  369. ),
  370. Text(
  371. "${((_value.sum?.distance ?? 0) / 1000).toStringAsFixed(2)}",
  372. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 34.0, color: _color, fontFamily: "DIN"),
  373. ),
  374. Text(
  375. "总里程 (公里)",
  376. style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Color(0xff333333)),
  377. ),
  378. Padding(
  379. padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 30.0),
  380. child: Column(
  381. children: [
  382. Row(
  383. children: [
  384. Expanded(
  385. child: Column(
  386. mainAxisSize: MainAxisSize.min,
  387. children: [
  388. Text(
  389. "${DateFormat.toTime(_value.sum?.duration ?? 0)}",
  390. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  391. ),
  392. Text(
  393. "总时长",
  394. style: Theme.of(context).textTheme.bodyText1!,
  395. ),
  396. ],
  397. ),
  398. ),
  399. _vDivider,
  400. Expanded(
  401. child: Column(
  402. mainAxisSize: MainAxisSize.min,
  403. children: [
  404. Row(
  405. mainAxisSize: MainAxisSize.min,
  406. children: [
  407. Text(
  408. "${SportUtils.pace11(SportUtils.calPace(_value.sum?.kmDurationAvg ?? 0, 1))}",
  409. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  410. ),
  411. Text(
  412. "′",
  413. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0),
  414. ),
  415. Text(
  416. "${SportUtils.pace12(SportUtils.calPace(_value.sum?.kmDurationAvg ?? 0, 1))}",
  417. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  418. ),
  419. Text(
  420. "″",
  421. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0),
  422. ),
  423. ],
  424. ),
  425. Text(
  426. "配速(每公里用时)",
  427. style: Theme.of(context).textTheme.bodyText1!,
  428. ),
  429. ],
  430. ),
  431. ),
  432. ],
  433. ),
  434. Row(
  435. children: [
  436. Expanded(
  437. child: Divider(
  438. indent: 40,
  439. endIndent: 40,
  440. height: 40,
  441. )),
  442. Expanded(
  443. child: Divider(
  444. indent: 40,
  445. endIndent: 40,
  446. height: 40,
  447. )),
  448. ],
  449. ),
  450. Row(
  451. children: [
  452. Expanded(
  453. child: Column(
  454. mainAxisSize: MainAxisSize.min,
  455. children: [
  456. Text(
  457. "${_value.sum?.times ?? 0}",
  458. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  459. ),
  460. Text(
  461. "运动次数(次)",
  462. style: Theme.of(context).textTheme.bodyText1!,
  463. ),
  464. ],
  465. ),
  466. ),
  467. _vDivider,
  468. Expanded(
  469. child: Column(
  470. mainAxisSize: MainAxisSize.min,
  471. children: [
  472. Text(
  473. "${_value.sum?.consume ?? 0}",
  474. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  475. ),
  476. Text(
  477. "总消耗(大卡)",
  478. style: Theme.of(context).textTheme.bodyText1!,
  479. ),
  480. ],
  481. ),
  482. ),
  483. ],
  484. ),
  485. ],
  486. ),
  487. ),
  488. GestureDetector(
  489. onTapDown: (details){
  490. // print("${details.globalPosition.dx}");
  491. // print("${details.globalPosition.dy}");
  492. // print("${details.localPosition.dx}");
  493. // print("${details.localPosition.dy}");
  494. setState(() {
  495. _dx = details.localPosition.dx;
  496. _dy= details.localPosition.dy;
  497. });
  498. },
  499. child: CustomPaint(
  500. painter: Chart(
  501. type: widget.type,
  502. decimal: 2,
  503. dx: _dx,
  504. dy: _dy,
  505. records: _value.records
  506. ?.map((e) => ChartItem(
  507. widget.type == 4
  508. ? "${e.year}"
  509. : widget.type == 3
  510. ? "${e.month}"
  511. : e.createdAt ?? "",
  512. (e.distance ?? 0) / 1000))
  513. .toList() ??
  514. [],
  515. dateTime: widget.time,
  516. drawMax: true,
  517. unit: "km")
  518. ..initData(valueSplit: 5, valueSize: 5),
  519. child: Container(
  520. height: 200,
  521. ),
  522. ),
  523. ),
  524. if (widget.type != 4)
  525. _label("$sportType运动量",
  526. right: PopupMenuTheme(
  527. data: PopupMenuThemeData(
  528. color: Colors.black.withOpacity(.5),
  529. shape: PopmenuShape(backgroundColor: Colors.black.withOpacity(.5), borderRadius: BorderRadius.all(Radius.circular(10.0)))),
  530. child: PopupMenuButton(
  531. padding: EdgeInsets.symmetric(horizontal: 17.0),
  532. offset: Offset(0, kToolbarHeight / 2 + 5),
  533. icon: ui.Image.asset("lib/assets/img/linkpop_icon_modify_1.png"),
  534. onSelected: (val) {},
  535. itemBuilder: (context) {
  536. return [
  537. menu.PopupMenuItem(
  538. value: "",
  539. child: Center(
  540. child: Padding(
  541. padding: const EdgeInsets.all(12.0),
  542. child: Text(
  543. "周运动量:世界卫生组织WHO建议,成年人每周应进行最少150分钟的中等强度运动或75分钟的高强度运动,可以实现延长寿命的好处。 \n\n运动次数和时长:权威医学杂志《柳叶刀》的研究表明,每次锻炼的最佳时长在45-60分钟之间,少于45分钟效果会减弱,大于60分钟不仅没有更高收益,还容易产生负效应。 每周3--5次。",
  544. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  545. ),
  546. )),
  547. ),
  548. ];
  549. },
  550. ))),
  551. if (widget.type != 4)
  552. Padding(
  553. padding: const EdgeInsets.all(12.0),
  554. child: Column(
  555. crossAxisAlignment: CrossAxisAlignment.start,
  556. children: <Widget>[
  557. GestureDetector(
  558. behavior: HitTestBehavior.opaque,
  559. onTap: () => NavigatorUtil.goPage(
  560. context,
  561. (context) => PlanPage(
  562. index: 1,
  563. )),
  564. child:
  565. _row(context, '$sportType强度', '${strengthMetToLabel(_value.avg?.met ?? 0)}', '(${(_value.avg?.met ?? 0).toStringAsFixed(1)}MET)',
  566. color: Theme.of(context).accentColor), ),
  567. _divider,
  568. _row(context, '$sportType时长', '${_value.avg?.durationMin}', '分钟'),
  569. _divider,
  570. GestureDetector(
  571. behavior: HitTestBehavior.opaque,
  572. onTap: () => NavigatorUtil.goPage(context, (context) => ConsumePage()),
  573. child: _row(context, '$sportType消耗', '${_value.avg?.consume}', '大卡')),
  574. ],
  575. ),
  576. ),
  577. _label("运动数据"),
  578. Padding(
  579. padding: const EdgeInsets.all(12.0),
  580. child: Column(
  581. crossAxisAlignment: CrossAxisAlignment.start,
  582. children: <Widget>[
  583. _row(context, '平均时速', "${_value.sum?.speed.toStringAsFixed(2)}", '公里/小时'),
  584. _divider,
  585. _row(context, '平均步频', "${_value.sum?.stepRateAvg ?? 0}", '步/分钟'),
  586. _divider,
  587. _row(context, '平均步幅', "${((_value.sum?.stepDistanceAvg ?? 0) * 100).toInt()}", '厘米'),
  588. ],
  589. ),
  590. ),
  591. _label("${TABS[toType()]}最佳"),
  592. Padding(
  593. padding: const EdgeInsets.all(12.0),
  594. child: Column(
  595. crossAxisAlignment: CrossAxisAlignment.start,
  596. children: [
  597. if (_value.best?.distance != null)
  598. GestureDetector(
  599. child: Padding(
  600. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  601. child: Row(
  602. children: [
  603. Expanded(
  604. child: Column(
  605. children: [
  606. Text('最远距离', style: _labelTextStyle),
  607. const SizedBox(
  608. height: 4.0,
  609. ),
  610. Text(
  611. _value.best?.distance?.createdAt?.isNotEmpty == true
  612. ? '${_value.best?.distance?.createdAt?.substring(0, _value.best?.distance?.createdAt?.indexOf(" ") ?? 0)}'
  613. : '',
  614. style: Theme.of(context).textTheme.bodyText1!),
  615. ],
  616. crossAxisAlignment: CrossAxisAlignment.start,
  617. ),
  618. flex: flexLeft,
  619. ),
  620. Expanded(
  621. child: Row(
  622. children: [
  623. Expanded(
  624. child: RichText(
  625. text: TextSpan(style: DefaultTextStyle.of(context).style, children: <InlineSpan>[
  626. TextSpan(
  627. text: '${((_value.best?.distance?.value ?? 0) / 1000).toStringAsFixed(2)}',
  628. style: Theme.of(context).textTheme.headline1!.copyWith(
  629. fontSize: 20.0,
  630. fontFamily: "DIN",
  631. )),
  632. WidgetSpan(
  633. alignment: PlaceholderAlignment.middle,
  634. child: Text(' 公里',
  635. style: Theme.of(context).textTheme.subtitle1!.copyWith(
  636. fontSize: 12.0,
  637. )),
  638. ),
  639. ]),
  640. ),
  641. ),
  642. arrowRight5()
  643. ],
  644. ),
  645. flex: flexRight,
  646. ),
  647. ],
  648. ),
  649. ),
  650. onTap: () {
  651. int id = _value.best?.distance?.id ?? 0;
  652. if (id != 0) {
  653. NavigatorUtil.goPage(
  654. context,
  655. (context) => RunDetailPage(
  656. id: id,
  657. ));
  658. } else {
  659. ToastUtil.show("暂无运动记录");
  660. }
  661. },
  662. behavior: HitTestBehavior.opaque,
  663. ),
  664. _divider,
  665. if (_value.best?.duration != null)
  666. GestureDetector(
  667. onTap: () {
  668. int id = _value.best?.duration?.id ?? 0;
  669. if (id != 0) {
  670. NavigatorUtil.goPage(
  671. context,
  672. (context) => RunDetailPage(
  673. id: id,
  674. ));
  675. } else {
  676. ToastUtil.show("暂无运动记录");
  677. }
  678. },
  679. behavior: HitTestBehavior.opaque,
  680. child: Padding(
  681. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  682. child: Row(
  683. children: [
  684. Expanded(
  685. child: Column(
  686. children: [
  687. Text('最长时间', style: _labelTextStyle),
  688. const SizedBox(
  689. height: 4.0,
  690. ),
  691. Text(
  692. _value.best?.duration?.createdAt?.isNotEmpty == true
  693. ? '${_value.best?.duration?.createdAt?.substring(0, _value.best?.duration?.createdAt?.indexOf(" ") ?? 0)}'
  694. : "",
  695. style: Theme.of(context).textTheme.bodyText1!),
  696. ],
  697. crossAxisAlignment: CrossAxisAlignment.start,
  698. ),
  699. flex: flexLeft,
  700. ),
  701. Expanded(
  702. child: Row(
  703. children: [
  704. Expanded(
  705. child: Text("${DateFormat.toTime(_value.best?.duration?.value ?? 0)}",
  706. style: Theme.of(context).textTheme.headline1!.copyWith(
  707. fontSize: 20.0,
  708. fontFamily: "DIN",
  709. ))),
  710. arrowRight5()
  711. ],
  712. ),
  713. flex: flexRight,
  714. ),
  715. ],
  716. ),
  717. ),
  718. ),
  719. _divider,
  720. if (_value.best?.kmDuration != null)
  721. GestureDetector(
  722. onTap: () {
  723. int id = _value.best?.kmDuration?.id ?? 0;
  724. if (id != 0) {
  725. NavigatorUtil.goPage(
  726. context,
  727. (context) => RunDetailPage(
  728. id: id,
  729. ));
  730. } else {
  731. ToastUtil.show("暂无运动记录");
  732. }
  733. },
  734. behavior: HitTestBehavior.opaque,
  735. child: Padding(
  736. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  737. child: Row(
  738. children: [
  739. Expanded(
  740. child: Column(
  741. children: [
  742. Text('最快配速', style: _labelTextStyle),
  743. const SizedBox(
  744. height: 4.0,
  745. ),
  746. Text(
  747. _value.best?.kmDuration?.createdAt?.isNotEmpty == true
  748. ? '${_value.best?.kmDuration?.createdAt?.substring(0, _value.best?.kmDuration?.createdAt?.indexOf(" ") ?? 0)}'
  749. : "",
  750. style: Theme.of(context).textTheme.bodyText1!),
  751. ],
  752. crossAxisAlignment: CrossAxisAlignment.start,
  753. ),
  754. flex: flexLeft,
  755. ),
  756. Expanded(
  757. child: Row(
  758. children: [
  759. Expanded(
  760. child: Text("${SportUtils.pace4(_value.best?.kmDuration?.value ?? 0, 1000)}",
  761. style: Theme.of(context).textTheme.headline1!.copyWith(
  762. fontSize: 20.0,
  763. fontFamily: "DIN",
  764. ))),
  765. arrowRight5()
  766. ],
  767. ),
  768. flex: flexRight,
  769. ),
  770. ],
  771. ),
  772. ),
  773. ),
  774. ],
  775. ),
  776. ),
  777. _label("${TABS[toType()]}最快"),
  778. Padding(
  779. padding: const EdgeInsets.all(12.0),
  780. child: Column(
  781. children: [
  782. if (_value.best?.kmDurationList?.isNotEmpty == true)
  783. ListView.separated(
  784. itemBuilder: (context, index) {
  785. KmDurationList? e = _value.best?.kmDurationList?[index];
  786. return GestureDetector(
  787. child: Padding(
  788. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  789. child: Row(
  790. children: [
  791. Expanded(
  792. child: Column(
  793. children: [
  794. Text('${toKm((e?.distance ?? 0) / 1000)}最快时间', style: _labelTextStyle),
  795. const SizedBox(
  796. height: 4.0,
  797. ),
  798. Text('${e?.createdAt?.substring(0, e.createdAt?.indexOf(" ") ?? 0)}', style: Theme.of(context).textTheme.bodyText1!),
  799. ],
  800. crossAxisAlignment: CrossAxisAlignment.start,
  801. ),
  802. flex: flexLeft,
  803. ),
  804. Expanded(
  805. child: Row(
  806. children: [
  807. Expanded(
  808. child: Text("${DateFormat.toTime(e?.duration ?? 0)}",
  809. style: Theme.of(context).textTheme.headline1!.copyWith(
  810. fontSize: 20.0,
  811. fontFamily: "DIN",
  812. )),
  813. ),
  814. arrowRight5()
  815. ],
  816. ),
  817. flex: flexRight,
  818. ),
  819. ],
  820. ),
  821. ),
  822. onTap: () => NavigatorUtil.goPage(
  823. context,
  824. (context) => RunDetailPage(
  825. id: e?.id ?? 0,
  826. )),
  827. behavior: HitTestBehavior.opaque,
  828. );
  829. },
  830. separatorBuilder: (context, index) {
  831. return _divider;
  832. },
  833. itemCount: _value.best?.kmDurationList?.length ?? 0,
  834. shrinkWrap: true,
  835. physics: NeverScrollableScrollPhysics(),
  836. ),
  837. if (_value.best?.kmDurationList?.isNotEmpty == true) _divider,
  838. ListView.separated(
  839. itemBuilder: (context, index) {
  840. var e = bestFilled[index];
  841. return Padding(
  842. padding: const EdgeInsets.symmetric(horizontal: 11.0),
  843. child: Row(
  844. children: [
  845. Expanded(
  846. child: Column(
  847. children: [
  848. Text('${toKm(e)}最快时间', style: _labelTextStyle),
  849. const SizedBox(
  850. height: 4.0,
  851. ),
  852. Text('暂无记录时间', style: Theme.of(context).textTheme.bodyText1!),
  853. ],
  854. crossAxisAlignment: CrossAxisAlignment.start,
  855. ),
  856. flex: flexLeft,
  857. ),
  858. Text("--",
  859. style: Theme.of(context).textTheme.headline1!.copyWith(
  860. fontSize: 20.0,
  861. fontFamily: "DIN",
  862. )),
  863. ],
  864. ),
  865. );
  866. },
  867. separatorBuilder: (context, index) {
  868. return _divider;
  869. },
  870. itemCount: bestFilled.length,
  871. shrinkWrap: true,
  872. physics: NeverScrollableScrollPhysics(),
  873. ),
  874. ],
  875. ),
  876. ),
  877. const SizedBox(
  878. height: 50.0,
  879. ),
  880. ],
  881. ),
  882. );
  883. });
  884. }
  885. }