feat:增加加载中显示

This commit is contained in:
2025-11-16 00:09:39 +08:00
parent d512a77f5f
commit 47b6fb9937
15 changed files with 697 additions and 541 deletions

View File

@@ -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";
}

View File

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

View File

@@ -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<BlogDetailPage> {
final tocController = TocController();
bool _showToc = false;
late Future<Blog> _blogDetail;
@override
void initState() {
super.initState();
_blogDetail = _loadBlogDetail();
}
Future<Blog> _loadBlogDetail() async {
try {
return await queryBlogByIdApi(widget.blogId);
} catch (e) {
throw Exception('获取博客详情失败: $e');
}
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().queryBlogById(widget.blogId);
});
}
Widget _buildBlogTitle(String title) {
@@ -43,113 +38,64 @@ class _BlogDetailPageState extends State<BlogDetailPage> {
);
}
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<Blog>(
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<BlogProvider>();
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),
);
}
}

View File

@@ -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<BlogPage> {
late List<Blog> 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<BlogProvider>().refreshBlogList();
});
}
@override
@@ -42,49 +39,17 @@ class _BlogPageState extends State<BlogPage> {
super.dispose();
}
Future<void> _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<void> _onRefresh() async {
await _loadData(isRefresh: true);
await context.read<BlogProvider>().refreshBlogList();
_freshController.finishRefresh();
}
// 上拉加载
Future<void> _onLoad() async {
if (_hasMore) {
await _loadData(isRefresh: false);
final blogProvider = context.read<BlogProvider>();
if (blogProvider.hasMore) {
await blogProvider.loadMoreBlogList();
_freshController.finishLoad(IndicatorResult.success);
} else {
_freshController.finishLoad(IndicatorResult.noMore);
@@ -111,15 +76,22 @@ class _BlogPageState extends State<BlogPage> {
// 滚动到顶部
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<BlogPage> {
@override
Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
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),

View File

@@ -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<CategoryListPage> {
late List<Blog> blogs = [];
@override
void initState() {
super.initState();
_loadBlogList();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogByCategory(widget.category);
});
}
Future<void> _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<CategoryListPage> {
);
}
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<BlogProvider>();
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),
],
),
),

View File

@@ -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<CategoryPage> {
late List<BlogCategory> categories = [];
@override
void initState() {
super.initState();
_loadBlogCategory();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogCategory();
});
}
Future<void> _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<CategoryPage> {
],
);
}
@override
Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
return Column(
children: [
Expanded(
child: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildContent(blogProvider),
],
),
),
],
);
}
}

View File

@@ -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<MarkdownPage> createState() => _MarkdownPageState();
}
class _MarkdownPageState extends State<MarkdownPage> {
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<void> _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,
),
);
}
}

View File

@@ -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<StatsPage> {
final double chartHeight = 400;
final double statsHeight = 120;
int currentYear = DateTime.now().year;
late List<ChartData> approvedStats = [];
late List<ChartData> visitStats = [];
late List<ChartData> visitRankStats = [];
late List<ChartData> categoryStats = [];
late List<ChartData> 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<BlogProvider>().refreshBlogStats();
});
}
Future<void> _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<StatsPage> {
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<StatsPage> {
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<StatsPage> {
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<StatsPage> {
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<StatsPage> {
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<StatsPage> {
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<BlogProvider>();
return Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
SingleChildScrollView(child: _buildContent(blogProvider))
]
);
}
}

View File

@@ -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<TimelinePage> createState() => _TimelinePageState();
State<TimelinePage> createState() => TimelinePageState();
}
class _TimelinePageState extends State<TimelinePage> {
late List<Blog> blogs = [];
class TimelinePageState extends State<TimelinePage> {
@override
void initState() {
super.initState();
_loadBlogList();
}
Future<void> _loadBlogList() async {
try {
final result = await queryBlogByConditionApi(
null,
null,
DateTime.now().year,
);
setState(() {
blogs = result;
});
} catch (e) {
throw Exception('获取博客列表失败: $e');
}
}
Future<void> refreshRecord(int year) async {
final result = await queryBlogByConditionApi(null, null, year);
setState(() {
blogs = result;
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().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<TimelinePage> {
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<TimelinePage> {
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<BlogProvider>();
return Column(
children: [
Expanded(
child: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildContent(blogProvider),
],
),
),
],
);
}

204
lib/provider/blog.dart Normal file
View File

@@ -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<Blog> blogList = [];
List<BlogCategory> 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<ChartData> approvedStats = [];
late List<ChartData> visitStats = [];
late List<ChartData> visitRankStats = [];
late List<ChartData> categoryStats = [];
late List<ChartData> readRankStats = [];
late BlogStats overviewStats = BlogStats(
blogCount: 0,
categoryCount: 0,
greatCount: 0,
visitCount: 0,
wordCount: 0,
);
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> refreshBlogList() async {
await queryBlogByPage(isRefresh: true);
}
Future<void> 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<void> loadMoreBlogList() async {
if (hasMore && !isLoading) {
await queryBlogByPage(isRefresh: false);
}
}
// 清除错误
void clearError() {
error = null;
notifyListeners();
}
}

View File

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

View File

@@ -33,4 +33,23 @@ Widget circleIconButton({
),
child: Icon(icon, color: Colors.white),
);
}
}
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());
}

63
lib/widget/markdown.dart Normal file
View File

@@ -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),
),
),
],
),
),
),
);