feat:增加博客统计模块
This commit is contained in:
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
lib/pages/category_list_page.dart
Normal file
74
lib/pages/category_list_page.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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('查看博客'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
304
lib/pages/stats_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/pages/timeline_page.dart
Normal file
85
lib/pages/timeline_page.dart
Normal 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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user