From 47b6fb9937f173f83ff325d53c95937520101017 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Sun, 16 Nov 2025 00:09:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E4=B8=AD=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config/app_config.dart | 4 +- lib/main.dart | 12 +- lib/pages/blog_detail_page.dart | 158 ++++++------------ lib/pages/blog_page.dart | 103 +++++------- lib/pages/category_list_page.dart | 73 +++++---- lib/pages/category_page.dart | 55 ++++--- lib/pages/markdown_page.dart | 122 -------------- lib/pages/stats_page.dart | 264 +++++++++++------------------- lib/pages/timeline_page.dart | 92 +++++++---- lib/provider/blog.dart | 204 +++++++++++++++++++++++ lib/widget/blog.dart | 50 ++++++ lib/widget/common.dart | 21 ++- lib/widget/markdown.dart | 63 +++++++ pubspec.lock | 16 ++ pubspec.yaml | 1 + 15 files changed, 697 insertions(+), 541 deletions(-) delete mode 100644 lib/pages/markdown_page.dart create mode 100644 lib/provider/blog.dart create mode 100644 lib/widget/markdown.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 8a96fdc..9f76317 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,5 +1,5 @@ /// 应用信息 class AppConfig { - // static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api"; - static const String baseApiUrl = "http://192.168.1.4:8082"; + static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api"; + // static const String baseApiUrl = "http://192.168.1.4:8082"; } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 550f17f..256d8eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,21 @@ import 'package:blog_app/pages/home_page.dart'; +import 'package:blog_app/provider/blog.dart'; import 'package:blog_app/utils/sp_utils.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await SPUtil.init(); - runApp(MyApp()); + + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => BlogProvider()), + ], + child: MyApp(), + ), + ); } class MyApp extends StatelessWidget { diff --git a/lib/pages/blog_detail_page.dart b/lib/pages/blog_detail_page.dart index 8d602b5..00bd7cd 100644 --- a/lib/pages/blog_detail_page.dart +++ b/lib/pages/blog_detail_page.dart @@ -1,8 +1,9 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/widget/common.dart'; +import 'package:blog_app/widget/markdown.dart'; +import 'package:provider/provider.dart'; +import 'package:blog_app/provider/blog.dart'; import 'package:flutter/material.dart'; import 'package:markdown_widget/config/toc.dart'; -import 'package:markdown_widget/widget/markdown.dart'; class BlogDetailPage extends StatefulWidget { final int blogId; @@ -16,20 +17,14 @@ class BlogDetailPage extends StatefulWidget { class _BlogDetailPageState extends State { final tocController = TocController(); bool _showToc = false; - late Future _blogDetail; @override void initState() { super.initState(); - _blogDetail = _loadBlogDetail(); - } - - Future _loadBlogDetail() async { - try { - return await queryBlogByIdApi(widget.blogId); - } catch (e) { - throw Exception('获取博客详情失败: $e'); - } + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().queryBlogById(widget.blogId); + }); } Widget _buildBlogTitle(String title) { @@ -43,113 +38,64 @@ class _BlogDetailPageState extends State { ); } - Widget _buildTocPanel() => Visibility( - visible: _showToc, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - width: 250, - height: 400, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(50), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - border: Border.all(color: Colors.grey[300]!), - ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '目录', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - ), - Expanded( - child: TocWidget( - controller: tocController, - tocTextStyle: TextStyle(fontSize: 14), - ), - ), - ], - ), - ), - ), - ); - - Widget _buildMarkdown(String data) => - MarkdownWidget(data: data, tocController: tocController); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('博客详情')), - body: Container( - padding: EdgeInsets.all(16), - child: FutureBuilder( - future: _blogDetail, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [Text('加载失败: ${snapshot.error}')], - ), - ); - } - - if (!snapshot.hasData) { - return Center(child: Text('暂无数据')); - } - - final blogDetail = snapshot.data!; - return _buildBlogDetailContent(context, blogDetail); - }, - ), - ), - floatingActionButton: FloatingActionButton( + Widget? buildFloatingActionButton(BlogProvider provider) { + if (!provider.isLoading) { + return FloatingActionButton( onPressed: () => setState(() => _showToc = !_showToc), mini: true, backgroundColor: Theme.of(context).primaryColor, child: Icon(_showToc ? Icons.close : Icons.list, color: Colors.white), - ), - ); + ); + } else { + return null; + } } - Widget _buildBlogDetailContent(BuildContext context, Blog blogDetail) { + Widget _buildContent(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.queryBlogById(widget.blogId), + ); + } + return Stack( children: [ Column( children: [ - _buildBlogTitle(blogDetail.title), + _buildBlogTitle(provider.currentBlog.title), SizedBox(height: 10), - Expanded(child: _buildMarkdown(blogDetail.content!)), + Expanded( + child: buildMarkdown( + tocController: tocController, + data: provider.currentBlog.content!, + ), + ), ], ), - _buildTocPanel(), + buildTocPanel(tocController: tocController, showToc: _showToc), ], ); } + + @override + Widget build(BuildContext context) { + final blogProvider = context.watch(); + + return Scaffold( + appBar: AppBar(title: Text('博客详情')), + body: Container( + padding: EdgeInsets.all(16), + child: Stack( + children: [ + if (blogProvider.isLoading) + buildLoadingIndicator() + else + _buildContent(blogProvider), + ], + ), + ), + floatingActionButton: buildFloatingActionButton(blogProvider), + ); + } } diff --git a/lib/pages/blog_page.dart b/lib/pages/blog_page.dart index 674b284..287010e 100644 --- a/lib/pages/blog_page.dart +++ b/lib/pages/blog_page.dart @@ -1,9 +1,10 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/provider/blog.dart'; +import 'package:blog_app/widget/common.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:blog_app/widget/easy_refresh.dart'; import 'package:blog_app/widget/blog.dart'; import 'package:easy_refresh/easy_refresh.dart'; -import 'package:flutter/material.dart'; class BlogPage extends StatefulWidget { const BlogPage({super.key}); @@ -13,25 +14,21 @@ class BlogPage extends StatefulWidget { } class _BlogPageState extends State { - late List blogList = []; - - int _currentPage = 1; - final int _pageSize = 5; - bool _hasMore = true; - bool _showScrollToTop = false; - final EasyRefreshController _freshController = EasyRefreshController( controlFinishRefresh: true, controlFinishLoad: true, ); - final ScrollController _scrollController = ScrollController(); + bool _showScrollToTop = false; @override void initState() { super.initState(); - _loadData(isRefresh: true); _scrollController.addListener(_onScroll); + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshBlogList(); + }); } @override @@ -42,49 +39,17 @@ class _BlogPageState extends State { super.dispose(); } - Future _loadData({required bool isRefresh}) async { - try { - // 如果是刷新,重置页码 - if (isRefresh) { - _currentPage = 1; - } - - final result = await queryBlogByPageApi(_currentPage, _pageSize); - - setState(() { - if (isRefresh) { - // 刷新时直接替换数据 - blogList = result.records; - } else { - // 加载更多时追加数据 - blogList.addAll(result.records); - } - - // 判断是否还有更多数据 - _hasMore = result.current < result.pages; - // 如果有更多数据,准备加载下一页 - if (_hasMore) { - _currentPage++; - } - }); - } catch (e) { - // 处理错误 - debugPrint('加载数据失败: $e'); - } finally { - _freshController.finishRefresh(); - _freshController.resetFooter(); - } - } - // 下拉刷新 Future _onRefresh() async { - await _loadData(isRefresh: true); + await context.read().refreshBlogList(); + _freshController.finishRefresh(); } // 上拉加载 Future _onLoad() async { - if (_hasMore) { - await _loadData(isRefresh: false); + final blogProvider = context.read(); + if (blogProvider.hasMore) { + await blogProvider.loadMoreBlogList(); _freshController.finishLoad(IndicatorResult.success); } else { _freshController.finishLoad(IndicatorResult.noMore); @@ -111,15 +76,22 @@ class _BlogPageState extends State { // 滚动到顶部 void _scrollToTop() => scrollToTopAnimateTo(_scrollController); - Widget buildBlogList() { + Widget _buildBlogList(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.refreshBlogList, + ); + } + return ListView.builder( controller: _scrollController, - itemCount: blogList.length, + itemCount: provider.blogList.length, itemBuilder: (context, index) { - final blog = blogList[index]; + final blog = provider.blogList[index]; return InkWell( onTap: () => navigatorToBlogDetail(context, blog.id), - child: BlogCard(blog: blogList[index]), + child: BlogCard(blog: blog), ); }, ); @@ -127,13 +99,28 @@ class _BlogPageState extends State { @override Widget build(BuildContext context) { + final blogProvider = context.watch(); + return Stack( children: [ - buildEasyRefresh( - freshController: _freshController, - onRefresh: _onRefresh, - onLoad: _onLoad, - body: buildBlogList(), + Column( + children: [ + Expanded( + child: buildEasyRefresh( + freshController: _freshController, + onRefresh: _onRefresh, + onLoad: _onLoad, + body: Stack( + children: [ + if (blogProvider.isLoading) + buildLoadingIndicator() + else + _buildBlogList(blogProvider), + ], + ), + ), + ), + ], ), if (_showScrollToTop) buildScrollToTop(context: context, scrollToTop: _scrollToTop), diff --git a/lib/pages/category_list_page.dart b/lib/pages/category_list_page.dart index 10688f5..1717aae 100644 --- a/lib/pages/category_list_page.dart +++ b/lib/pages/category_list_page.dart @@ -1,5 +1,6 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/widget/common.dart'; +import 'package:provider/provider.dart'; +import 'package:blog_app/provider/blog.dart'; import 'package:blog_app/widget/blog.dart'; import 'package:flutter/material.dart'; @@ -13,30 +14,20 @@ class CategoryListPage extends StatefulWidget { } class _CategoryListPageState extends State { - late List blogs = []; - @override void initState() { super.initState(); - _loadBlogList(); + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshBlogByCategory(widget.category); + }); } - Future _loadBlogList() async { - try { - final result = await queryBlogByConditionApi(widget.category, null, null); - setState(() { - blogs = result; - }); - } catch (e) { - throw Exception('获取博客列表失败: $e'); - } - } - - Widget _buildBlogList() { + Widget _buildBlogList(BlogProvider provider) { return ListView.builder( - itemCount: blogs.length, + itemCount: provider.blogList.length, itemBuilder: (context, index) { - final blog = blogs[index]; + final blog = provider.blogList[index]; return InkWell( onTap: () => navigatorToBlogDetail(context, blog.id), child: buildBlogListItem(context, blog, index + 1), @@ -45,27 +36,45 @@ class _CategoryListPageState extends State { ); } + Widget _buildContent(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.refreshBlogByYear(), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text(widget.category, style: TextStyle(fontSize: 20))), + const SizedBox(height: 8), + buildListTitle(provider.blogList.length), + const SizedBox(height: 8), + Expanded( + child: + provider.blogList.isEmpty + ? buildEmpty() + : _buildBlogList(provider), + ), + ], + ); + } + @override Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; + final blogProvider = context.watch(); return Scaffold( appBar: AppBar(title: Text('博客详情')), body: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Stack( 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()), + if (blogProvider.isLoading) + buildLoadingIndicator() + else + _buildContent(blogProvider), ], ), ), diff --git a/lib/pages/category_page.dart b/lib/pages/category_page.dart index 6789313..9f99d4e 100644 --- a/lib/pages/category_page.dart +++ b/lib/pages/category_page.dart @@ -1,5 +1,6 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; +import 'package:blog_app/widget/common.dart'; +import 'package:provider/provider.dart'; +import 'package:blog_app/provider/blog.dart'; import 'package:blog_app/widget/blog.dart'; import 'package:flutter/material.dart'; @@ -11,38 +12,34 @@ class CategoryPage extends StatefulWidget { } class _CategoryPageState extends State { - late List categories = []; - @override void initState() { super.initState(); - _loadBlogCategory(); + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshBlogCategory(); + }); } - Future _loadBlogCategory() async { - try { - final result = await queryBlogCategoryApi(); - setState(() { - categories = result; - }); - } catch (e) { - throw Exception('获取博客分类失败: $e'); + Widget _buildContent(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.refreshBlogByYear(), + ); } - } - @override - Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildCategoryTitle(context, categories.length), + buildCategoryTitle(context, provider.categoryList.length), SizedBox(height: 10), // 分类列表 Expanded( child: ListView.builder( - itemCount: categories.length, + itemCount: provider.categoryList.length, itemBuilder: (context, index) { - final category = categories[index]; + final category = provider.categoryList[index]; return buildCategoryCard(context, category); }, ), @@ -50,4 +47,24 @@ class _CategoryPageState extends State { ], ); } + + @override + Widget build(BuildContext context) { + final blogProvider = context.watch(); + + return Column( + children: [ + Expanded( + child: Stack( + children: [ + if (blogProvider.isLoading) + buildLoadingIndicator() + else + _buildContent(blogProvider), + ], + ), + ), + ], + ); + } } diff --git a/lib/pages/markdown_page.dart b/lib/pages/markdown_page.dart deleted file mode 100644 index 8009723..0000000 --- a/lib/pages/markdown_page.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:blog_app/utils/http_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:markdown_widget/config/toc.dart'; -import 'package:markdown_widget/widget/markdown.dart'; - -class MarkdownPage extends StatefulWidget { - const MarkdownPage({super.key}); - - @override - State createState() => _MarkdownPageState(); -} - -class _MarkdownPageState extends State { - final tocController = TocController(); - bool _showToc = false; - late String markdownData = ''; - - Widget _buildTocPanel() => AnimatedOpacity( - opacity: _showToc ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: Visibility( - visible: _showToc, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - width: 250, - height: 400, - margin: const EdgeInsets.only(bottom: 60, right: 60), - // 调整位置在按钮左侧 - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - border: Border.all(color: Colors.grey[300]!), - ), - child: Column( - children: [ - // 标题栏 - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '目录', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => setState(() => _showToc = false), - ), - ], - ), - ), - Expanded(child: TocWidget(controller: tocController)), - ], - ), - ), - ), - ), - ); - - Widget buildMarkdown() => - MarkdownWidget(data: markdownData, tocController: tocController); - - @override - void initState() { - super.initState(); - // 初始加载数据 - _loadData(); - } - - Future _loadData() async { - final result = await HttpUtil().get("/condition"); - - setState(() { - markdownData = result[0]['content']; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('文档')), - body: Padding( - padding: EdgeInsets.all(16), - child: Stack( - children: [ - // 主内容 - buildMarkdown(), - - // 悬浮TOC面板 - _buildTocPanel(), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => setState(() => _showToc = !_showToc), - child: Icon(_showToc ? Icons.close : Icons.list), - mini: true, - backgroundColor: - _showToc ? Colors.grey : Theme.of(context).primaryColor, - ), - ); - } -} \ No newline at end of file diff --git a/lib/pages/stats_page.dart b/lib/pages/stats_page.dart index 2935ae5..23774fe 100644 --- a/lib/pages/stats_page.dart +++ b/lib/pages/stats_page.dart @@ -1,6 +1,6 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; -import 'package:blog_app/models/common.dart'; +import 'package:provider/provider.dart'; +import 'package:blog_app/provider/blog.dart'; +import 'package:blog_app/widget/blog.dart'; import 'package:blog_app/widget/chart.dart'; import 'package:blog_app/widget/common.dart'; import 'package:blog_app/widget/year_selector.dart'; @@ -16,50 +16,18 @@ class StatsPage extends StatefulWidget { 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(); + + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshBlogStats(); + }); } - 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) { + Widget _buildApprovedStats(BlogProvider provider) { return Column( children: [ buildChartTitle(context, '每月博客发布统计', Icons.show_chart), @@ -72,14 +40,14 @@ class StatsPageState extends State { xAxisName: '月份', yAxisName: '数量', unit: '篇', - data: approvedStats, + data: provider.approvedStats, ), ), ], ); } - Widget _buildVisitStats(BuildContext context) { + Widget _buildVisitStats(BlogProvider provider) { return Column( children: [ buildChartTitle(context, '每月博客访问统计', Icons.show_chart), @@ -92,14 +60,14 @@ class StatsPageState extends State { xAxisName: '月份', yAxisName: '访问量', unit: '人次', - data: visitStats, + data: provider.visitStats, ), ), ], ); } - Widget _buildVisitRankStats(BuildContext context) { + Widget _buildVisitRankStats(BlogProvider provider) { return Column( children: [ buildChartTitle(context, '博客访问数量排行', Icons.bar_chart), @@ -112,14 +80,14 @@ class StatsPageState extends State { xAxisName: '名称', yAxisName: '访问量', unit: '人次', - data: visitRankStats, + data: provider.visitRankStats, ), ), ], ); } - Widget _buildCategoryStats(BuildContext context) { + Widget _buildCategoryStats(BlogProvider provider) { return Column( children: [ buildChartTitle(context, '博客类别统计', Icons.pie_chart), @@ -127,13 +95,17 @@ class StatsPageState extends State { buildChartDivider(context), const SizedBox(height: 3), Expanded( - child: pieChart(context: context, unit: '篇', data: categoryStats), + child: pieChart( + context: context, + unit: '篇', + data: provider.categoryStats, + ), ), ], ); } - Widget _buildReadRankStats(BuildContext context) { + Widget _buildReadRankStats(BlogProvider provider) { return Column( children: [ buildChartTitle(context, '博客阅读时长排行', Icons.bar_chart), @@ -146,64 +118,14 @@ class StatsPageState extends State { xAxisName: '名称', yAxisName: '时长', unit: '分钟', - data: readRankStats, + data: provider.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) { + Widget _buildBlogStatsGrid(BlogProvider provider) { return GridView.count( crossAxisCount: 2, shrinkWrap: true, @@ -213,92 +135,106 @@ class StatsPageState extends State { buildStatsCard( context: context, title: '博客数量', - count: overviewStats.blogCount, + count: provider.overviewStats.blogCount, unit: '篇', ), buildStatsCard( context: context, title: '分类数量', - count: overviewStats.categoryCount, + count: provider.overviewStats.categoryCount, unit: '个', ), buildStatsCard( context: context, title: '访问量', - count: overviewStats.visitCount, + count: provider.overviewStats.visitCount, unit: '次', ), buildStatsCard( context: context, title: '总字数', - count: overviewStats.wordCount, + count: provider.overviewStats.wordCount, unit: '字', ), ], ); } + Widget _buildContent(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.refreshBlogStats(), + ); + } + + return Column( + children: [ + YearSelector( + initialYear: provider.currentYear, + minYear: 2000, + maxYear: 2100, + onYearChanged: (year) { + setState(() { + provider.currentYear = year; + provider.refreshBlogStats(); + }); + }, + ), + const SizedBox(height: 6), + _buildBlogStatsGrid(provider), + const SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildApprovedStats(provider), + ), + ), + const SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard(context: context, child: _buildVisitStats(provider)), + ), + const SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildVisitRankStats(provider), + ), + ), + const SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildCategoryStats(provider), + ), + ), + const SizedBox(height: 6), + SizedBox( + height: chartHeight, + child: buildCard( + context: context, + child: _buildReadRankStats(provider), + ), + ), + ], + ); + } + @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), - ), - ), - ], - ), + final blogProvider = context.watch(); + + return Stack( + children: [ + if (blogProvider.isLoading) + buildLoadingIndicator() + else + SingleChildScrollView(child: _buildContent(blogProvider)) + ] ); } } diff --git a/lib/pages/timeline_page.dart b/lib/pages/timeline_page.dart index a2a9393..1f087dc 100644 --- a/lib/pages/timeline_page.dart +++ b/lib/pages/timeline_page.dart @@ -1,6 +1,7 @@ -import 'package:blog_app/apis/blog.dart'; -import 'package:blog_app/models/blog.dart'; +import 'package:provider/provider.dart'; +import 'package:blog_app/provider/blog.dart'; import 'package:blog_app/widget/blog.dart'; +import 'package:blog_app/widget/common.dart'; import 'package:blog_app/widget/year_selector.dart'; import 'package:flutter/material.dart'; import 'package:timelines_plus/timelines_plus.dart'; @@ -9,48 +10,27 @@ class TimelinePage extends StatefulWidget { const TimelinePage({super.key}); @override - State createState() => _TimelinePageState(); + State createState() => TimelinePageState(); } -class _TimelinePageState extends State { - late List blogs = []; - +class TimelinePageState extends State { @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; + // 初始化加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshBlogByYear(); }); } - Widget _buildBlogList() { + Widget _buildBlogList(BlogProvider provider) { 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, + itemCount: provider.blogList.length, connectorBuilder: (context, index, type) => Connector.solidLine(thickness: 2, color: colors.primary), @@ -58,14 +38,24 @@ class _TimelinePageState extends State { return Indicator.dot(size: 12.0, color: colors.primary); }, contentsBuilder: (context, index) { - return buildBlogListItem(context, blogs[index], index + 1); + return buildBlogListItem( + context, + provider.blogList[index], + index + 1, + ); }, ), ); } - @override - Widget build(BuildContext context) { + Widget _buildContent(BlogProvider provider) { + if (provider.error != null) { + return buildErrorInfo( + errorInfo: provider.error!, + onPressed: () => provider.refreshBlogByYear(), + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -73,12 +63,42 @@ class _TimelinePageState extends State { initialYear: DateTime.now().year, minYear: 2000, maxYear: 2100, - onYearChanged: (year) => refreshRecord(year), + onYearChanged: (year) { + setState(() { + provider.currentYear = year; + provider.refreshBlogByYear(); + }); + }, ), const SizedBox(height: 8), - buildListTitle(blogs.length), + buildListTitle(provider.blogList.length), const SizedBox(height: 8), - Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()), + Expanded( + child: + provider.blogList.isEmpty + ? buildEmpty() + : _buildBlogList(provider), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final blogProvider = context.watch(); + + return Column( + children: [ + Expanded( + child: Stack( + children: [ + if (blogProvider.isLoading) + buildLoadingIndicator() + else + _buildContent(blogProvider), + ], + ), + ), ], ); } diff --git a/lib/provider/blog.dart b/lib/provider/blog.dart new file mode 100644 index 0000000..7d1c4cf --- /dev/null +++ b/lib/provider/blog.dart @@ -0,0 +1,204 @@ +import 'package:blog_app/models/common.dart'; +import 'package:flutter/foundation.dart'; +import 'package:blog_app/apis/blog.dart'; +import 'package:blog_app/models/blog.dart'; + +class BlogProvider with ChangeNotifier { + List blogList = []; + List categoryList = []; + Blog currentBlog = Blog( + id: 0, + title: '', + topValue: 0, + isGreat: false, + category: '', + summary: '', + content: '', + wordCount: 0, + readDuration: 0, + visitCount: 0, + createTime: '', + updateTime: '', + ); + int currentPage = 1; + final int pageSize = 5; + bool hasMore = true; + bool isLoading = false; + int currentYear = DateTime.now().year; + String? error; + + 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, + ); + + Future queryBlogByPage({bool isRefresh = true}) async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + notifyListeners(); + + // 如果是刷新,重置页码 + if (isRefresh) { + currentPage = 1; + } + + final result = await queryBlogByPageApi(currentPage, pageSize); + + // 更新数据 + if (isRefresh) { + blogList = result.records; + } else { + blogList.addAll(result.records); + } + + // 判断是否还有更多数据 + hasMore = result.current < result.pages; + if (hasMore) { + currentPage++; + } + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future refreshBlogCategory() async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + categoryList = []; + notifyListeners(); + + final result = await queryBlogCategoryApi(); + categoryList = result; + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future refreshBlogStats() async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + notifyListeners(); + + 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(); + + approvedStats = approvedResult; + visitStats = visitResult; + visitRankStats = visitRankResult; + categoryStats = categoryResult; + readRankStats = readRankResult; + overviewStats = overviewResult; + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future refreshBlogByCategory(String category) async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + blogList = []; + notifyListeners(); + + final result = await queryBlogByConditionApi(category, null, null); + blogList = result; + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future refreshBlogByYear() async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + blogList = []; + notifyListeners(); + + final result = await queryBlogByConditionApi(null, null, currentYear); + blogList = result; + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + Future refreshBlogList() async { + await queryBlogByPage(isRefresh: true); + } + + Future queryBlogById(int blogId) async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + notifyListeners(); + + final result = await queryBlogByIdApi(blogId); + currentBlog = result; + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + + // 加载更多 + Future loadMoreBlogList() async { + if (hasMore && !isLoading) { + await queryBlogByPage(isRefresh: false); + } + } + + // 清除错误 + void clearError() { + error = null; + notifyListeners(); + } +} diff --git a/lib/widget/blog.dart b/lib/widget/blog.dart index 539555c..8286d33 100644 --- a/lib/widget/blog.dart +++ b/lib/widget/blog.dart @@ -267,6 +267,56 @@ Widget buildBlogListItem(BuildContext context, Blog blog, int index) { ); } +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, + ), + ), + ], + ), + ], + ), + ); +} + void navigatorToBlogDetail(BuildContext context, int blogId) { Navigator.push( context, diff --git a/lib/widget/common.dart b/lib/widget/common.dart index 68f8ca6..cf26c57 100644 --- a/lib/widget/common.dart +++ b/lib/widget/common.dart @@ -33,4 +33,23 @@ Widget circleIconButton({ ), child: Icon(icon, color: Colors.white), ); -} \ No newline at end of file +} + +Widget buildErrorInfo({ + required String errorInfo, + required VoidCallback onPressed, +}) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('加载失败: $errorInfo'), + ElevatedButton(onPressed: onPressed, child: Text('重试')), + ], + ), + ); +} + +Widget buildLoadingIndicator() { + return Center(child: CircularProgressIndicator()); +} diff --git a/lib/widget/markdown.dart b/lib/widget/markdown.dart new file mode 100644 index 0000000..aec3dc6 --- /dev/null +++ b/lib/widget/markdown.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:markdown_widget/config/toc.dart'; +import 'package:markdown_widget/widget/markdown.dart'; + +Widget buildMarkdown({ + required TocController tocController, + required String data, +}) => MarkdownWidget(data: data, tocController: tocController); + +Widget buildTocPanel({ + required TocController tocController, + required bool showToc, +}) => Visibility( + visible: showToc, + child: Align( + alignment: Alignment.bottomRight, + child: Container( + width: 250, + height: 400, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(50), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '目录', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ], + ), + ), + Expanded( + child: TocWidget( + controller: tocController, + tocTextStyle: TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + ), +); diff --git a/pubspec.lock b/pubspec.lock index 20a2b49..7c1978c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -536,6 +544,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.2" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 076f609..448819d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: timelines_plus: ^1.0.7 syncfusion_localizations: ^30.1.37 syncfusion_flutter_charts: ^30.1.41 + provider: ^6.1.1 dev_dependencies: flutter_test: