Compare commits
9 Commits
d512a77f5f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a10aae25e | |||
| f11b8d6a30 | |||
| e38e26e685 | |||
| 8f0f641d37 | |||
| 50d7657a93 | |||
| 59b439a8ac | |||
| 1132b78ecd | |||
| 61922cbcd8 | |||
| 47b6fb9937 |
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 6.5 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 34 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
@@ -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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/layout/app_navbar.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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 _buildBlogDetailContent(BuildContext context, Blog blogDetail) {
|
Widget _buildContent(BlogProvider provider) {
|
||||||
|
if (provider.error != null) {
|
||||||
|
return buildErrorInfo(
|
||||||
|
errorInfo: provider.error!,
|
||||||
|
onPressed: () => provider.queryBlogById(widget.blogId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
freshController: _freshController,
|
children: [
|
||||||
onRefresh: _onRefresh,
|
Expanded(
|
||||||
onLoad: _onLoad,
|
child: buildEasyRefresh(
|
||||||
body: buildBlogList(),
|
freshController: _freshController,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onLoad: _onLoad,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
if (blogProvider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
_buildBlogList(blogProvider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (_showScrollToTop)
|
if (_showScrollToTop)
|
||||||
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
|
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
|
||||||
|
|||||||
@@ -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 {
|
Widget _buildBlogList(BlogProvider provider) {
|
||||||
try {
|
return ListView.separated(
|
||||||
final result = await queryBlogByConditionApi(widget.category, null, null);
|
itemCount: provider.blogList.length,
|
||||||
setState(() {
|
|
||||||
blogs = result;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception('获取博客列表失败: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBlogList() {
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: blogs.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()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 {
|
Widget _buildContent(BlogProvider provider) {
|
||||||
try {
|
if (provider.error != null) {
|
||||||
final result = await queryBlogCategoryApi();
|
return buildErrorInfo(
|
||||||
setState(() {
|
errorInfo: provider.error!,
|
||||||
categories = result;
|
onPressed: () => provider.refreshBlogByYear(),
|
||||||
});
|
);
|
||||||
} catch (e) {
|
|
||||||
throw Exception('获取博客分类失败: $e');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
Widget _buildDrawer() {
|
onTapNavbarItem: _onTapNavbarItem,
|
||||||
return Drawer(
|
|
||||||
child: Container(
|
|
||||||
color: Colors.white,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 抽屉头部
|
|
||||||
buildDrawerHeader(context),
|
|
||||||
// 菜单项列表
|
|
||||||
Expanded(child: _buildDrawerBody()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<BlogProvider>().refreshBlogStats();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadStatsList() async {
|
Widget _buildBlogStatsGrid(BlogProvider provider) {
|
||||||
try {
|
|
||||||
final approvedResult = await queryBlogApprovedStatsApi(currentYear);
|
|
||||||
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) {
|
|
||||||
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(
|
|
||||||
children: [
|
return Stack(
|
||||||
YearSelector(
|
children: [
|
||||||
initialYear: DateTime.now().year,
|
if (blogProvider.isLoading)
|
||||||
minYear: 2000,
|
buildLoadingIndicator()
|
||||||
maxYear: 2100,
|
else
|
||||||
onYearChanged:
|
SingleChildScrollView(child: _buildContent(blogProvider)),
|
||||||
(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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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')),
|
|
||||||
);
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,26 +120,20 @@ 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: [
|
_buildBlogTitle(context),
|
||||||
_buildBlogTitle(context),
|
SizedBox(height: 16),
|
||||||
SizedBox(height: 16),
|
_buildBlogLabel(),
|
||||||
_buildBlogLabel(),
|
SizedBox(height: 16),
|
||||||
SizedBox(height: 16),
|
Container(height: 1, width: 80, color: colors.primary),
|
||||||
Container(
|
SizedBox(height: 16),
|
||||||
height: 1,
|
_buildBlogSummary(context),
|
||||||
width: 80,
|
],
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
_buildBlogSummary(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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(
|
style: TextStyle(
|
||||||
index.toString(),
|
color: colors.onSurface,
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: colors.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
// 发布时间
|
Text(
|
||||||
SizedBox(
|
formatDateString(blog.createTime),
|
||||||
width: 150,
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
child: Text(
|
|
||||||
blog.createTime,
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
// 标题
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
blog.title,
|
blog.title,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
@@ -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])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
rive_native
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
183
pubspec.lock
@@ -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:
|
||||||
|
|||||||
72
pubspec.yaml
@@ -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
|
|
||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
rive_native
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||