import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/material.dart' as ui; import 'package:flutter/painting.dart'; import 'package:sport/bean/jog/record.dart'; import 'package:sport/bean/sport_index.dart'; import 'package:sport/pages/home/consume_page.dart'; import 'package:sport/pages/home/plan_page.dart'; import 'package:sport/pages/run/run_detail_page.dart'; import 'package:sport/pages/run/run_share_statistics.dart'; import 'package:sport/provider/lib/provider_widget.dart'; import 'package:sport/provider/lib/simple_model.dart'; import 'package:sport/provider/lib/view_state_lifecycle.dart'; import 'package:sport/router/navigator_util.dart'; import 'package:sport/services/api/inject_api.dart'; import 'package:sport/services/api/resp.dart'; import 'package:sport/utils/DateFormat.dart'; import 'package:sport/utils/date.dart'; import 'package:sport/utils/sport_utils.dart'; import 'package:sport/utils/toast.dart'; import 'package:sport/widgets/appbar.dart'; import 'package:sport/widgets/chart.dart'; import 'package:sport/widgets/dialog/popupmenu.dart' as menu; import 'package:sport/widgets/dialog/request_dialog.dart'; import 'package:sport/widgets/image.dart'; import 'package:sport/widgets/loading.dart'; import 'package:sport/widgets/misc.dart'; import 'package:sport/widgets/popmenu_bg.dart'; const Color _color = Color(0xffFFC400); const List TABS = ["日", "周", "月", "年", "总"]; class RunStatisticsPage extends StatefulWidget { @override State createState() => _PageState(); } class _PageState extends State with TickerProviderStateMixin, InjectApi, InjectLoginApi { ValueNotifier _tab = ValueNotifier("周"); ValueNotifier _valueNotifierSportDetail = ValueNotifier(null); ValueNotifier _valueNotifierDate = ValueNotifier(DateTime.now()); late PageController _pageController; @override void initState() { super.initState(); _pageController = PageController(initialPage: 0); changeTab(); } @override void dispose() { _pageController.dispose(); super.dispose(); _tab.dispose(); _valueNotifierSportDetail.dispose(); _valueNotifierDate.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: buildAppBar(context, title: "跑步统计", actions: [ IconButton(icon: Image.asset("lib/assets/img/bbs_icon_share.png"), onPressed: () => _share()), ]), body: Container( color: Colors.white, child: ValueListenableBuilder( valueListenable: _tab, builder: (BuildContext context, String value, Widget? child) { return Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: ["周", "/", "月", "/", "年", "/", "总"] .map((e) => e == "/" ? Container( margin: const EdgeInsets.fromLTRB(5, 0, 1, 0), color: const Color(0xffdcdcdc), width: 0.5, height: 14, transform: Matrix4.rotationZ(0.35), ) : InkWell( onTap: () { _tab.value = e; _valueNotifierDate.value = DateTime.now(); _pageController.jumpToPage(0); // _pageController= PageController(initialPage: 0); changeTab(); }, child: Container( margin: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.all(8.0), decoration: value == e ? BoxDecoration(color: _color, shape: BoxShape.circle) : null, child: Text( "$e", style: value == e ? Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white) : Theme.of(context).textTheme.subtitle1!, ), ))) .toList(), ), ), if (toType() != 4) Center( child: ValueListenableBuilder( valueListenable: _valueNotifierDate, builder: (_, time, ___) { int type = toType(); String text = ""; if (type == 0) { text = "${time.year}.${'${time.month}'.padLeft(2, '0')}.${'${time.day}'.padLeft(2, '0')} 6:00 - 24:00 "; } else if (type == 1) { DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1); DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1); print("$time ${time.weekday} == $start $end"); text = "${start.year}.${'${start.month}'.padLeft(2, '0')}.${'${start.day}'.padLeft(2, '0')} 至 ${end.year}.${'${end.month}'.padLeft(2, '0')}.${'${end.day}'.padLeft(2, '0')}"; } else if (type == 2) { text = ("${time.year}年${'${time.month}'.padLeft(2, '0')}月"); } else if (type == 3) { text = ("${time.year}年"); } return Row( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _pageController.nextPage(duration: Duration(milliseconds: 500), curve: Curves.linear); }, child: Padding( padding: const EdgeInsets.all(18.0), child: arrowLeft(), ), ), Text( text, style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Color(0xff333333)), strutStyle: fixedLine, ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (_pageController.page == 0.0) { ToastUtil.show("没有数据了"); return; } _pageController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear); }, child: Padding( padding: const EdgeInsets.all(18.0), child: arrowRight(), ), ), ], ); }), ), Expanded( child: Center( child: PageView.builder( key: ValueKey("${toType()}"), reverse: true, itemCount: toType() != 4 ? 10240 : 1, controller: _pageController, onPageChanged: (page) { rollDate(-page); }, itemBuilder: (context, index) { int type = toType(); DateTime time = offsetDate(type, -index); return _PageChild(type: type, time: time, future: createFuture(time)); }, ), ), ), ], ); }, ), )); } int toType() { return TABS.indexOf(_tab.value); } void changeTab() async { _valueNotifierSportDetail.value = null; try { RespData data = await createFuture(_valueNotifierDate.value); _valueNotifierSportDetail.value = data.data; } catch (e) { print(e); } } Future> createQueryFuture(int offset) { int type = toType(); DateTime next = offsetDate(type, offset); return createFuture(next); } Future> createFuture(DateTime time) { int type = toType(); Future> data; switch (type) { case 1: DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1); DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1); data = api.jogListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}'); break; case 2: DateTime start = DateTime(time.year, time.month, 1); DateTime end = DateTime(time.year, time.month + 1, 0); data = api.jogListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}'); break; case 3: data = api.jogListByMonth(time.year); break; case 4: data = api.jogListByYear(); break; default: data = api.jogListByDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}', '${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}'); break; } return data; } void rollDate(int offset) { if (_valueNotifierSportDetail.value == null) return; int type = toType(); DateTime next = offsetDate(type, offset); _valueNotifierDate.value = next; changeTab(); } _share() async { int type = toType(); int index = _pageController.page?.toInt() ?? 0; DateTime time = offsetDate(type, -index); JogRecord? jogRecord = await request(context, () async { RespData resp = await createFuture(time); if (resp.code == 0) return resp.data; return null; }); if (jogRecord == null) return; NavigatorUtil.goPage(context, (context) => RunShareStatistics(record: jogRecord, type: type, time: time)); } } class _PageChild extends StatefulWidget { final int type; final DateTime time; final Future> future; const _PageChild({Key? key, required this.type, required this.time, required this.future}) : super(key: key); @override State createState() { return _PageChildState(); } } class _PageChildState extends ViewStateLifecycle<_PageChild, SimpleModel> { var contentPadding = const EdgeInsets.symmetric(horizontal: 0.0); var flexLeft = 7; var flexRight = 3; var _labelTextStyle = const TextStyle(fontSize: 16.0, color: Color(0xff333333)); JogRecord? jogRecord; double _dx =0; double _dy = 0; @override SimpleModel createModel() => SimpleModel((page) async { RespData resp = await widget.future; jogRecord = resp.data; return []; }); int toType() { return widget.type; } String toKm(double km) { if (km > 20 && km < 25) { return "半程马拉松"; } else if (km > 40 && km < 43) { return "全程马拉松"; } return "${km.toInt()}公里"; } Widget _label(String text, {Widget? right}) { return Container( color: Color(0xffF1F1F1), padding: EdgeInsets.only(left: 13.0), margin: EdgeInsets.only(top: 10.0, bottom: 6.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "$text", style: Theme.of(context).textTheme.bodyText2!, strutStyle: fixedLine, ), if (right != null) right, ], ), width: double.infinity, height: 25.0, ); } Widget _row(BuildContext context, String label, String text, String unit, {Color? color}) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Text('$label', style: _labelTextStyle), flex: flexLeft, ), Expanded( child: RichText( text: TextSpan(style: DefaultTextStyle.of(context).style, children: [ TextSpan( text: '$text', style: color == null ? Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", ) : Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 15.0, fontFamily: "DIN", color: color)), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Text(' $unit', style: Theme.of(context).textTheme.subtitle1!.copyWith( fontSize: 12.0, )), ), ]), ), flex: flexRight, ), ], ), ); } @override Widget build(BuildContext context) { String sportType = widget.type == 1 ? "周" : "周均"; sportType = "周均"; DateTime next = offsetDateNext(widget.time, widget.type, 1); int day = next.difference(widget.time).inDays; return ProviderWidget( model: model, onModelReady: (model) => model.initData(), builder: (_, model, __) { JogRecord? _value = jogRecord; if (_value == null) { return RequestLoadingWidget(); } List items = [1, 3, 5, 10, 15, 20, 21.09, 25, 30, 35, 40, 42.19]; int best = _value.best?.kmDurationList?.length ?? 0; List bestFilled = (best < 3 ? items.skip(best).take(3 - best) : items.skip(best).take(1)).toList(); var _divider = const Divider( height: 32, ); var _vDivider = Container( height: 42.0, width: .5, color: const Color(0xffDCDCDC), ); return SingleChildScrollView( child: Column( children: [ SizedBox( height: 10.0, ), Text( "${((_value.sum?.distance ?? 0) / 1000).toStringAsFixed(2)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 34.0, color: _color, fontFamily: "DIN"), ), Text( "总里程 (公里)", style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Color(0xff333333)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 30.0), child: Column( children: [ Row( children: [ Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "${DateFormat.toTime(_value.sum?.duration ?? 0)}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "总时长", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ), _vDivider, Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Text( "${SportUtils.pace11(SportUtils.calPace(_value.sum?.kmDurationAvg ?? 0, 1))}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "′", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0), ), Text( "${SportUtils.pace12(SportUtils.calPace(_value.sum?.kmDurationAvg ?? 0, 1))}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "″", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0), ), ], ), Text( "配速(每公里用时)", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ), ], ), Row( children: [ Expanded( child: Divider( indent: 40, endIndent: 40, height: 40, )), Expanded( child: Divider( indent: 40, endIndent: 40, height: 40, )), ], ), Row( children: [ Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "${_value.sum?.times ?? 0}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "运动次数(次)", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ), _vDivider, Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "${_value.sum?.consume ?? 0}", style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"), ), Text( "总消耗(大卡)", style: Theme.of(context).textTheme.bodyText1!, ), ], ), ), ], ), ], ), ), GestureDetector( onTapDown: (details){ // print("${details.globalPosition.dx}"); // print("${details.globalPosition.dy}"); // print("${details.localPosition.dx}"); // print("${details.localPosition.dy}"); setState(() { _dx = details.localPosition.dx; _dy= details.localPosition.dy; }); }, child: CustomPaint( painter: Chart( type: widget.type, decimal: 2, dx: _dx, dy: _dy, records: _value.records ?.map((e) => ChartItem( widget.type == 4 ? "${e.year}" : widget.type == 3 ? "${e.month}" : e.createdAt ?? "", (e.distance ?? 0) / 1000)) .toList() ?? [], dateTime: widget.time, drawMax: true, unit: "km") ..initData(valueSplit: 5, valueSize: 5), child: Container( height: 200, ), ), ), if (widget.type != 4) _label("$sportType运动量", right: PopupMenuTheme( data: PopupMenuThemeData( color: Colors.black.withOpacity(.5), shape: PopmenuShape(backgroundColor: Colors.black.withOpacity(.5), borderRadius: BorderRadius.all(Radius.circular(10.0)))), child: PopupMenuButton( padding: EdgeInsets.symmetric(horizontal: 17.0), offset: Offset(0, kToolbarHeight / 2 + 5), icon: ui.Image.asset("lib/assets/img/linkpop_icon_modify_1.png"), onSelected: (val) {}, itemBuilder: (context) { return [ menu.PopupMenuItem( value: "", child: Center( child: Padding( padding: const EdgeInsets.all(12.0), child: Text( "周运动量:世界卫生组织WHO建议,成年人每周应进行最少150分钟的中等强度运动或75分钟的高强度运动,可以实现延长寿命的好处。 \n\n运动次数和时长:权威医学杂志《柳叶刀》的研究表明,每次锻炼的最佳时长在45-60分钟之间,少于45分钟效果会减弱,大于60分钟不仅没有更高收益,还容易产生负效应。 每周3--5次。", style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white), ), )), ), ]; }, ))), if (widget.type != 4) Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => NavigatorUtil.goPage( context, (context) => PlanPage( index: 1, )), child: _row(context, '$sportType强度', '${strengthMetToLabel(_value.avg?.met ?? 0)}', '(${(_value.avg?.met ?? 0).toStringAsFixed(1)}MET)', color: Theme.of(context).accentColor), ), _divider, _row(context, '$sportType时长', '${_value.avg?.durationMin}', '分钟'), _divider, GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => NavigatorUtil.goPage(context, (context) => ConsumePage()), child: _row(context, '$sportType消耗', '${_value.avg?.consume}', '大卡')), ], ), ), _label("运动数据"), Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _row(context, '平均时速', "${_value.sum?.speed.toStringAsFixed(2)}", '公里/小时'), _divider, _row(context, '平均步频', "${_value.sum?.stepRateAvg ?? 0}", '步/分钟'), _divider, _row(context, '平均步幅', "${((_value.sum?.stepDistanceAvg ?? 0) * 100).toInt()}", '厘米'), ], ), ), _label("${TABS[toType()]}最佳"), Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_value.best?.distance != null) GestureDetector( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Column( children: [ Text('最远距离', style: _labelTextStyle), const SizedBox( height: 4.0, ), Text( _value.best?.distance?.createdAt?.isNotEmpty == true ? '${_value.best?.distance?.createdAt?.substring(0, _value.best?.distance?.createdAt?.indexOf(" ") ?? 0)}' : '', style: Theme.of(context).textTheme.bodyText1!), ], crossAxisAlignment: CrossAxisAlignment.start, ), flex: flexLeft, ), Expanded( child: Row( children: [ Expanded( child: RichText( text: TextSpan(style: DefaultTextStyle.of(context).style, children: [ TextSpan( text: '${((_value.best?.distance?.value ?? 0) / 1000).toStringAsFixed(2)}', style: Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", )), WidgetSpan( alignment: PlaceholderAlignment.middle, child: Text(' 公里', style: Theme.of(context).textTheme.subtitle1!.copyWith( fontSize: 12.0, )), ), ]), ), ), arrowRight5() ], ), flex: flexRight, ), ], ), ), onTap: () { int id = _value.best?.distance?.id ?? 0; if (id != 0) { NavigatorUtil.goPage( context, (context) => RunDetailPage( id: id, )); } else { ToastUtil.show("暂无运动记录"); } }, behavior: HitTestBehavior.opaque, ), _divider, if (_value.best?.duration != null) GestureDetector( onTap: () { int id = _value.best?.duration?.id ?? 0; if (id != 0) { NavigatorUtil.goPage( context, (context) => RunDetailPage( id: id, )); } else { ToastUtil.show("暂无运动记录"); } }, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Column( children: [ Text('最长时间', style: _labelTextStyle), const SizedBox( height: 4.0, ), Text( _value.best?.duration?.createdAt?.isNotEmpty == true ? '${_value.best?.duration?.createdAt?.substring(0, _value.best?.duration?.createdAt?.indexOf(" ") ?? 0)}' : "", style: Theme.of(context).textTheme.bodyText1!), ], crossAxisAlignment: CrossAxisAlignment.start, ), flex: flexLeft, ), Expanded( child: Row( children: [ Expanded( child: Text("${DateFormat.toTime(_value.best?.duration?.value ?? 0)}", style: Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", ))), arrowRight5() ], ), flex: flexRight, ), ], ), ), ), _divider, if (_value.best?.kmDuration != null) GestureDetector( onTap: () { int id = _value.best?.kmDuration?.id ?? 0; if (id != 0) { NavigatorUtil.goPage( context, (context) => RunDetailPage( id: id, )); } else { ToastUtil.show("暂无运动记录"); } }, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Column( children: [ Text('最快配速', style: _labelTextStyle), const SizedBox( height: 4.0, ), Text( _value.best?.kmDuration?.createdAt?.isNotEmpty == true ? '${_value.best?.kmDuration?.createdAt?.substring(0, _value.best?.kmDuration?.createdAt?.indexOf(" ") ?? 0)}' : "", style: Theme.of(context).textTheme.bodyText1!), ], crossAxisAlignment: CrossAxisAlignment.start, ), flex: flexLeft, ), Expanded( child: Row( children: [ Expanded( child: Text("${SportUtils.pace4(_value.best?.kmDuration?.value ?? 0, 1000)}", style: Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", ))), arrowRight5() ], ), flex: flexRight, ), ], ), ), ), ], ), ), _label("${TABS[toType()]}最快"), Padding( padding: const EdgeInsets.all(12.0), child: Column( children: [ if (_value.best?.kmDurationList?.isNotEmpty == true) ListView.separated( itemBuilder: (context, index) { KmDurationList? e = _value.best?.kmDurationList?[index]; return GestureDetector( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Column( children: [ Text('${toKm((e?.distance ?? 0) / 1000)}最快时间', style: _labelTextStyle), const SizedBox( height: 4.0, ), Text('${e?.createdAt?.substring(0, e.createdAt?.indexOf(" ") ?? 0)}', style: Theme.of(context).textTheme.bodyText1!), ], crossAxisAlignment: CrossAxisAlignment.start, ), flex: flexLeft, ), Expanded( child: Row( children: [ Expanded( child: Text("${DateFormat.toTime(e?.duration ?? 0)}", style: Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", )), ), arrowRight5() ], ), flex: flexRight, ), ], ), ), onTap: () => NavigatorUtil.goPage( context, (context) => RunDetailPage( id: e?.id ?? 0, )), behavior: HitTestBehavior.opaque, ); }, separatorBuilder: (context, index) { return _divider; }, itemCount: _value.best?.kmDurationList?.length ?? 0, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), ), if (_value.best?.kmDurationList?.isNotEmpty == true) _divider, ListView.separated( itemBuilder: (context, index) { var e = bestFilled[index]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0), child: Row( children: [ Expanded( child: Column( children: [ Text('${toKm(e)}最快时间', style: _labelTextStyle), const SizedBox( height: 4.0, ), Text('暂无记录时间', style: Theme.of(context).textTheme.bodyText1!), ], crossAxisAlignment: CrossAxisAlignment.start, ), flex: flexLeft, ), Text("--", style: Theme.of(context).textTheme.headline1!.copyWith( fontSize: 20.0, fontFamily: "DIN", )), ], ), ); }, separatorBuilder: (context, index) { return _divider; }, itemCount: bestFilled.length, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), ), ], ), ), const SizedBox( height: 50.0, ), ], ), ); }); } }