feat:增加统计图表模块

This commit is contained in:
2025-11-14 11:41:05 +08:00
parent e384db7845
commit 49ecfb2e5e
10 changed files with 621 additions and 26 deletions

View File

@@ -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<AppProvider>(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<AppProvider>(
builder: (context, appProvider, child) {
return buildVersionInfo(
context: context,
fullVersion: appProvider.fullVersion,
);
},
),
],

View File

@@ -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<MainScreen> {
icon: Icon(Icons.calendar_month_rounded),
label: '日程',
),
BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'),
];
@override
@@ -89,7 +91,9 @@ class _MainScreenState extends State<MainScreen> {
}
}
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<MainScreen> {
return TodoPage(key: _todoPageKey);
case 2:
return CalendarPage(key: _calendarPageKey);
case 3:
return StatsPage();
default:
return FlispPage(key: _flispPageKey);
}

View File

@@ -54,8 +54,8 @@ class CalendarPageState extends State<CalendarPage> {
void _showDialog(bool isEditing, Calendar? calendar) {
final provider = Provider.of<CalendarProvider>(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<CalendarPage> {
dataSource: AppointmentDataSource(_appointments),
onAdd: () {
if (_calendarController.view == CalendarView.week) {
_showDialog(true, provider.formItem);
_showDialog(false, provider.formItem);
}
},
onEdit: (appointment) {

209
lib/pages/stats_page.dart Normal file
View File

@@ -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<StatsPage> {
final double chartHeight = 400;
final FlispService flispService = FlispService();
final TodoService todoService = TodoService();
late List<ChartData> flispMonthlyStats;
late List<ChartData> flispCategoryStats;
late List<ChartData> todoMonthlyStats;
late List<ChartData> todoMonthlyCompleteStats;
late List<ChartData> 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<Flisp> flisps) {
Map<String, double> countMap = {};
for (var flisp in flisps) {
String month = '${flisp.createTime.month}';
countMap[month] = (countMap[month] ?? 0) + 1;
}
flispMonthlyStats = convert2ChartData(countMap);
}
void getFlispCategoryStats(List<Flisp> flisps) {
Map<String, double> countMap = {};
for (var flisp in flisps) {
String category = flisp.tag.label;
countMap[category] = (countMap[category] ?? 0) + 1;
}
flispCategoryStats = convert2ChartData(countMap);
}
void getTodoMonthlyStats(List<Todo> todos) {
Map<String, double> countMap = {};
for (var todo in todos) {
String month = '${todo.createTime.month}';
countMap[month] = (countMap[month] ?? 0) + 1;
}
todoMonthlyStats = convert2ChartData(countMap);
}
void getTodoMonthlyCompleteStats(List<Todo> todos) {
Map<String, double> 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<Todo> todos) {
Map<String, double> countMap = {};
for (var todo in todos) {
String priority = todo.priority.label;
countMap[priority] = (countMap[priority] ?? 0) + 1;
}
todoPriorityStats = convert2ChartData(countMap);
}
List<ChartData> convert2ChartData(Map<String, double> 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)),
),
],
),
),
);
}
}

View File

@@ -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<void> _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<void> _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',
);
}
}
}

View File

@@ -146,6 +146,13 @@ final List<ThemeColor> 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),

297
lib/widgets/chart.dart Normal file
View File

@@ -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<ChartData> 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<ChartData, String>(
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<ChartData> 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<ChartData, String>(
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<ChartData> data1,
required List<ChartData> 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<ChartData, String>>[
ColumnSeries<ChartData, String>(
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<ChartData, String>(
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<ChartData> 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<ChartData, String>(
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,
),
],
);
}

View File

@@ -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"))

View File

@@ -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:

View File

@@ -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: