sport_data_detail.dart 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  1. import 'dart:math' as math;
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/scheduler.dart';
  4. import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
  5. import 'package:get_it/get_it.dart';
  6. import 'package:provider/provider.dart';
  7. import 'package:sport/bean/sport_detail.dart';
  8. import 'package:sport/bean/sport_index.dart';
  9. import 'package:sport/pages/data/sport_data_page.dart';
  10. import 'package:sport/pages/data/sport_reference_page.dart';
  11. import 'package:sport/pages/home/duration_setting_page.dart';
  12. import 'package:sport/pages/home/strength_page.dart';
  13. import 'package:sport/pages/run/statistics_page.dart';
  14. import 'package:sport/provider/bluetooth.dart';
  15. import 'package:sport/provider/user_model.dart';
  16. import 'package:sport/router/navigator_util.dart';
  17. import 'package:sport/services/api/inject_api.dart';
  18. import 'package:sport/services/api/resp.dart';
  19. import 'package:sport/utils/sport_utils.dart';
  20. import 'package:sport/widgets/chart.dart';
  21. import 'package:sport/widgets/circular_percent_indicator.dart';
  22. import 'package:sport/widgets/data_chart.dart';
  23. import 'package:sport/widgets/decoration.dart';
  24. import 'package:sport/widgets/image.dart';
  25. import 'package:sport/widgets/loading.dart';
  26. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  27. class SportDataDetailPage extends StatefulWidget {
  28. final int type;
  29. final int sportType;
  30. final DateTime time;
  31. final DateTime? selectDate;
  32. final int index;
  33. const SportDataDetailPage({Key? key, required this.type, required this.sportType, required this.time, this.selectDate, required this.index}) : super(key: key);
  34. @override
  35. State<StatefulWidget> createState() => SportDataDetailPageState();
  36. }
  37. class SportDataDetailPageState extends State<SportDataDetailPage> with InjectApi, AutomaticKeepAliveClientMixin {
  38. int _tabIndex = 0;
  39. int _index = -1;
  40. int _initialPage = 0;
  41. SportDetail? _detail;
  42. final List<Map<String, dynamic>> tabs = [
  43. {"icon": "icon_table_consume_default", "icon_select": "icon_table_consume_select", "name": "消耗", "color": 0xffFFC400},
  44. {"icon": "icon_table_steps_default", "icon_select": "icon_table_steps_select", "name": "步数", "color": 0xff27D171},
  45. {"icon": "icon_table_duration_default", "icon_select": "icon_table_duration_select", "name": "时长", "color": 0xff5498FF},
  46. {"icon": "icon_table_strength_default", "icon_select": "icon_table_strength_select", "name": "强度", "color": 0xffFF5B1D},
  47. ];
  48. @override
  49. void initState() {
  50. super.initState();
  51. _tabIndex = widget.sportType;
  52. _loadData();
  53. }
  54. _loadData() async {
  55. setState(() {
  56. _detail = null;
  57. });
  58. var bluetooth = GetIt.I<Bluetooth>();
  59. if (bluetooth.isConnected == true) {
  60. await bluetooth.queryDeviceStep();
  61. }
  62. createFuture(widget.time).then((value) {
  63. if (mounted)
  64. setState(() {
  65. _detail = value;
  66. });
  67. });
  68. }
  69. Text title() {
  70. int type = widget.type;
  71. DateTime now = DateTime.now();
  72. DateTime time = widget.time;
  73. if (type == 0) {
  74. return Text(
  75. "${time.year} ${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')} ${WEEK[time.weekday == 0 ? 6 : time.weekday - 1]}${now.day == time.day ? "(今天)" : ""}",
  76. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  77. );
  78. } else if (type == 1) {
  79. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  80. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  81. return Text(
  82. "${start.year} ${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')} 至 ${start.year == end.year ? "" : "${end.year} "}${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}${now.day >= time.day && now.day < end.day ? "(本周)" : ""}",
  83. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  84. );
  85. } else if (type == 2) {
  86. return Text(
  87. "${time.year} ${'${time.month}'.padLeft(2, '0')}月${now.month == time.month ? "(本月)" : ""}",
  88. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  89. );
  90. } else if (type == 3) {
  91. return Text(
  92. "${time.year}年",
  93. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  94. );
  95. }
  96. return Text("");
  97. }
  98. Future<SportDetail?> createFuture(DateTime time) {
  99. int type = widget.type;
  100. return createFutureType(type, time);
  101. }
  102. Future<SportDetail?> createFutureType(int type, DateTime time) async {
  103. Future<RespData<SportDetailSimple>>? data;
  104. DateTime now = widget.selectDate == null ? DateTime.now() : widget.selectDate!;
  105. switch (type) {
  106. case 0:
  107. data = api.getSportRecordListOneDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}');
  108. break;
  109. case 1:
  110. _initialPage = now.weekday - 1;
  111. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  112. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  113. data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  114. break;
  115. case 2:
  116. _initialPage = now.day - 1;
  117. DateTime start = DateTime(time.year, time.month, 1);
  118. DateTime end = DateTime(time.year, time.month + 1, 0);
  119. data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  120. break;
  121. case 3:
  122. _initialPage = now.month - 1;
  123. data = api.getSportRecordListByMonth(time.year);
  124. break;
  125. }
  126. _index = _initialPage = -1;
  127. if (data != null) {
  128. var simple = await data;
  129. if (simple.code == 0) {
  130. return SportDetail(
  131. target: simple.data?.target,
  132. exerDayTotal: simple.data?.exerDayTotal ?? 0,
  133. recordsTodaySum: simple.data?.sum ?? RecordsTodaySum(consume: 0, duration: 0, crouch: 0, jump: 0),
  134. recordsTodayAvg: simple.data?.avg ?? RecordsTodaySum(consume: 0, duration: 0, crouch: 0, jump: 0, times: 0, step: 0),
  135. recordsToday: simple.data?.records,
  136. targetFinish: simple.data?.targetFinish ?? [],
  137. exerDay: simple.data?.exerDay ?? []);
  138. }
  139. }
  140. return null;
  141. }
  142. String numToStr(num? v, {int asFixed = -1}) {
  143. return SportUtils.numToStr(v, asFixed: asFixed);
  144. }
  145. @override
  146. Widget build(BuildContext context) {
  147. super.build(context);
  148. final List<String> xAxisList = xAxis();
  149. final List<double> dataList = data();
  150. final List<double> yAxisList = yAxis(dataList);
  151. int initialPage = _initialPage;
  152. String avgConsume = "0";
  153. String avgStep = "0";
  154. String avgDuration = "0";
  155. List<String> tabTitles = ["", "", "", ""];
  156. if (_detail != null) {
  157. tabTitles[0] = numToStr(_detail?.recordsTodaySum?.consume);
  158. tabTitles[1] = numToStr(_detail?.recordsTodaySum?.step);
  159. tabTitles[2] = numToStr((_detail?.recordsTodaySum?.durationMin ?? 0), asFixed: 0);
  160. tabTitles[3] = "${_detail?.recordsTodaySum?.met.toStringAsFixed(1)}";
  161. }
  162. if (widget.type != 0) {
  163. avgConsume = numToStr(_detail?.recordsTodayAvg?.consume);
  164. avgStep = numToStr(_detail?.recordsTodayAvg?.step);
  165. avgDuration = numToStr(_detail?.recordsTodayAvg?.durationMin ?? 0, asFixed: 0);
  166. }
  167. var _typeColor = Color(tabs[_tabIndex]["color"] as int);
  168. RecordsTodaySum? item = selectRecord(_index);
  169. var _bg = const Color(0xff241D19);
  170. return Scaffold(
  171. backgroundColor: _bg,
  172. body: RefreshIndicator(
  173. color: Theme.of(context).colorScheme.secondary,
  174. onRefresh: () async {
  175. _loadData();
  176. },
  177. child: CustomScrollView(
  178. slivers: [
  179. SliverFillRemaining(
  180. child: Column(
  181. children: [
  182. Container(
  183. color: _bg,
  184. child: Column(
  185. children: [
  186. Center(
  187. child: Row(
  188. mainAxisSize: MainAxisSize.min,
  189. children: <Widget>[
  190. GestureDetector(
  191. behavior: HitTestBehavior.opaque,
  192. onTap: () => PageNotification(page: 1).dispatch(context),
  193. child: Padding(
  194. padding: const EdgeInsets.all(25.0),
  195. child: arrowLeft(color: Colors.white),
  196. ),
  197. ),
  198. InkWell(
  199. onTap: () {
  200. SportDataPage.of(context)?.showCalendar(widget.selectDate ?? widget.time);
  201. UmengCommonSdk.onEvent("sport_data_calendar", {});
  202. },
  203. child: Padding(
  204. padding: const EdgeInsets.all(8.0),
  205. child: Row(
  206. mainAxisSize: MainAxisSize.min,
  207. children: [
  208. const SizedBox(
  209. width: 18,
  210. ),
  211. title(),
  212. const SizedBox(
  213. width: 4,
  214. ),
  215. Image.asset(
  216. "lib/assets/img/btn_date_bottom.png",
  217. width: 14,
  218. ),
  219. ],
  220. ),
  221. )),
  222. widget.index == 0
  223. ? Padding(
  224. padding: const EdgeInsets.all(25.0),
  225. child: arrowRight(color: Color(0x90cccccc)),
  226. )
  227. : GestureDetector(
  228. behavior: HitTestBehavior.opaque,
  229. onTap: () => PageNotification(page: -1).dispatch(context),
  230. child: Padding(
  231. padding: const EdgeInsets.all(25.0),
  232. child: arrowRight(color: Colors.white),
  233. ),
  234. ),
  235. ],
  236. ),
  237. ),
  238. // if (widget.type != 0)
  239. // Padding(
  240. // padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 20.0),
  241. // child: Row(
  242. // children: [
  243. // Expanded(
  244. // child: Column(
  245. // children: [
  246. // Text(
  247. // "日均大卡",
  248. // style: Theme
  249. // .of(context)
  250. // .textTheme
  251. // .bodyText1,
  252. // ),
  253. // const SizedBox(
  254. // height: 10,
  255. // ),
  256. // Text(
  257. // avgConsume,
  258. // style: Theme
  259. // .of(context)
  260. // .textTheme
  261. // .subtitle1
  262. // ?.copyWith(fontSize: 25, fontFamily: "DIN", color: Colors.white),
  263. // )
  264. // ],
  265. // ),
  266. // ),
  267. // Container(
  268. // width: 0.5,
  269. // height: 46,
  270. // color: Color(0xffDCDCDC).withOpacity(.3),
  271. // ),
  272. // Expanded(
  273. // child: Column(
  274. // children: [
  275. // Text(
  276. // "日均步数",
  277. // style: Theme
  278. // .of(context)
  279. // .textTheme
  280. // .bodyText1,
  281. // ),
  282. // const SizedBox(
  283. // height: 10,
  284. // ),
  285. // Text(
  286. // avgStep,
  287. // style: Theme
  288. // .of(context)
  289. // .textTheme
  290. // .subtitle1
  291. // ?.copyWith(fontSize: 25, fontFamily: "DIN", color: Colors.white),
  292. // )
  293. // ],
  294. // ),
  295. // ),
  296. // Container(
  297. // width: 0.5,
  298. // height: 46,
  299. // color: Color(0xffDCDCDC).withOpacity(.3),
  300. // ),
  301. // Expanded(
  302. // child: Column(
  303. // children: [
  304. // Text(
  305. // "周均分钟",
  306. // style: Theme
  307. // .of(context)
  308. // .textTheme
  309. // .bodyText1,
  310. // ),
  311. // const SizedBox(
  312. // height: 10,
  313. // ),
  314. // Text(
  315. // avgDuration,
  316. // style: Theme
  317. // .of(context)
  318. // .textTheme
  319. // .subtitle1
  320. // ?.copyWith(fontSize: 25, fontFamily: "DIN", color: Colors.white),
  321. // )
  322. // ],
  323. // ),
  324. // )
  325. // ],
  326. // ),
  327. // ),
  328. AlignedGridView.count(
  329. shrinkWrap: true,
  330. crossAxisCount: 4,
  331. crossAxisSpacing: 8,
  332. padding: const EdgeInsets.fromLTRB(16.0, 10.0, 16.0, 0),
  333. physics: NeverScrollableScrollPhysics(),
  334. itemCount: 4,
  335. itemBuilder: (context, index) {
  336. var tab = tabs[index];
  337. return InkWell(
  338. onTap: () {
  339. SportTypeNotification(index: index).dispatch(context);
  340. setState(() {
  341. _tabIndex = index;
  342. });
  343. UmengCommonSdk.onEvent("sport_data_type_$index", {});
  344. },
  345. child: Column(
  346. children: [
  347. Container(
  348. width: double.infinity,
  349. padding: const EdgeInsets.symmetric(vertical: 10.0),
  350. decoration: BoxDecoration(
  351. borderRadius: BorderRadius.circular(6),
  352. color: _tabIndex == index ? Color(tab["color"] as int) : Color(0xfff1f1f1).withOpacity(0.15),
  353. ),
  354. child: Center(
  355. child: Row(
  356. mainAxisSize: MainAxisSize.min,
  357. children: [
  358. Image.asset(
  359. "lib/assets/img/${_tabIndex == index ? tab["icon_select"] : tab["icon"]}.png",
  360. width: 16,
  361. ),
  362. const SizedBox(
  363. width: 5,
  364. ),
  365. Text(
  366. "${(tab["name"] is String) ? tab["name"] : (tab["name"] as List)[widget.type]}",
  367. style: Theme.of(context).textTheme.bodyText2?.copyWith(color: _tabIndex == index ? Colors.white : Color(0xff999999)),
  368. ),
  369. ],
  370. ),
  371. ),
  372. ),
  373. _tabIndex == index
  374. ? CustomPaint(
  375. painter: TrianglePath(),
  376. child: Container(
  377. height: 20,
  378. width: 20,
  379. ),
  380. )
  381. : Container(
  382. height: 20,
  383. ),
  384. ],
  385. ),
  386. );
  387. },
  388. ),
  389. ],
  390. ),
  391. ),
  392. Expanded(
  393. child: Container(
  394. color: Colors.white,
  395. child: _detail == null
  396. ? RequestLoadingWidget()
  397. : Column(
  398. children: [
  399. IndexedStack(
  400. index: _tabIndex,
  401. children: [
  402. Header(
  403. index: 0,
  404. type: widget.type,
  405. params: {
  406. "date": "${_index < 0 ? "${TABS[widget.type]}总消耗" : item?.getDate(widget.type)}",
  407. "unit": "大卡",
  408. "total": "${item?.consume ?? tabTitles[_tabIndex]}",
  409. "current": "${widget.type > 0 && _index > -1 ? "1" : null}",
  410. "game": "游戏消耗",
  411. "jog": "跑步消耗",
  412. "daily": "日常消耗",
  413. "game_value": "${item?.consume_game ?? 0}",
  414. "jog_value": "${item?.consume_jog ?? 0}",
  415. "daily_value": "${item?.consume_daily ?? 0}",
  416. "times": "${item?.times}",
  417. "avg": "$avgConsume"
  418. },
  419. recordsTodaySum: item,
  420. color: _typeColor,
  421. ),
  422. Header(
  423. index: 1,
  424. type: widget.type,
  425. params: {
  426. "date": "${_index < 0 ? "${TABS[widget.type]}总步数" : item?.getDate(widget.type)}",
  427. "unit": "步",
  428. "total": "${item?.step ?? tabTitles[_tabIndex]}",
  429. "current": "${widget.type > 0 && _index > -1 ? "1" : null}",
  430. "game": "游戏步数",
  431. "jog": "跑步步数",
  432. "daily": "日常步数",
  433. "game_value": "${item?.step_game ?? 0}",
  434. "jog_value": "${item?.step_jog ?? 0}",
  435. "daily_value": "${item?.step_daily ?? 0}",
  436. "times": "${item?.times}",
  437. "avg": "$avgStep"
  438. },
  439. color: _typeColor,
  440. ),
  441. Header(
  442. index: 2,
  443. type: widget.type,
  444. params: {
  445. "date": "${_index < 0 ? "${TABS[widget.type]}总时长" : item?.getDate(widget.type)}",
  446. "unit": "分钟",
  447. "total": "${item?.durationMin.toStringAsFixed(0) ?? tabTitles[_tabIndex]}",
  448. "current": "${widget.type > 0 && _index > -1 ? "1" : null}",
  449. "game": "游戏时长",
  450. "jog": "跑步时长",
  451. "daily": widget.type == 0 || _index > -1 ? "运动目标" : "达标天数",
  452. "game_value": "${item?.durationMinGame.toStringAsFixed(0) ?? 0}",
  453. "jog_value": "${item?.durationMinJog.toStringAsFixed(0) ?? 0}",
  454. "daily_value": widget.type == 0 || _index > -1 ? "${(_detail?.target?.duration ?? 0) ~/ 60}" : "${yAxisList.where((element) => element > (_detail?.target?.duration ?? 0)).isNotEmpty ? yAxisList.where((element) => element > (_detail?.target?.duration ?? 0)).reduce((value, element) => value + element) : 0}",
  455. "times": "${item?.times}",
  456. "avg": "$avgDuration"
  457. },
  458. settingDuration: widget.type == 0 || _index > -1,
  459. color: _typeColor,
  460. ),
  461. Header(
  462. index: 3,
  463. type: widget.type,
  464. params: {
  465. "date": "${_index < 0 ? "${TABS[widget.type]}MET" : item?.getDate(widget.type)}",
  466. "unit": "MET",
  467. "total": "${(item?.met.toStringAsFixed(1) ?? tabTitles[_tabIndex])}",
  468. "current": "${widget.type > 0 && _index > -1 ? "1" : null}",
  469. "game": "游戏消耗",
  470. "jog": "跑步消耗",
  471. "daily": "日常消耗",
  472. "game_value": "${item?.consume_game ?? 0}",
  473. "jog_value": "${item?.consume_jog ?? 0}",
  474. "daily_value": "${item?.consume_daily ?? 0}",
  475. "met": "强度评级",
  476. "met_value": "${metToLabel(item?.met ?? 0.0)}",
  477. "met_double": "${item?.met ?? 0.0}",
  478. "met_desc": "${metToDetail(item?.met ?? 0.0)}",
  479. },
  480. color: _typeColor,
  481. ),
  482. ],
  483. ),
  484. Expanded(child: LayoutBuilder(
  485. builder: (context, size) {
  486. return Container(
  487. height: size.maxHeight,
  488. child: widget.type == 0 && _tabIndex == 3
  489. ? Container(
  490. width: double.infinity,
  491. child: Center(
  492. child: Container(
  493. transform: Matrix4.translationValues(0, -100, 0),
  494. width: 300,
  495. height: 300,
  496. child: CustomPaint(
  497. painter: StrengthBg(),
  498. child: Container(
  499. child: Center(
  500. child: CircularPercentIndicator(
  501. radius: 200.0,
  502. lineWidth: 10.0,
  503. percent: (item?.met ?? 0) / 12.0,
  504. // percent: .7,
  505. center: Column(
  506. children: <Widget>[
  507. Text(
  508. "${item?.met.toStringAsFixed(1)}",
  509. style: Theme.of(context).textTheme.headline2!.copyWith(fontSize: 40.0, fontFamily: "DIN"),
  510. ),
  511. Text(
  512. "MET值",
  513. style: Theme.of(context).textTheme.subtitle2,
  514. )
  515. ],
  516. mainAxisSize: MainAxisSize.min,
  517. ),
  518. animation: true,
  519. animationDuration: 1000,
  520. animateFromLastPercent: true,
  521. startAngle: 200.0,
  522. arcType: ArcType.CUSTOM_3,
  523. backgroundColor: Color(0xfff1f1f1),
  524. rotateLinearGradient: true,
  525. circularStrokeCap: CircularStrokeCap.butt,
  526. linearGradient: LinearGradient(
  527. colors: <Color>[Color(0xffFFE600), Color(0xffFF7323)],
  528. ),
  529. ),
  530. ),
  531. ),
  532. ),
  533. ),
  534. ),
  535. )
  536. : ChartWidget(
  537. xAxis: xAxisList,
  538. yAxis: yAxisList,
  539. data: dataList,
  540. initialPage: initialPage,
  541. color: _typeColor,
  542. xAxisType: widget.type,
  543. chartHeight: size.maxHeight,
  544. targetLine: ((widget.type != 0 && widget.type != 3 && _tabIndex == 2) ? (_detail?.target?.duration.toDouble() ?? 0.0) : 0.0) / 60.0,
  545. unit: ["大卡", "步", "分钟", ""][_tabIndex],
  546. onTap: (index, drag) {
  547. final old = _index;
  548. setState(() {
  549. _index = drag
  550. ? index
  551. : _index == index
  552. ? -1
  553. : index;
  554. });
  555. return drag ? false : old == index;
  556. },
  557. ),
  558. );
  559. },
  560. ))
  561. ],
  562. ),
  563. ),
  564. ),
  565. ],
  566. ))
  567. ],
  568. ),
  569. ),
  570. );
  571. }
  572. List<double> data() {
  573. switch (widget.type) {
  574. case 0:
  575. {
  576. List<double> data = List.generate(24, (index) => 0);
  577. if (_detail != null) {
  578. for (var i = 0; i < (_detail!.recordsToday?.length ?? 0); i++) {
  579. var item = _detail!.recordsToday![i];
  580. for (var j = 0; j < data.length; j++) {
  581. if (item.isSameHour(j)) {
  582. data[j] += item.getValue(_tabIndex);
  583. }
  584. }
  585. }
  586. }
  587. return data;
  588. }
  589. case 1:
  590. DateTime t = widget.time;
  591. DateTime week = DateTime(t.year, t.month, t.day).subtract(Duration(days: t.weekday - 1));
  592. List<double> data = List.filled(7, 0);
  593. if (_detail != null) {
  594. for (var i = 0; i < (_detail!.recordsToday?.length ?? 0); i++) {
  595. var item = _detail!.recordsToday![i];
  596. for (var j = 0; j < data.length; j++) {
  597. if (item.isSameDay(week.add(Duration(days: j)).day)) {
  598. data[j] = item.getValue(_tabIndex);
  599. }
  600. }
  601. }
  602. }
  603. return data;
  604. case 2:
  605. DateTime t = widget.time;
  606. DateTime now = DateTime(t.year, t.month, 1);
  607. DateTime next = DateTime(t.year, t.month + 1, 1);
  608. int diff = now.difference(next).inDays.abs();
  609. List<double> data = List.filled(diff, 0);
  610. if (_detail != null) {
  611. for (var i = 0; i < (_detail!.recordsToday?.length ?? 0); i++) {
  612. var item = _detail!.recordsToday![i];
  613. for (var j = 0; j < data.length; j++) {
  614. if (item.isSameDay(now.add(Duration(days: j)).day)) {
  615. data[j] = item.getValue(_tabIndex);
  616. }
  617. }
  618. }
  619. }
  620. return data;
  621. case 3:
  622. List<double> data = List.filled(12, 0);
  623. if (_detail != null) {
  624. for (var i = 0; i < (_detail!.recordsToday?.length ?? 0); i++) {
  625. var item = _detail!.recordsToday![i];
  626. data[(item.month) - 1] = item.getValue(_tabIndex);
  627. }
  628. }
  629. return data;
  630. default:
  631. return [];
  632. }
  633. }
  634. List<String> xAxis() {
  635. switch (widget.type) {
  636. case 0:
  637. return ["00:00", "06:00", "12:00", "18:00", "00:00"];
  638. case 1:
  639. DateTime t = widget.time;
  640. DateTime now = DateTime(t.year, t.month, t.day - t.weekday);
  641. return List.generate(7, (index) {
  642. DateTime time = DateTime(now.year, now.month, now.day + index + 1);
  643. return "${time.month}/${time.day}\n${WEEK[index]}";
  644. });
  645. case 2:
  646. DateTime t = widget.time;
  647. DateTime now = DateTime(t.year, t.month, 1);
  648. DateTime next = DateTime(t.year, t.month + 1, 1);
  649. int diff = now.difference(next).inDays.abs();
  650. return ["1", "5", "10", "15", "20", "25", if (diff > 29) "30"];
  651. case 3:
  652. return List.generate(12, (index) => "${index + 1}");
  653. default:
  654. return [];
  655. }
  656. }
  657. List<double> yAxis(List<double> dataList) {
  658. if (dataList.isNotEmpty == true) {
  659. double max = dataList.reduce((value, element) => value > element ? value : element);
  660. max = math.max(max, ((widget.type != 0 && widget.type != 3 && _tabIndex == 2) ? (_detail?.target?.duration.toDouble() ?? 0.0) : 0.0) / 60.0);
  661. int count = 5;
  662. double split = max / count;
  663. int length = split.round().toString().length;
  664. int one = int.parse("1" + List.filled(math.max(0, length - 2), 0).join(""));
  665. int p = one;
  666. while (p < split) {
  667. p += one;
  668. }
  669. print("1111111111111111 max = $max $split $length $p");
  670. // print("max $max $p $split");
  671. return List.generate(count + 1, (index) => index * p.toDouble());
  672. }
  673. return [];
  674. }
  675. RecordsTodaySum? selectRecord(int index) {
  676. if (_detail == null) return null;
  677. if (widget.type == 0 || index < 0) {
  678. return _detail?.recordsTodaySum;
  679. }
  680. DateTime t = widget.time;
  681. DateTime week = DateTime(t.year, t.month, t.day).subtract(Duration(days: t.weekday - 1));
  682. var recordsToday = _detail?.recordsToday ?? [];
  683. if (recordsToday.isNotEmpty == true) {
  684. for (var element in recordsToday) {
  685. switch (widget.type) {
  686. case 1:
  687. if (element.isSameDay(week.add(Duration(days: index)).day)) return element;
  688. break;
  689. case 2:
  690. if (element.isSameDay(index + 1)) return element;
  691. break;
  692. case 3:
  693. if (element.month == index + 1) return element;
  694. break;
  695. }
  696. }
  697. }
  698. switch (widget.type) {
  699. case 0:
  700. return RecordsTodaySum(createdAt: "${t.year}-${t.month.toString().padLeft(2, '0')}-${t.day.toString().padLeft(2, '0')}");
  701. case 1:
  702. return RecordsTodaySum(createdAt: "${t.year}-${t.month.toString().padLeft(2, '0')}-${(index + week.day).toString().padLeft(2, '0')}");
  703. case 2:
  704. return RecordsTodaySum(createdAt: "${t.year}-${t.month.toString().padLeft(2, '0')}-${(index + 1).toString().padLeft(2, '0')}");
  705. case 3:
  706. return RecordsTodaySum(createdAt: "${t.year}-${(index + 1).toString().padLeft(2, '0')}-01", year: t.year, month: index + 1);
  707. default:
  708. return null;
  709. }
  710. }
  711. @override
  712. bool get wantKeepAlive => true;
  713. void refresh() {
  714. _loadData();
  715. }
  716. }
  717. class Header extends StatelessWidget {
  718. final int index;
  719. final int type;
  720. final Map<String, String> params;
  721. final RecordsTodaySum? recordsTodaySum;
  722. final Color color;
  723. final bool settingDuration;
  724. const Header({Key? key, required this.index, required this.type, required this.params, this.recordsTodaySum, required this.color, this.settingDuration = false}) : super(key: key);
  725. @override
  726. Widget build(BuildContext context) {
  727. final _marginBox = EdgeInsets.zero;
  728. final current = params["current"] == "1";
  729. final color = current ? this.color : const Color(0xff999999);
  730. final _textStyle = Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 14.0, color: const Color(0xff999999));
  731. final _textStyleValue = Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 18.0, color: color, fontWeight: FontWeight.w500);
  732. return Container(
  733. decoration: circular(),
  734. padding: const EdgeInsets.all(16.0),
  735. margin: _marginBox,
  736. child: Column(
  737. crossAxisAlignment: CrossAxisAlignment.start,
  738. children: [
  739. if (!(index == 3 && type == 0))
  740. Container(
  741. margin: const EdgeInsets.only(bottom: 16.0),
  742. height: 70,
  743. child: Center(
  744. child: Column(
  745. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  746. children: [
  747. params["date"] != "null"
  748. ? Text(
  749. "${params["date"]}",
  750. style: Theme.of(context).textTheme.bodyText1?.copyWith(color: color),
  751. )
  752. : Container(),
  753. Row(
  754. children: <Widget>[
  755. Text(
  756. "${params["total"]}",
  757. style: current ? Theme.of(context).textTheme.headline2!.copyWith(fontSize: 35.0, fontFamily: "DIN", color: this.color) : Theme.of(context).textTheme.headline2!.copyWith(fontSize: 35.0, fontFamily: "DIN"),
  758. ),
  759. Text(
  760. " ${params["unit"]}",
  761. style: current ? Theme.of(context).textTheme.subtitle2?.copyWith(color: this.color) : Theme.of(context).textTheme.subtitle2,
  762. )
  763. ],
  764. crossAxisAlignment: CrossAxisAlignment.baseline,
  765. textBaseline: TextBaseline.alphabetic,
  766. mainAxisSize: MainAxisSize.min,
  767. ),
  768. ],
  769. ),
  770. ),
  771. ),
  772. if (recordsTodaySum != null)
  773. FutureBuilder<String>(
  774. future: Provider.of<UserModel>(context, listen: false).sport(recordsTodaySum),
  775. builder: (context, snapshot) {
  776. return InkWell(
  777. onTap: () async {
  778. if (await showDialog(
  779. context: context,
  780. builder: (context) => SportReferencePage(
  781. sum: recordsTodaySum!,
  782. ),
  783. ) ==
  784. true) {
  785. SportDataDetailPageState? state = context.findAncestorStateOfType();
  786. state?.refresh();
  787. }
  788. },
  789. child: Stack(
  790. children: [
  791. Container(
  792. padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
  793. decoration: BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor, borderRadius: BorderRadius.circular(50)),
  794. child: Center(
  795. child: Text(
  796. "相当于 ${snapshot.data ?? ""}",
  797. style: Theme.of(context).textTheme.bodyText1,
  798. ),
  799. ),
  800. ),
  801. Positioned(top: 0, bottom: 0, right: 16.0, child: arrowRight4()),
  802. ],
  803. ),
  804. );
  805. }),
  806. if (!params.containsKey("met"))
  807. Padding(
  808. padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 6),
  809. child: Row(
  810. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  811. children: [
  812. Column(
  813. crossAxisAlignment: CrossAxisAlignment.center,
  814. children: [
  815. Text(
  816. "${params["game"]}",
  817. style: _textStyle,
  818. ),
  819. const SizedBox(
  820. height: 6,
  821. ),
  822. Text(
  823. "${params["game_value"]}",
  824. style: _textStyleValue,
  825. ),
  826. ],
  827. ),
  828. Column(
  829. crossAxisAlignment: CrossAxisAlignment.center,
  830. children: [
  831. Text(
  832. "${params["jog"]}",
  833. style: _textStyle,
  834. ),
  835. const SizedBox(
  836. height: 6,
  837. ),
  838. Text(
  839. "${params["jog_value"]}",
  840. style: _textStyleValue,
  841. ),
  842. ],
  843. ),
  844. settingDuration
  845. ? InkWell(
  846. onTap: () async {
  847. await NavigatorUtil.goPage(context, (context) => DurationSettingPage());
  848. SportDataDetailPageState? state = context.findAncestorStateOfType();
  849. state?.refresh();
  850. },
  851. child: Column(
  852. crossAxisAlignment: CrossAxisAlignment.center,
  853. children: [
  854. Row(
  855. children: [
  856. Text(
  857. "${params["daily"]}",
  858. style: _textStyle,
  859. ),
  860. const SizedBox(
  861. width: 5,
  862. ),
  863. arrowRight4()
  864. ],
  865. ),
  866. const SizedBox(
  867. height: 6,
  868. ),
  869. Text(
  870. "${params["daily_value"]}",
  871. style: _textStyleValue,
  872. ),
  873. ],
  874. ),
  875. )
  876. : Column(
  877. crossAxisAlignment: CrossAxisAlignment.center,
  878. children: [
  879. Text(
  880. "${params["daily"]}",
  881. style: _textStyle,
  882. ),
  883. const SizedBox(
  884. height: 6,
  885. ),
  886. Text(
  887. "${params["daily_value"]}",
  888. style: _textStyleValue,
  889. ),
  890. ],
  891. ),
  892. ],
  893. ),
  894. ),
  895. if (params.containsKey("times"))
  896. const Divider(
  897. height: 1,
  898. ),
  899. if (params.containsKey("times"))
  900. Padding(
  901. padding: const EdgeInsets.symmetric(vertical: 6),
  902. child: Row(
  903. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  904. children: [
  905. Text(
  906. "运动${params["times"]}次",
  907. style: Theme.of(context).textTheme.bodyText1,
  908. ),
  909. if (params["current"] != "1" && type != 0) Text("${index == 2 ? "周均" : "日均"}${params["avg"]}${params["unit"]}", style: Theme.of(context).textTheme.bodyText1),
  910. ],
  911. ),
  912. ),
  913. if (params.containsKey("met"))
  914. SizedBox(
  915. height: 20,
  916. ),
  917. if (params.containsKey("met"))
  918. Padding(
  919. padding: const EdgeInsets.symmetric(vertical: 8.0),
  920. child: Row(
  921. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  922. children: [
  923. Text(
  924. "${params["met"]}",
  925. style: Theme.of(context).textTheme.headline3,
  926. ),
  927. Text(
  928. "${params["met_value"]}",
  929. style: _textStyle.copyWith(color: metToColor(double.parse(params["met_double"] ?? "0.0"))),
  930. ),
  931. ],
  932. ),
  933. ),
  934. if (params.containsKey("met"))
  935. Padding(
  936. padding: const EdgeInsets.symmetric(vertical: 8.0),
  937. child: Text(
  938. "${params["met_desc"]}",
  939. style: _textStyle.copyWith(fontSize: 14.0, color: Color(0xff999999), height: 1.5),
  940. )),
  941. SizedBox(
  942. height: 20.0,
  943. ),
  944. ],
  945. ),
  946. );
  947. }
  948. }
  949. class ChartWidget extends StatefulWidget {
  950. final List<String> xAxis;
  951. final List<double> yAxis;
  952. final List<double> data;
  953. final int initialPage;
  954. final int? xAxisType;
  955. final Color color;
  956. final double chartHeight;
  957. final double targetLine;
  958. final String? unit;
  959. final bool Function(int index, bool drag) onTap;
  960. const ChartWidget({Key? key, required this.xAxis, required this.yAxis, required this.data, this.initialPage = 0, this.xAxisType, required this.color, required this.chartHeight, this.targetLine = 0, this.unit, required this.onTap}) : super(key: key);
  961. @override
  962. State<StatefulWidget> createState() => _ChartWidget();
  963. }
  964. class _ChartWidget extends State<ChartWidget> {
  965. double _dx = 0;
  966. List<Rect> area = [];
  967. onTap(Offset offset, bool drag) {
  968. var dx = offset.dx;
  969. for (var i = 0; i < area.length; i++) {
  970. var rect = area[i];
  971. if (rect.contains(offset)) {
  972. if (widget.onTap(i, drag)) {
  973. dx = 0;
  974. }
  975. break;
  976. }
  977. }
  978. if (offset == Offset.zero) {
  979. widget.onTap(-1, drag);
  980. }
  981. setState(() {
  982. _dx = dx;
  983. });
  984. }
  985. @override
  986. Widget build(BuildContext context) {
  987. return GestureDetector(
  988. onHorizontalDragStart: (details) {
  989. setState(() {
  990. _dx = details.localPosition.dx;
  991. });
  992. },
  993. onHorizontalDragUpdate: (details) {
  994. // print("${details.localPosition.dx} -- ${details.localPosition.dy}");
  995. onTap(details.localPosition, true);
  996. },
  997. onHorizontalDragCancel: () {},
  998. onHorizontalDragEnd: (details) {
  999. if (details.velocity.pixelsPerSecond.dx > 0) {
  1000. // 右滑
  1001. PageNotification(page: 1).dispatch(context);
  1002. } else if (details.velocity.pixelsPerSecond.dx < 0) {
  1003. // 左滑
  1004. PageNotification(page: -1).dispatch(context);
  1005. } else {}
  1006. onTap(Offset.zero, false);
  1007. },
  1008. onTapUp: (details) {
  1009. onTap(details.localPosition, false);
  1010. },
  1011. child: Container(
  1012. child: CustomPaint(
  1013. size: Size.fromHeight(widget.chartHeight),
  1014. painter: DataChart(
  1015. callback: (area) {
  1016. this.area = area;
  1017. },
  1018. xAxis: widget.xAxis,
  1019. yAxis: widget.yAxis,
  1020. data: widget.data,
  1021. dx: _dx,
  1022. initialPage: widget.initialPage,
  1023. color: widget.color,
  1024. xAxisType: widget.xAxisType,
  1025. targetLine: widget.targetLine,
  1026. unit: widget.unit),
  1027. ),
  1028. ),
  1029. );
  1030. }
  1031. }
  1032. class TrianglePath extends CustomPainter {
  1033. @override
  1034. void paint(Canvas canvas, Size size) {
  1035. var path = Path();
  1036. path.moveTo(size.width / 2, size.height / 2);
  1037. path.lineTo(0, size.height + 2);
  1038. path.lineTo(size.width, size.height + 2);
  1039. canvas.drawPath(path, Paint()..color = Colors.white);
  1040. }
  1041. @override
  1042. bool shouldRepaint(covariant CustomPainter oldDelegate) {
  1043. return false;
  1044. }
  1045. }