feat:增加博客分类模块

This commit is contained in:
2025-11-15 13:03:10 +08:00
parent 3b167508dd
commit 2cfb2ce592
19 changed files with 901 additions and 201 deletions

View File

@@ -12,20 +12,41 @@ Future<PageResult<Blog>> queryBlogByPageApi(int currentPage, int pageSize) {
}
Future<List<Blog>> queryBlogByConditionApi(
String category,
String title,
int year,
String? category,
String? title,
int? year,
) {
final Map<String, dynamic> queryParams = {};
if (category != null && category.isNotEmpty) {
queryParams['category'] = category;
}
if (title != null && title.isNotEmpty) {
queryParams['title'] = title;
}
if (year != null && year > 0) {
queryParams['year'] = year;
}
return HttpUtil().get<List<Blog>>(
"/blog/condition",
queryParameters: {"category": category, "title": title, "year": year},
queryParameters: queryParams,
converter: (data) => convertListResponse<Blog>(data, Blog.fromJson),
);
}
Future<Blog> queryBlogByIdApi(int id) {
return HttpUtil().get<Blog>(
"blog/$id/content",
"/blog/$id/content",
converter: (data) => Blog.fromJson(data),
);
}
Future<List<BlogCategory>> queryBlogCategoryApi() {
return HttpUtil().get<List<BlogCategory>>(
"/blog/category",
converter:
(data) =>
convertListResponse<BlogCategory>(data, BlogCategory.fromJson),
);
}

View File

@@ -1,8 +1,5 @@
/// 应用信息
class AppConfig {
// 网络配置
// http://192.168.1.3:8100
// http://14.103.235.151:81/food-service
static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api";
// static const String baseApiUrl = "http://172.29.101.108:8100";
// static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api";
static const String baseApiUrl = "http://192.168.1.4:8082";
}

79
lib/layout/drawer.dart Normal file
View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'menu.dart';
Widget buildDrawerHeader(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colors.primary, colors.inversePrimary],
),
),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipOval(
child: Image.asset(
'assets/images/avatar.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
SizedBox(height: 16),
Text(
'Cxx0822',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget buildDrawerItem({
required BuildContext context,
required PageInfo page,
required VoidCallback onTap,
required bool isSelected,
}) {
final colors = Theme.of(context).colorScheme;
return Container(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? colors.primary.withAlpha(50) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
page.icon,
color: isSelected ? colors.primary : Colors.grey[700],
size: 24,
),
title: Text(
page.title,
style: TextStyle(
color: isSelected ? colors.primary : Colors.grey[800],
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 16,
),
),
trailing:
isSelected
? Icon(Icons.arrow_forward_ios, size: 16, color: colors.primary)
: null,
onTap: onTap,
),
);
}

16
lib/layout/menu.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class PageInfo {
final String title;
final IconData icon;
const PageInfo({required this.title, required this.icon});
}
final List<PageInfo> pages = [
PageInfo(title: '博客', icon: Icons.article),
PageInfo(title: '分类', icon: Icons.folder),
PageInfo(title: '个人中心', icon: Icons.person),
PageInfo(title: '设置', icon: Icons.settings),
PageInfo(title: '关于我们', icon: Icons.info),
];

View File

@@ -9,6 +9,8 @@ void main() async {
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(

View File

@@ -59,4 +59,20 @@ class Blog {
factory Blog.fromJson(Map<String, dynamic> json) => _$BlogFromJson(json);
Map<String, dynamic> toJson() => _$BlogToJson(this);
}
}
@JsonSerializable()
class BlogCategory {
@JsonKey(name: 'name')
final String name;
@JsonKey(name: 'count')
final int count;
BlogCategory({required this.name, required this.count});
factory BlogCategory.fromJson(Map<String, dynamic> json) =>
_$BlogCategoryFromJson(json);
Map<String, dynamic> toJson() => _$BlogCategoryToJson(this);
}

View File

@@ -35,3 +35,11 @@ Map<String, dynamic> _$BlogToJson(Blog instance) => <String, dynamic>{
'createTime': instance.createTime,
'updateTime': instance.updateTime,
};
BlogCategory _$BlogCategoryFromJson(Map<String, dynamic> json) => BlogCategory(
name: json['name'] as String,
count: (json['count'] as num).toInt(),
);
Map<String, dynamic> _$BlogCategoryToJson(BlogCategory instance) =>
<String, dynamic>{'name': instance.name, 'count': instance.count};

View File

@@ -0,0 +1,155 @@
import 'package:blog_app/apis/blog.dart';
import 'package:blog_app/models/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;
const BlogDetailPage({super.key, required this.blogId});
@override
State<BlogDetailPage> createState() => _BlogDetailPageState();
}
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');
}
}
Widget _buildBlogTitle(String title) {
return Text(
title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
);
}
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(
onPressed: () => setState(() => _showToc = !_showToc),
mini: true,
backgroundColor: Theme.of(context).primaryColor,
child: Icon(_showToc ? Icons.close : Icons.list, color: Colors.white),
),
);
}
Widget _buildBlogDetailContent(BuildContext context, Blog blogDetail) {
return Stack(
children: [
Column(
children: [
_buildBlogTitle(blogDetail.title),
SizedBox(height: 10),
Expanded(child: _buildMarkdown(blogDetail.content!)),
],
),
_buildTocPanel(),
],
);
}
}

View File

@@ -0,0 +1,139 @@
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

@@ -1,6 +1,8 @@
import 'package:blog_app/apis/blog.dart';
import 'package:blog_app/models/blog.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 {
@@ -13,26 +15,129 @@ 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();
@override
void initState() {
super.initState();
// 初始加载数据
_loadData();
_loadData(isRefresh: true);
_scrollController.addListener(_onScroll);
}
Future<void> _loadData() async {
final result = await queryBlogByPageApi(1, 10);
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_freshController.dispose();
_scrollController.dispose();
super.dispose();
}
setState(() {
blogList = result.records;
});
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);
}
// 上拉加载
Future<void> _onLoad() async {
if (_hasMore) {
await _loadData(isRefresh: false);
_freshController.finishLoad(IndicatorResult.success);
} else {
_freshController.finishLoad(IndicatorResult.noMore);
}
}
void _onScroll() {
// 当滚动距离超过300时显示返回顶部按钮
if (_scrollController.offset > 300) {
if (!_showScrollToTop) {
setState(() {
_showScrollToTop = true;
});
}
} else {
if (_showScrollToTop) {
setState(() {
_showScrollToTop = false;
});
}
}
}
// 滚动到顶部
void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
Widget buildBlogList() {
return ListView.builder(
controller: _scrollController,
itemCount: blogList.length,
itemBuilder: (context, index) {
final blog = blogList[index];
return InkWell(
onTap: () => navigatorToBlogDetail(context, blog.id),
child: BlogCard(blog: blogList[index]),
);
},
);
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: blogList.length,
itemBuilder: (context, index) => BlogCard(blog: blogList[index]),
return Stack(
children: [
buildEasyRefresh(
freshController: _freshController,
onRefresh: _onRefresh,
onLoad: _onLoad,
body: buildBlogList(),
),
if (_showScrollToTop)
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
],
);
}
}

View File

@@ -0,0 +1,154 @@
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:flutter/material.dart';
class CategoryPage extends StatefulWidget {
const CategoryPage({super.key});
@override
State<CategoryPage> createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
late List<BlogCategory> categories = [];
@override
void initState() {
super.initState();
_loadBlogCategory();
}
Future<void> _loadBlogCategory() async {
try {
final result = await queryBlogCategoryApi();
setState(() {
categories = result;
});
} catch (e) {
throw Exception('获取博客分类失败: $e');
}
}
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(),
SizedBox(height: 10),
// 分类列表
Expanded(
child: ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
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,7 +1,8 @@
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/message_page.dart';
import 'package:blog_app/pages/profile_page.dart';
import 'package:blog_app/pages/category_page.dart';
import 'package:blog_app/pages/settings_page.dart';
import 'package:flutter/material.dart';
@@ -14,18 +15,6 @@ class _HomePageState extends State<HomePage> {
int _currentPageIndex = 0;
late PageController _pageController;
// 页面标题列表
final List<String> _pageTitles = ['博客', '消息中心', '设置', '关于我们'];
// 页面图标列表
final List<IconData> _pageIcons = [
Icons.article,
Icons.person,
Icons.message,
Icons.settings,
Icons.info,
];
@override
void initState() {
super.initState();
@@ -46,7 +35,7 @@ class _HomePageState extends State<HomePage> {
_currentPageIndex = index;
});
},
children: [BlogPage(), MessagePage(), SettingsPage(), AboutPage()],
children: [BlogPage(), CategoryPage(), SettingsPage(), AboutPage()],
);
}
@@ -55,7 +44,7 @@ class _HomePageState extends State<HomePage> {
return Scaffold(
appBar: AppBar(
title: Text(
_pageTitles[_currentPageIndex],
pages[_currentPageIndex].title,
style: TextStyle(color: Colors.white),
),
backgroundColor: Theme.of(context).colorScheme.primary,
@@ -79,6 +68,35 @@ class _HomePageState extends State<HomePage> {
);
}
void onTapDrawerItem(int index) {
setState(() {
_currentPageIndex = index;
});
_pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
Navigator.pop(context);
}
Widget _buildDrawerBody() {
return ListView(
padding: EdgeInsets.zero,
children: [
...List.generate(
pages.length,
(index) => buildDrawerItem(
context: context,
page: pages[index],
isSelected: index == _currentPageIndex,
onTap: () => onTapDrawerItem(index),
),
),
],
);
}
Widget _buildDrawer() {
return Drawer(
child: Container(
@@ -86,145 +104,12 @@ class _HomePageState extends State<HomePage> {
child: Column(
children: [
// 抽屉头部
_buildDrawerHeader(),
buildDrawerHeader(context),
// 菜单项列表
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
...List.generate(
_pageTitles.length,
(index) => _buildDrawerItem(
icon: _pageIcons[index],
title: _pageTitles[index],
index: index,
),
),
],
),
),
Expanded(child: _buildDrawerBody()),
],
),
),
);
}
Widget _buildDrawerHeader() {
return Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.blue, Colors.lightBlue],
),
),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.white.withOpacity(0.3),
child: Icon(Icons.person, size: 50, color: Colors.white),
),
SizedBox(height: 16),
Text(
'用户名',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'user@example.com',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
);
}
Widget _buildDrawerItem({
required IconData icon,
required String title,
required int index,
}) {
final bool isSelected = index == _currentPageIndex;
final bool isSpecialItem = index == -1; // 特殊菜单项标识
return Container(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
icon,
color:
isSelected
? Colors.blue
: isSpecialItem
? Colors.grey[600]
: Colors.grey[700],
size: 24,
),
title: Text(
title,
style: TextStyle(
color:
isSelected
? Colors.blue
: isSpecialItem
? Colors.grey[600]
: Colors.grey[800],
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 16,
),
),
trailing:
isSelected
? Icon(Icons.arrow_forward_ios, size: 16, color: Colors.blue)
: null,
onTap: () {
if (!isSpecialItem) {
// 正常页面切换
setState(() {
_currentPageIndex = index;
});
_pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
// 特殊菜单项处理
_handleSpecialItemTap(title);
}
Navigator.pop(context); // 关闭抽屉
},
),
);
}
void _handleSpecialItemTap(String title) {
// 处理特殊菜单项的点击事件
switch (title) {
case '帮助中心':
print('打开帮助中心');
break;
case '意见反馈':
print('打开意见反馈');
break;
case '退出登录':
print('执行退出登录');
break;
}
}
}

View File

@@ -1,5 +1,7 @@
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/pages/blog_detail_page.dart';
import 'package:blog_app/utils/date_utils.dart';
import 'package:blog_app/widget/common.dart';
import 'package:flutter/material.dart';
class BlogLabel {
@@ -111,34 +113,45 @@ class BlogCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildBlogTitle(context),
SizedBox(height: 16),
_buildBlogLabel(),
SizedBox(height: 16),
Container(height: 1, width: 80, color: Theme.of(context).colorScheme.primary),
SizedBox(height: 16),
_buildBlogSummary(),
],
),
return buildCard(
context: context,
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildBlogTitle(context),
SizedBox(height: 16),
_buildBlogLabel(),
SizedBox(height: 16),
Container(
height: 1,
width: 80,
color: Theme.of(context).colorScheme.primary,
),
SizedBox(height: 16),
_buildBlogSummary(),
],
),
),
);
}
}
void navigatorToBlogDetail(BuildContext context, int blogId) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BlogDetailPage(blogId: blogId)),
);
}
Widget buildEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey)),
],
),
);
}

18
lib/widget/common.dart Normal file
View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
Widget buildCard({required BuildContext context, required Widget child}) {
final colors = Theme.of(context).colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
child: Padding(padding: EdgeInsets.all(8), child: child),
),
);
}

View File

@@ -0,0 +1,65 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
Widget buildEasyRefresh({
required EasyRefreshController freshController,
required Future<void> Function()? onRefresh,
required Future<void> Function()? onLoad,
required Widget body,
}) {
return EasyRefresh(
controller: freshController,
header: ClassicHeader(
dragText: '下拉刷新',
armedText: '释放刷新',
readyText: '准备刷新',
processingText: '刷新中...',
processedText: '刷新完成',
failedText: '刷新失败',
noMoreText: '没有更多数据',
showText: true,
messageText: '更新于 %T',
showMessage: true,
),
footer: ClassicFooter(
dragText: '上拉加载',
armedText: '释放加载',
readyText: '准备加载',
processingText: '加载中...',
processedText: '加载完成',
failedText: '加载失败',
noMoreText: '没有更多数据',
showText: true,
messageText: '更新于 %T',
showMessage: true,
),
onRefresh: onRefresh,
onLoad: onLoad,
child: body,
);
}
Widget buildScrollToTop({
required BuildContext context,
required VoidCallback scrollToTop,
}) {
return Positioned(
right: 0,
bottom: 20,
child: FloatingActionButton(
onPressed: scrollToTop,
backgroundColor: Theme.of(context).colorScheme.primary,
elevation: 2,
mini: true,
child: const Icon(Icons.arrow_upward, color: Colors.white),
),
);
}
void scrollToTopAnimateTo(ScrollController controller) {
controller.animateTo(
0,
duration: const Duration(milliseconds: 500), // 滚动动画时长
curve: Curves.easeInOut, // 滚动动画曲线
);
}