diff --git a/fonts/custom.ttf b/fonts/custom.ttf new file mode 100644 index 0000000..2ca97e8 Binary files /dev/null and b/fonts/custom.ttf differ diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart new file mode 100644 index 0000000..0ec7c85 --- /dev/null +++ b/lib/config/app_config.dart @@ -0,0 +1,8 @@ +/// 应用信息 +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/blog"; + // static const String baseApiUrl = "http://172.29.101.108:8100"; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index ae99d07..a1def36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,130 +1,200 @@ -import 'package:blog_app/test.dart'; import 'package:flutter/material.dart'; -import 'package:markdown_widget/markdown_widget.dart'; void main() { - runApp(const MyApp()); + runApp(MyApp()); } class MyApp extends StatelessWidget { - const MyApp({super.key}); - @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('Markdown导航'), + title: Text('博客卡片Demo'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, ), - body: const MarkdownWithToc(), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue.shade50, Colors.purple.shade50], + ), + ), + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + children: [ + BlogCard( + title: 'Flutter开发实战', + description: '本文详细介绍了如何使用Flutter开发高性能的跨平台应用,包含最佳实践和性能优化技巧。通过实际案例演示了状态管理、路由导航、网络请求等核心功能的实现方式,帮助开发者快速掌握Flutter开发精髓。', + labels: [ + BlogLabel('精品', Icons.star, Colors.amber), + BlogLabel('置顶', Icons.push_pin, Colors.red), + BlogLabel('技术', Icons.category, Colors.blue), + BlogLabel('2024-01-15', Icons.calendar_today, Colors.green), + BlogLabel('2024-01-20', Icons.update, Colors.orange), + BlogLabel('2.3k', Icons.remove_red_eye, Colors.purple), + BlogLabel('3.2k字', Icons.text_fields, Colors.indigo), + BlogLabel('8分钟', Icons.access_time, Colors.brown), + BlogLabel('24', Icons.comment, Colors.teal), + ], + ), + + SizedBox(height: 20), + + BlogCard( + title: 'Dart语言特性深入解析', + description: '深入探讨Dart语言的现代特性,包括空安全、异步编程、扩展方法等高级功能。结合实例讲解如何利用Dart的特性编写更安全、更高效的代码,提升开发效率和代码质量。', + labels: [ + BlogLabel('推荐', Icons.thumb_up, Colors.pink), + BlogLabel('编程', Icons.code, Colors.blue), + BlogLabel('2024-01-10', Icons.calendar_today, Colors.green), + BlogLabel('2024-01-18', Icons.update, Colors.orange), + BlogLabel('1.5k', Icons.remove_red_eye, Colors.purple), + BlogLabel('2.1k字', Icons.text_fields, Colors.indigo), + BlogLabel('6分钟', Icons.access_time, Colors.brown), + BlogLabel('18', Icons.comment, Colors.teal), + ], + ), + ], + ), + ), + ) + ), + theme: ThemeData( + fontFamily: 'CustomFont' ), ); } } -class MarkdownWithToc extends StatefulWidget { - const MarkdownWithToc({super.key}); +class BlogLabel { + final String text; + final IconData icon; + final Color color; - @override - State createState() => _MarkdownWithTocState(); + BlogLabel(this.text, this.icon, this.color); } -class _MarkdownWithTocState extends State { - final tocController = TocController(); - bool _showToc = false; +class BlogCard extends StatelessWidget { + final String title; + final String description; + final List labels; - // ... markdownData 同上 + const BlogCard({ + super.key, + required this.title, + required this.description, + required this.labels, + }); - Widget _buildTocPanel() => AnimatedOpacity( - opacity: _showToc ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: Visibility( - visible: _showToc, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - width: 250, - height: 400, - margin: const EdgeInsets.only(bottom: 60, right: 60), // 调整位置在按钮左侧 - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - border: Border.all(color: Colors.grey[300]!), + @override + Widget build(BuildContext context) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + width: double.infinity, // 确保宽度对齐 + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white, Colors.grey.shade50], ), + ), + child: Padding( + padding: EdgeInsets.all(20), child: Column( + crossAxisAlignment: CrossAxisAlignment.center, 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), - ), + // 标题 - 居中 + Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '目录', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + SizedBox(height: 16), + + // 单行标签 - 居中,更小的尺寸 + Container( + width: double.infinity, + child: Wrap( + spacing: 6, + runSpacing: 6, + alignment: WrapAlignment.center, + children: labels.map((label) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: label.color.withAlpha(30), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: label.color.withAlpha(20), + width: 1, + ), ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => setState(() => _showToc = false), - ), - ], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(label.icon, size: 12, color: label.color), + SizedBox(width: 4), + Text( + label.text, + style: TextStyle( + fontSize: 10, + color: label.color, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + }).toList(), ), ), - Expanded( - child: TocWidget( - controller: tocController, + + SizedBox(height: 16), + + // 分隔线 + Container( + height: 1, + width: 80, + color: Colors.blue.shade200, + ), + + SizedBox(height: 16), + + // 描述 - 放在下面,大约100字 + Container( + width: double.infinity, + child: Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + height: 1.6, + ), + textAlign: TextAlign.justify, + maxLines: 4, + overflow: TextOverflow.ellipsis, ), ), ], ), ), ), - ), - ); - - Widget buildMarkdown() => MarkdownWidget( - data: markdownData, - tocController: tocController, - ); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('文档'), - ), - body: Stack( - children: [ - // 主内容 - buildMarkdown(), - - // 悬浮TOC面板 - _buildTocPanel(), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () => setState(() => _showToc = !_showToc), - child: Icon(_showToc ? Icons.close : Icons.list), - mini: true, - backgroundColor: _showToc ? Colors.grey : Theme.of(context).primaryColor, - ), ); } } \ No newline at end of file diff --git a/lib/pages/about_page.dart b/lib/pages/about_page.dart new file mode 100644 index 0000000..6a97f58 --- /dev/null +++ b/lib/pages/about_page.dart @@ -0,0 +1,62 @@ +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)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..e862e53 --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,328 @@ +import 'package:blog_app/pages/about_page.dart'; +import 'package:blog_app/pages/message_page.dart'; +import 'package:blog_app/pages/profile_page.dart'; +import 'package:blog_app/pages/settings_page.dart'; +import 'package:flutter/material.dart'; + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _currentPageIndex = 0; + late PageController _pageController; + + // 页面标题列表 + final List _pageTitles = [ + '个人资料', + '消息中心', + '设置', + '关于我们', + ]; + + // 页面图标列表 + final List _pageIcons = [ + Icons.home, + Icons.person, + Icons.message, + Icons.settings, + Icons.info, + ]; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: _currentPageIndex); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_pageTitles[_currentPageIndex]), + backgroundColor: Colors.blue, + elevation: 0, + leading: Builder( + builder: (context) => IconButton( + icon: Icon(Icons.menu, color: Colors.white), + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), + actions: [ + IconButton( + icon: Icon(Icons.search, color: Colors.white), + onPressed: () {}, + ), + ], + ), + drawer: _buildDrawer(), + body: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentPageIndex = index; + }); + }, + children: [ + ProfilePage(), + MessagePage(), + SettingsPage(), + AboutPage(), + ], + ), + ); + } + + Widget _buildDrawer() { + return Drawer( + child: Container( + color: Colors.white, + child: Column( + children: [ + // 抽屉头部 + _buildDrawerHeader(), + + // 菜单项列表 + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ...List.generate(_pageTitles.length, (index) => + _buildDrawerItem( + icon: _pageIcons[index], + title: _pageTitles[index], + index: index, + ) + ), + ], + ), + ), + ], + ), + ), + ); + } + + 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; + } + } +} + +// 各个页面组件 +class HomeContentPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 欢迎卡片 + Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: EdgeInsets.all(20), + child: Row( + children: [ + Icon(Icons.waving_hand, size: 40, color: Colors.amber), + 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: 20), + + // 功能网格 + GridView.count( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + _buildFeatureCard('数据统计', Icons.analytics, Colors.blue), + _buildFeatureCard('文件管理', Icons.folder, Colors.green), + _buildFeatureCard('消息通知', Icons.notifications, Colors.orange), + _buildFeatureCard('系统设置', Icons.settings, Colors.purple), + ], + ), + ], + ), + ); + } + + Widget _buildFeatureCard(String title, IconData icon, Color color) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () {}, + child: Container( + padding: EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 40, color: color), + SizedBox(height: 8), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/markdown_page.dart b/lib/pages/markdown_page.dart new file mode 100644 index 0000000..9a539b8 --- /dev/null +++ b/lib/pages/markdown_page.dart @@ -0,0 +1,122 @@ +import 'package:blog_app/utils/http_util.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown_widget/config/toc.dart'; +import 'package:markdown_widget/widget/markdown.dart'; + +class MarkdownPage extends StatefulWidget { + const MarkdownPage({super.key}); + + @override + State createState() => _MarkdownPageState(); +} + +class _MarkdownPageState extends State { + final tocController = TocController(); + bool _showToc = false; + late String markdownData = ''; + + Widget _buildTocPanel() => AnimatedOpacity( + opacity: _showToc ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: Visibility( + visible: _showToc, + child: Align( + alignment: Alignment.bottomRight, + child: Container( + width: 250, + height: 400, + margin: const EdgeInsets.only(bottom: 60, right: 60), + // 调整位置在按钮左侧 + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + children: [ + // 标题栏 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '目录', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => setState(() => _showToc = false), + ), + ], + ), + ), + Expanded(child: TocWidget(controller: tocController)), + ], + ), + ), + ), + ), + ); + + Widget buildMarkdown() => + MarkdownWidget(data: markdownData, tocController: tocController); + + @override + void initState() { + super.initState(); + // 初始加载数据 + _loadData(); + } + + Future _loadData() async { + final result = await HttpUtil().get("/condition"); + + setState(() { + markdownData = result[0]['content']; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('文档')), + body: Padding( + padding: EdgeInsets.all(16), + child: Stack( + children: [ + // 主内容 + buildMarkdown(), + + // 悬浮TOC面板 + _buildTocPanel(), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => setState(() => _showToc = !_showToc), + child: Icon(_showToc ? Icons.close : Icons.list), + mini: true, + backgroundColor: + _showToc ? Colors.grey : Theme.of(context).primaryColor, + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/message_page.dart b/lib/pages/message_page.dart new file mode 100644 index 0000000..60c2217 --- /dev/null +++ b/lib/pages/message_page.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class MessagePage extends StatelessWidget { + final List> 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: () {}, + ), + ); + }, + ); + } +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart new file mode 100644 index 0000000..d26f1ba --- /dev/null +++ b/lib/pages/profile_page.dart @@ -0,0 +1,78 @@ +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), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..51f0ad6 --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,71 @@ +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 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: () {}, + ); + } +} diff --git a/lib/test.dart b/lib/test.dart deleted file mode 100644 index e7a0589..0000000 --- a/lib/test.dart +++ /dev/null @@ -1,109 +0,0 @@ -final String markdownData = ''' - # 一、简介 -Spring Boot Starter是一组预定义的依赖项集合,旨在简化Maven或Gradle等构建工具中的依赖管理。每个Starter都包含了实现特定功能所需的库和组件,以及相应的配置文件。开发者只需在项目中引入相应的Starter依赖,即可快速搭建起具备该功能的项目骨架。 - -Starter=依赖+自动配置+配置文件 - -# 二、实现原理 -## 2.1 传统实现 -例如引入Spring中的jpa,则需要以下步骤: - -1. 在Maven中引入数据库依赖 -2. 在Maven中引入jpa依赖 -3. 在配置文件中配置属性 -4. 调试程序 - -每次新建项目都需要重复此流程,操作繁琐。 - -## 2.2 自定义Starter实现 -1. 新建Maven项目,在pom.xml文件中定义需要的依赖项。 -2. 创建自动配置类 AutoConfigurationTest,添加@configuration注解,使其能够被SpringBoot自动扫描到。 -3. 添加自动装配机制,在src/main/resources/META-INF文件夹下创建spring.factories文件,添加以下配置: - -```bash -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.example.springbootstartercustom.AutoConfigurationTest -``` - -注:这里文件夹和文件名一定要正确,因此SpringFactoriesLoader中就是这么定义的。 - -4. 在配置文件中自定义属性(可选)。 -5. 安装打包到maven仓库中。 -6. 其他项目通过pom.xml文件引入该starter。 - -## 2.3 Starter实现原理 -加载依赖->扫描自动配置类->加载配置文件 - -# 三、高级特性 -## 3.1 可插拔Starter -所谓可插拔就是可以自行决定是否需要加载该starter的功能。例如可以通过注解的方式决定是否加载。 - -1. 定义注解 - -```java -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface EnableAutoConfigTest { - -} -``` - -2. 在自动配置类 AutoConfiguration中增加条件注解 - -```java -@Configuration -@ConditionalOnBean(annotation = EnableAutoConfigTest.class) -public class AutoConfigurationTest { -} -``` - -3. 在相应位置添加@EnableAutoConfigTest注解该stater才会生效。 - -## 3.2 自定义配置文件 -所谓自定义配置文件,就是可以在引入stater后,可以通过修改配置文件覆盖原来的配置属性,从而灵活配置stater功能。 - -1. 引用spring-boot-configuration-processor - -```xml - - org.springframework.boot - spring-boot-configuration-processor - true - -``` - -2. 定义Properties配置类 - -```java -@ConfigurationProperties(prefix = "test") -public class TestProperties { - private String name = "test"; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - -3. 在自动配置类 AutoConfigurationTest中引用 - -```java -@Configuration(proxyBeanMethods = false) -@Import({TestProperties.class}) -public class AutoConfigurationTest { - @Resource - private TestProperties testProperties; -} -``` - -4. 在引入的stater工程中,修改配置文件: - -```yaml -test: - name: test1 -``` - '''; \ No newline at end of file diff --git a/lib/utils/http_util.dart b/lib/utils/http_util.dart new file mode 100644 index 0000000..0ce3afc --- /dev/null +++ b/lib/utils/http_util.dart @@ -0,0 +1,244 @@ +import 'package:blog_app/config/app_config.dart'; +import 'package:blog_app/utils/sp_util.dart'; +import 'package:dio/dio.dart'; + +import 'log_util.dart'; + +class HttpUtil { + static final HttpUtil _instance = HttpUtil._internal(); + + factory HttpUtil() => _instance; + + late Dio _dio; + + // 请求头配置 + Map headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Accept': 'application/json', + }; + + // 超时时间 + final int timeout = 5; + + HttpUtil._internal() { + // 初始化Dio实例 + BaseOptions options = BaseOptions( + baseUrl: AppConfig.baseApiUrl, + connectTimeout: Duration(seconds: timeout), + receiveTimeout: Duration(seconds: timeout), + headers: headers, + ); + + _dio = Dio(options); + + // 添加请求拦截器 + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + logger.d("请求URL: ${options.uri}"); + if (options.data != null) { + logger.d("请求参数: ${options.data}"); + } + + if (SPUtil.getString('token').isNotEmpty) { + options.headers['satoken'] = SPUtil.getString('token'); + } + return handler.next(options); + }, + ), + ); + + // 添加响应拦截器 + _dio.interceptors.add( + InterceptorsWrapper( + onResponse: (response, handler) { + logger.d("响应状态码: ${response.statusCode}"); + // logger.d("响应数据: ${response.data}"); + return handler.next(response); + }, + ), + ); + + // 添加错误拦截器 + _dio.interceptors.add( + InterceptorsWrapper( + onError: (DioException e, handler) { + _handleError(e); + return handler.next(e); + }, + ), + ); + } + + // 基础请求方法(处理所有类型的请求) + Future _request( + String path, { + required String method, + dynamic data, + Map? queryParameters, + T Function(dynamic data)? converter, + }) async { + try { + Response response = await _dio.request( + path, + data: data, + queryParameters: queryParameters, + options: Options(method: method), + ); + + // 处理响应数据 + if (converter != null) { + return converter(response.data); + } + + // 没有转换器时尝试直接返回(可能不安全,建议提供转换器) + return response.data; + } catch (e) { + rethrow; + } + } + + // GET请求 + Future get( + String path, { + Map? queryParameters, + T Function(dynamic data)? converter, + }) => _request( + path, + method: "GET", + queryParameters: queryParameters, + converter: converter, + ); + + // POST请求 + Future post( + String path, { + dynamic data, + Map? queryParameters, + T Function(dynamic data)? converter, + }) => _request( + path, + method: "POST", + data: data, + queryParameters: queryParameters, + converter: converter, + ); + + // PUT请求 + Future put( + String path, { + dynamic data, + Map? queryParameters, + T Function(dynamic data)? converter, + }) => _request( + path, + method: "PUT", + data: data, + queryParameters: queryParameters, + converter: converter, + ); + + // DELETE请求 + Future delete( + String path, { + dynamic data, + Map? queryParameters, + T Function(dynamic data)? converter, + }) => _request( + path, + method: "DELETE", + data: data, + queryParameters: queryParameters, + converter: converter, + ); + + // 文件上传 + Future upload( + String path, + String filePath, { + Map? queryParameters, + }) async { + try { + FormData formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(filePath), + }); + + Response response = await _dio.post( + path, + data: formData, + queryParameters: queryParameters, + ); + return response.data; + } catch (e) { + _handleError(e); + rethrow; + } + } + + // 错误处理 + void _handleError(dynamic error) { + String errorMessage = '未知错误'; + if (error is DioException) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + errorMessage = '连接超时,请检查网络连接'; + break; + case DioExceptionType.sendTimeout: + errorMessage = '发送超时,请检查网络连接'; + break; + case DioExceptionType.receiveTimeout: + errorMessage = '接收超时,请检查网络连接'; + break; + case DioExceptionType.cancel: + errorMessage = '请求已取消'; + break; + case DioExceptionType.badCertificate: + errorMessage = '证书验证失败'; + break; + case DioExceptionType.badResponse: + // 处理HTTP响应错误(4xx, 5xx) + final statusCode = error.response?.statusCode ?? 0; + final responseData = error.response?.data; + + if (responseData is Map && responseData.containsKey('message')) { + // 服务器返回了自定义错误消息 + errorMessage = responseData['message']?.toString() ?? '未知错误'; + } else { + errorMessage = _getHttpErrorMessage(statusCode); + } + break; + case DioExceptionType.connectionError: + errorMessage = '网络连接错误,请检查网络设置'; + break; + case DioExceptionType.unknown: + if (error.error != null) { + errorMessage = '未知错误: ${error.error.toString()}'; + } + errorMessage = '发生未知错误'; + break; + } + } else { + errorMessage = '非Dio错误: $error'; + } + + print(errorMessage); + // showErrorToast(errorMessage); + } + + String _getHttpErrorMessage(int statusCode) { + // 根据HTTP状态码返回对应的错误消息 + switch (statusCode) { + case 400: return '错误请求,请检查参数'; + case 401: return '未授权,请登录'; + case 403: return '禁止访问,权限不足'; + case 404: return '资源不存在'; + case 405: return '方法不允许'; + case 408: return '请求超时'; + case 500: return '服务器内部错误'; + case 502: return '网关错误'; + case 503: return '服务不可用'; + case 504: return '网关超时'; + default: return 'HTTP错误: $statusCode'; + } + } +} diff --git a/lib/utils/log_util.dart b/lib/utils/log_util.dart new file mode 100644 index 0000000..f571317 --- /dev/null +++ b/lib/utils/log_util.dart @@ -0,0 +1,12 @@ +import 'package:logger/logger.dart'; + +// 全局日志实例,在整个项目中共享 +final Logger logger = Logger( + printer: PrettyPrinter( + methodCount: 1, + colors: true, + dateTimeFormat: DateTimeFormat.dateAndTime, + ), + // 可选:配置输出到文件(需配合文件操作库) + // output: FileOutput(file: File('logs/app.log')), +); \ No newline at end of file diff --git a/lib/utils/sp_util.dart b/lib/utils/sp_util.dart new file mode 100644 index 0000000..b255633 --- /dev/null +++ b/lib/utils/sp_util.dart @@ -0,0 +1,61 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SPUtil { + static late SharedPreferences _prefs; + + /// 初始化 + static Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + /// 存储数据 + static Future set(String key, dynamic value) { + if (value is String) return _prefs.setString(key, value); + if (value is int) return _prefs.setInt(key, value); + if (value is bool) return _prefs.setBool(key, value); + if (value is double) return _prefs.setDouble(key, value); + if (value is List) return _prefs.setStringList(key, value); + throw ArgumentError('Unsupported value type: ${value.runtimeType}'); + } + + /// 获取数据 + static dynamic get(String key, [dynamic defaultValue]) { + if (!_prefs.containsKey(key)) return defaultValue; + return _prefs.get(key) ?? defaultValue; + } + + /// 获取字符串 + static String getString(String key, [String defaultValue = '']) { + return _prefs.getString(key) ?? defaultValue; + } + + /// 获取布尔值 + static bool getBool(String key, [bool defaultValue = false]) { + return _prefs.getBool(key) ?? defaultValue; + } + + /// 获取整数 + static int getInt(String key, [int defaultValue = 0]) { + return _prefs.getInt(key) ?? defaultValue; + } + + /// 获取浮点数 + static double getDouble(String key, [double defaultValue = 0.0]) { + return _prefs.getDouble(key) ?? defaultValue; + } + + /// 获取字符串列表 + static List getStringList(String key, [List defaultValue = const []]) { + return _prefs.getStringList(key) ?? defaultValue; + } + + /// 删除数据 + static Future remove(String key) { + return _prefs.remove(key); + } + + /// 清空所有数据 + static Future clear() { + return _prefs.clear(); + } +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8236f57..997e35d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 475e71d..8712106 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -65,6 +81,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -104,6 +136,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -136,6 +176,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.2" markdown: dependency: transitive description: @@ -176,6 +224,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -184,6 +240,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -200,6 +288,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -253,6 +397,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" url_launcher: dependency: transitive description: @@ -349,6 +501,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" sdks: dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index bcb83c2..3a9d993 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,9 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 markdown_widget: ^2.3.2+8 + dio: ^5.7.0 + shared_preferences: ^2.3.0 + logger: ^2.6.0 dev_dependencies: flutter_test: @@ -50,41 +53,9 @@ dev_dependencies: # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + fonts: + - family: CustomFont + fonts: + - asset: fonts/custom.ttf diff --git a/test/widget_test.dart b/test/widget_test.dart index f3b85eb..76d41d9 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -11,20 +11,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:blog_app/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); }