run_list.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_sticky_header/flutter_sticky_header.dart';
  4. import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
  5. import 'package:sport/application.dart';
  6. import 'package:sport/bean/jog/sum.dart';
  7. import 'package:sport/pages/run/run_detail_page.dart';
  8. import 'package:sport/pages/run/run_index.dart';
  9. import 'package:sport/pages/run/run_page.dart';
  10. import 'package:sport/pages/run/statistics_page.dart';
  11. import 'package:sport/router/navigator_util.dart';
  12. import 'package:sport/services/api/inject_api.dart';
  13. import 'package:sport/services/api/resp.dart';
  14. import 'package:sport/services/app_lifecycle_state.dart';
  15. import 'package:sport/utils/DateFormat.dart';
  16. import 'package:sport/utils/sport_utils.dart';
  17. import 'package:sport/widgets/appbar.dart';
  18. import 'package:sport/widgets/error.dart';
  19. import 'package:sport/widgets/image.dart';
  20. import 'package:sport/widgets/loading.dart';
  21. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  22. class RunListPage extends StatefulWidget {
  23. @override
  24. State<StatefulWidget> createState() => _PageState();
  25. }
  26. class _PageState extends LifecycleState<RunListPage> with InjectApi {
  27. ValueNotifier<int> _notifierYear = ValueNotifier(DateTime.now().year);
  28. bool expand = true;
  29. bool loading = true;
  30. Object? error;
  31. List<Records>? records;
  32. Sum? sum;
  33. @override
  34. void initState() {
  35. super.initState();
  36. runDelete = false;
  37. _notifierYear.addListener(() {
  38. _loadData();
  39. });
  40. records = [];
  41. _loadData();
  42. }
  43. @override
  44. void dispose() {
  45. super.dispose();
  46. _notifierYear.dispose();
  47. }
  48. _loadData() async {
  49. setState(() {
  50. this.loading = true;
  51. this.records = [];
  52. });
  53. try {
  54. RespData<JogSum> data = await api.jogListOneYear(_notifierYear.value);
  55. setState(() {
  56. this.loading = false;
  57. this.sum = data.data?.sum;
  58. this.records = data.data?.records ?? [];
  59. });
  60. } catch (e) {
  61. print(e);
  62. if(isDebugShoe) {
  63. setState(() {
  64. this.error = e;
  65. });
  66. }
  67. }
  68. }
  69. @override
  70. Widget build(BuildContext context) {
  71. final List<Widget> slivers = [];
  72. slivers.add(SliverAppBar(
  73. backgroundColor: const Color(0xff241D19),
  74. expandedHeight: 300.0,
  75. elevation: 0.0,
  76. pinned: true,
  77. leading: IconButton(
  78. icon: Image.asset(
  79. "lib/assets/img/topbar_return.png",
  80. color: Colors.white,
  81. ),
  82. onPressed: () {
  83. Navigator.maybePop(context);
  84. },
  85. ),
  86. titleSpacing: -16.0,
  87. centerTitle: false,
  88. title: Text(
  89. "累计记录",
  90. style: titleStyle.copyWith(color: Colors.white),
  91. ),
  92. brightness: Brightness.dark,
  93. actions: [
  94. TextButton(
  95. child: Text(
  96. "跑步统计",
  97. style: titleStyle.copyWith(
  98. color: Colors.white,
  99. fontSize: 16.0,
  100. fontWeight: FontWeight.normal,
  101. ),
  102. ),
  103. onPressed: () {
  104. NavigatorUtil.goPage(context, (context) => RunStatisticsPage());
  105. UmengCommonSdk.onEvent("run_statistics", {});
  106. })
  107. ],
  108. flexibleSpace: FlexibleSpaceBar(
  109. collapseMode: CollapseMode.none,
  110. background: Column(
  111. mainAxisAlignment: MainAxisAlignment.end,
  112. children: [
  113. Text("${formatNum((sum?.distance ?? 0) / 1000, 2)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 45.0, fontFamily: "DIN", color: Theme.of(context).accentColor)),
  114. Text(
  115. "累计里程(公里)",
  116. style: Theme.of(context).textTheme.headline6!,
  117. ),
  118. const SizedBox(
  119. height: 10.0,
  120. ),
  121. Padding(
  122. padding: const EdgeInsets.symmetric(vertical: 25.0),
  123. child: Row(
  124. children: [
  125. Expanded(
  126. child: Column(
  127. mainAxisSize: MainAxisSize.min,
  128. children: [
  129. Text("${DateFormat.toTime(sum?.duration ?? 0)}",
  130. style: Theme.of(context).textTheme.headline4!.copyWith(
  131. fontSize: 20.0,
  132. fontFamily: "DIN",
  133. )),
  134. Text(
  135. "总时长",
  136. style: Theme.of(context).textTheme.headline6!.copyWith(fontSize: 12.0),
  137. ),
  138. ],
  139. )),
  140. Container(
  141. height: 42.0,
  142. width: .5,
  143. color: Colors.white54,
  144. ),
  145. Expanded(
  146. child: Column(
  147. mainAxisSize: MainAxisSize.min,
  148. children: [
  149. Row(
  150. mainAxisSize: MainAxisSize.min,
  151. children: [
  152. Text(
  153. "${SportUtils.pace11(SportUtils.calPace(sum?.duration ?? 0, (sum?.distance ?? 0.0).toDouble() / 1000))}",
  154. style: Theme.of(context).textTheme.headline4!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  155. ),
  156. Text(
  157. "′",
  158. style: Theme.of(context).textTheme.headline4!.copyWith(fontSize: 20.0),
  159. ),
  160. Text(
  161. "${SportUtils.pace12(SportUtils.calPace(sum?.duration ?? 0, (sum?.distance ?? 0.0).toDouble() / 1000))}",
  162. style: Theme.of(context).textTheme.headline4!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  163. ),
  164. Text(
  165. "″",
  166. style: Theme.of(context).textTheme.headline4!.copyWith(fontSize: 20.0),
  167. ),
  168. ],
  169. ),
  170. Text(
  171. "配速(每公里用时)",
  172. style: Theme.of(context).textTheme.headline6!.copyWith(fontSize: 12.0),
  173. ),
  174. ],
  175. )),
  176. Container(
  177. height: 42.0,
  178. width: .5,
  179. color: Colors.white54,
  180. ),
  181. Expanded(
  182. child: Column(
  183. mainAxisSize: MainAxisSize.min,
  184. children: [
  185. Text("${sum?.times ?? 0}",
  186. style: Theme.of(context).textTheme.headline4!.copyWith(
  187. fontSize: 20.0,
  188. fontFamily: "DIN",
  189. )),
  190. Text(
  191. "运动次数(次)",
  192. style: Theme.of(context).textTheme.headline6!.copyWith(fontSize: 12.0),
  193. ),
  194. ],
  195. )),
  196. ],
  197. ),
  198. ),
  199. const SizedBox(
  200. height: 40.0,
  201. ),
  202. ],
  203. ),
  204. ),
  205. bottom: PreferredSize(
  206. preferredSize: Size.fromHeight(50.0),
  207. child: Container(
  208. height: 50.0,
  209. transform: Matrix4.translationValues(0, 2, 0),
  210. decoration: BoxDecoration(
  211. borderRadius: BorderRadius.only(topRight: Radius.circular(10), topLeft: Radius.circular(10)),
  212. color: Colors.white,
  213. ),
  214. child: Padding(
  215. padding: const EdgeInsets.symmetric(horizontal: 20.0),
  216. child: Row(
  217. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  218. children: [
  219. GestureDetector(
  220. behavior: HitTestBehavior.opaque,
  221. onTap: () async {
  222. var year = await showDialog(
  223. context: context,
  224. builder: (context) => Dialog(
  225. child: ConstrainedBox(
  226. constraints: BoxConstraints(minHeight: 300, maxHeight: 400),
  227. child: ScrollablePositionedList.builder(
  228. itemCount: 10,
  229. itemBuilder: (context, index) {
  230. var now = DateTime.now();
  231. int year = now.year - index;
  232. return GestureDetector(
  233. onTap: () => Navigator.pop(context, year),
  234. child: Container(
  235. width: double.infinity,
  236. margin: EdgeInsets.symmetric(vertical: 20, horizontal: 30),
  237. alignment: Alignment.center,
  238. child: Text('$year'),
  239. ),
  240. );
  241. },
  242. initialScrollIndex: DateTime.now().year - _notifierYear.value,
  243. ))));
  244. if (year != null) _notifierYear.value = year;
  245. },
  246. child: ValueListenableBuilder(
  247. valueListenable: _notifierYear,
  248. builder: (_, year, __) => RichText(
  249. text: TextSpan(children: <InlineSpan>[
  250. TextSpan(text: '$year', style: Theme.of(context).textTheme.subtitle1!),
  251. WidgetSpan(
  252. alignment: PlaceholderAlignment.middle,
  253. child: Padding(padding: const EdgeInsets.only(left: 7.0), child: arrowBottom()),
  254. ),
  255. ]),
  256. ),
  257. ),
  258. ),
  259. GestureDetector(
  260. onTap: () {
  261. setState(() {
  262. expand = !expand;
  263. records?.forEach((element) {
  264. element.expand = expand;
  265. });
  266. });
  267. },
  268. child: RichText(
  269. text: TextSpan(children: <InlineSpan>[
  270. TextSpan(text: expand == true ? '收起' : '展开', style: Theme.of(context).textTheme.bodyText1!),
  271. WidgetSpan(
  272. alignment: PlaceholderAlignment.middle,
  273. child: Padding(
  274. padding: const EdgeInsets.only(left: 7.0),
  275. child: Image.asset("lib/assets/img/run_data_${expand == true ? 'retract' : 'open'}.png"),
  276. ),
  277. ),
  278. ]),
  279. ),
  280. ),
  281. ],
  282. ),
  283. ),
  284. )),
  285. ));
  286. if (loading == true) {
  287. slivers.add(SliverToBoxAdapter(
  288. child: RequestLoadingWidget(),
  289. ));
  290. } else {
  291. if (records?.isNotEmpty != true) {
  292. slivers.add(SliverToBoxAdapter(
  293. child: RequestErrorWidget(
  294. null,
  295. assets: RequestErrorWidget.ASSETS_NO_MOTION,
  296. msg: "暂无记录",
  297. ),
  298. ));
  299. }
  300. }
  301. if(error != null){
  302. slivers.add(SliverToBoxAdapter(
  303. child: RequestErrorWidget(
  304. null,
  305. assets: RequestErrorWidget.ASSETS_NO_MOTION,
  306. msg: "${error}",
  307. ),
  308. ));
  309. }
  310. if (records?.isNotEmpty == true) {
  311. for (var i = 0; i < records!.length; i++) {
  312. var e = records![i];
  313. var child = e.records ?? [];
  314. final SizedBox space = const SizedBox(
  315. width: 4,
  316. );
  317. slivers.add(SliverStickyHeader(
  318. header: GestureDetector(
  319. behavior: HitTestBehavior.opaque,
  320. onTap: () {
  321. setState(() {
  322. e.expand = !e.expand;
  323. if (e.expand == true) expand = true;
  324. });
  325. },
  326. child: Container(
  327. color: Colors.white,
  328. padding: const EdgeInsets.fromLTRB(12.0, 10, 12.0, 0),
  329. child: Container(
  330. width: double.infinity,
  331. color: const Color(0xfff1f1f1),
  332. padding: const EdgeInsets.fromLTRB(9, 7, 9, 5),
  333. child: Row(
  334. mainAxisAlignment: MainAxisAlignment.spaceAround,
  335. children: [
  336. Container(
  337. width: 50.0,
  338. child: Align(
  339. alignment: Alignment.centerLeft,
  340. child: Column(
  341. children: [
  342. Text(
  343. '${e.month}月',
  344. style: Theme.of(context).textTheme.headline1!,
  345. ),
  346. Text(
  347. '共${child.length}次',
  348. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 10),
  349. ),
  350. ],
  351. ),
  352. ),
  353. ),
  354. Expanded(
  355. flex: 3,
  356. child: Row(
  357. mainAxisSize: MainAxisSize.min,
  358. children: [
  359. Image.asset("lib/assets/img/home_title_steps.png"),
  360. space,
  361. Text(
  362. '${formatNum((e.distance ?? 0) / 1000, 2)}',
  363. style: Theme.of(context).textTheme.bodyText2!,
  364. ),
  365. ],
  366. ),
  367. ),
  368. Expanded(
  369. flex: 4,
  370. child: Center(
  371. child: Row(
  372. mainAxisSize: MainAxisSize.min,
  373. children: [
  374. Image.asset("lib/assets/img/rundata_icon_duration.png"),
  375. space,
  376. Text(
  377. '${DateFormat.toTime(e.duration ?? 0)}',
  378. style: Theme.of(context).textTheme.bodyText2!,
  379. ),
  380. ],
  381. ),
  382. ),
  383. ),
  384. Expanded(
  385. flex: 3,
  386. child: Center(
  387. child: Row(
  388. mainAxisSize: MainAxisSize.min,
  389. children: [
  390. Image.asset("lib/assets/img/home_title_duration.png"),
  391. space,
  392. Text(
  393. '${SportUtils.pace4((e.duration ?? 0), (e.distance ?? 0))}',
  394. style: Theme.of(context).textTheme.bodyText2!,
  395. ),
  396. ],
  397. ),
  398. ),
  399. ),
  400. Container(
  401. width: 20,
  402. alignment: Alignment.centerRight,
  403. child: e.expand == false ? Image.asset("lib/assets/img/run_data_open.png") : Container(),
  404. ),
  405. ],
  406. ),
  407. ),
  408. ),
  409. ),
  410. sliver: SliverList(
  411. delegate: SliverChildBuilderDelegate(
  412. (context, i) {
  413. var e = child[i];
  414. int day = 0;
  415. int hour = 0;
  416. int minute = 0;
  417. if (e.begin != null) {
  418. var time = DateTime.parse(e.begin!);
  419. day = time.day;
  420. hour = time.hour;
  421. minute = time.minute;
  422. }
  423. return Column(
  424. children: [
  425. Padding(
  426. padding: const EdgeInsets.fromLTRB(21.0, 8.0, 12.0, 12.0),
  427. child: GestureDetector(
  428. behavior: HitTestBehavior.opaque,
  429. onTap: () {
  430. NavigatorUtil.goPage(
  431. context,
  432. (context) => RunDetailPage(
  433. id: e.id!,
  434. ));
  435. },
  436. onLongPress: () async{
  437. var result = await showCupertinoModalPopup(
  438. context: context,
  439. builder: (BuildContext context) {
  440. return CupertinoActionSheet(
  441. title: Text('提示'),
  442. message: Text('请选择要操作的选项'),
  443. cancelButton: TextButton(onPressed: () {Navigator.pop(context, false); },
  444. child: Center(child: Text("取消", style: Theme.of(context).textTheme.bodyText1,)),),
  445. actions: <Widget>[
  446. CupertinoActionSheetAction(
  447. child: Text(
  448. '拷贝该条记录到今天',
  449. style: TextStyle(color: Color(0xff333333),fontSize: 16.0),
  450. ),
  451. onPressed: () async {
  452. api.jogCopyRecord(e.id!).then((value) => _loadData());
  453. Navigator.pop(context, false);
  454. },
  455. isDefaultAction: true,
  456. ),
  457. CupertinoActionSheetAction(
  458. child: Text('删除该条记录', style: TextStyle(fontSize: 16.0),),
  459. onPressed: () {
  460. api.jogDelRecord(e.id!).then((value) => _loadData());
  461. runDelete = true;
  462. Navigator.pop(context, true);
  463. },
  464. isDestructiveAction: true,
  465. ),
  466. ],
  467. );
  468. });
  469. if (result == true) {}
  470. },
  471. child: Container(
  472. key: Key("item_${e.id}"),
  473. child: Column(
  474. children: [
  475. Row(
  476. children: [
  477. Container(
  478. width: 45.0,
  479. child: Column(
  480. crossAxisAlignment: CrossAxisAlignment.start,
  481. children: [
  482. Row(
  483. children: [
  484. Text(
  485. '$day',
  486. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, color: Theme.of(context).accentColor, fontFamily: "DIN"),
  487. ),
  488. Text(
  489. '日',
  490. style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 12.0, color: Theme.of(context).accentColor),
  491. ),
  492. ],
  493. ),
  494. const SizedBox(
  495. height: 3.0,
  496. ),
  497. Text(
  498. '${hour.toString().padLeft(2, "0")}:${minute.toString().padLeft(2, "0")}',
  499. style: Theme.of(context).textTheme.bodyText1!,
  500. ),
  501. ],
  502. ),
  503. ),
  504. Expanded(
  505. child: Row(
  506. children: [
  507. Expanded(
  508. child: Column(
  509. children: [
  510. Text(
  511. '${formatNum((e.distance ?? 0) / 1000, 2)}',
  512. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  513. ),
  514. Text(
  515. '公里',
  516. style: Theme.of(context).textTheme.bodyText1!,
  517. ),
  518. ],
  519. ),
  520. ),
  521. Expanded(
  522. child: Column(
  523. children: [
  524. Text(
  525. '${DateFormat.toTime(e.duration ?? 0)}',
  526. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  527. ),
  528. Text(
  529. '时长',
  530. style: Theme.of(context).textTheme.bodyText1!,
  531. ),
  532. ],
  533. ),
  534. ),
  535. Expanded(
  536. child: Column(
  537. children: [
  538. Text(
  539. '${SportUtils.pace4(e.duration ?? 0, e.distance ?? 0)}',
  540. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  541. ),
  542. Text(
  543. '配速',
  544. style: Theme.of(context).textTheme.bodyText1!,
  545. ),
  546. ],
  547. ),
  548. )
  549. ],
  550. ),
  551. )
  552. ],
  553. ),
  554. ],
  555. ),
  556. ),
  557. // Table(
  558. // defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
  559. // children: [
  560. // TableRow(children: [
  561. // TableCell(
  562. // child: ,
  563. // ),
  564. // TableCell(
  565. // child: Text(
  566. // '${formatNum((e.distance ?? 0) / 1000, 2)}',
  567. // style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  568. // )),
  569. // TableCell(
  570. // child: Text(
  571. // '${DateFormat.toTime(e.duration ?? 0)}',
  572. // style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  573. // )),
  574. // TableCell(
  575. // child: Text(
  576. // '${SportUtils.pace4(e.duration ?? 0, e.distance ?? 0)}',
  577. // style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  578. // )),
  579. // ]),
  580. // TableRow(children: [
  581. // TableCell(child: Text('${hour.toString().padLeft(2, "0")}:${minute.toString().padLeft(2, "0")}')),
  582. // TableCell(
  583. // child: Text(
  584. // '公里',
  585. // style: Theme.of(context).textTheme.bodyText1!,
  586. // )),
  587. // TableCell(child: Text('时长', style: Theme.of(context).textTheme.bodyText1!)),
  588. // TableCell(child: Text('配速', style: Theme.of(context).textTheme.bodyText1!)),
  589. // ]),
  590. // ],
  591. // ),
  592. ),
  593. ),
  594. if (i < child.length - 1)
  595. Divider(
  596. height: 1,
  597. ),
  598. ],
  599. );
  600. },
  601. childCount: e.expand == true ? child.length : 0,
  602. ),
  603. ),
  604. ));
  605. }
  606. }
  607. return Scaffold(
  608. backgroundColor: Colors.white,
  609. body: CustomScrollView(
  610. slivers: slivers,
  611. ),
  612. );
  613. }
  614. }