diff --git a/lib/providers/daily_sign.dart b/lib/providers/daily_sign.dart index 915f5e5..92f3fcb 100644 --- a/lib/providers/daily_sign.dart +++ b/lib/providers/daily_sign.dart @@ -2,9 +2,30 @@ import 'package:get/get.dart'; import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/models/daily_sign.dart'; +import 'package:solian/models/pagination.dart'; import 'package:solian/providers/auth.dart'; class DailySignProvider extends GetxController { + Future> listLastRecord(int take) async { + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) throw const UnauthorizedException(); + + final client = auth.configureClient('id'); + + final resp = await client.get('/daily?take=$take'); + if (resp.statusCode != 200 && resp.statusCode != 404) { + throw RequestException(resp); + } else if (resp.statusCode == 404) { + return List.empty(); + } + + final result = PaginationResult.fromJson(resp.body); + + return List.from( + result.data?.map((x) => DailySignRecord.fromJson(x)) ?? [], + ); + } + Future getToday() async { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) throw const UnauthorizedException(); diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 2532705..9b04ae2 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -24,6 +24,7 @@ import 'package:solian/providers/websocket.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/widgets/chat/chat_event.dart'; +import 'package:solian/widgets/daily_sign/history_chart.dart'; import 'package:solian/widgets/posts/post_list.dart'; class DashboardScreen extends StatefulWidget { @@ -80,10 +81,14 @@ class _DashboardScreenState extends State { bool _signingDaily = true; DailySignRecord? _signRecord; + List? _signRecordHistory; Future _pullDaily() async { try { _signRecord = await _dailySign.getToday(); + _dailySign.listLastRecord(30).then((value) { + setState(() => _signRecordHistory = value); + }); } catch (e) { context.showErrorDialog(e); } @@ -137,61 +142,79 @@ class _DashboardScreenState extends State { ], ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12), Card( - child: ListTile( - leading: AnimatedSwitcher( - switchInCurve: Curves.fastOutSlowIn, - switchOutCurve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return ScaleTransition( - scale: animation, - child: child, - ); - }, - child: _signRecord == null - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - DateFormat('dd').format(DateTime.now()), - style: GoogleFonts.robotoMono( - fontSize: 22, height: 1.2), + child: Column( + children: [ + ListTile( + leading: AnimatedSwitcher( + switchInCurve: Curves.fastOutSlowIn, + switchOutCurve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + child: _signRecord == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('dd').format(DateTime.now()), + style: GoogleFonts.robotoMono( + fontSize: 22, height: 1.2), + ), + Text( + DateFormat('yy/MM').format(DateTime.now()), + style: GoogleFonts.robotoMono(fontSize: 12), + ), + ], + ) + : Text( + _signRecord!.symbol, + style: GoogleFonts.notoSerifHk( + fontSize: 20, height: 1), + ).paddingSymmetric(horizontal: 9), + ).paddingOnly(left: 4), + title: _signRecord == null + ? Text('dailySign'.tr) + : Text(_signRecord!.overviewSuggestion), + subtitle: _signRecord == null + ? Text('dailySignNone'.tr) + : Text('+${_signRecord!.resultExperience} EXP'), + trailing: AnimatedSwitcher( + switchInCurve: Curves.fastOutSlowIn, + switchOutCurve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + child: _signRecord == null + ? IconButton( + tooltip: '上香求签', + icon: const Icon(Icons.local_fire_department), + onPressed: _signingDaily ? null : _signDaily, + ) + : IconButton( + tooltip: '查看运势历史', + icon: const Icon(Icons.history), + onPressed: () { + showDialog( + context: context, + useRootNavigator: true, + builder: (context) => + DailySignHistoryChartDialog( + data: _signRecordHistory, + ), + ); + }, ), - Text( - DateFormat('yy/MM').format(DateTime.now()), - style: GoogleFonts.robotoMono(fontSize: 12), - ), - ], - ) - : Text( - _signRecord!.symbol, - style: GoogleFonts.notoSerifHk(fontSize: 20, height: 1), - ).paddingSymmetric(horizontal: 9), - ).paddingOnly(left: 4), - title: _signRecord == null - ? Text('dailySign'.tr) - : Text(_signRecord!.overviewSuggestion), - subtitle: _signRecord == null - ? Text('dailySignNone'.tr) - : Text('+${_signRecord!.resultExperience} EXP'), - trailing: AnimatedSwitcher( - switchInCurve: Curves.fastOutSlowIn, - switchOutCurve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return ScaleTransition( - scale: animation, - child: child, - ); - }, - child: _signRecord == null - ? IconButton( - tooltip: '上香求签', - icon: const Icon(Icons.local_fire_department), - onPressed: _signingDaily ? null : _signDaily, - ) - : const SizedBox.shrink(), - ), + ), + ), + ], ), ).paddingSymmetric(horizontal: 8), const Divider(thickness: 0.3).paddingSymmetric(vertical: 8), diff --git a/lib/widgets/daily_sign/history_chart.dart b/lib/widgets/daily_sign/history_chart.dart new file mode 100644 index 0000000..0067f84 --- /dev/null +++ b/lib/widgets/daily_sign/history_chart.dart @@ -0,0 +1,253 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:solian/models/daily_sign.dart'; + +class DailySignHistoryChartDialog extends StatelessWidget { + final List? data; + + const DailySignHistoryChartDialog({super.key, required this.data}); + + static List signSymbols = ['大凶', '凶', '中平', '吉', '大吉']; + + DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce( + (a, b) => DateTime.fromMillisecondsSinceEpoch( + min(a.millisecondsSinceEpoch, b.millisecondsSinceEpoch))); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('运势历史'), + Text( + '${DateFormat('yyyy/MM/dd').format(_firstRecordDate!)} - ${DateFormat('yyyy/MM/dd').format(DateTime.now())}', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ), + ], + ), + content: data == null + ? SizedBox( + height: 180, + width: max(640, MediaQuery.of(context).size.width), + child: const Center( + child: CircularProgressIndicator(), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('近期运势', style: Theme.of(context).textTheme.titleMedium) + .paddingOnly(bottom: 18), + SizedBox( + height: 180, + width: max(640, MediaQuery.of(context).size.width), + child: LineChart( + LineChartData( + lineBarsData: [ + LineChartBarData( + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + color: Theme.of(context).colorScheme.primary, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: List.filled( + data!.length, + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.3), + ).toList(), + ), + ), + spots: data! + .map( + (x) => FlSpot( + x.createdAt + .copyWith( + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ) + .millisecondsSinceEpoch + .toDouble(), + x.resultTier.toDouble(), + ), + ) + .toList(), + ) + ], + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (spots) => spots + .map((spot) => LineTooltipItem( + '${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}', + TextStyle( + color: + Theme.of(context).colorScheme.onSurface, + ), + )) + .toList(), + getTooltipColor: (_) => + Theme.of(context).colorScheme.surfaceContainerHigh, + )), + titlesData: FlTitlesData( + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + interval: 1, + getTitlesWidget: (value, _) => Align( + alignment: Alignment.centerRight, + child: Text( + signSymbols[value.toInt()], + textAlign: TextAlign.right, + ).paddingOnly(right: 8), + ), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: 86400000, + getTitlesWidget: (value, _) => Text( + DateFormat('MM/dd').format( + DateTime.fromMillisecondsSinceEpoch( + value.toInt(), + ), + ), + textAlign: TextAlign.center, + ).paddingOnly(top: 8), + ), + ), + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + ), + ), + ).marginOnly(right: 24, bottom: 8, top: 8), + const Gap(16), + Text('功德趋势', style: Theme.of(context).textTheme.titleMedium) + .paddingOnly(bottom: 18), + SizedBox( + height: 180, + width: max(640, MediaQuery.of(context).size.width), + child: LineChart( + LineChartData( + lineBarsData: [ + LineChartBarData( + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + color: Theme.of(context).colorScheme.primary, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: List.filled( + data!.length, + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.3), + ).toList(), + ), + ), + spots: data! + .map( + (x) => FlSpot( + x.createdAt + .copyWith( + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ) + .millisecondsSinceEpoch + .toDouble(), + x.resultExperience.toDouble(), + ), + ) + .toList(), + ) + ], + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (spots) => spots + .map((spot) => LineTooltipItem( + '+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}', + TextStyle( + color: + Theme.of(context).colorScheme.onSurface, + ), + )) + .toList(), + getTooltipColor: (_) => + Theme.of(context).colorScheme.surfaceContainerHigh, + )), + titlesData: FlTitlesData( + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (value, _) => Align( + alignment: Alignment.centerRight, + child: Text( + value.toStringAsFixed(0), + textAlign: TextAlign.right, + ).paddingOnly(right: 8), + ), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: 86400000, + getTitlesWidget: (value, _) => Text( + DateFormat('MM/dd').format( + DateTime.fromMillisecondsSinceEpoch( + value.toInt(), + ), + ), + textAlign: TextAlign.center, + ).paddingOnly(top: 8), + ), + ), + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + ), + ), + ).marginOnly(right: 24, bottom: 8, top: 8), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index aed2af3..77434da 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -390,6 +390,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.9" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -582,6 +590,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + url: "https://pub.dev" + source: hosted + version: "0.69.0" floor: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5983cb2..00c0315 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 gap: ^3.0.1 + fl_chart: ^0.69.0 dev_dependencies: flutter_test: