factor:布局颜色重构
This commit is contained in:
121
lib/layout/app_drawer.dart
Normal file
121
lib/layout/app_drawer.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:blog_app/layout/menu.dart';
|
||||||
|
import 'package:blog_app/layout/theme_layout.dart';
|
||||||
|
import 'package:flutter/material.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
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'menu.dart';
|
|
||||||
|
|
||||||
Widget buildBottomNavigationBar({
|
|
||||||
required BuildContext context,
|
|
||||||
required int index,
|
|
||||||
required Function(int) onTap,
|
|
||||||
}) {
|
|
||||||
return BottomNavigationBar(
|
|
||||||
currentIndex: index,
|
|
||||||
onTap: onTap,
|
|
||||||
items: bottomNavItems,
|
|
||||||
type: BottomNavigationBarType.fixed,
|
|
||||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
|
||||||
unselectedItemColor: Colors.grey,
|
|
||||||
showSelectedLabels: true,
|
|
||||||
showUnselectedLabels: true,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
elevation: 8,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
128
lib/layout/theme_layout.dart
Normal file
128
lib/layout/theme_layout.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'package:blog_app/provider/theme_provider.dart';
|
||||||
|
import 'package:blog_app/utils/theme_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ThemeLayout extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const ThemeLayout({super.key, this.child});
|
||||||
|
|
||||||
|
Widget _buildThemeColorList(BuildContext context, ThemeProvider provider) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 1.2,
|
||||||
|
),
|
||||||
|
itemCount: provider.availableThemes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final themeColor = provider.availableThemes[index];
|
||||||
|
final isSelected = provider.currentTheme == themeColor;
|
||||||
|
|
||||||
|
return _buildThemeColorItem(
|
||||||
|
themeColor: themeColor,
|
||||||
|
isSelected: isSelected,
|
||||||
|
onTap: () => provider.changeTheme(themeColor),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildThemeColorItem({
|
||||||
|
required ThemeColor themeColor,
|
||||||
|
required bool isSelected,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
isSelected
|
||||||
|
? themeColor.primaryColor.withAlpha(50)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? themeColor.primaryColor : Colors.grey.shade300,
|
||||||
|
width: isSelected ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 颜色圆点
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeColor.primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(10),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
themeColor.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color:
|
||||||
|
isSelected ? themeColor.primaryColor : Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDarkModeSection(BuildContext context, ThemeProvider provider) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
provider.isDarkMode ? Icons.dark_mode : Icons.light_mode,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: const Text('暗黑模式'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: provider.isDarkMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.toggleDarkMode(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
provider.toggleDarkMode(!provider.isDarkMode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<ThemeProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Text('主题颜色', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
),
|
||||||
|
_buildDarkModeSection(context, provider),
|
||||||
|
_buildThemeColorList(context, provider),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:blog_app/pages/home_page.dart';
|
import 'package:blog_app/pages/home_page.dart';
|
||||||
import 'package:blog_app/provider/blog.dart';
|
import 'package:blog_app/provider/blog.dart';
|
||||||
|
import 'package:blog_app/provider/theme_provider.dart';
|
||||||
import 'package:blog_app/utils/sp_utils.dart';
|
import 'package:blog_app/utils/sp_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -12,6 +13,7 @@ void main() async {
|
|||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => BlogProvider()),
|
ChangeNotifierProvider(create: (context) => BlogProvider()),
|
||||||
|
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||||
],
|
],
|
||||||
child: MyApp(),
|
child: MyApp(),
|
||||||
),
|
),
|
||||||
@@ -23,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,
|
||||||
useMaterial3: true,
|
|
||||||
fontFamily: 'CustomFont',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class _BlogDetailPageState extends State<BlogDetailPage> {
|
|||||||
return FloatingActionButton(
|
return 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 {
|
} else {
|
||||||
@@ -73,7 +73,11 @@ class _BlogDetailPageState extends State<BlogDetailPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
buildTocPanel(tocController: tocController, showToc: _showToc),
|
buildTocPanel(
|
||||||
|
context: context,
|
||||||
|
tocController: tocController,
|
||||||
|
showToc: _showToc,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,13 +80,14 @@ class _BlogPageState extends State<BlogPage> {
|
|||||||
if (provider.error != null) {
|
if (provider.error != null) {
|
||||||
return buildErrorInfo(
|
return buildErrorInfo(
|
||||||
errorInfo: provider.error!,
|
errorInfo: provider.error!,
|
||||||
onPressed: () => provider.refreshBlogList,
|
onPressed: () => provider.refreshBlogList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
itemCount: provider.blogList.length,
|
itemCount: provider.blogList.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final blog = provider.blogList[index];
|
final blog = provider.blogList[index];
|
||||||
return InkWell(
|
return InkWell(
|
||||||
@@ -94,9 +95,6 @@ class _BlogPageState extends State<BlogPage> {
|
|||||||
child: BlogCard(blog: blog),
|
child: BlogCard(blog: blog),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return SizedBox(height: 8);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ class _CategoryPageState extends State<CategoryPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
itemCount: provider.categoryList.length,
|
itemCount: provider.categoryList.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final category = provider.categoryList[index];
|
final category = provider.categoryList[index];
|
||||||
return buildCategoryCard(context, category);
|
return buildCategoryCard(context, category);
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return SizedBox(height: 8);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:blog_app/apis/blog.dart';
|
import 'package:blog_app/apis/blog.dart';
|
||||||
import 'package:blog_app/layout/drawer.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/layout/nav_bar.dart';
|
|
||||||
import 'package:blog_app/models/blog.dart';
|
import 'package:blog_app/models/blog.dart';
|
||||||
import 'package:blog_app/widget/search.dart';
|
import 'package:blog_app/widget/search.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -165,7 +165,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
return buildSearchResultList(context, _searchResults);
|
return buildSearchResultList(context, _searchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onBottomNavTap(int index) {
|
void _onTapNavbarItem(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentPageIndex = index;
|
_currentPageIndex = index;
|
||||||
if (_isSearching) {
|
if (_isSearching) {
|
||||||
@@ -194,37 +194,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDrawerBody() {
|
|
||||||
return ListView(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
...List.generate(
|
|
||||||
pages.length,
|
|
||||||
(index) => buildDrawerItem(
|
|
||||||
context: context,
|
|
||||||
page: pages[index],
|
|
||||||
isSelected: index == _currentPageIndex,
|
|
||||||
onTap: () => onTapDrawerItem(index),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDrawer() {
|
|
||||||
return Drawer(
|
|
||||||
child: Container(
|
|
||||||
color: Colors.white,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
buildDrawerHeader(context),
|
|
||||||
Expanded(child: _buildDrawerBody()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -235,12 +204,14 @@ class _HomePageState extends State<HomePage> {
|
|||||||
leading: _buildLeading(),
|
leading: _buildLeading(),
|
||||||
actions: _buildAppBarActions(),
|
actions: _buildAppBarActions(),
|
||||||
),
|
),
|
||||||
drawer: _buildDrawer(),
|
drawer: AppDrawer(
|
||||||
|
currentPageIndex: _currentPageIndex,
|
||||||
|
onTapDrawerItem: onTapDrawerItem,
|
||||||
|
),
|
||||||
body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
|
body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
|
||||||
bottomNavigationBar: buildBottomNavigationBar(
|
bottomNavigationBar: AppNavbar(
|
||||||
context: context,
|
currentPageIndex: _currentPageIndex,
|
||||||
index: _currentPageIndex,
|
onTapNavbarItem: _onTapNavbarItem,
|
||||||
onTap: _onBottomNavTap,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,10 +76,8 @@ class StatsPageState extends State<StatsPage> {
|
|||||||
buildChartDivider(context),
|
buildChartDivider(context),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: barChart(
|
child: rankChart(
|
||||||
context: context,
|
context: context,
|
||||||
xAxisName: '名称',
|
|
||||||
yAxisName: '访问量',
|
|
||||||
unit: '人次',
|
unit: '人次',
|
||||||
data: provider.visitRankStats,
|
data: provider.visitRankStats,
|
||||||
),
|
),
|
||||||
@@ -114,10 +112,8 @@ class StatsPageState extends State<StatsPage> {
|
|||||||
buildChartDivider(context),
|
buildChartDivider(context),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: barChart(
|
child: rankChart(
|
||||||
context: context,
|
context: context,
|
||||||
xAxisName: '名称',
|
|
||||||
yAxisName: '时长',
|
|
||||||
unit: '分钟',
|
unit: '分钟',
|
||||||
data: provider.readRankStats,
|
data: provider.readRankStats,
|
||||||
),
|
),
|
||||||
|
|||||||
76
lib/provider/theme_provider.dart
Normal file
76
lib/provider/theme_provider.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:blog_app/utils/theme_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class ThemeProvider with ChangeNotifier {
|
||||||
|
static const String _themeKey = 'selected_theme';
|
||||||
|
static const String _darkModeKey = 'is_dark_mode';
|
||||||
|
|
||||||
|
bool isDarkMode = false;
|
||||||
|
ThemeColor currentTheme = defaultThemes[0];
|
||||||
|
|
||||||
|
late SharedPreferences _prefs;
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
|
ThemeProvider() {
|
||||||
|
_initPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 SharedPreferences
|
||||||
|
Future<void> _initPreferences() async {
|
||||||
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
_loadPreferences();
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载存储的设置
|
||||||
|
void _loadPreferences() {
|
||||||
|
isDarkMode = _prefs.getBool(_darkModeKey) ?? false;
|
||||||
|
|
||||||
|
// 加载主题色
|
||||||
|
final themeName = _prefs.getString(_themeKey);
|
||||||
|
if (themeName != null) {
|
||||||
|
currentTheme = getThemeColor(themeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存主题色
|
||||||
|
Future<void> _saveTheme() async {
|
||||||
|
await _prefs.setString(_themeKey, currentTheme.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存暗黑模式
|
||||||
|
Future<void> _saveDarkMode() async {
|
||||||
|
await _prefs.setBool(_darkModeKey, isDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ThemeColor> get availableThemes => defaultThemes;
|
||||||
|
|
||||||
|
// 切换明暗模式
|
||||||
|
void toggleDarkMode(bool value) {
|
||||||
|
isDarkMode = value;
|
||||||
|
_saveDarkMode();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更改主题色
|
||||||
|
void changeTheme(ThemeColor theme) {
|
||||||
|
currentTheme = theme;
|
||||||
|
_saveTheme();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData get currentThemeData {
|
||||||
|
return ThemeData(
|
||||||
|
primarySwatch: currentTheme.materialColor,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: currentTheme.primaryColor,
|
||||||
|
brightness: isDarkMode ? Brightness.dark : Brightness.light,
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/utils/theme_utils.dart
Normal file
87
lib/utils/theme_utils.dart
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ThemeColor {
|
||||||
|
final String name;
|
||||||
|
final Color primaryColor;
|
||||||
|
final MaterialColor materialColor;
|
||||||
|
|
||||||
|
ThemeColor({
|
||||||
|
required this.name,
|
||||||
|
required this.primaryColor,
|
||||||
|
required this.materialColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ThemeColor> defaultThemes = [
|
||||||
|
ThemeColor(
|
||||||
|
name: '梦幻紫',
|
||||||
|
primaryColor: Color(0xFF8B5CF6),
|
||||||
|
materialColor: MaterialColor(0xFF8B5CF6, {
|
||||||
|
50: Color(0xFFF5F3FF),
|
||||||
|
100: Color(0xFFEDE9FE),
|
||||||
|
200: Color(0xFFDDD6FE),
|
||||||
|
300: Color(0xFFC4B5FD),
|
||||||
|
400: Color(0xFFA78BFA),
|
||||||
|
500: Color(0xFF8B5CF6),
|
||||||
|
600: Color(0xFF7C3AED),
|
||||||
|
700: Color(0xFF6D28D9),
|
||||||
|
800: Color(0xFF5B21B6),
|
||||||
|
900: Color(0xFF4C1D95),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ThemeColor(
|
||||||
|
name: '活力橙',
|
||||||
|
primaryColor: Color(0xFFF59E0B),
|
||||||
|
materialColor: MaterialColor(0xFFF59E0B, {
|
||||||
|
50: Color(0xFFFFFBEB),
|
||||||
|
100: Color(0xFFFEF3C7),
|
||||||
|
200: Color(0xFFFDE68A),
|
||||||
|
300: Color(0xFFFCD34D),
|
||||||
|
400: Color(0xFFFBBF24),
|
||||||
|
500: Color(0xFFF59E0B),
|
||||||
|
600: Color(0xFFD97706),
|
||||||
|
700: Color(0xFFB45309),
|
||||||
|
800: Color(0xFF92400E),
|
||||||
|
900: Color(0xFF78350F),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ThemeColor(
|
||||||
|
name: '浪漫粉',
|
||||||
|
primaryColor: Color(0xFFEC4899),
|
||||||
|
materialColor: MaterialColor(0xFFEC4899, {
|
||||||
|
50: Color(0xFFFDF2F8),
|
||||||
|
100: Color(0xFFFCE7F3),
|
||||||
|
200: Color(0xFFFBCFE8),
|
||||||
|
300: Color(0xFFF9A8D4),
|
||||||
|
400: Color(0xFFF472B6),
|
||||||
|
500: Color(0xFFEC4899),
|
||||||
|
600: Color(0xFFDB2777),
|
||||||
|
700: Color(0xFFBE185D),
|
||||||
|
800: Color(0xFF9D174D),
|
||||||
|
900: Color(0xFF831843),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ThemeColor(
|
||||||
|
name: '清新青',
|
||||||
|
primaryColor: Color(0xFF06B6D4),
|
||||||
|
materialColor: MaterialColor(0xFF06B6D4, {
|
||||||
|
50: Color(0xFFF0FDFA),
|
||||||
|
100: Color(0xFFCCFBF1),
|
||||||
|
200: Color(0xFF99F6E4),
|
||||||
|
300: Color(0xFF5EEAD4),
|
||||||
|
400: Color(0xFF2DD4BF),
|
||||||
|
500: Color(0xFF06B6D4),
|
||||||
|
600: Color(0xFF0891B2),
|
||||||
|
700: Color(0xFF0E7490),
|
||||||
|
800: Color(0xFF155E75),
|
||||||
|
900: Color(0xFF164E63),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
ThemeColor getThemeColor(String themeName) {
|
||||||
|
return defaultThemes.firstWhere(
|
||||||
|
(theme) => theme.name == themeName,
|
||||||
|
orElse: () => defaultThemes[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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.deepOrange),
|
_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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -126,7 +132,7 @@ class BlogCard extends StatelessWidget {
|
|||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Container(height: 1, width: 80, color: colors.primary),
|
Container(height: 1, width: 80, color: colors.primary),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
_buildBlogSummary(),
|
_buildBlogSummary(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -181,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),
|
||||||
@@ -220,11 +228,17 @@ Widget buildBlogListItem(BuildContext context, Blog blog, int index) {
|
|||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text('$index.', style: TextStyle(fontWeight: FontWeight.w500)),
|
Text(
|
||||||
|
'$index.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurface,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
formatDateString(blog.createTime),
|
formatDateString(blog.createTime),
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -283,3 +283,67 @@ Widget pieChart({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color _getRankColor(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0: // 金牌
|
||||||
|
return const Color(0xFFFFD700); // 标准金色
|
||||||
|
case 1: // 银牌
|
||||||
|
return const Color(0xFFC0C0C0); // 标准银色
|
||||||
|
case 2: // 铜牌
|
||||||
|
return const Color(0xFFCD7F32); // 标准铜色
|
||||||
|
default:
|
||||||
|
return Theme.of(context).colorScheme.onSurface; // 普通颜色
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget rankChart({
|
||||||
|
required BuildContext context,
|
||||||
|
required String unit,
|
||||||
|
required List<ChartData> data,
|
||||||
|
}) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = data[index];
|
||||||
|
final rank = index + 1;
|
||||||
|
return SizedBox(
|
||||||
|
height: 35,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 25,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'$rank.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _getRankColor(context, index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
item.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: _getRankColor(context, index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${item.value.toStringAsFixed(0)} $unit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: index < 3 ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: _getRankColor(context, index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Widget buildMarkdown({
|
|||||||
}) => MarkdownWidget(data: data, tocController: tocController);
|
}) => MarkdownWidget(data: data, tocController: tocController);
|
||||||
|
|
||||||
Widget buildTocPanel({
|
Widget buildTocPanel({
|
||||||
|
required BuildContext context,
|
||||||
required TocController tocController,
|
required TocController tocController,
|
||||||
required bool showToc,
|
required bool showToc,
|
||||||
}) => Visibility(
|
}) => Visibility(
|
||||||
@@ -18,7 +19,7 @@ Widget buildTocPanel({
|
|||||||
width: 250,
|
width: 250,
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -34,7 +35,7 @@ Widget buildTocPanel({
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[100],
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
topRight: Radius.circular(12),
|
topRight: Radius.circular(12),
|
||||||
|
|||||||
@@ -27,14 +27,13 @@ Widget buildSearchField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
|
Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return BuildCard(
|
return BuildCard(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
blog.title,
|
blog.title,
|
||||||
style: TextStyle(
|
style: TextStyle(fontWeight: FontWeight.bold, color: colors.primary),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -42,7 +41,7 @@ Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
|
|||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
blog.content,
|
blog.content,
|
||||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
style: TextStyle(color: colors.secondary, fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class _YearSelectorState extends State<YearSelector>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user