Compare commits

...

9 Commits

Author SHA1 Message Date
2a10aae25e feat:更新统计卡片 2025-11-23 20:38:57 +08:00
f11b8d6a30 feat:增加common框架 2025-11-23 20:28:45 +08:00
e38e26e685 factor:布局颜色重构 2025-11-21 14:11:35 +08:00
8f0f641d37 factor:布局颜色重构 2025-11-20 13:37:56 +08:00
50d7657a93 fix:修复下拉加载会跳转至顶部的问题 2025-11-19 22:46:27 +08:00
59b439a8ac feat:增加应用图标 2025-11-17 10:18:32 +08:00
1132b78ecd feat:增加底部标题栏 2025-11-16 22:10:53 +08:00
61922cbcd8 feat:增加博客搜索功能 2025-11-16 21:56:26 +08:00
47b6fb9937 feat:增加加载中显示 2025-11-16 00:09:39 +08:00
51 changed files with 1395 additions and 1835 deletions

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="blog_app" android:label="拾光记"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,13 +1,14 @@
import 'package:blog_app/config/app_config.dart';
import 'package:blog_app/models/blog.dart'; import 'package:blog_app/models/blog.dart';
import 'package:blog_app/models/common.dart'; import 'package:flutter_common/models/common_model.dart';
import 'package:blog_app/utils/http_utils.dart'; import 'package:flutter_common/utils/convert_utils.dart';
import 'package:blog_app/utils/index.dart'; import 'package:flutter_common/utils/http_utils.dart';
Future<PageResult<Blog>> queryBlogByPageApi(int currentPage, int pageSize) { Future<PageResult<Blog>> queryBlogByPageApi(int currentPage, int pageSize) {
return HttpUtil().get<PageResult<Blog>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<PageResult<Blog>>(
"/blog/page", "/blog/page",
queryParameters: {"currentPage": currentPage, "pageSize": pageSize}, queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
converter: (data) => convertPageResponse(data, Blog.fromJson), converter: (data) => convertPage(data, Blog.fromJson),
); );
} }
@@ -28,74 +29,74 @@ Future<List<Blog>> queryBlogByConditionApi(
queryParams['year'] = year; queryParams['year'] = year;
} }
return HttpUtil().get<List<Blog>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<Blog>>(
"/blog/condition", "/blog/condition",
queryParameters: queryParams, queryParameters: queryParams,
converter: (data) => convertListResponse<Blog>(data, Blog.fromJson), converter: (data) => convertList<Blog>(data, Blog.fromJson),
);
}
Future<List<BlogSearch>> searchBlog(String keyword) {
return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<BlogSearch>>(
"/blog/search",
queryParameters: {"keyword": keyword},
converter: (data) => convertList<BlogSearch>(data, BlogSearch.fromJson),
); );
} }
Future<Blog> queryBlogByIdApi(int id) { Future<Blog> queryBlogByIdApi(int id) {
return HttpUtil().get<Blog>( return HttpUtil(
"/blog/$id/content", baseUrl: AppConfig.baseApiUrl,
converter: (data) => Blog.fromJson(data), ).get<Blog>("/blog/$id/content", converter: (data) => Blog.fromJson(data));
);
} }
Future<List<BlogCategory>> queryBlogCategoryApi() { Future<List<BlogCategory>> queryBlogCategoryApi() {
return HttpUtil().get<List<BlogCategory>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<BlogCategory>>(
"/blog/category", "/blog/category",
converter: converter: (data) => convertList<BlogCategory>(data, BlogCategory.fromJson),
(data) =>
convertListResponse<BlogCategory>(data, BlogCategory.fromJson),
); );
} }
Future<BlogStats> queryBlogOverviewStatsApi() { Future<BlogStats> queryBlogOverviewStatsApi() {
return HttpUtil().get<BlogStats>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<BlogStats>(
"/stats/overview", "/stats/overview",
converter: (data) => BlogStats.fromJson(data), converter: (data) => BlogStats.fromJson(data),
); );
} }
Future<List<ChartData>> queryBlogApprovedStatsApi(int year) { Future<List<ChartData>> queryBlogApprovedStatsApi(int year) {
return HttpUtil().get<List<ChartData>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<ChartData>>(
"/stats/approved/monthly", "/stats/approved/monthly",
queryParameters: {"year": year}, queryParameters: {"year": year},
converter: converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
(data) => convertListResponse<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryBlogVisitStatsApi(int year) { Future<List<ChartData>> queryBlogVisitStatsApi(int year) {
return HttpUtil().get<List<ChartData>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<ChartData>>(
"/stats/visit/monthly", "/stats/visit/monthly",
queryParameters: {"year": year}, queryParameters: {"year": year},
converter: converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
(data) => convertListResponse<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryBlogVisitRankStatsApi() { Future<List<ChartData>> queryBlogVisitRankStatsApi() {
return HttpUtil().get<List<ChartData>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<ChartData>>(
"/stats/visit/rank", "/stats/visit/rank",
converter: converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
(data) => convertListResponse<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryBlogCategoryStatsApi() { Future<List<ChartData>> queryBlogCategoryStatsApi() {
return HttpUtil().get<List<ChartData>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<ChartData>>(
"/stats/category", "/stats/category",
converter: converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
(data) => convertListResponse<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryBlogReadRankStatsApi() { Future<List<ChartData>> queryBlogReadRankStatsApi() {
return HttpUtil().get<List<ChartData>>( return HttpUtil(baseUrl: AppConfig.baseApiUrl).get<List<ChartData>>(
"/stats/read/rank", "/stats/read/rank",
converter: converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
(data) => convertListResponse<ChartData>(data, ChartData.fromJson),
); );
} }

View File

@@ -1,5 +1,5 @@
/// 应用信息 /// 应用信息
class AppConfig { class AppConfig {
// static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api"; static const String baseApiUrl = "https://cxx0822.iepose.cn/blog-api";
static const String baseApiUrl = "http://192.168.1.4:8082"; // static const String baseApiUrl = "http://192.168.1.4:8082";
} }

121
lib/layout/app_drawer.dart Normal file
View File

@@ -0,0 +1,121 @@
import 'package:blog_app/layout/menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/layout/theme_layout.dart';
class AppDrawer extends StatefulWidget {
final int currentPageIndex;
final Function(int) onTapDrawerItem;
const AppDrawer({
super.key,
required this.currentPageIndex,
required this.onTapDrawerItem,
});
@override
State<AppDrawer> createState() => AppDrawerState();
}
class AppDrawerState extends State<AppDrawer> {
Widget _buildDrawerHeader() {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colors.primary, colors.inversePrimary],
),
),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipOval(
child: Image.asset(
'assets/images/avatar.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
SizedBox(height: 16),
Text(
'Cxx0822',
style: TextStyle(
color: colors.surface,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget _buildDrawerBody() {
return ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: pages.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) => _buildDrawerItem(index),
);
}
Widget _buildDrawerItem(int index) {
final colors = Theme.of(context).colorScheme;
final isSelected = index == widget.currentPageIndex;
final boxColor =
isSelected ? colors.primary.withAlpha(50) : Colors.transparent;
final iconColor =
isSelected ? colors.primary : colors.onSurface.withAlpha(100);
final textColor =
isSelected ? colors.primary : colors.onSurface.withAlpha(200);
return Container(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: boxColor,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(pages[index].icon, color: iconColor, size: 24),
title: Text(
pages[index].title,
style: TextStyle(
color: textColor,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 16,
),
),
trailing:
isSelected
? Icon(Icons.arrow_forward_ios, size: 16, color: iconColor)
: null,
onTap: () => widget.onTapDrawerItem(index),
),
);
}
@override
Widget build(BuildContext context) {
return Drawer(
child: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
_buildDrawerHeader(),
_buildDrawerBody(),
const Divider(),
ThemeLayout(),
],
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:blog_app/layout/menu.dart';
import 'package:flutter/material.dart';
class AppNavbar extends StatefulWidget {
final int currentPageIndex;
final Function(int) onTapNavbarItem;
const AppNavbar({
super.key,
required this.currentPageIndex,
required this.onTapNavbarItem,
});
@override
State<AppNavbar> createState() => AppNavbarState();
}
class AppNavbarState extends State<AppNavbar> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return BottomNavigationBar(
currentIndex: widget.currentPageIndex,
onTap: widget.onTapNavbarItem,
items: bottomNavItems,
type: BottomNavigationBarType.fixed,
selectedItemColor: colors.primary,
unselectedItemColor: colors.onSurface.withAlpha(150),
showSelectedLabels: true,
showUnselectedLabels: true,
backgroundColor: colors.surface,
elevation: 8,
);
}
}

View File

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

View File

@@ -20,3 +20,8 @@ final List<PageInfo> pages = [
]; ];
final List<Widget> pageWidgets = pages.map((item) => item.page).toList(); final List<Widget> pageWidgets = pages.map((item) => item.page).toList();
final List<BottomNavigationBarItem> bottomNavItems =
pages.map((page) {
return BottomNavigationBarItem(icon: Icon(page.icon), label: page.title);
}).toList();

View File

@@ -1,11 +1,23 @@
import 'package:blog_app/pages/home_page.dart'; import 'package:blog_app/pages/home_page.dart';
import 'package:blog_app/utils/sp_utils.dart'; import 'package:blog_app/provider/blog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/provider/theme_provider.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:provider/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SPUtil.init(); await SPUtil.init();
runApp(MyApp());
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => BlogProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
],
child: MyApp(),
),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@@ -13,13 +25,12 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return MaterialApp( return MaterialApp(
home: HomePage(), home: HomePage(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: themeProvider.currentThemeData,
scaffoldBackgroundColor: Color(0xFFF5F5F5),
fontFamily: 'CustomFont',
),
); );
} }
} }

View File

@@ -107,3 +107,30 @@ class BlogStats {
Map<String, dynamic> toJson() => _$BlogStatsToJson(this); Map<String, dynamic> toJson() => _$BlogStatsToJson(this);
} }
@JsonSerializable(genericArgumentFactories: true)
class BlogSearch {
@JsonKey(name: 'id')
final int id;
@JsonKey(name: 'title')
final String title;
@JsonKey(name: 'content')
final String content;
@JsonKey(name: 'highlight')
final String highlight;
const BlogSearch({
required this.id,
required this.title,
required this.content,
required this.highlight,
});
factory BlogSearch.fromJson(Map<String, dynamic> json) =>
_$BlogSearchFromJson(json);
Map<String, dynamic> toJson() => _$BlogSearchToJson(this);
}

View File

@@ -59,3 +59,18 @@ Map<String, dynamic> _$BlogStatsToJson(BlogStats instance) => <String, dynamic>{
'visitCount': instance.visitCount, 'visitCount': instance.visitCount,
'wordCount': instance.wordCount, 'wordCount': instance.wordCount,
}; };
BlogSearch _$BlogSearchFromJson(Map<String, dynamic> json) => BlogSearch(
id: (json['id'] as num).toInt(),
title: json['title'] as String,
content: json['content'] as String,
highlight: json['highlight'] as String,
);
Map<String, dynamic> _$BlogSearchToJson(BlogSearch instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'content': instance.content,
'highlight': instance.highlight,
};

View File

@@ -1,53 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'common.g.dart';
@JsonSerializable(genericArgumentFactories: true)
class PageResult<T> {
@JsonKey(name: 'records')
final List<T> records;
@JsonKey(name: 'total')
final int total;
@JsonKey(name: 'size')
final int size;
@JsonKey(name: 'current')
final int current;
@JsonKey(name: 'pages')
final int pages;
const PageResult({
required this.records,
required this.total,
required this.size,
required this.current,
required this.pages,
});
factory PageResult.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => _$PageResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$PageResultToJson(this, toJsonT);
}
@JsonSerializable(genericArgumentFactories: true)
class ChartData {
@JsonKey(name: 'name')
final String name;
@JsonKey(name: 'value')
final double value;
const ChartData({required this.name, required this.value});
factory ChartData.fromJson(Map<String, dynamic> json) =>
_$ChartDataFromJson(json);
Map<String, dynamic> toJson() => _$ChartDataToJson(this);
}

View File

@@ -1,39 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'common.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PageResult<T> _$PageResultFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => PageResult<T>(
records: (json['records'] as List<dynamic>).map(fromJsonT).toList(),
total: (json['total'] as num).toInt(),
size: (json['size'] as num).toInt(),
current: (json['current'] as num).toInt(),
pages: (json['pages'] as num).toInt(),
);
Map<String, dynamic> _$PageResultToJson<T>(
PageResult<T> instance,
Object? Function(T value) toJsonT,
) => <String, dynamic>{
'records': instance.records.map(toJsonT).toList(),
'total': instance.total,
'size': instance.size,
'current': instance.current,
'pages': instance.pages,
};
ChartData _$ChartDataFromJson(Map<String, dynamic> json) => ChartData(
name: json['name'] as String,
value: (json['value'] as num).toDouble(),
);
Map<String, dynamic> _$ChartDataToJson(ChartData instance) => <String, dynamic>{
'name': instance.name,
'value': instance.value,
};

View File

@@ -1,8 +1,9 @@
import 'package:blog_app/apis/blog.dart'; import 'package:blog_app/widget/markdown.dart';
import 'package:blog_app/models/blog.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:provider/provider.dart';
import 'package:blog_app/provider/blog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:markdown_widget/config/toc.dart'; import 'package:markdown_widget/config/toc.dart';
import 'package:markdown_widget/widget/markdown.dart';
class BlogDetailPage extends StatefulWidget { class BlogDetailPage extends StatefulWidget {
final int blogId; final int blogId;
@@ -16,20 +17,14 @@ class BlogDetailPage extends StatefulWidget {
class _BlogDetailPageState extends State<BlogDetailPage> { class _BlogDetailPageState extends State<BlogDetailPage> {
final tocController = TocController(); final tocController = TocController();
bool _showToc = false; bool _showToc = false;
late Future<Blog> _blogDetail;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_blogDetail = _loadBlogDetail(); // 初始化加载数据
} WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().queryBlogById(widget.blogId);
Future<Blog> _loadBlogDetail() async { });
try {
return await queryBlogByIdApi(widget.blogId);
} catch (e) {
throw Exception('获取博客详情失败: $e');
}
} }
Widget _buildBlogTitle(String title) { Widget _buildBlogTitle(String title) {
@@ -43,113 +38,68 @@ class _BlogDetailPageState extends State<BlogDetailPage> {
); );
} }
Widget _buildTocPanel() => Visibility( Widget? buildFloatingActionButton(BlogProvider provider) {
visible: _showToc, if (!provider.isLoading) {
child: Align( return FloatingActionButton(
alignment: Alignment.bottomRight,
child: Container(
width: 250,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(50),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'目录',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
),
Expanded(
child: TocWidget(
controller: tocController,
tocTextStyle: TextStyle(fontSize: 14),
),
),
],
),
),
),
);
Widget _buildMarkdown(String data) =>
MarkdownWidget(data: data, tocController: tocController);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('博客详情')),
body: Container(
padding: EdgeInsets.all(16),
child: FutureBuilder<Blog>(
future: _blogDetail,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('加载失败: ${snapshot.error}')],
),
);
}
if (!snapshot.hasData) {
return Center(child: Text('暂无数据'));
}
final blogDetail = snapshot.data!;
return _buildBlogDetailContent(context, blogDetail);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _showToc = !_showToc), onPressed: () => setState(() => _showToc = !_showToc),
mini: true, mini: true,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).colorScheme.primary,
child: Icon(_showToc ? Icons.close : Icons.list, color: Colors.white), child: Icon(_showToc ? Icons.close : Icons.list, color: Colors.white),
), );
} else {
return null;
}
}
Widget _buildContent(BlogProvider provider) {
if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.queryBlogById(widget.blogId),
); );
} }
Widget _buildBlogDetailContent(BuildContext context, Blog blogDetail) {
return Stack( return Stack(
children: [ children: [
Column( Column(
children: [ children: [
_buildBlogTitle(blogDetail.title), _buildBlogTitle(provider.currentBlog.title),
SizedBox(height: 10), SizedBox(height: 10),
Expanded(child: _buildMarkdown(blogDetail.content!)), Expanded(
child: buildMarkdown(
tocController: tocController,
data: provider.currentBlog.content!,
),
),
], ],
), ),
_buildTocPanel(), buildTocPanel(
context: context,
tocController: tocController,
showToc: _showToc,
),
], ],
); );
} }
@override
Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
return Scaffold(
appBar: AppBar(title: Text('博客详情')),
body: Container(
padding: EdgeInsets.all(16),
child: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildContent(blogProvider),
],
),
),
floatingActionButton: buildFloatingActionButton(blogProvider),
);
}
} }

View File

@@ -1,9 +1,10 @@
import 'package:blog_app/apis/blog.dart'; import 'package:blog_app/provider/blog.dart';
import 'package:blog_app/models/blog.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:provider/provider.dart';
import 'package:blog_app/widget/easy_refresh.dart'; import 'package:blog_app/widget/easy_refresh.dart';
import 'package:blog_app/widget/blog.dart'; import 'package:blog_app/widget/blog.dart';
import 'package:easy_refresh/easy_refresh.dart'; import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
class BlogPage extends StatefulWidget { class BlogPage extends StatefulWidget {
const BlogPage({super.key}); const BlogPage({super.key});
@@ -13,25 +14,21 @@ class BlogPage extends StatefulWidget {
} }
class _BlogPageState extends State<BlogPage> { class _BlogPageState extends State<BlogPage> {
late List<Blog> blogList = [];
int _currentPage = 1;
final int _pageSize = 5;
bool _hasMore = true;
bool _showScrollToTop = false;
final EasyRefreshController _freshController = EasyRefreshController( final EasyRefreshController _freshController = EasyRefreshController(
controlFinishRefresh: true, controlFinishRefresh: true,
controlFinishLoad: true, controlFinishLoad: true,
); );
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
bool _showScrollToTop = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadData(isRefresh: true);
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogList();
});
} }
@override @override
@@ -42,49 +39,17 @@ class _BlogPageState extends State<BlogPage> {
super.dispose(); super.dispose();
} }
Future<void> _loadData({required bool isRefresh}) async {
try {
// 如果是刷新,重置页码
if (isRefresh) {
_currentPage = 1;
}
final result = await queryBlogByPageApi(_currentPage, _pageSize);
setState(() {
if (isRefresh) {
// 刷新时直接替换数据
blogList = result.records;
} else {
// 加载更多时追加数据
blogList.addAll(result.records);
}
// 判断是否还有更多数据
_hasMore = result.current < result.pages;
// 如果有更多数据,准备加载下一页
if (_hasMore) {
_currentPage++;
}
});
} catch (e) {
// 处理错误
debugPrint('加载数据失败: $e');
} finally {
_freshController.finishRefresh();
_freshController.resetFooter();
}
}
// 下拉刷新 // 下拉刷新
Future<void> _onRefresh() async { Future<void> _onRefresh() async {
await _loadData(isRefresh: true); await context.read<BlogProvider>().refreshBlogList();
_freshController.finishRefresh();
} }
// 上拉加载 // 上拉加载
Future<void> _onLoad() async { Future<void> _onLoad() async {
if (_hasMore) { final blogProvider = context.read<BlogProvider>();
await _loadData(isRefresh: false); if (blogProvider.hasMore) {
await blogProvider.loadMoreBlogList();
_freshController.finishLoad(IndicatorResult.success); _freshController.finishLoad(IndicatorResult.success);
} else { } else {
_freshController.finishLoad(IndicatorResult.noMore); _freshController.finishLoad(IndicatorResult.noMore);
@@ -111,15 +76,23 @@ class _BlogPageState extends State<BlogPage> {
// 滚动到顶部 // 滚动到顶部
void _scrollToTop() => scrollToTopAnimateTo(_scrollController); void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
Widget buildBlogList() { Widget _buildBlogList(BlogProvider provider) {
return ListView.builder( if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.refreshBlogList(),
);
}
return ListView.separated(
controller: _scrollController, controller: _scrollController,
itemCount: blogList.length, itemCount: provider.blogList.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final blog = blogList[index]; final blog = provider.blogList[index];
return InkWell( return InkWell(
onTap: () => navigatorToBlogDetail(context, blog.id), onTap: () => navigatorToBlogDetail(context, blog.id),
child: BlogCard(blog: blogList[index]), child: BlogCard(blog: blog),
); );
}, },
); );
@@ -127,13 +100,28 @@ class _BlogPageState extends State<BlogPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
return Stack( return Stack(
children: [ children: [
buildEasyRefresh( Column(
children: [
Expanded(
child: buildEasyRefresh(
freshController: _freshController, freshController: _freshController,
onRefresh: _onRefresh, onRefresh: _onRefresh,
onLoad: _onLoad, onLoad: _onLoad,
body: buildBlogList(), body: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildBlogList(blogProvider),
],
),
),
),
],
), ),
if (_showScrollToTop) if (_showScrollToTop)
buildScrollToTop(context: context, scrollToTop: _scrollToTop), buildScrollToTop(context: context, scrollToTop: _scrollToTop),

View File

@@ -1,5 +1,6 @@
import 'package:blog_app/apis/blog.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:blog_app/models/blog.dart'; import 'package:provider/provider.dart';
import 'package:blog_app/provider/blog.dart';
import 'package:blog_app/widget/blog.dart'; import 'package:blog_app/widget/blog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -13,59 +14,70 @@ class CategoryListPage extends StatefulWidget {
} }
class _CategoryListPageState extends State<CategoryListPage> { class _CategoryListPageState extends State<CategoryListPage> {
late List<Blog> blogs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBlogList(); // 初始化加载数据
} WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogByCategory(widget.category);
Future<void> _loadBlogList() async {
try {
final result = await queryBlogByConditionApi(widget.category, null, null);
setState(() {
blogs = result;
}); });
} catch (e) {
throw Exception('获取博客列表失败: $e');
}
} }
Widget _buildBlogList() { Widget _buildBlogList(BlogProvider provider) {
return ListView.builder( return ListView.separated(
itemCount: blogs.length, itemCount: provider.blogList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final blog = blogs[index]; final blog = provider.blogList[index];
return InkWell( return InkWell(
onTap: () => navigatorToBlogDetail(context, blog.id), onTap: () => navigatorToBlogDetail(context, blog.id),
child: buildBlogListItem(context, blog, index + 1), child: buildBlogListItem(context, blog, index + 1),
); );
}, },
separatorBuilder: (BuildContext context, int index) {
return SizedBox(height: 8);
},
);
}
Widget _buildContent(BlogProvider provider) {
if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.refreshBlogByYear(),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text(widget.category, style: TextStyle(fontSize: 20))),
const SizedBox(height: 8),
buildListTitle(provider.blogList.length),
const SizedBox(height: 8),
Expanded(
child:
provider.blogList.isEmpty
? buildEmpty()
: _buildBlogList(provider),
),
],
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; final blogProvider = context.watch<BlogProvider>();
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('博客详情')), appBar: AppBar(title: Text('博客详情')),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Center( if (blogProvider.isLoading)
child: Text( buildLoadingIndicator()
widget.category, else
style: TextStyle(fontSize: 20, color: colors.primary), _buildContent(blogProvider),
),
),
const SizedBox(height: 8),
buildListTitle(blogs.length),
const SizedBox(height: 8),
Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()),
], ],
), ),
), ),

View File

@@ -1,5 +1,6 @@
import 'package:blog_app/apis/blog.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:blog_app/models/blog.dart'; import 'package:provider/provider.dart';
import 'package:blog_app/provider/blog.dart';
import 'package:blog_app/widget/blog.dart'; import 'package:blog_app/widget/blog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -11,38 +12,35 @@ class CategoryPage extends StatefulWidget {
} }
class _CategoryPageState extends State<CategoryPage> { class _CategoryPageState extends State<CategoryPage> {
late List<BlogCategory> categories = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBlogCategory(); // 初始化加载数据
} WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogCategory();
Future<void> _loadBlogCategory() async {
try {
final result = await queryBlogCategoryApi();
setState(() {
categories = result;
}); });
} catch (e) {
throw Exception('获取博客分类失败: $e');
}
} }
@override Widget _buildContent(BlogProvider provider) {
Widget build(BuildContext context) { if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.refreshBlogByYear(),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildCategoryTitle(context, categories.length), buildCategoryTitle(context, provider.categoryList.length),
SizedBox(height: 10), SizedBox(height: 10),
// 分类列表 // 分类列表
Expanded( Expanded(
child: ListView.builder( child: ListView.separated(
itemCount: categories.length, itemCount: provider.categoryList.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final category = categories[index]; final category = provider.categoryList[index];
return buildCategoryCard(context, category); return buildCategoryCard(context, category);
}, },
), ),
@@ -50,4 +48,24 @@ class _CategoryPageState extends State<CategoryPage> {
], ],
); );
} }
@override
Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
return Column(
children: [
Expanded(
child: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildContent(blogProvider),
],
),
),
],
);
}
} }

View File

@@ -1,8 +1,14 @@
import 'package:blog_app/layout/drawer.dart'; import 'package:blog_app/apis/blog.dart';
import 'package:blog_app/layout/app_drawer.dart';
import 'package:blog_app/layout/app_navbar.dart';
import 'package:blog_app/layout/menu.dart'; import 'package:blog_app/layout/menu.dart';
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/widget/search.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key});
@override @override
_HomePageState createState() => _HomePageState(); _HomePageState createState() => _HomePageState();
} }
@@ -11,6 +17,12 @@ class _HomePageState extends State<HomePage> {
int _currentPageIndex = 0; int _currentPageIndex = 0;
late PageController _pageController; late PageController _pageController;
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
List<BlogSearch> _searchResults = [];
String _searchQuery = '';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -20,53 +32,159 @@ class _HomePageState extends State<HomePage> {
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController.dispose();
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose(); super.dispose();
} }
void _startSearch() {
setState(() {
_isSearching = true;
});
Future.delayed(Duration(milliseconds: 100), () {
_searchFocusNode.requestFocus();
});
}
void _exitSearch() {
setState(() {
_isSearching = false;
_searchController.clear();
_searchResults.clear();
_searchQuery = '';
});
FocusScope.of(context).unfocus();
}
void _clearSearch() {
_searchController.clear();
setState(() {
_searchResults.clear();
_searchQuery = '';
});
}
void _onSearchTextChanged(String query) async {
setState(() {
_searchQuery = query;
});
if (query.isEmpty) {
setState(() {
_searchResults.clear();
});
return;
}
final result = await searchBlog(query);
setState(() {
_searchResults = result;
});
}
Widget _buildLeading() {
if (_isSearching) {
return IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: _exitSearch,
);
}
return Builder(
builder:
(context) => IconButton(
icon: Icon(Icons.menu, color: Colors.white),
onPressed: () => Scaffold.of(context).openDrawer(),
),
);
}
Widget _buildAppBarTitle() {
if (_isSearching) {
return buildSearchField(
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: _onSearchTextChanged,
);
}
return Text(
pages[_currentPageIndex].title,
style: TextStyle(color: Colors.white),
);
}
List<Widget> _buildAppBarActions() {
if (_isSearching) {
return [
if (_searchController.text.isNotEmpty)
IconButton(
icon: Icon(Icons.clear, color: Colors.white),
onPressed: _clearSearch,
),
];
} else {
return [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: _startSearch,
),
];
}
}
Widget _buildPageView() { Widget _buildPageView() {
return _isSearching ? _buildSearchResults() : _buildNormalPageView();
}
Widget _buildNormalPageView() {
return PageView( return PageView(
controller: _pageController, controller: _pageController,
onPageChanged: (index) { onPageChanged: (index) {
setState(() { setState(() {
_currentPageIndex = index; _currentPageIndex = index;
if (_isSearching) {
_exitSearch();
}
}); });
}, },
children: pageWidgets, children: pageWidgets,
); );
} }
@override Widget _buildSearchResults() {
Widget build(BuildContext context) { if (_searchQuery.isEmpty) {
return Scaffold( return buildSearchQueryEmpty();
appBar: AppBar( }
title: Text(
pages[_currentPageIndex].title, if (_searchResults.isEmpty) {
style: TextStyle(color: Colors.white), return buildSearchResultEmpty(_searchQuery);
), }
backgroundColor: Theme.of(context).colorScheme.primary,
elevation: 0, return buildSearchResultList(context, _searchResults);
leading: Builder( }
builder:
(context) => IconButton( void _onTapNavbarItem(int index) {
icon: Icon(Icons.menu, color: Colors.white), setState(() {
onPressed: () => Scaffold.of(context).openDrawer(), _currentPageIndex = index;
), if (_isSearching) {
), _exitSearch();
actions: [ }
IconButton( });
icon: Icon(Icons.search, color: Colors.white), _pageController.animateToPage(
onPressed: () {}, index,
), duration: Duration(milliseconds: 300),
], curve: Curves.easeInOut,
),
drawer: _buildDrawer(),
body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
); );
} }
void onTapDrawerItem(int index) { void onTapDrawerItem(int index) {
setState(() { setState(() {
_currentPageIndex = index; _currentPageIndex = index;
if (_isSearching) {
_exitSearch();
}
}); });
_pageController.animateToPage( _pageController.animateToPage(
index, index,
@@ -76,35 +194,24 @@ class _HomePageState extends State<HomePage> {
Navigator.pop(context); Navigator.pop(context);
} }
Widget _buildDrawerBody() { @override
return ListView( Widget build(BuildContext context) {
padding: EdgeInsets.zero, return Scaffold(
children: [ appBar: AppBar(
...List.generate( title: _buildAppBarTitle(),
pages.length, backgroundColor: Theme.of(context).colorScheme.primary,
(index) => buildDrawerItem( elevation: 0,
context: context, leading: _buildLeading(),
page: pages[index], actions: _buildAppBarActions(),
isSelected: index == _currentPageIndex,
onTap: () => onTapDrawerItem(index),
), ),
drawer: AppDrawer(
currentPageIndex: _currentPageIndex,
onTapDrawerItem: onTapDrawerItem,
), ),
], body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
); bottomNavigationBar: AppNavbar(
} currentPageIndex: _currentPageIndex,
onTapNavbarItem: _onTapNavbarItem,
Widget _buildDrawer() {
return Drawer(
child: Container(
color: Colors.white,
child: Column(
children: [
// 抽屉头部
buildDrawerHeader(context),
// 菜单项列表
Expanded(child: _buildDrawerBody()),
],
),
), ),
); );
} }

View File

@@ -1,122 +0,0 @@
import 'package:blog_app/utils/http_utils.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,
),
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:blog_app/apis/blog.dart'; import 'package:flutter_common/utils/number_utils.dart';
import 'package:blog_app/models/blog.dart'; import 'package:flutter_common/widget/chart.dart';
import 'package:blog_app/models/common.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:blog_app/widget/chart.dart'; import 'package:flutter_common/widget/year_selector.dart';
import 'package:blog_app/widget/common.dart'; import 'package:provider/provider.dart';
import 'package:blog_app/widget/year_selector.dart'; import 'package:blog_app/provider/blog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class StatsPage extends StatefulWidget { class StatsPage extends StatefulWidget {
@@ -16,289 +16,147 @@ class StatsPage extends StatefulWidget {
class StatsPageState extends State<StatsPage> { class StatsPageState extends State<StatsPage> {
final double chartHeight = 400; final double chartHeight = 400;
final double statsHeight = 120; final double statsHeight = 120;
int currentYear = DateTime.now().year;
late List<ChartData> approvedStats = [];
late List<ChartData> visitStats = [];
late List<ChartData> visitRankStats = [];
late List<ChartData> categoryStats = [];
late List<ChartData> readRankStats = [];
late BlogStats overviewStats = BlogStats(
blogCount: 0,
categoryCount: 0,
greatCount: 0,
visitCount: 0,
wordCount: 0,
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadStatsList();
}
Future<void> _loadStatsList() async { // 初始化加载数据
try { WidgetsBinding.instance.addPostFrameCallback((_) {
final approvedResult = await queryBlogApprovedStatsApi(currentYear); context.read<BlogProvider>().refreshBlogStats();
final visitResult = await queryBlogVisitStatsApi(currentYear);
final visitRankResult = await queryBlogVisitRankStatsApi();
final categoryResult = await queryBlogCategoryStatsApi();
final readRankResult = await queryBlogReadRankStatsApi();
final overviewResult = await queryBlogOverviewStatsApi();
setState(() {
approvedStats = approvedResult;
visitStats = visitResult;
visitRankStats = visitRankResult;
categoryStats = categoryResult;
readRankStats = readRankResult;
overviewStats = overviewResult;
}); });
} catch (e) {
throw Exception('获取统计数据失败: $e');
}
} }
Widget _buildApprovedStats(BuildContext context) { Widget _buildBlogStatsGrid(BlogProvider provider) {
return Column(
children: [
buildChartTitle(context, '每月博客发布统计', Icons.show_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: lineChart(
context: context,
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data: approvedStats,
),
),
],
);
}
Widget _buildVisitStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '每月博客访问统计', Icons.show_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: lineChart(
context: context,
xAxisName: '月份',
yAxisName: '访问量',
unit: '人次',
data: visitStats,
),
),
],
);
}
Widget _buildVisitRankStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '博客访问数量排行', Icons.bar_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: barChart(
context: context,
xAxisName: '名称',
yAxisName: '访问量',
unit: '人次',
data: visitRankStats,
),
),
],
);
}
Widget _buildCategoryStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '博客类别统计', Icons.pie_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: pieChart(context: context, unit: '', data: categoryStats),
),
],
);
}
Widget _buildReadRankStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '博客阅读时长排行', Icons.bar_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: barChart(
context: context,
xAxisName: '名称',
yAxisName: '时长',
unit: '分钟',
data: readRankStats,
),
),
],
);
}
Widget buildStatsCard({
required BuildContext context,
required String title,
required int count,
required String unit,
}) {
return buildCard(
context: context,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
count.toString(),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
height: 1.0,
),
),
const SizedBox(width: 4),
Text(
unit,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
Widget buildBlogStatsGrid(BuildContext context) {
return GridView.count( return GridView.count(
crossAxisCount: 2, crossAxisCount: 2,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 2, childAspectRatio: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [ children: [
buildStatsCard( StatsCard(
context: context, icon: Icons.article,
title: '博客数量', title: '博客数量',
count: overviewStats.blogCount, value: provider.overviewStats.blogCount.toString(),
unit: '', unit: '',
), ),
buildStatsCard( StatsCard(
context: context, icon: Icons.folder,
title: '分类数量', title: '分类数量',
count: overviewStats.categoryCount, value: provider.overviewStats.categoryCount.toString(),
unit: '', unit: '',
), ),
buildStatsCard( StatsCard(
context: context, icon: Icons.remove_red_eye,
title: '访问量', title: '访问量',
count: overviewStats.visitCount, value: formatChineseDecimal(provider.overviewStats.visitCount),
unit: '', unit: '',
), ),
buildStatsCard( StatsCard(
context: context, icon: Icons.text_fields,
title: '总字数', title: '总字数',
count: overviewStats.wordCount, value: formatChineseDecimal(provider.overviewStats.wordCount),
unit: '', unit: '',
), ),
], ],
); );
} }
Widget _buildContent(BlogProvider provider) {
if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.refreshBlogStats(),
);
}
return Column(
children: [
YearSelector(
currentYear: provider.currentStatsYear,
onYearChanged: (year) {
provider.currentStatsYear = year;
provider.refreshBlogStats();
},
),
const SizedBox(height: 8),
_buildBlogStatsGrid(provider),
const SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: LineChart(
title: '每月博客发布统计',
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data: provider.approvedStats,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: LineChart(
title: '每月博客访问统计',
xAxisName: '月份',
yAxisName: '访问量',
unit: '人次',
data: provider.visitStats,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: RankChart(
title: '博客访问数量排行',
unit: '人次',
data: provider.visitRankStats,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: PieChart(
title: '博客类别统计',
unit: '',
data: provider.categoryStats,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: RankChart(
title: '博客阅读时长排行',
unit: '分钟',
data: provider.readRankStats,
),
),
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( final blogProvider = context.watch<BlogProvider>();
child: Column(
return Stack(
children: [ children: [
YearSelector( if (blogProvider.isLoading)
initialYear: DateTime.now().year, buildLoadingIndicator()
minYear: 2000, else
maxYear: 2100, SingleChildScrollView(child: _buildContent(blogProvider)),
onYearChanged:
(year) => {
setState(() {
currentYear = year;
_loadStatsList();
}),
},
),
SizedBox(height: 6),
buildBlogStatsGrid(context),
SizedBox(height: 6),
SizedBox(
height: chartHeight,
child: buildCard(
context: context,
child: _buildApprovedStats(context),
),
),
SizedBox(height: 6),
SizedBox(
height: chartHeight,
child: buildCard(
context: context,
child: _buildVisitStats(context),
),
),
SizedBox(height: 6),
SizedBox(
height: chartHeight,
child: buildCard(
context: context,
child: _buildVisitRankStats(context),
),
),
SizedBox(height: 6),
SizedBox(
height: chartHeight,
child: buildCard(
context: context,
child: _buildCategoryStats(context),
),
),
SizedBox(height: 6),
SizedBox(
height: chartHeight,
child: buildCard(
context: context,
child: _buildReadRankStats(context),
),
),
], ],
),
); );
} }
} }

View File

@@ -1,7 +1,8 @@
import 'package:blog_app/apis/blog.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:blog_app/models/blog.dart'; import 'package:flutter_common/widget/year_selector.dart';
import 'package:provider/provider.dart';
import 'package:blog_app/provider/blog.dart';
import 'package:blog_app/widget/blog.dart'; import 'package:blog_app/widget/blog.dart';
import 'package:blog_app/widget/year_selector.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:timelines_plus/timelines_plus.dart'; import 'package:timelines_plus/timelines_plus.dart';
@@ -9,48 +10,27 @@ class TimelinePage extends StatefulWidget {
const TimelinePage({super.key}); const TimelinePage({super.key});
@override @override
State<TimelinePage> createState() => _TimelinePageState(); State<TimelinePage> createState() => TimelinePageState();
} }
class _TimelinePageState extends State<TimelinePage> { class TimelinePageState extends State<TimelinePage> {
late List<Blog> blogs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBlogList(); // 初始化加载数据
} WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BlogProvider>().refreshBlogByYear();
Future<void> _loadBlogList() async {
try {
final result = await queryBlogByConditionApi(
null,
null,
DateTime.now().year,
);
setState(() {
blogs = result;
});
} catch (e) {
throw Exception('获取博客列表失败: $e');
}
}
Future<void> refreshRecord(int year) async {
final result = await queryBlogByConditionApi(null, null, year);
setState(() {
blogs = result;
}); });
} }
Widget _buildBlogList() { Widget _buildBlogList(BlogProvider provider) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return Timeline.tileBuilder( return Timeline.tileBuilder(
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0), theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
padding: EdgeInsets.all(6), padding: EdgeInsets.all(0),
builder: TimelineTileBuilder.connected( builder: TimelineTileBuilder.connected(
itemCount: blogs.length, itemCount: provider.blogList.length,
connectorBuilder: connectorBuilder:
(context, index, type) => (context, index, type) =>
Connector.solidLine(thickness: 2, color: colors.primary), Connector.solidLine(thickness: 2, color: colors.primary),
@@ -58,27 +38,66 @@ class _TimelinePageState extends State<TimelinePage> {
return Indicator.dot(size: 12.0, color: colors.primary); return Indicator.dot(size: 12.0, color: colors.primary);
}, },
contentsBuilder: (context, index) { contentsBuilder: (context, index) {
return buildBlogListItem(context, blogs[index], index + 1); final blog = provider.blogList[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: InkWell(
onTap: () => navigatorToBlogDetail(context, blog.id),
child: buildBlogListItem(context, blog, index + 1),
),
);
}, },
), ),
); );
} }
@override Widget _buildContent(BlogProvider provider) {
Widget build(BuildContext context) { if (provider.error != null) {
return buildErrorInfo(
errorInfo: provider.error!,
onPressed: () => provider.refreshBlogByYear(),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
YearSelector( YearSelector(
initialYear: DateTime.now().year, currentYear: provider.currentQueryYear,
minYear: 2000, onYearChanged: (year) {
maxYear: 2100, provider.currentQueryYear = year;
onYearChanged: (year) => refreshRecord(year), provider.refreshBlogByYear();
},
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
buildListTitle(blogs.length), buildListTitle(provider.blogList.length),
const SizedBox(height: 8), const SizedBox(height: 8),
Expanded(child: blogs.isEmpty ? buildEmpty() : _buildBlogList()), Expanded(
child:
provider.blogList.isEmpty
? buildEmpty()
: _buildBlogList(provider),
),
],
);
}
@override
Widget build(BuildContext context) {
final blogProvider = context.watch<BlogProvider>();
return Column(
children: [
Expanded(
child: Stack(
children: [
if (blogProvider.isLoading)
buildLoadingIndicator()
else
_buildContent(blogProvider),
],
),
),
], ],
); );
} }

208
lib/provider/blog.dart Normal file
View File

@@ -0,0 +1,208 @@
import 'package:flutter/foundation.dart';
import 'package:blog_app/apis/blog.dart';
import 'package:blog_app/models/blog.dart';
import 'package:flutter_common/models/common_model.dart';
class BlogProvider with ChangeNotifier {
List<Blog> blogList = [];
List<BlogCategory> categoryList = [];
Blog currentBlog = Blog(
id: 0,
title: '',
topValue: 0,
isGreat: false,
category: '',
summary: '',
content: '',
wordCount: 0,
readDuration: 0,
visitCount: 0,
createTime: '',
updateTime: '',
);
int currentPage = 1;
final int pageSize = 5;
bool hasMore = true;
bool isLoading = false;
int currentStatsYear = DateTime.now().year;
int currentQueryYear = DateTime.now().year;
String? error;
late List<ChartData> approvedStats = [];
late List<ChartData> visitStats = [];
late List<ChartData> visitRankStats = [];
late List<ChartData> categoryStats = [];
late List<ChartData> readRankStats = [];
late BlogStats overviewStats = BlogStats(
blogCount: 0,
categoryCount: 0,
greatCount: 0,
visitCount: 0,
wordCount: 0,
);
Future<void> queryBlogByPage({bool isRefresh = true}) async {
if (isLoading) return;
try {
if (currentPage == 1) {
isLoading = true;
}
error = null;
notifyListeners();
// 如果是刷新,重置页码
if (isRefresh) {
currentPage = 1;
}
final result = await queryBlogByPageApi(currentPage, pageSize);
// 更新数据
if (isRefresh) {
blogList = result.records;
} else {
blogList.addAll(result.records);
}
// 判断是否还有更多数据
hasMore = result.current < result.pages;
if (hasMore) {
currentPage++;
}
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshBlogCategory() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
categoryList = [];
notifyListeners();
final result = await queryBlogCategoryApi();
categoryList = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshBlogStats() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final approvedResult = await queryBlogApprovedStatsApi(currentStatsYear);
final visitResult = await queryBlogVisitStatsApi(currentStatsYear);
final visitRankResult = await queryBlogVisitRankStatsApi();
final categoryResult = await queryBlogCategoryStatsApi();
final readRankResult = await queryBlogReadRankStatsApi();
final overviewResult = await queryBlogOverviewStatsApi();
approvedStats = approvedResult;
visitStats = visitResult;
visitRankStats = visitRankResult;
categoryStats = categoryResult;
readRankStats = readRankResult;
overviewStats = overviewResult;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshBlogByCategory(String category) async {
if (isLoading) return;
try {
isLoading = true;
error = null;
blogList = [];
notifyListeners();
final result = await queryBlogByConditionApi(category, null, null);
blogList = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshBlogByYear() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
blogList = [];
notifyListeners();
final result = await queryBlogByConditionApi(null, null, currentQueryYear);
blogList = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshBlogList() async {
await queryBlogByPage(isRefresh: true);
}
Future<void> queryBlogById(int blogId) async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final result = await queryBlogByIdApi(blogId);
currentBlog = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
// 加载更多
Future<void> loadMoreBlogList() async {
if (hasMore && !isLoading) {
await queryBlogByPage(isRefresh: false);
}
}
// 清除错误
void clearError() {
error = null;
notifyListeners();
}
}

View File

@@ -1,13 +0,0 @@
import 'package:intl/intl.dart';
String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
String formatDateString(String date) {
return DateFormat('yyyy-MM-dd').format(DateTime.parse(date));
}
String formatTime(DateTime datetime) {
return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime);
}

View File

@@ -1,244 +0,0 @@
import 'package:blog_app/config/app_config.dart';
import 'package:blog_app/utils/sp_utils.dart';
import 'package:dio/dio.dart';
import 'log_utils.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';
}
}
}

View File

@@ -1,28 +0,0 @@
import 'package:blog_app/models/common.dart';
List<T> convertListResponse<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is List) {
return data.map((item) => fromJson(item as Map<String, dynamic>)).toList();
}
throw FormatException(
'Expected a list of items for conversion, but got ${data.runtimeType}',
);
}
PageResult<T> convertPageResponse<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is Map<String, dynamic>) {
return PageResult<T>.fromJson(
data,
(json) => fromJson(json as Map<String, dynamic>),
);
}
throw FormatException(
'Expected a Map for page response conversion, but got ${data.runtimeType}',
);
}

View File

@@ -1,12 +0,0 @@
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')),
);

View File

@@ -1,61 +0,0 @@
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();
}
}

View File

@@ -1,9 +1,9 @@
import 'package:blog_app/models/blog.dart'; import 'package:blog_app/models/blog.dart';
import 'package:blog_app/pages/blog_detail_page.dart'; import 'package:blog_app/pages/blog_detail_page.dart';
import 'package:blog_app/pages/category_list_page.dart'; import 'package:blog_app/pages/category_list_page.dart';
import 'package:blog_app/utils/date_utils.dart';
import 'package:blog_app/widget/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/utils/date_utils.dart';
import 'package:flutter_common/widget/common_widget.dart';
class BlogLabel { class BlogLabel {
final String text; final String text;
@@ -53,7 +53,11 @@ class BlogCard extends StatelessWidget {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children: [
if (blog.isGreat) if (blog.isGreat)
_buildBlogLabelItem('精品', Icons.workspace_premium, Colors.amber), _buildBlogLabelItem(
'精品',
Icons.workspace_premium,
Colors.deepOrange,
),
if (blog.topValue > 1) if (blog.topValue > 1)
_buildBlogLabelItem('置顶', Icons.push_pin, Colors.red), _buildBlogLabelItem('置顶', Icons.push_pin, Colors.red),
_buildBlogLabelItem(blog.category, Icons.folder, Colors.blue), _buildBlogLabelItem(blog.category, Icons.folder, Colors.blue),
@@ -101,12 +105,14 @@ class BlogCard extends StatelessWidget {
); );
} }
Widget _buildBlogSummary() { Widget _buildBlogSummary(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Text( child: Text(
blog.summary, blog.summary,
style: TextStyle(color: Colors.grey.shade700, height: 1.6), style: TextStyle(color: colors.secondary, height: 1.6),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
); );
@@ -114,10 +120,9 @@ class BlogCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return buildCard( final colors = Theme.of(context).colorScheme;
context: context,
child: Padding( return CommonCard(
padding: EdgeInsets.all(12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -125,16 +130,11 @@ class BlogCard extends StatelessWidget {
SizedBox(height: 16), SizedBox(height: 16),
_buildBlogLabel(), _buildBlogLabel(),
SizedBox(height: 16), SizedBox(height: 16),
Container( Container(height: 1, width: 80, color: colors.primary),
height: 1,
width: 80,
color: Theme.of(context).colorScheme.primary,
),
SizedBox(height: 16), SizedBox(height: 16),
_buildBlogSummary(), _buildBlogSummary(context),
], ],
), ),
),
); );
} }
} }
@@ -164,8 +164,7 @@ Widget buildCategoryTitle(BuildContext context, int count) {
Widget buildCategoryCard(BuildContext context, BlogCategory category) { Widget buildCategoryCard(BuildContext context, BlogCategory category) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return buildCard( return CommonCard(
context: context,
child: ListTile( child: ListTile(
leading: Container( leading: Container(
width: 50, width: 50,
@@ -188,14 +187,16 @@ Widget buildCategoryCard(BuildContext context, BlogCategory category) {
), ),
), ),
trailing: Container( trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), width: 30,
height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.primary.withAlpha(50), color: Theme.of(context).colorScheme.primary.withAlpha(50),
borderRadius: BorderRadius.circular(16), shape: BoxShape.circle,
), ),
child: Text( child: Icon(
category.count.toString(), Icons.arrow_forward,
style: TextStyle(color: colors.primary, fontWeight: FontWeight.bold), size: 16,
color: Theme.of(context).colorScheme.primary,
), ),
), ),
onTap: () => navigateToBlogList(context, category), onTap: () => navigateToBlogList(context, category),
@@ -222,33 +223,24 @@ Widget buildListTitle(int count) {
Widget buildBlogListItem(BuildContext context, Blog blog, int index) { Widget buildBlogListItem(BuildContext context, Blog blog, int index) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return buildCard( return CommonCard(
context: context,
child: Padding( child: Padding(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(0),
child: Row( child: Row(
children: [ children: [
SizedBox( Text(
width: 25, '$index.',
child: Text(
index.toString(),
style: TextStyle( style: TextStyle(
color: colors.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: colors.primary,
), ),
), ),
SizedBox(width: 10),
Text(
formatDateString(blog.createTime),
style: TextStyle(fontSize: 14, color: colors.onSurface),
), ),
SizedBox(width: 10),
// 发布时间
SizedBox(
width: 150,
child: Text(
blog.createTime,
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
),
),
// 标题
Expanded( Expanded(
child: Text( child: Text(
blog.title, blog.title,

View File

@@ -1,285 +0,0 @@
import 'package:blog_app/models/common.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
Widget buildChartTitle(BuildContext context, String title, IconData icon) {
return Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
Text(title),
],
);
}
Widget buildChartDivider(BuildContext context) {
return Divider(
height: 1,
thickness: 1,
color: Theme.of(context).colorScheme.primary,
indent: 0,
endIndent: 0,
);
}
Widget lineChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// 图表标题
// title: ChartTitle(text: '2023年上半年销售额万元'),
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用图例
legend: Legend(isVisible: true, position: LegendPosition.top),
// 启用交互提示(点击数据点显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 折线图数据系列
series: [
LineSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 线条颜色
color: Theme.of(context).colorScheme.primary,
// 线条宽度
width: 3,
// 数据点样式
markerSettings: const MarkerSettings(
isVisible: true,
color: Colors.white,
shape: DataMarkerType.circle,
height: 6,
width: 6,
),
// 折线名称(会显示在图例中)
name: yAxisName,
// 启用数据标签(直接显示数值)
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
),
// 动画效果
animationDuration: 2000, // 动画时长(毫秒)
),
],
);
}
Widget barChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用交互提示
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 柱状图数据系列
series: [
ColumnSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 名称
name: yAxisName,
// 柱子颜色
color: Theme.of(context).colorScheme.primary,
// 柱子宽度0-1之间1表示占满类别间隔
width: 0.6,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget doubleBarChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data1,
required List<ChartData> data2,
required String series1Name,
required String series2Name,
}) {
return SfCartesianChart(
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: const MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 图例配置
legend: Legend(
isVisible: true,
position: LegendPosition.top,
overflowMode: LegendItemOverflowMode.wrap,
),
// 启用交互提示
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'series.name: point.y $unit',
),
// 双柱状图数据系列
series: <ColumnSeries<ChartData, String>>[
ColumnSeries<ChartData, String>(
dataSource: data1,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 系列名称
name: series1Name,
// 柱子颜色
color: Theme.of(context).colorScheme.primary,
// 柱子宽度
width: 0.3,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
ColumnSeries<ChartData, String>(
dataSource: data2,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 系列名称
name: series2Name,
// 柱子颜色
color: Theme.of(context).colorScheme.inversePrimary,
// 柱子宽度
width: 0.3,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget pieChart({
required BuildContext context,
required String unit,
required List<ChartData> data,
}) {
// 计算 value 的总和
double sumValue = data.fold(0.0, (sum, item) => sum + item.value);
return SfCircularChart(
// 饼图标题
// title: ChartTitle(text: '菜谱类别占比分布'),
// 启用图例
legend: const Legend(isVisible: true, position: LegendPosition.right),
// 启用交互提示(点击扇区显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 饼图系列配置
series: [
PieSeries<ChartData, String>(
dataSource: data,
// 类别映射(饼图扇区名称)
xValueMapper: (ChartData data, _) => data.name,
// 数值映射(扇区大小占比)
yValueMapper: (ChartData data, _) => data.value,
// 扇区半径0-1之间1表示充满容器
// radius: '50%',
// 启用扇区分离效果
explode: true,
// 指定分离的扇区索引(这里分离第一个扇区)
explodeIndex: 0,
// 分离距离
explodeOffset: '5%',
dataLabelMapper: (ChartData data, _) {
final percentage = (data.value / sumValue * 100).toStringAsFixed(0);
return '$percentage%';
},
// 数据标签(显示在扇区上的文本)
dataLabelSettings: DataLabelSettings(isVisible: true),
// 动画效果
animationDuration: 2000,
),
],
);
}

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
Widget buildCard({required BuildContext context, required Widget child}) {
final colors = Theme.of(context).colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
child: Padding(padding: EdgeInsets.all(8), child: child),
),
);
}
Widget circleIconButton({
required IconData icon,
required VoidCallback onPressed,
required BuildContext context,
}) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.primary,
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
),
child: Icon(icon, color: Colors.white),
);
}

64
lib/widget/markdown.dart Normal file
View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:markdown_widget/config/toc.dart';
import 'package:markdown_widget/widget/markdown.dart';
Widget buildMarkdown({
required TocController tocController,
required String data,
}) => MarkdownWidget(data: data, tocController: tocController);
Widget buildTocPanel({
required BuildContext context,
required TocController tocController,
required bool showToc,
}) => Visibility(
visible: showToc,
child: Align(
alignment: Alignment.bottomRight,
child: Container(
width: 250,
height: 400,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(50),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'目录',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
),
Expanded(
child: TocWidget(
controller: tocController,
tocTextStyle: TextStyle(fontSize: 14),
),
),
],
),
),
),
);

99
lib/widget/search.dart Normal file
View File

@@ -0,0 +1,99 @@
import 'package:blog_app/models/blog.dart';
import 'package:blog_app/widget/blog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
Widget buildSearchField({
required TextEditingController controller,
required FocusNode focusNode,
required Function(String) onChanged,
}) {
return TextField(
controller: controller,
focusNode: focusNode,
autofocus: true,
style: TextStyle(color: Colors.white, fontSize: 18),
cursorColor: Colors.white,
decoration: InputDecoration(
hintText: '搜索文章内容...',
hintStyle: TextStyle(color: Colors.white70),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
onChanged: onChanged,
textInputAction: TextInputAction.search,
);
}
Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
final colors = Theme.of(context).colorScheme;
return CommonCard(
child: ListTile(
title: Text(
blog.title,
style: TextStyle(fontWeight: FontWeight.bold, color: colors.primary),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4),
Text(
blog.content,
style: TextStyle(color: colors.secondary, fontSize: 14),
),
],
),
onTap: () => navigatorToBlogDetail(context, blog.id),
),
);
}
Widget buildSearchResultList(
BuildContext context,
List<BlogSearch> searchResultList,
) {
return ListView.separated(
itemCount: searchResultList.length,
itemBuilder: (context, index) {
return buildSearchResultItem(context, searchResultList[index]);
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(height: 8);
},
);
}
Widget buildSearchQueryEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text('搜索文章内容', style: TextStyle(color: Colors.grey[600], fontSize: 16)),
SizedBox(height: 8),
Text('输入标题或内容进行搜索', style: TextStyle(color: Colors.grey[500])),
],
),
);
}
Widget buildSearchResultEmpty(String query) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
'没有找到"$query"相关文章',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
SizedBox(height: 8),
Text('请尝试其他关键词', style: TextStyle(color: Colors.grey[500])),
],
),
);
}

View File

@@ -1,119 +0,0 @@
import 'package:blog_app/widget/common.dart';
import 'package:flutter/material.dart';
class YearSelector extends StatefulWidget {
final int initialYear;
final int? minYear;
final int? maxYear;
final Function(int) onYearChanged;
const YearSelector({
super.key,
required this.initialYear,
required this.onYearChanged,
this.minYear,
this.maxYear,
});
@override
State<YearSelector> createState() => _YearSelectorState();
}
// SingleTickerProviderStateMixin 动画控制器
class _YearSelectorState extends State<YearSelector>
with SingleTickerProviderStateMixin {
late int _currentYear;
// 用于动画效果
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_currentYear = widget.initialYear;
// 初始化动画控制器
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
// 缩放动画
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// 切换到上一年
void _previousYear() {
if (widget.minYear == null || _currentYear > widget.minYear!) {
_animateYearChange(() {
setState(() {
_currentYear--;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 切换到下一年
void _nextYear() {
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
_animateYearChange(() {
setState(() {
_currentYear++;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 年份变化时的动画效果
void _animateYearChange(VoidCallback onComplete) {
_animationController.forward().then((_) {
onComplete();
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
circleIconButton(
context: context,
icon: Icons.chevron_left,
onPressed: () => _previousYear(),
),
// 年份显示
AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: Text(
'$_currentYear',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
circleIconButton(
context: context,
icon: Icons.chevron_right,
onPressed: () => _nextYear(),
)
],
);
}
}

View File

@@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <rive_native/rive_native_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) rive_native_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin");
rive_native_plugin_register_with_registrar(rive_native_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
rive_native
url_launcher_linux url_launcher_linux
) )

View File

@@ -5,10 +5,16 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_picker
import flutter_image_compress_macos
import rive_native
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@@ -33,6 +33,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
awesome_dialog:
dependency: transitive
description:
name: awesome_dialog
sha256: "4c5821a0a637ceee022084e78c1b8237dd4b8bfca4dd24ac2484662a56707338"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +49,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
buffer:
dependency: transitive
description:
name: buffer
sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.3"
build: build:
dependency: transitive dependency: transitive
description: description:
@@ -153,6 +169,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.5"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -177,8 +201,16 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
dio: dio:
dependency: "direct main" dependency: transitive
description: description:
name: dio name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
@@ -225,6 +257,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: transitive
description:
name: file_picker
sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.3.7"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -238,6 +278,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_common:
dependency: "direct main"
description:
path: "D:\\Projects\\FlutterProjects\\flutter_common"
relative: false
source: path
version: "1.0.0+1"
flutter_highlight: flutter_highlight:
dependency: transitive dependency: transitive
description: description:
@@ -246,6 +293,54 @@ 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"
flutter_image_compress:
dependency: transitive
description:
name: flutter_image_compress
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
flutter_image_compress_common:
dependency: transitive
description:
name: flutter_image_compress_common
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.6"
flutter_image_compress_macos:
dependency: transitive
description:
name: flutter_image_compress_macos
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
flutter_image_compress_ohos:
dependency: transitive
description:
name: flutter_image_compress_ohos
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.3"
flutter_image_compress_platform_interface:
dependency: transitive
description:
name: flutter_image_compress_platform_interface
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
flutter_image_compress_web:
dependency: transitive
description:
name: flutter_image_compress_web
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.5"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -254,6 +349,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.31"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -264,6 +367,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: transitive
description:
name: fluttertoast
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.14"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -393,7 +504,7 @@ packages:
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
logger: logger:
dependency: "direct main" dependency: transitive
description: description:
name: logger name: logger
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
@@ -456,6 +567,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
minio:
dependency: transitive
description:
name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.5.8"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -512,6 +639,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -536,6 +671,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.2" version: "1.5.2"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -552,6 +695,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
rive:
dependency: transitive
description:
name: rive
sha256: fc0abf65d03d1c9afaeb35be9e71c7cf04d2d1f76e94e69d2af1b3ba413cddf9
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.0-dev.14"
rive_native:
dependency: transitive
description:
name: rive_native
sha256: e9c7d36f19eb6d32f563825d4e9d5032b19a36d3ca3341641431035bca022d19
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.17"
scroll_to_index: scroll_to_index:
dependency: transitive dependency: transitive
description: description:
@@ -561,7 +720,7 @@ packages:
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: transitive
description: description:
name: shared_preferences name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
@@ -694,7 +853,7 @@ packages:
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
syncfusion_flutter_charts: syncfusion_flutter_charts:
dependency: "direct main" dependency: transitive
description: description:
name: syncfusion_flutter_charts name: syncfusion_flutter_charts
sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8" sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8"
@@ -877,6 +1036,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.13.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -885,6 +1052,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,71 +1 @@
name: blog_app name: blog_app
description: "博客App"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.7.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# 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
json_annotation: ^4.9.0
intl: ^0.19.0
easy_refresh: ^3.4.0
timelines_plus: ^1.0.7
syncfusion_localizations: ^30.1.37
syncfusion_flutter_charts: ^30.1.41
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
build_runner: ^2.4.5
json_serializable: ^6.7.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
flutter:
uses-material-design: true
assets:
- assets/images/
fonts:
- family: CustomFont
fonts:
- asset: assets/fonts/custom.ttf

View File

@@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <rive_native/rive_native_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
RiveNativePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RiveNativePlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
rive_native
url_launcher_windows url_launcher_windows
) )