From 49ecfb2e5ee8ebd79ea9bed8ad08e18396468037 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Fri, 14 Nov 2025 11:41:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/layout/app_drawer.dart | 64 +++- lib/layout/main_screen.dart | 8 +- lib/pages/calendar_page.dart | 6 +- lib/pages/stats_page.dart | 209 ++++++++++++ lib/provider/app_provider.dart | 28 +- lib/utils/theme_utils.dart | 7 + lib/widgets/chart.dart | 297 ++++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 ++ pubspec.yaml | 2 + 10 files changed, 621 insertions(+), 26 deletions(-) create mode 100644 lib/pages/stats_page.dart create mode 100644 lib/widgets/chart.dart diff --git a/lib/layout/app_drawer.dart b/lib/layout/app_drawer.dart index 5d7c30c..95cfb36 100644 --- a/lib/layout/app_drawer.dart +++ b/lib/layout/app_drawer.dart @@ -86,28 +86,62 @@ class AppDrawer extends StatelessWidget { ); } + Widget buildVersionInfo({ + required BuildContext context, + required String fullVersion, + }) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: Text( + '版本 v$fullVersion', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.primary, + ), + ), + ); + } + @override Widget build(BuildContext context) { final appProvider = Provider.of(context); return Drawer( - child: ListView( - padding: EdgeInsets.zero, + child: Column( children: [ - // 抽屉头部 - buildDrawerHeader(context), + // 主要内容区域,可以滚动 + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + // 抽屉头部 + buildDrawerHeader(context), + _buildThemeSection(context, appProvider), + const Divider(), + _buildDrawerItem( + context: context, + icon: Icons.help, + title: '帮助与反馈', + onTap: () { + Navigator.pop(context); + // 这里可以导航到帮助页面 + }, + ), + ], + ), + ), - _buildThemeSection(context, appProvider), - - const Divider(), - - _buildDrawerItem( - context: context, - icon: Icons.help, - title: '帮助与反馈', - onTap: () { - Navigator.pop(context); - // 这里可以导航到帮助页面 + Consumer( + builder: (context, appProvider, child) { + return buildVersionInfo( + context: context, + fullVersion: appProvider.fullVersion, + ); }, ), ], diff --git a/lib/layout/main_screen.dart b/lib/layout/main_screen.dart index 6363088..026ba76 100644 --- a/lib/layout/main_screen.dart +++ b/lib/layout/main_screen.dart @@ -1,6 +1,7 @@ import 'package:flisp_app/layout/app_drawer.dart'; import 'package:flisp_app/pages/calendar_page.dart'; import 'package:flisp_app/pages/flisp_page.dart'; +import 'package:flisp_app/pages/stats_page.dart'; import 'package:flisp_app/pages/todo_page.dart'; import 'package:flisp_app/provider/app_provider.dart'; import 'package:flutter/material.dart'; @@ -25,6 +26,7 @@ class _MainScreenState extends State { icon: Icon(Icons.calendar_month_rounded), label: '日程', ), + BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'), ]; @override @@ -89,7 +91,9 @@ class _MainScreenState extends State { } } - Widget _buildFloatingButton(BuildContext context, int index) { + Widget? _buildFloatingButton(BuildContext context, int index) { + if (index == 3) return null; + return FloatingActionButton( onPressed: () => _onPressFloatingButton(index), backgroundColor: Colors.transparent, @@ -115,6 +119,8 @@ class _MainScreenState extends State { return TodoPage(key: _todoPageKey); case 2: return CalendarPage(key: _calendarPageKey); + case 3: + return StatsPage(); default: return FlispPage(key: _flispPageKey); } diff --git a/lib/pages/calendar_page.dart b/lib/pages/calendar_page.dart index 9641f79..3b2e379 100644 --- a/lib/pages/calendar_page.dart +++ b/lib/pages/calendar_page.dart @@ -54,8 +54,8 @@ class CalendarPageState extends State { void _showDialog(bool isEditing, Calendar? calendar) { final provider = Provider.of(context, listen: false); - if (isEditing) { - provider.initForm(calendar!); + if (calendar != null) { + provider.initForm(calendar); } else { provider.resetForm(); } @@ -153,7 +153,7 @@ class CalendarPageState extends State { dataSource: AppointmentDataSource(_appointments), onAdd: () { if (_calendarController.view == CalendarView.week) { - _showDialog(true, provider.formItem); + _showDialog(false, provider.formItem); } }, onEdit: (appointment) { diff --git a/lib/pages/stats_page.dart b/lib/pages/stats_page.dart new file mode 100644 index 0000000..79780b2 --- /dev/null +++ b/lib/pages/stats_page.dart @@ -0,0 +1,209 @@ +import 'package:flisp_app/models/flisp.dart'; +import 'package:flisp_app/models/todo.dart'; +import 'package:flisp_app/service/flisp_service.dart'; +import 'package:flisp_app/service/todo_service.dart'; +import 'package:flisp_app/widgets/chart.dart'; +import 'package:flisp_app/widgets/common.dart'; +import 'package:flutter/material.dart'; + +class StatsPage extends StatefulWidget { + const StatsPage({super.key}); + + @override + StatsPageState createState() => StatsPageState(); +} + +class StatsPageState extends State { + final double chartHeight = 400; + final FlispService flispService = FlispService(); + final TodoService todoService = TodoService(); + + late List flispMonthlyStats; + late List flispCategoryStats; + late List todoMonthlyStats; + late List todoMonthlyCompleteStats; + late List todoPriorityStats; + + @override + void initState() { + super.initState(); + final allFlisps = flispService.getAllFlisps(); + getFlispMonthlyStats(allFlisps); + getFlispCategoryStats(allFlisps); + + final allTodos = todoService.getAllTodos(); + getTodoMonthlyStats(allTodos); + getTodoMonthlyCompleteStats(allTodos); + getTodoCategoryStats(allTodos); + } + + void getFlispMonthlyStats(List flisps) { + Map countMap = {}; + + for (var flisp in flisps) { + String month = '${flisp.createTime.month}月'; + countMap[month] = (countMap[month] ?? 0) + 1; + } + + flispMonthlyStats = convert2ChartData(countMap); + } + + void getFlispCategoryStats(List flisps) { + Map countMap = {}; + + for (var flisp in flisps) { + String category = flisp.tag.label; + countMap[category] = (countMap[category] ?? 0) + 1; + } + + flispCategoryStats = convert2ChartData(countMap); + } + + void getTodoMonthlyStats(List todos) { + Map countMap = {}; + + for (var todo in todos) { + String month = '${todo.createTime.month}月'; + countMap[month] = (countMap[month] ?? 0) + 1; + } + + todoMonthlyStats = convert2ChartData(countMap); + } + + void getTodoMonthlyCompleteStats(List todos) { + Map countMap = {}; + + for (var todo in todos) { + if (todo.isCompleted) { + String month = '${todo.createTime.month}月'; + countMap[month] = (countMap[month] ?? 0) + 1; + } + } + + todoMonthlyCompleteStats = convert2ChartData(countMap); + } + + void getTodoCategoryStats(List todos) { + Map countMap = {}; + + for (var todo in todos) { + String priority = todo.priority.label; + countMap[priority] = (countMap[priority] ?? 0) + 1; + } + + todoPriorityStats = convert2ChartData(countMap); + } + + List convert2ChartData(Map countMap) { + return countMap.entries + .map((e) => ChartData(name: e.key, value: e.value)) + .toList(); + } + + Widget _buildFlispMonthlyStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '每月闪灵数量统计'), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: lineChart( + context: context, + xAxisName: '月份', + yAxisName: '数量', + unit: '篇', + data: flispMonthlyStats, + ), + ), + ], + ); + } + + Widget _buildFlispCategoryStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '闪灵分类统计'), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: pieChart( + context: context, + unit: '篇', + data: flispCategoryStats, + ), + ), + ], + ); + } + + Widget _buildTodoMonthlyStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '每月待办数量统计'), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: doubleBarChart( + context: context, + xAxisName: '月份', + yAxisName: '数量', + unit: '个', + data1: todoMonthlyStats, + data2: todoMonthlyCompleteStats, + series1Name: '总数', + series2Name: '完成数', + ), + ), + ], + ); + } + + Widget _buildTodoPriorityStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '待办分类统计'), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: pieChart(context: context, unit: '个', data: todoPriorityStats), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + SizedBox( + height: chartHeight, + child: BuildCard(child: _buildFlispMonthlyStats(context)), + ), + SizedBox(height: 10), + SizedBox( + height: chartHeight, + child: BuildCard(child: _buildFlispCategoryStats(context)), + ), + SizedBox(height: 10), + SizedBox( + height: chartHeight, + child: BuildCard(child: _buildTodoMonthlyStats(context)), + ), + SizedBox(height: 10), + SizedBox( + height: chartHeight, + child: BuildCard(child: _buildTodoPriorityStats(context)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/provider/app_provider.dart b/lib/provider/app_provider.dart index 31b1728..6fbf263 100644 --- a/lib/provider/app_provider.dart +++ b/lib/provider/app_provider.dart @@ -1,5 +1,6 @@ import 'package:flisp_app/utils/theme_utils.dart'; import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AppProvider with ChangeNotifier { @@ -9,6 +10,9 @@ class AppProvider with ChangeNotifier { int _currentTab = 0; bool _isDarkMode = false; ThemeColor _currentTheme = defaultThemes[0]; + String _appVersion = '1.0.0'; + String _buildNumber = '1'; + late SharedPreferences _prefs; bool _isInitialized = false; @@ -20,6 +24,8 @@ class AppProvider with ChangeNotifier { Future _initPreferences() async { _prefs = await SharedPreferences.getInstance(); _loadPreferences(); + await _getVersionInfo(); + _isInitialized = true; notifyListeners(); } @@ -31,12 +37,18 @@ class AppProvider with ChangeNotifier { // 加载主题色 final themeName = _prefs.getString(_themeKey); if (themeName != null) { - // 根据主题名称查找对应的主题 - final savedTheme = defaultThemes.firstWhere( - (theme) => theme.name == themeName, - orElse: () => defaultThemes[0], - ); - _currentTheme = savedTheme; + _currentTheme = getThemeColor(themeName); + } + } + + Future _getVersionInfo() async { + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + _appVersion = packageInfo.version; + _buildNumber = packageInfo.buildNumber; + notifyListeners(); + } catch (e) { + print('获取版本信息失败: $e'); } } @@ -60,6 +72,8 @@ class AppProvider with ChangeNotifier { bool get isInitialized => _isInitialized; + String get fullVersion => '$_appVersion+$_buildNumber'; + void changeTab(int index) { _currentTab = index; notifyListeners(); @@ -90,4 +104,4 @@ class AppProvider with ChangeNotifier { fontFamily: 'CustomFont', ); } -} \ No newline at end of file +} diff --git a/lib/utils/theme_utils.dart b/lib/utils/theme_utils.dart index bbcbd8c..b1dc092 100644 --- a/lib/utils/theme_utils.dart +++ b/lib/utils/theme_utils.dart @@ -146,6 +146,13 @@ final List defaultThemes = [ ), ]; +ThemeColor getThemeColor(String themeName) { + return defaultThemes.firstWhere( + (theme) => theme.name == themeName, + orElse: () => defaultThemes[0], + ); +} + Widget buildThemeColorList(BuildContext context, AppProvider appProvider) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), diff --git a/lib/widgets/chart.dart b/lib/widgets/chart.dart new file mode 100644 index 0000000..550fd78 --- /dev/null +++ b/lib/widgets/chart.dart @@ -0,0 +1,297 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; + +class ChartData { + final String name; + final double value; + + ChartData({required this.name, required this.value}); +} + +Widget buildChartTitle(BuildContext context, String title) { + return Row( + children: [ + Icon(Icons.insert_chart, color: Theme.of(context).colorScheme.primary), + Text(title), + ], + ); +} + +Widget buildChartDivider(BuildContext context) { + return Divider( + height: 1, + thickness: 1, + color: Theme.of(context).colorScheme.primary, + indent: 0, + endIndent: 0, + ); +} + +Widget lineChart({ + required BuildContext context, + required String xAxisName, + required String yAxisName, + required String unit, + required List data, +}) { + return SfCartesianChart( + // 图表标题 + // title: ChartTitle(text: '2023年上半年销售额(万元)'), + + // X轴配置(类别轴) + primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)), + + // Y轴配置(数值轴) + // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), + + // 启用图例 + legend: Legend(isVisible: true, position: LegendPosition.top), + + // 启用交互提示(点击数据点显示详情) + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.x: point.y $unit', + ), + + // 折线图数据系列 + series: [ + LineSeries( + dataSource: data, + // X轴数据映射 + xValueMapper: (ChartData chart, _) => chart.name, + // Y轴数据映射 + yValueMapper: (ChartData chart, _) => chart.value, + // 线条颜色 + color: Theme.of(context).colorScheme.primary, + // 线条宽度 + width: 3, + + // 数据点样式 + markerSettings: const MarkerSettings( + isVisible: true, + color: Colors.white, + shape: DataMarkerType.circle, + height: 6, + width: 6, + ), + + // 折线名称(会显示在图例中) + name: yAxisName, + + // 启用数据标签(直接显示数值) + dataLabelSettings: const DataLabelSettings( + isVisible: true, + color: Colors.white, + opacity: 0, + ), + + // 动画效果 + animationDuration: 2000, // 动画时长(毫秒) + ), + ], + ); +} + +Widget barChart({ + required BuildContext context, + required String xAxisName, + required String yAxisName, + required String unit, + required List data, +}) { + return SfCartesianChart( + // X轴配置(类别轴) + primaryXAxis: CategoryAxis( + majorGridLines: MajorGridLines(width: 0), + title: AxisTitle(text: xAxisName), + ), + + // Y轴配置(数值轴) + // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), + + // 启用交互提示 + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.x: point.y $unit', + ), + + // 柱状图数据系列 + series: [ + ColumnSeries( + dataSource: data, + // X轴数据映射 + xValueMapper: (ChartData chart, _) => chart.name, + // Y轴数据映射 + yValueMapper: (ChartData chart, _) => chart.value, + + // 名称 + name: yAxisName, + + // 柱子颜色 + color: Theme.of(context).colorScheme.primary, + + // 柱子宽度(0-1之间,1表示占满类别间隔) + width: 0.6, + + // 柱子边框 + borderWidth: 1, + borderColor: Colors.black12, + + // 数据标签 + dataLabelSettings: const DataLabelSettings( + isVisible: true, + color: Colors.white, + opacity: 0, + alignment: ChartAlignment.center, + ), + + // 动画效果 + animationDuration: 2000, + ), + ], + ); +} + +Widget doubleBarChart({ + required BuildContext context, + required String xAxisName, + required String yAxisName, + required String unit, + required List data1, + required List data2, + required String series1Name, + required String series2Name, +}) { + return SfCartesianChart( + // X轴配置(类别轴) + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + title: AxisTitle(text: xAxisName), + ), + + // Y轴配置(数值轴) + // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), + + // 图例配置 + legend: Legend( + isVisible: true, + position: LegendPosition.top, + overflowMode: LegendItemOverflowMode.wrap, + ), + + // 启用交互提示 + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'series.name: point.y $unit', + ), + + // 双柱状图数据系列 + series: >[ + ColumnSeries( + dataSource: data1, + // X轴数据映射 + xValueMapper: (ChartData chart, _) => chart.name, + // Y轴数据映射 + yValueMapper: (ChartData chart, _) => chart.value, + // 系列名称 + name: series1Name, + // 柱子颜色 + color: Theme.of(context).colorScheme.primary, + // 柱子宽度 + width: 0.3, + // 柱子边框 + borderWidth: 1, + borderColor: Colors.black12, + // 数据标签 + dataLabelSettings: const DataLabelSettings( + isVisible: true, + color: Colors.white, + opacity: 0, + alignment: ChartAlignment.center, + ), + // 动画效果 + animationDuration: 2000, + ), + ColumnSeries( + dataSource: data2, + // X轴数据映射 + xValueMapper: (ChartData chart, _) => chart.name, + // Y轴数据映射 + yValueMapper: (ChartData chart, _) => chart.value, + // 系列名称 + name: series2Name, + // 柱子颜色 + color: Theme.of(context).colorScheme.inversePrimary, + // 柱子宽度 + width: 0.3, + // 柱子边框 + borderWidth: 1, + borderColor: Colors.black12, + // 数据标签 + dataLabelSettings: const DataLabelSettings( + isVisible: true, + color: Colors.white, + opacity: 0, + alignment: ChartAlignment.center, + ), + // 动画效果 + animationDuration: 2000, + ), + ], + ); +} + +Widget pieChart({ + required BuildContext context, + required String unit, + required List data, +}) { + // 计算 value 的总和 + double sumValue = data.fold(0.0, (sum, item) => sum + item.value); + + return SfCircularChart( + // 饼图标题 + // title: ChartTitle(text: '菜谱类别占比分布'), + + // 启用图例 + legend: const Legend(isVisible: true, position: LegendPosition.right), + + // 启用交互提示(点击扇区显示详情) + tooltipBehavior: TooltipBehavior( + enable: true, + format: 'point.x: point.y $unit', + ), + + // 饼图系列配置 + series: [ + PieSeries( + dataSource: data, + // 类别映射(饼图扇区名称) + xValueMapper: (ChartData data, _) => data.name, + // 数值映射(扇区大小占比) + yValueMapper: (ChartData data, _) => data.value, + + // 扇区半径(0-1之间,1表示充满容器) + // radius: '50%', + + // 启用扇区分离效果 + explode: true, + // 指定分离的扇区索引(这里分离第一个扇区) + explodeIndex: 0, + // 分离距离 + explodeOffset: '5%', + + dataLabelMapper: (ChartData data, _) { + final percentage = (data.value / sumValue * 100).toStringAsFixed(0); + return '$percentage%'; + }, + + // 数据标签(显示在扇区上的文本) + dataLabelSettings: DataLabelSettings(isVisible: true), + + // 动画效果 + animationDuration: 2000, + ), + ], + ); +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a6cc016..02d535d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import file_picker import flutter_local_notifications +import package_info_plus import path_provider_foundation import rive_native import shared_preferences_foundation @@ -14,6 +15,7 @@ import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 720c686..2811f10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -546,6 +546,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -815,6 +831,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "30.2.7" + syncfusion_flutter_charts: + dependency: "direct main" + description: + name: syncfusion_flutter_charts + sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" syncfusion_flutter_core: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a6a131d..e881b68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: crypto: ^3.0.7 syncfusion_flutter_calendar: ^30.1.37 syncfusion_localizations: ^30.1.37 + syncfusion_flutter_charts: ^30.1.41 + package_info_plus: ^8.0.0 dev_dependencies: flutter_test: