feat:增加统计图表模块
This commit is contained in:
@@ -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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appProvider = Provider.of<AppProvider>(context);
|
final appProvider = Provider.of<AppProvider>(context);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: ListView(
|
child: Column(
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
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),
|
Consumer<AppProvider>(
|
||||||
|
builder: (context, appProvider, child) {
|
||||||
const Divider(),
|
return buildVersionInfo(
|
||||||
|
context: context,
|
||||||
_buildDrawerItem(
|
fullVersion: appProvider.fullVersion,
|
||||||
context: context,
|
);
|
||||||
icon: Icons.help,
|
|
||||||
title: '帮助与反馈',
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// 这里可以导航到帮助页面
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flisp_app/layout/app_drawer.dart';
|
import 'package:flisp_app/layout/app_drawer.dart';
|
||||||
import 'package:flisp_app/pages/calendar_page.dart';
|
import 'package:flisp_app/pages/calendar_page.dart';
|
||||||
import 'package:flisp_app/pages/flisp_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/pages/todo_page.dart';
|
||||||
import 'package:flisp_app/provider/app_provider.dart';
|
import 'package:flisp_app/provider/app_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -25,6 +26,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
icon: Icon(Icons.calendar_month_rounded),
|
icon: Icon(Icons.calendar_month_rounded),
|
||||||
label: '日程',
|
label: '日程',
|
||||||
),
|
),
|
||||||
|
BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@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(
|
return FloatingActionButton(
|
||||||
onPressed: () => _onPressFloatingButton(index),
|
onPressed: () => _onPressFloatingButton(index),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -115,6 +119,8 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
return TodoPage(key: _todoPageKey);
|
return TodoPage(key: _todoPageKey);
|
||||||
case 2:
|
case 2:
|
||||||
return CalendarPage(key: _calendarPageKey);
|
return CalendarPage(key: _calendarPageKey);
|
||||||
|
case 3:
|
||||||
|
return StatsPage();
|
||||||
default:
|
default:
|
||||||
return FlispPage(key: _flispPageKey);
|
return FlispPage(key: _flispPageKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class CalendarPageState extends State<CalendarPage> {
|
|||||||
void _showDialog(bool isEditing, Calendar? calendar) {
|
void _showDialog(bool isEditing, Calendar? calendar) {
|
||||||
final provider = Provider.of<CalendarProvider>(context, listen: false);
|
final provider = Provider.of<CalendarProvider>(context, listen: false);
|
||||||
|
|
||||||
if (isEditing) {
|
if (calendar != null) {
|
||||||
provider.initForm(calendar!);
|
provider.initForm(calendar);
|
||||||
} else {
|
} else {
|
||||||
provider.resetForm();
|
provider.resetForm();
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ class CalendarPageState extends State<CalendarPage> {
|
|||||||
dataSource: AppointmentDataSource(_appointments),
|
dataSource: AppointmentDataSource(_appointments),
|
||||||
onAdd: () {
|
onAdd: () {
|
||||||
if (_calendarController.view == CalendarView.week) {
|
if (_calendarController.view == CalendarView.week) {
|
||||||
_showDialog(true, provider.formItem);
|
_showDialog(false, provider.formItem);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEdit: (appointment) {
|
onEdit: (appointment) {
|
||||||
|
|||||||
209
lib/pages/stats_page.dart
Normal file
209
lib/pages/stats_page.dart
Normal 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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flisp_app/utils/theme_utils.dart';
|
import 'package:flisp_app/utils/theme_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class AppProvider with ChangeNotifier {
|
class AppProvider with ChangeNotifier {
|
||||||
@@ -9,6 +10,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
int _currentTab = 0;
|
int _currentTab = 0;
|
||||||
bool _isDarkMode = false;
|
bool _isDarkMode = false;
|
||||||
ThemeColor _currentTheme = defaultThemes[0];
|
ThemeColor _currentTheme = defaultThemes[0];
|
||||||
|
String _appVersion = '1.0.0';
|
||||||
|
String _buildNumber = '1';
|
||||||
|
|
||||||
late SharedPreferences _prefs;
|
late SharedPreferences _prefs;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@@ -20,6 +24,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
Future<void> _initPreferences() async {
|
Future<void> _initPreferences() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
_loadPreferences();
|
_loadPreferences();
|
||||||
|
await _getVersionInfo();
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -31,12 +37,18 @@ class AppProvider with ChangeNotifier {
|
|||||||
// 加载主题色
|
// 加载主题色
|
||||||
final themeName = _prefs.getString(_themeKey);
|
final themeName = _prefs.getString(_themeKey);
|
||||||
if (themeName != null) {
|
if (themeName != null) {
|
||||||
// 根据主题名称查找对应的主题
|
_currentTheme = getThemeColor(themeName);
|
||||||
final savedTheme = defaultThemes.firstWhere(
|
}
|
||||||
(theme) => theme.name == themeName,
|
}
|
||||||
orElse: () => defaultThemes[0],
|
|
||||||
);
|
Future<void> _getVersionInfo() async {
|
||||||
_currentTheme = savedTheme;
|
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;
|
bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
|
String get fullVersion => '$_appVersion+$_buildNumber';
|
||||||
|
|
||||||
void changeTab(int index) {
|
void changeTab(int index) {
|
||||||
_currentTab = index;
|
_currentTab = index;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -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) {
|
Widget buildThemeColorList(BuildContext context, AppProvider appProvider) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
|||||||
297
lib/widgets/chart.dart
Normal file
297
lib/widgets/chart.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import rive_native
|
import rive_native
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
@@ -14,6 +15,7 @@ import shared_preferences_foundation
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
|
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@@ -546,6 +546,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -815,6 +831,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "30.2.7"
|
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:
|
syncfusion_flutter_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ dependencies:
|
|||||||
crypto: ^3.0.7
|
crypto: ^3.0.7
|
||||||
syncfusion_flutter_calendar: ^30.1.37
|
syncfusion_flutter_calendar: ^30.1.37
|
||||||
syncfusion_localizations: ^30.1.37
|
syncfusion_localizations: ^30.1.37
|
||||||
|
syncfusion_flutter_charts: ^30.1.41
|
||||||
|
package_info_plus: ^8.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user