diff --git a/fonts/custom.ttf b/assets/fonts/custom.ttf similarity index 100% rename from fonts/custom.ttf rename to assets/fonts/custom.ttf diff --git a/assets/images/avatar.jpg b/assets/images/avatar.jpg new file mode 100644 index 0000000..7f31048 Binary files /dev/null and b/assets/images/avatar.jpg differ diff --git a/lib/apis/blog.dart b/lib/apis/blog.dart index a0419e3..38f5640 100644 --- a/lib/apis/blog.dart +++ b/lib/apis/blog.dart @@ -12,20 +12,41 @@ Future> queryBlogByPageApi(int currentPage, int pageSize) { } Future> queryBlogByConditionApi( - String category, - String title, - int year, + String? category, + String? title, + int? year, ) { + final Map 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>( "/blog/condition", - queryParameters: {"category": category, "title": title, "year": year}, + queryParameters: queryParams, converter: (data) => convertListResponse(data, Blog.fromJson), ); } Future queryBlogByIdApi(int id) { return HttpUtil().get( - "blog/$id/content", + "/blog/$id/content", converter: (data) => Blog.fromJson(data), ); } + +Future> queryBlogCategoryApi() { + return HttpUtil().get>( + "/blog/category", + converter: + (data) => + convertListResponse(data, BlogCategory.fromJson), + ); +} diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index f85fa85..8a96fdc 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -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"; } \ No newline at end of file diff --git a/lib/layout/drawer.dart b/lib/layout/drawer.dart new file mode 100644 index 0000000..0b3cce7 --- /dev/null +++ b/lib/layout/drawer.dart @@ -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, + ), + ); +} diff --git a/lib/layout/menu.dart b/lib/layout/menu.dart new file mode 100644 index 0000000..ccec3eb --- /dev/null +++ b/lib/layout/menu.dart @@ -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 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), +]; diff --git a/lib/main.dart b/lib/main.dart index 3a6b73f..550f17f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,8 @@ void main() async { } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( diff --git a/lib/models/blog.dart b/lib/models/blog.dart index 1a11766..9a548bf 100644 --- a/lib/models/blog.dart +++ b/lib/models/blog.dart @@ -59,4 +59,20 @@ class Blog { factory Blog.fromJson(Map json) => _$BlogFromJson(json); Map toJson() => _$BlogToJson(this); -} \ No newline at end of file +} + +@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 json) => + _$BlogCategoryFromJson(json); + + Map toJson() => _$BlogCategoryToJson(this); +} diff --git a/lib/models/blog.g.dart b/lib/models/blog.g.dart index 43fbbdf..3ba4cf3 100644 --- a/lib/models/blog.g.dart +++ b/lib/models/blog.g.dart @@ -35,3 +35,11 @@ Map _$BlogToJson(Blog instance) => { 'createTime': instance.createTime, 'updateTime': instance.updateTime, }; + +BlogCategory _$BlogCategoryFromJson(Map json) => BlogCategory( + name: json['name'] as String, + count: (json['count'] as num).toInt(), +); + +Map _$BlogCategoryToJson(BlogCategory instance) => + {'name': instance.name, 'count': instance.count}; diff --git a/lib/pages/blog_detail_page.dart b/lib/pages/blog_detail_page.dart new file mode 100644 index 0000000..8d602b5 --- /dev/null +++ b/lib/pages/blog_detail_page.dart @@ -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 createState() => _BlogDetailPageState(); +} + +class _BlogDetailPageState extends State { + final tocController = TocController(); + bool _showToc = false; + late Future _blogDetail; + + @override + void initState() { + super.initState(); + _blogDetail = _loadBlogDetail(); + } + + Future _loadBlogDetail() async { + try { + return await queryBlogByIdApi(widget.blogId); + } catch (e) { + throw Exception('获取博客详情失败: $e'); + } + } + + 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( + 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(), + ], + ); + } +} diff --git a/lib/pages/blog_list_page.dart b/lib/pages/blog_list_page.dart new file mode 100644 index 0000000..bf4c285 --- /dev/null +++ b/lib/pages/blog_list_page.dart @@ -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 createState() => _BlogListPageState(); +} + +class _BlogListPageState extends State { + late List blogs = []; + + @override + void initState() { + super.initState(); + _loadBlogList(); + } + + Future _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()), + ], + ), + ), + ); + } +} diff --git a/lib/pages/blog_page.dart b/lib/pages/blog_page.dart index dac6bed..674b284 100644 --- a/lib/pages/blog_page.dart +++ b/lib/pages/blog_page.dart @@ -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 { late List blogList = []; + int _currentPage = 1; + final int _pageSize = 5; + bool _hasMore = true; + bool _showScrollToTop = false; + + final EasyRefreshController _freshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + + final ScrollController _scrollController = ScrollController(); + @override void initState() { super.initState(); - // 初始加载数据 - _loadData(); + _loadData(isRefresh: true); + _scrollController.addListener(_onScroll); } - Future _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 _loadData({required bool isRefresh}) async { + try { + // 如果是刷新,重置页码 + if (isRefresh) { + _currentPage = 1; + } + + final result = await queryBlogByPageApi(_currentPage, _pageSize); + + setState(() { + if (isRefresh) { + // 刷新时直接替换数据 + blogList = result.records; + } else { + // 加载更多时追加数据 + blogList.addAll(result.records); + } + + // 判断是否还有更多数据 + _hasMore = result.current < result.pages; + // 如果有更多数据,准备加载下一页 + if (_hasMore) { + _currentPage++; + } + }); + } catch (e) { + // 处理错误 + debugPrint('加载数据失败: $e'); + } finally { + _freshController.finishRefresh(); + _freshController.resetFooter(); + } + } + + // 下拉刷新 + Future _onRefresh() async { + await _loadData(isRefresh: true); + } + + // 上拉加载 + Future _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), + ], ); } } diff --git a/lib/pages/category_page.dart b/lib/pages/category_page.dart new file mode 100644 index 0000000..7153bde --- /dev/null +++ b/lib/pages/category_page.dart @@ -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 createState() => _CategoryPageState(); +} + +class _CategoryPageState extends State { + late List categories = []; + + @override + void initState() { + super.initState(); + _loadBlogCategory(); + } + + Future _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('查看博客'), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 78c3187..21bb32b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -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 { int _currentPageIndex = 0; late PageController _pageController; - // 页面标题列表 - final List _pageTitles = ['博客', '消息中心', '设置', '关于我们']; - - // 页面图标列表 - final List _pageIcons = [ - Icons.article, - Icons.person, - Icons.message, - Icons.settings, - Icons.info, - ]; - @override void initState() { super.initState(); @@ -46,7 +35,7 @@ class _HomePageState extends State { _currentPageIndex = index; }); }, - children: [BlogPage(), MessagePage(), SettingsPage(), AboutPage()], + children: [BlogPage(), CategoryPage(), SettingsPage(), AboutPage()], ); } @@ -55,7 +44,7 @@ class _HomePageState extends State { 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 { ); } + 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 { 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; - } - } } diff --git a/lib/widget/blog.dart b/lib/widget/blog.dart index f5603e0..b036a36 100644 --- a/lib/widget/blog.dart +++ b/lib/widget/blog.dart @@ -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)), + ], + ), + ); +} diff --git a/lib/widget/common.dart b/lib/widget/common.dart new file mode 100644 index 0000000..c7b7e4f --- /dev/null +++ b/lib/widget/common.dart @@ -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), + ), + ); +} diff --git a/lib/widget/easy_refresh.dart b/lib/widget/easy_refresh.dart new file mode 100644 index 0000000..fc42b24 --- /dev/null +++ b/lib/widget/easy_refresh.dart @@ -0,0 +1,65 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; + +Widget buildEasyRefresh({ + required EasyRefreshController freshController, + required Future Function()? onRefresh, + required Future 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, // 滚动动画曲线 + ); +} diff --git a/pubspec.lock b/pubspec.lock index 8411136..84204aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" + easy_refresh: + dependency: "direct main" + description: + name: easy_refresh + sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" fake_async: dependency: transitive description: @@ -313,7 +321,7 @@ packages: source: hosted version: "4.1.2" intl: - dependency: "direct dev" + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf @@ -464,6 +472,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" path_provider_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d245317..a1f6f02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: shared_preferences: ^2.3.0 logger: ^2.6.0 json_annotation: ^4.9.0 + intl: ^0.19.0 + easy_refresh: ^3.4.0 dev_dependencies: flutter_test: @@ -52,14 +54,15 @@ dev_dependencies: flutter_lints: ^5.0.0 build_runner: ^2.4.5 json_serializable: ^6.7.1 - intl: ^0.19.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec flutter: uses-material-design: true + assets: + - assets/images/ fonts: - family: CustomFont fonts: - - asset: fonts/custom.ttf + - asset: assets/fonts/custom.ttf