feat:增加菜单栏模块
This commit is contained in:
BIN
fonts/custom.ttf
Normal file
BIN
fonts/custom.ttf
Normal file
Binary file not shown.
8
lib/config/app_config.dart
Normal file
8
lib/config/app_config.dart
Normal file
@@ -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";
|
||||||
|
}
|
||||||
254
lib/main.dart
254
lib/main.dart
@@ -1,130 +1,200 @@
|
|||||||
import 'package:blog_app/test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:markdown_widget/markdown_widget.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
appBar: AppBar(
|
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 {
|
class BlogLabel {
|
||||||
const MarkdownWithToc({super.key});
|
final String text;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
@override
|
BlogLabel(this.text, this.icon, this.color);
|
||||||
State<MarkdownWithToc> createState() => _MarkdownWithTocState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MarkdownWithTocState extends State<MarkdownWithToc> {
|
class BlogCard extends StatelessWidget {
|
||||||
final tocController = TocController();
|
final String title;
|
||||||
bool _showToc = false;
|
final String description;
|
||||||
|
final List<BlogLabel> labels;
|
||||||
|
|
||||||
// ... markdownData 同上
|
const BlogCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.labels,
|
||||||
|
});
|
||||||
|
|
||||||
Widget _buildTocPanel() => AnimatedOpacity(
|
@override
|
||||||
opacity: _showToc ? 1.0 : 0.0,
|
Widget build(BuildContext context) {
|
||||||
duration: const Duration(milliseconds: 300),
|
return Card(
|
||||||
child: Visibility(
|
elevation: 4,
|
||||||
visible: _showToc,
|
shape: RoundedRectangleBorder(
|
||||||
child: Align(
|
borderRadius: BorderRadius.circular(16),
|
||||||
alignment: Alignment.bottomRight,
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 250,
|
width: double.infinity, // 确保宽度对齐
|
||||||
height: 400,
|
decoration: BoxDecoration(
|
||||||
margin: const EdgeInsets.only(bottom: 60, right: 60), // 调整位置在按钮左侧
|
borderRadius: BorderRadius.circular(16),
|
||||||
decoration: BoxDecoration(
|
gradient: LinearGradient(
|
||||||
color: Colors.white,
|
begin: Alignment.topLeft,
|
||||||
borderRadius: BorderRadius.circular(12),
|
end: Alignment.bottomRight,
|
||||||
boxShadow: [
|
colors: [Colors.white, Colors.grey.shade50],
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// 标题栏
|
// 标题 - 居中
|
||||||
Container(
|
Text(
|
||||||
padding: const EdgeInsets.all(12),
|
title,
|
||||||
decoration: BoxDecoration(
|
style: TextStyle(
|
||||||
color: Colors.grey[100],
|
fontSize: 20,
|
||||||
borderRadius: const BorderRadius.only(
|
fontWeight: FontWeight.bold,
|
||||||
topLeft: Radius.circular(12),
|
color: Colors.blue.shade800,
|
||||||
topRight: Radius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
textAlign: TextAlign.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
maxLines: 2,
|
||||||
children: [
|
overflow: TextOverflow.ellipsis,
|
||||||
const Text(
|
),
|
||||||
'目录',
|
|
||||||
style: TextStyle(
|
SizedBox(height: 16),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
IconButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
icon: const Icon(Icons.close, size: 20),
|
children: [
|
||||||
onPressed: () => setState(() => _showToc = false),
|
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(
|
SizedBox(height: 16),
|
||||||
controller: tocController,
|
|
||||||
|
// 分隔线
|
||||||
|
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
62
lib/pages/about_page.dart
Normal file
62
lib/pages/about_page.dart
Normal file
@@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
328
lib/pages/home_page.dart
Normal file
328
lib/pages/home_page.dart
Normal file
@@ -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<HomePage> {
|
||||||
|
int _currentPageIndex = 0;
|
||||||
|
late PageController _pageController;
|
||||||
|
|
||||||
|
// 页面标题列表
|
||||||
|
final List<String> _pageTitles = [
|
||||||
|
'个人资料',
|
||||||
|
'消息中心',
|
||||||
|
'设置',
|
||||||
|
'关于我们',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 页面图标列表
|
||||||
|
final List<IconData> _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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
lib/pages/markdown_page.dart
Normal file
122
lib/pages/markdown_page.dart
Normal file
@@ -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<MarkdownPage> createState() => _MarkdownPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkdownPageState extends State<MarkdownPage> {
|
||||||
|
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<void> _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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/pages/message_page.dart
Normal file
59
lib/pages/message_page.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MessagePage extends StatelessWidget {
|
||||||
|
final List<Map<String, dynamic>> messages = [
|
||||||
|
{'title': '系统通知', 'content': '您的账号安全等级已提升', 'time': '10:30', 'unread': false},
|
||||||
|
{'title': '活动提醒', 'content': '新活动即将开始,敬请期待', 'time': '昨天', 'unread': true},
|
||||||
|
{'title': '版本更新', 'content': '新版本v2.1.0已发布', 'time': '2024-01-20', 'unread': false},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: messages.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = messages[index];
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: message['unread'] ? Colors.blue : Colors.grey,
|
||||||
|
child: Icon(
|
||||||
|
Icons.notifications,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
message['title'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: message['unread'] ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(message['content']),
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message['time'],
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
),
|
||||||
|
if (message['unread'])
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 4),
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/pages/profile_page.dart
Normal file
78
lib/pages/profile_page.dart
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/pages/settings_page.dart
Normal file
71
lib/pages/settings_page.dart
Normal file
@@ -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<Widget> children) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingsItem(String title, IconData icon, {bool hasSwitch = false}) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: Colors.blue),
|
||||||
|
title: Text(title),
|
||||||
|
trailing: hasSwitch
|
||||||
|
? Switch(value: true, onChanged: (value) {})
|
||||||
|
: Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/test.dart
109
lib/test.dart
@@ -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
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
''';
|
|
||||||
244
lib/utils/http_util.dart
Normal file
244
lib/utils/http_util.dart
Normal file
@@ -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<String, dynamic> 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<T> _request<T>(
|
||||||
|
String path, {
|
||||||
|
required String method,
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? 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<T> get<T>(
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
T Function(dynamic data)? converter,
|
||||||
|
}) => _request(
|
||||||
|
path,
|
||||||
|
method: "GET",
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
converter: converter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST请求
|
||||||
|
Future<T> post<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
T Function(dynamic data)? converter,
|
||||||
|
}) => _request(
|
||||||
|
path,
|
||||||
|
method: "POST",
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
converter: converter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT请求
|
||||||
|
Future<T> put<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
T Function(dynamic data)? converter,
|
||||||
|
}) => _request(
|
||||||
|
path,
|
||||||
|
method: "PUT",
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
converter: converter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE请求
|
||||||
|
Future<T> delete<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
T Function(dynamic data)? converter,
|
||||||
|
}) => _request(
|
||||||
|
path,
|
||||||
|
method: "DELETE",
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
converter: converter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
Future<dynamic> upload(
|
||||||
|
String path,
|
||||||
|
String filePath, {
|
||||||
|
Map<String, dynamic>? 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<String, dynamic> && 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/utils/log_util.dart
Normal file
12
lib/utils/log_util.dart
Normal file
@@ -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')),
|
||||||
|
);
|
||||||
61
lib/utils/sp_util.dart
Normal file
61
lib/utils/sp_util.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class SPUtil {
|
||||||
|
static late SharedPreferences _prefs;
|
||||||
|
|
||||||
|
/// 初始化
|
||||||
|
static Future<void> init() async {
|
||||||
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 存储数据
|
||||||
|
static Future<bool> 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<String>) 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<String> getStringList(String key, [List<String> defaultValue = const []]) {
|
||||||
|
return _prefs.getStringList(key) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除数据
|
||||||
|
static Future<bool> remove(String key) {
|
||||||
|
return _prefs.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清空所有数据
|
||||||
|
static Future<bool> clear() {
|
||||||
|
return _prefs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
160
pubspec.lock
160
pubspec.lock
@@ -57,6 +57,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +81,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -104,6 +136,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -136,6 +176,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
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:
|
markdown:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -176,6 +224,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -184,6 +240,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -200,6 +288,62 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -253,6 +397,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
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:
|
url_launcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -349,6 +501,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
43
pubspec.yaml
43
pubspec.yaml
@@ -35,6 +35,9 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
markdown_widget: ^2.3.2+8
|
markdown_widget: ^2.3.2+8
|
||||||
|
dio: ^5.7.0
|
||||||
|
shared_preferences: ^2.3.0
|
||||||
|
logger: ^2.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -50,41 +53,9 @@ dev_dependencies:
|
|||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
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
|
uses-material-design: true
|
||||||
|
fonts:
|
||||||
# To add assets to your application, add an assets section, like this:
|
- family: CustomFont
|
||||||
# assets:
|
fonts:
|
||||||
# - images/a_dot_burr.jpeg
|
- asset: fonts/custom.ttf
|
||||||
# - 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
|
|
||||||
|
|||||||
@@ -11,20 +11,5 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:blog_app/main.dart';
|
import 'package:blog_app/main.dart';
|
||||||
|
|
||||||
void main() {
|
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user