From d512a77f5fd7b26da8bcad4547ccaa222d57ad6f Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Sat, 15 Nov 2025 16:56:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=8D=9A=E5=AE=A2?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/apis/blog.dart | 49 +++++ lib/layout/menu.dart | 18 +- lib/models/blog.dart | 31 +++ lib/models/blog.g.dart | 16 ++ lib/models/common.dart | 16 ++ lib/models/common.g.dart | 10 + lib/pages/about_page.dart | 62 ------ lib/pages/blog_list_page.dart | 139 -------------- lib/pages/category_list_page.dart | 74 ++++++++ lib/pages/category_page.dart | 107 +---------- lib/pages/home_page.dart | 6 +- lib/pages/message_page.dart | 59 ------ lib/pages/profile_page.dart | 78 -------- lib/pages/settings_page.dart | 71 ------- lib/pages/stats_page.dart | 304 ++++++++++++++++++++++++++++++ lib/pages/timeline_page.dart | 85 +++++++++ lib/widget/blog.dart | 138 ++++++++++++++ lib/widget/chart.dart | 285 ++++++++++++++++++++++++++++ lib/widget/common.dart | 18 ++ lib/widget/year_selector.dart | 119 ++++++++++++ pubspec.lock | 32 ++++ pubspec.yaml | 3 + 22 files changed, 1196 insertions(+), 524 deletions(-) delete mode 100644 lib/pages/about_page.dart delete mode 100644 lib/pages/blog_list_page.dart create mode 100644 lib/pages/category_list_page.dart delete mode 100644 lib/pages/message_page.dart delete mode 100644 lib/pages/profile_page.dart delete mode 100644 lib/pages/settings_page.dart create mode 100644 lib/pages/stats_page.dart create mode 100644 lib/pages/timeline_page.dart create mode 100644 lib/widget/chart.dart create mode 100644 lib/widget/year_selector.dart diff --git a/lib/apis/blog.dart b/lib/apis/blog.dart index 38f5640..c3e4db9 100644 --- a/lib/apis/blog.dart +++ b/lib/apis/blog.dart @@ -50,3 +50,52 @@ Future> queryBlogCategoryApi() { convertListResponse(data, BlogCategory.fromJson), ); } + +Future queryBlogOverviewStatsApi() { + return HttpUtil().get( + "/stats/overview", + converter: (data) => BlogStats.fromJson(data), + ); +} + +Future> queryBlogApprovedStatsApi(int year) { + return HttpUtil().get>( + "/stats/approved/monthly", + queryParameters: {"year": year}, + converter: + (data) => convertListResponse(data, ChartData.fromJson), + ); +} + +Future> queryBlogVisitStatsApi(int year) { + return HttpUtil().get>( + "/stats/visit/monthly", + queryParameters: {"year": year}, + converter: + (data) => convertListResponse(data, ChartData.fromJson), + ); +} + +Future> queryBlogVisitRankStatsApi() { + return HttpUtil().get>( + "/stats/visit/rank", + converter: + (data) => convertListResponse(data, ChartData.fromJson), + ); +} + +Future> queryBlogCategoryStatsApi() { + return HttpUtil().get>( + "/stats/category", + converter: + (data) => convertListResponse(data, ChartData.fromJson), + ); +} + +Future> queryBlogReadRankStatsApi() { + return HttpUtil().get>( + "/stats/read/rank", + converter: + (data) => convertListResponse(data, ChartData.fromJson), + ); +} diff --git a/lib/layout/menu.dart b/lib/layout/menu.dart index ccec3eb..e89b548 100644 --- a/lib/layout/menu.dart +++ b/lib/layout/menu.dart @@ -1,16 +1,22 @@ +import 'package:blog_app/pages/blog_page.dart'; +import 'package:blog_app/pages/category_page.dart'; +import 'package:blog_app/pages/stats_page.dart'; +import 'package:blog_app/pages/timeline_page.dart'; import 'package:flutter/material.dart'; class PageInfo { final String title; final IconData icon; + final Widget page; - const PageInfo({required this.title, required this.icon}); + const PageInfo({required this.title, required this.icon, required this.page}); } final List pages = [ - PageInfo(title: '博客', icon: Icons.article), - PageInfo(title: '分类', icon: Icons.folder), - PageInfo(title: '个人中心', icon: Icons.person), - PageInfo(title: '设置', icon: Icons.settings), - PageInfo(title: '关于我们', icon: Icons.info), + PageInfo(title: '博客', icon: Icons.article, page: BlogPage()), + PageInfo(title: '分类', icon: Icons.folder, page: CategoryPage()), + PageInfo(title: '归档', icon: Icons.archive, page: TimelinePage()), + PageInfo(title: '统计', icon: Icons.insert_chart, page: StatsPage()), ]; + +final List pageWidgets = pages.map((item) => item.page).toList(); diff --git a/lib/models/blog.dart b/lib/models/blog.dart index 9a548bf..e9dfbe7 100644 --- a/lib/models/blog.dart +++ b/lib/models/blog.dart @@ -76,3 +76,34 @@ class BlogCategory { Map toJson() => _$BlogCategoryToJson(this); } + +@JsonSerializable(genericArgumentFactories: true) +class BlogStats { + @JsonKey(name: 'blogCount') + final int blogCount; + + @JsonKey(name: 'categoryCount') + final int categoryCount; + + @JsonKey(name: 'greatCount') + final int greatCount; + + @JsonKey(name: 'visitCount') + final int visitCount; + + @JsonKey(name: 'wordCount') + final int wordCount; + + const BlogStats({ + required this.blogCount, + required this.categoryCount, + required this.greatCount, + required this.visitCount, + required this.wordCount, + }); + + factory BlogStats.fromJson(Map json) => + _$BlogStatsFromJson(json); + + Map toJson() => _$BlogStatsToJson(this); +} diff --git a/lib/models/blog.g.dart b/lib/models/blog.g.dart index 3ba4cf3..671ee98 100644 --- a/lib/models/blog.g.dart +++ b/lib/models/blog.g.dart @@ -43,3 +43,19 @@ BlogCategory _$BlogCategoryFromJson(Map json) => BlogCategory( Map _$BlogCategoryToJson(BlogCategory instance) => {'name': instance.name, 'count': instance.count}; + +BlogStats _$BlogStatsFromJson(Map json) => BlogStats( + blogCount: (json['blogCount'] as num).toInt(), + categoryCount: (json['categoryCount'] as num).toInt(), + greatCount: (json['greatCount'] as num).toInt(), + visitCount: (json['visitCount'] as num).toInt(), + wordCount: (json['wordCount'] as num).toInt(), +); + +Map _$BlogStatsToJson(BlogStats instance) => { + 'blogCount': instance.blogCount, + 'categoryCount': instance.categoryCount, + 'greatCount': instance.greatCount, + 'visitCount': instance.visitCount, + 'wordCount': instance.wordCount, +}; diff --git a/lib/models/common.dart b/lib/models/common.dart index 40f91d5..de296e0 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -35,3 +35,19 @@ class PageResult { Map toJson(Object? Function(T value) toJsonT) => _$PageResultToJson(this, toJsonT); } + +@JsonSerializable(genericArgumentFactories: true) +class ChartData { + @JsonKey(name: 'name') + final String name; + + @JsonKey(name: 'value') + final double value; + + const ChartData({required this.name, required this.value}); + + factory ChartData.fromJson(Map json) => + _$ChartDataFromJson(json); + + Map toJson() => _$ChartDataToJson(this); +} diff --git a/lib/models/common.g.dart b/lib/models/common.g.dart index f0f50ed..55aa9e9 100644 --- a/lib/models/common.g.dart +++ b/lib/models/common.g.dart @@ -27,3 +27,13 @@ Map _$PageResultToJson( 'current': instance.current, 'pages': instance.pages, }; + +ChartData _$ChartDataFromJson(Map json) => ChartData( + name: json['name'] as String, + value: (json['value'] as num).toDouble(), +); + +Map _$ChartDataToJson(ChartData instance) => { + 'name': instance.name, + 'value': instance.value, +}; diff --git a/lib/pages/about_page.dart b/lib/pages/about_page.dart deleted file mode 100644 index 6a97f58..0000000 --- a/lib/pages/about_page.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -class AboutPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlutterLogo(size: 100), - SizedBox(height: 24), - Text( - '我的Flutter应用', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - SizedBox(height: 8), - Text( - '版本 2.1.0', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - SizedBox(height: 32), - Container( - width: 200, - child: Column( - children: [ - _buildAboutItem('编译版本', '2.1.0 (20240115)'), - _buildAboutItem('更新时间', '2024年1月15日'), - _buildAboutItem('开发者', 'Flutter开发团队'), - ], - ), - ), - SizedBox(height: 40), - ElevatedButton.icon( - onPressed: () {}, - icon: Icon(Icons.star), - label: Text('给我们评分'), - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), - ), - ], - ), - ); - } - - Widget _buildAboutItem(String label, String value) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, style: TextStyle(color: Colors.grey)), - Text(value, style: TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/pages/blog_list_page.dart b/lib/pages/blog_list_page.dart deleted file mode 100644 index bf4c285..0000000 --- a/lib/pages/blog_list_page.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; -import 'package:blog_app/widget/blog.dart'; -import 'package:blog_app/widget/common.dart'; -import 'package:flutter/material.dart'; - -class BlogListPage extends StatefulWidget { - final String category; - - const BlogListPage({super.key, required this.category}); - - @override - State createState() => _BlogListPageState(); -} - -class _BlogListPageState extends State { - late List blogs = []; - - @override - void initState() { - super.initState(); - _loadBlogList(); - } - - Future _loadBlogList() async { - try { - final result = await queryBlogByConditionApi(widget.category, null, null); - setState(() { - blogs = result; - }); - } catch (e) { - throw Exception('获取博客列表失败: $e'); - } - } - - Widget _buildTitle() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '博客列表', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - Text( - '共 ${blogs.length} 篇', - style: TextStyle(color: Colors.grey.shade600, fontSize: 14), - ), - ], - ); - } - - Widget _buildBlogItem(BuildContext context, Blog blog, int index) { - final colors = Theme.of(context).colorScheme; - - return buildCard( - context: context, - child: Padding( - padding: EdgeInsets.all(10), - child: Row( - children: [ - SizedBox( - width: 25, - child: Text( - index.toString(), - style: TextStyle( - fontWeight: FontWeight.w500, - color: colors.primary, - ), - ), - ), - - // 发布时间 - SizedBox( - width: 150, - child: Text( - blog.createTime, - style: TextStyle(fontSize: 14, color: Colors.grey.shade600), - ), - ), - - // 标题 - Expanded( - child: Text( - blog.title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: colors.primary, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - } - - Widget _buildBlogList() { - return ListView.builder( - itemCount: blogs.length, - itemBuilder: (context, index) { - final blog = blogs[index]; - return InkWell( - onTap: () => navigatorToBlogDetail(context, blog.id), - child: _buildBlogItem(context, blog, index + 1), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return Scaffold( - appBar: AppBar(title: Text('博客详情')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Text( - widget.category, - style: TextStyle(fontSize: 20, color: colors.primary), - ), - ), - const SizedBox(height: 8), - _buildTitle(), - const SizedBox(height: 8), - Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()), - ], - ), - ), - ); - } -} diff --git a/lib/pages/category_list_page.dart b/lib/pages/category_list_page.dart new file mode 100644 index 0000000..10688f5 --- /dev/null +++ b/lib/pages/category_list_page.dart @@ -0,0 +1,74 @@ +import 'package:blog_app/apis/blog.dart'; +import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/widget/blog.dart'; +import 'package:flutter/material.dart'; + +class CategoryListPage extends StatefulWidget { + final String category; + + const CategoryListPage({super.key, required this.category}); + + @override + State createState() => _CategoryListPageState(); +} + +class _CategoryListPageState extends State { + late List blogs = []; + + @override + void initState() { + super.initState(); + _loadBlogList(); + } + + Future _loadBlogList() async { + try { + final result = await queryBlogByConditionApi(widget.category, null, null); + setState(() { + blogs = result; + }); + } catch (e) { + throw Exception('获取博客列表失败: $e'); + } + } + + Widget _buildBlogList() { + return ListView.builder( + itemCount: blogs.length, + itemBuilder: (context, index) { + final blog = blogs[index]; + return InkWell( + onTap: () => navigatorToBlogDetail(context, blog.id), + child: buildBlogListItem(context, blog, index + 1), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar(title: Text('博客详情')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + widget.category, + style: TextStyle(fontSize: 20, color: colors.primary), + ), + ), + const SizedBox(height: 8), + buildListTitle(blogs.length), + const SizedBox(height: 8), + Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()), + ], + ), + ), + ); + } +} diff --git a/lib/pages/category_page.dart b/lib/pages/category_page.dart index 7153bde..6789313 100644 --- a/lib/pages/category_page.dart +++ b/lib/pages/category_page.dart @@ -1,7 +1,6 @@ import 'package:blog_app/apis/blog.dart'; import 'package:blog_app/models/blog.dart'; -import 'package:blog_app/pages/blog_list_page.dart'; -import 'package:blog_app/widget/common.dart'; +import 'package:blog_app/widget/blog.dart'; import 'package:flutter/material.dart'; class CategoryPage extends StatefulWidget { @@ -31,34 +30,12 @@ class _CategoryPageState extends State { } } - Widget _buildTitle() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - '共 ${categories.length} 个分类', - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ), - ], - ); - } - @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitle(), + buildCategoryTitle(context, categories.length), SizedBox(height: 10), // 分类列表 Expanded( @@ -66,89 +43,11 @@ class _CategoryPageState extends State { itemCount: categories.length, itemBuilder: (context, index) { final category = categories[index]; - return _buildCategoryCard(context, category); + return buildCategoryCard(context, category); }, ), ), ], ); } - - Widget _buildCategoryCard(BuildContext context, BlogCategory category) { - final colors = Theme.of(context).colorScheme; - - return buildCard( - context: context, - child: ListTile( - leading: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: colors.primary.withAlpha(50), - shape: BoxShape.circle, - ), - child: Icon(Icons.folder, color: colors.primary, size: 28), - ), - title: Text( - category.name, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), - ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - '${category.count} 篇博客', - style: TextStyle(fontSize: 14, color: Colors.grey.shade600), - ), - ), - trailing: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: colors.primary.withAlpha(50), - borderRadius: BorderRadius.circular(16), - ), - child: Text( - category.count.toString(), - style: TextStyle( - color: colors.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - onTap: () => _navigateToBlogList(context, category), - ), - ); - } - - void _navigateToBlogList(BuildContext context, BlogCategory category) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlogListPage(category: category.name), - ), - ); - } - - void _showCategoryDetail(BuildContext context, BlogCategory category) { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: Text(category.name), - content: Text('该分类下有 ${category.count} 篇博客'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('关闭'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - // 这里可以添加跳转到该分类博客列表的逻辑 - }, - child: const Text('查看博客'), - ), - ], - ), - ); - } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 21bb32b..bbf626f 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,9 +1,5 @@ import 'package:blog_app/layout/drawer.dart'; import 'package:blog_app/layout/menu.dart'; -import 'package:blog_app/pages/about_page.dart'; -import 'package:blog_app/pages/blog_page.dart'; -import 'package:blog_app/pages/category_page.dart'; -import 'package:blog_app/pages/settings_page.dart'; import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { @@ -35,7 +31,7 @@ class _HomePageState extends State { _currentPageIndex = index; }); }, - children: [BlogPage(), CategoryPage(), SettingsPage(), AboutPage()], + children: pageWidgets, ); } diff --git a/lib/pages/message_page.dart b/lib/pages/message_page.dart deleted file mode 100644 index 60c2217..0000000 --- a/lib/pages/message_page.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; - -class MessagePage extends StatelessWidget { - final List> messages = [ - {'title': '系统通知', 'content': '您的账号安全等级已提升', 'time': '10:30', 'unread': false}, - {'title': '活动提醒', 'content': '新活动即将开始,敬请期待', 'time': '昨天', 'unread': true}, - {'title': '版本更新', 'content': '新版本v2.1.0已发布', 'time': '2024-01-20', 'unread': false}, - ]; - - @override - Widget build(BuildContext context) { - return ListView.builder( - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages[index]; - return Card( - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: ListTile( - leading: CircleAvatar( - backgroundColor: message['unread'] ? Colors.blue : Colors.grey, - child: Icon( - Icons.notifications, - color: Colors.white, - size: 20, - ), - ), - title: Text( - message['title'], - style: TextStyle( - fontWeight: message['unread'] ? FontWeight.bold : FontWeight.normal, - ), - ), - subtitle: Text(message['content']), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - message['time'], - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - if (message['unread']) - Container( - margin: EdgeInsets.only(top: 4), - width: 8, - height: 8, - decoration: BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - ), - ], - ), - onTap: () {}, - ), - ); - }, - ); - } -} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart deleted file mode 100644 index d26f1ba..0000000 --- a/lib/pages/profile_page.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; - -class ProfilePage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - children: [ - // 个人信息卡片 - Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: EdgeInsets.all(20), - child: Row( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: Colors.blue, - child: Icon(Icons.person, size: 40, color: Colors.white), - ), - SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '张小明', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 4), - Text('高级用户', style: TextStyle(color: Colors.grey)), - SizedBox(height: 8), - Row( - children: [ - Icon(Icons.star, size: 16, color: Colors.amber), - SizedBox(width: 4), - Text('会员等级: VIP3'), - ], - ), - ], - ), - ), - ], - ), - ), - ), - - SizedBox(height: 20), - - // 详细信息 - _buildInfoItem('手机号码', '138****1234', Icons.phone), - _buildInfoItem('邮箱地址', 'zhang@example.com', Icons.email), - _buildInfoItem('注册时间', '2024年1月15日', Icons.calendar_today), - _buildInfoItem('所在地区', '北京市朝阳区', Icons.location_on), - ], - ), - ); - } - - Widget _buildInfoItem(String title, String value, IconData icon) { - return Card( - margin: EdgeInsets.only(bottom: 12), - child: ListTile( - leading: Icon(icon, color: Colors.blue), - title: Text(title), - subtitle: Text(value), - trailing: Icon(Icons.edit, size: 20), - ), - ); - } -} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart deleted file mode 100644 index 51f0ad6..0000000 --- a/lib/pages/settings_page.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Column( - children: [ - _buildSettingsSection('账户设置', [ - _buildSettingsItem('隐私设置', Icons.privacy_tip), - _buildSettingsItem('安全设置', Icons.security), - _buildSettingsItem('账号绑定', Icons.link), - ]), - - SizedBox(height: 20), - - _buildSettingsSection('通知设置', [ - _buildSettingsItem('推送通知', Icons.notifications_active, hasSwitch: true), - _buildSettingsItem('声音提醒', Icons.volume_up, hasSwitch: true), - _buildSettingsItem('震动提醒', Icons.vibration, hasSwitch: true), - ]), - - SizedBox(height: 20), - - _buildSettingsSection('其他设置', [ - _buildSettingsItem('清理缓存', Icons.cleaning_services), - _buildSettingsItem('语言设置', Icons.language), - _buildSettingsItem('主题设置', Icons.color_lens), - _buildSettingsItem('关于应用', Icons.info_outline), - ]), - ], - ), - ); - } - - Widget _buildSettingsSection(String title, List children) { - return Card( - elevation: 2, - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[700], - ), - ), - SizedBox(height: 8), - ...children, - ], - ), - ), - ); - } - - Widget _buildSettingsItem(String title, IconData icon, {bool hasSwitch = false}) { - return ListTile( - leading: Icon(icon, color: Colors.blue), - title: Text(title), - trailing: hasSwitch - ? Switch(value: true, onChanged: (value) {}) - : Icon(Icons.arrow_forward_ios, size: 16), - onTap: () {}, - ); - } -} diff --git a/lib/pages/stats_page.dart b/lib/pages/stats_page.dart new file mode 100644 index 0000000..2935ae5 --- /dev/null +++ b/lib/pages/stats_page.dart @@ -0,0 +1,304 @@ +import 'package:blog_app/apis/blog.dart'; +import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/models/common.dart'; +import 'package:blog_app/widget/chart.dart'; +import 'package:blog_app/widget/common.dart'; +import 'package:blog_app/widget/year_selector.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 double statsHeight = 120; + int currentYear = DateTime.now().year; + + late List approvedStats = []; + late List visitStats = []; + late List visitRankStats = []; + late List categoryStats = []; + late List readRankStats = []; + late BlogStats overviewStats = BlogStats( + blogCount: 0, + categoryCount: 0, + greatCount: 0, + visitCount: 0, + wordCount: 0, + ); + + @override + void initState() { + super.initState(); + _loadStatsList(); + } + + Future _loadStatsList() async { + try { + final approvedResult = await queryBlogApprovedStatsApi(currentYear); + final visitResult = await queryBlogVisitStatsApi(currentYear); + final visitRankResult = await queryBlogVisitRankStatsApi(); + final categoryResult = await queryBlogCategoryStatsApi(); + final readRankResult = await queryBlogReadRankStatsApi(); + final overviewResult = await queryBlogOverviewStatsApi(); + + setState(() { + approvedStats = approvedResult; + visitStats = visitResult; + visitRankStats = visitRankResult; + categoryStats = categoryResult; + readRankStats = readRankResult; + overviewStats = overviewResult; + }); + } catch (e) { + throw Exception('获取统计数据失败: $e'); + } + } + + Widget _buildApprovedStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '每月博客发布统计', Icons.show_chart), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: lineChart( + context: context, + xAxisName: '月份', + yAxisName: '数量', + unit: '篇', + data: approvedStats, + ), + ), + ], + ); + } + + Widget _buildVisitStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '每月博客访问统计', Icons.show_chart), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: lineChart( + context: context, + xAxisName: '月份', + yAxisName: '访问量', + unit: '人次', + data: visitStats, + ), + ), + ], + ); + } + + Widget _buildVisitRankStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '博客访问数量排行', Icons.bar_chart), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: barChart( + context: context, + xAxisName: '名称', + yAxisName: '访问量', + unit: '人次', + data: visitRankStats, + ), + ), + ], + ); + } + + Widget _buildCategoryStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '博客类别统计', Icons.pie_chart), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: pieChart(context: context, unit: '篇', data: categoryStats), + ), + ], + ); + } + + Widget _buildReadRankStats(BuildContext context) { + return Column( + children: [ + buildChartTitle(context, '博客阅读时长排行', Icons.bar_chart), + const SizedBox(height: 3), + buildChartDivider(context), + const SizedBox(height: 3), + Expanded( + child: barChart( + context: context, + xAxisName: '名称', + yAxisName: '时长', + unit: '分钟', + data: readRankStats, + ), + ), + ], + ); + } + + Widget buildStatsCard({ + required BuildContext context, + required String title, + required int count, + required String unit, + }) { + return buildCard( + context: context, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + count.toString(), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.0, + ), + ), + const SizedBox(width: 4), + Text( + unit, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } + + Widget buildBlogStatsGrid(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 2, + children: [ + buildStatsCard( + context: context, + title: '博客数量', + count: overviewStats.blogCount, + unit: '篇', + ), + buildStatsCard( + context: context, + title: '分类数量', + count: overviewStats.categoryCount, + unit: '个', + ), + buildStatsCard( + context: context, + title: '访问量', + count: overviewStats.visitCount, + unit: '次', + ), + buildStatsCard( + context: context, + title: '总字数', + count: overviewStats.wordCount, + unit: '字', + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + YearSelector( + initialYear: DateTime.now().year, + minYear: 2000, + maxYear: 2100, + onYearChanged: + (year) => { + setState(() { + currentYear = year; + _loadStatsList(); + }), + }, + ), + SizedBox(height: 6), + buildBlogStatsGrid(context), + SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildApprovedStats(context), + ), + ), + SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildVisitStats(context), + ), + ), + SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildVisitRankStats(context), + ), + ), + SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildCategoryStats(context), + ), + ), + SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildReadRankStats(context), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/timeline_page.dart b/lib/pages/timeline_page.dart new file mode 100644 index 0000000..a2a9393 --- /dev/null +++ b/lib/pages/timeline_page.dart @@ -0,0 +1,85 @@ +import 'package:blog_app/apis/blog.dart'; +import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/widget/blog.dart'; +import 'package:blog_app/widget/year_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:timelines_plus/timelines_plus.dart'; + +class TimelinePage extends StatefulWidget { + const TimelinePage({super.key}); + + @override + State createState() => _TimelinePageState(); +} + +class _TimelinePageState extends State { + late List blogs = []; + + @override + void initState() { + super.initState(); + _loadBlogList(); + } + + Future _loadBlogList() async { + try { + final result = await queryBlogByConditionApi( + null, + null, + DateTime.now().year, + ); + setState(() { + blogs = result; + }); + } catch (e) { + throw Exception('获取博客列表失败: $e'); + } + } + + Future refreshRecord(int year) async { + final result = await queryBlogByConditionApi(null, null, year); + setState(() { + blogs = result; + }); + } + + Widget _buildBlogList() { + final colors = Theme.of(context).colorScheme; + + return Timeline.tileBuilder( + theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0), + padding: EdgeInsets.all(6), + builder: TimelineTileBuilder.connected( + itemCount: blogs.length, + connectorBuilder: + (context, index, type) => + Connector.solidLine(thickness: 2, color: colors.primary), + indicatorBuilder: (context, index) { + return Indicator.dot(size: 12.0, color: colors.primary); + }, + contentsBuilder: (context, index) { + return buildBlogListItem(context, blogs[index], index + 1); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + YearSelector( + initialYear: DateTime.now().year, + minYear: 2000, + maxYear: 2100, + onYearChanged: (year) => refreshRecord(year), + ), + const SizedBox(height: 8), + buildListTitle(blogs.length), + const SizedBox(height: 8), + Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()), + ], + ); + } +} diff --git a/lib/widget/blog.dart b/lib/widget/blog.dart index b036a36..539555c 100644 --- a/lib/widget/blog.dart +++ b/lib/widget/blog.dart @@ -1,5 +1,6 @@ import 'package:blog_app/models/blog.dart'; import 'package:blog_app/pages/blog_detail_page.dart'; +import 'package:blog_app/pages/category_list_page.dart'; import 'package:blog_app/utils/date_utils.dart'; import 'package:blog_app/widget/common.dart'; import 'package:flutter/material.dart'; @@ -138,6 +139,134 @@ class BlogCard extends StatelessWidget { } } +Widget buildCategoryTitle(BuildContext context, int count) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '共 $count 个分类', + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ], + ); +} + +Widget buildCategoryCard(BuildContext context, BlogCategory category) { + final colors = Theme.of(context).colorScheme; + + return buildCard( + context: context, + child: ListTile( + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: colors.primary.withAlpha(50), + shape: BoxShape.circle, + ), + child: Icon(Icons.folder, color: colors.primary, size: 28), + ), + title: Text( + category.name, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + '${category.count} 篇博客', + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + ), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: colors.primary.withAlpha(50), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + category.count.toString(), + style: TextStyle(color: colors.primary, fontWeight: FontWeight.bold), + ), + ), + onTap: () => navigateToBlogList(context, category), + ), + ); +} + +Widget buildListTitle(int count) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '博客列表', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + Text( + '共 $count 篇', + style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + ), + ], + ); +} + +Widget buildBlogListItem(BuildContext context, Blog blog, int index) { + final colors = Theme.of(context).colorScheme; + + return buildCard( + context: context, + child: Padding( + padding: EdgeInsets.all(10), + child: Row( + children: [ + SizedBox( + width: 25, + child: Text( + index.toString(), + style: TextStyle( + fontWeight: FontWeight.w500, + color: colors.primary, + ), + ), + ), + + // 发布时间 + SizedBox( + width: 150, + child: Text( + blog.createTime, + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + ), + + // 标题 + Expanded( + child: Text( + blog.title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: colors.primary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); +} + void navigatorToBlogDetail(BuildContext context, int blogId) { Navigator.push( context, @@ -145,6 +274,15 @@ void navigatorToBlogDetail(BuildContext context, int blogId) { ); } +void navigateToBlogList(BuildContext context, BlogCategory category) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryListPage(category: category.name), + ), + ); +} + Widget buildEmpty() { return Center( child: Column( diff --git a/lib/widget/chart.dart b/lib/widget/chart.dart new file mode 100644 index 0000000..9862c18 --- /dev/null +++ b/lib/widget/chart.dart @@ -0,0 +1,285 @@ +import 'package:blog_app/models/common.dart'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; + +Widget buildChartTitle(BuildContext context, String title, IconData icon) { + return Row( + children: [ + Icon(icon, 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)), + + // 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)), + + // 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/lib/widget/common.dart b/lib/widget/common.dart index c7b7e4f..68f8ca6 100644 --- a/lib/widget/common.dart +++ b/lib/widget/common.dart @@ -16,3 +16,21 @@ Widget buildCard({required BuildContext context, required Widget child}) { ), ); } + +Widget circleIconButton({ + required IconData icon, + required VoidCallback onPressed, + required BuildContext context, +}) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + shape: CircleBorder(), + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.primary, + padding: EdgeInsets.zero, + minimumSize: const Size(0, 0), + ), + child: Icon(icon, color: Colors.white), + ); +} \ No newline at end of file diff --git a/lib/widget/year_selector.dart b/lib/widget/year_selector.dart new file mode 100644 index 0000000..e08b136 --- /dev/null +++ b/lib/widget/year_selector.dart @@ -0,0 +1,119 @@ +import 'package:blog_app/widget/common.dart'; +import 'package:flutter/material.dart'; + +class YearSelector extends StatefulWidget { + final int initialYear; + final int? minYear; + final int? maxYear; + final Function(int) onYearChanged; + + const YearSelector({ + super.key, + required this.initialYear, + required this.onYearChanged, + this.minYear, + this.maxYear, + }); + + @override + State createState() => _YearSelectorState(); +} + +// SingleTickerProviderStateMixin 动画控制器 +class _YearSelectorState extends State + with SingleTickerProviderStateMixin { + late int _currentYear; + + // 用于动画效果 + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _currentYear = widget.initialYear; + + // 初始化动画控制器 + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + + // 缩放动画 + _scaleAnimation = Tween(begin: 1.0, end: 1.1).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + /// 切换到上一年 + void _previousYear() { + if (widget.minYear == null || _currentYear > widget.minYear!) { + _animateYearChange(() { + setState(() { + _currentYear--; + }); + widget.onYearChanged(_currentYear); + }); + } + } + + /// 切换到下一年 + void _nextYear() { + if (widget.maxYear == null || _currentYear < widget.maxYear!) { + _animateYearChange(() { + setState(() { + _currentYear++; + }); + widget.onYearChanged(_currentYear); + }); + } + } + + /// 年份变化时的动画效果 + void _animateYearChange(VoidCallback onComplete) { + _animationController.forward().then((_) { + onComplete(); + _animationController.reverse(); + }); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + circleIconButton( + context: context, + icon: Icons.chevron_left, + onPressed: () => _previousYear(), + ), + // 年份显示 + AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale(scale: _scaleAnimation.value, child: child); + }, + child: Text( + '$_currentYear', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + ), + circleIconButton( + context: context, + icon: Icons.chevron_right, + onPressed: () => _nextYear(), + ) + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 84204aa..20a2b49 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -693,6 +693,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + 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: + name: syncfusion_flutter_core + sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" + syncfusion_localizations: + dependency: "direct main" + description: + name: syncfusion_localizations + sha256: bb32b07879b4c1dee5d4c8ad1c57343a4fdae55d65a87f492727c11b68f23164 + url: "https://pub.flutter-io.cn" + source: hosted + version: "30.2.7" term_glyph: dependency: transitive description: @@ -709,6 +733,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" + timelines_plus: + dependency: "direct main" + description: + name: timelines_plus + sha256: d621d8724bc8f64957127c1195436996548e166296682d081bedcdb0abe1b638 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a1f6f02..076f609 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,9 @@ dependencies: json_annotation: ^4.9.0 intl: ^0.19.0 easy_refresh: ^3.4.0 + timelines_plus: ^1.0.7 + syncfusion_localizations: ^30.1.37 + syncfusion_flutter_charts: ^30.1.41 dev_dependencies: flutter_test: