feat:增加博客统计模块

This commit is contained in:
2025-11-15 16:56:47 +08:00
parent 2cfb2ce592
commit d512a77f5f
22 changed files with 1196 additions and 524 deletions

View File

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

View File

@@ -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<BlogListPage> createState() => _BlogListPageState();
}
class _BlogListPageState extends State<BlogListPage> {
late List<Blog> blogs = [];
@override
void initState() {
super.initState();
_loadBlogList();
}
Future<void> _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()),
],
),
),
);
}
}

View File

@@ -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<CategoryListPage> createState() => _CategoryListPageState();
}
class _CategoryListPageState extends State<CategoryListPage> {
late List<Blog> blogs = [];
@override
void initState() {
super.initState();
_loadBlogList();
}
Future<void> _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()),
],
),
),
);
}
}

View File

@@ -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<CategoryPage> {
}
}
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<CategoryPage> {
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('查看博客'),
),
],
),
);
}
}

View File

@@ -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<HomePage> {
_currentPageIndex = index;
});
},
children: [BlogPage(), CategoryPage(), SettingsPage(), AboutPage()],
children: pageWidgets,
);
}

View File

@@ -1,59 +0,0 @@
import 'package:flutter/material.dart';
class MessagePage extends StatelessWidget {
final List<Map<String, dynamic>> 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: () {},
),
);
},
);
}
}

View File

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

View File

@@ -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<Widget> 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: () {},
);
}
}

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

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

View File

@@ -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<TimelinePage> createState() => _TimelinePageState();
}
class _TimelinePageState extends State<TimelinePage> {
late List<Blog> blogs = [];
@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;
});
}
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()),
],
);
}
}