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/provider/blog.dart';
|
||||
import 'package:blog_app/provider/theme_provider.dart';
|
||||
import 'package:blog_app/utils/sp_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -12,6 +13,7 @@ void main() async {
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => BlogProvider()),
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
@@ -23,13 +25,12 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeProvider = context.watch<ThemeProvider>();
|
||||
|
||||
return MaterialApp(
|
||||
home: HomePage(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: 'CustomFont',
|
||||
),
|
||||
theme: themeProvider.currentThemeData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class _BlogDetailPageState extends State<BlogDetailPage> {
|
||||
return FloatingActionButton(
|
||||
onPressed: () => setState(() => _showToc = !_showToc),
|
||||
mini: true,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Icon(_showToc ? Icons.close : Icons.list, color: Colors.white),
|
||||
);
|
||||
} 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) {
|
||||
return buildErrorInfo(
|
||||
errorInfo: provider.error!,
|
||||
onPressed: () => provider.refreshBlogList,
|
||||
onPressed: () => provider.refreshBlogList(),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemCount: provider.blogList.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final blog = provider.blogList[index];
|
||||
return InkWell(
|
||||
@@ -94,9 +95,6 @@ class _BlogPageState extends State<BlogPage> {
|
||||
child: BlogCard(blog: blog),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return SizedBox(height: 8);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,11 @@ class _CategoryPageState extends State<CategoryPage> {
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: provider.categoryList.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final category = provider.categoryList[index];
|
||||
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/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/nav_bar.dart';
|
||||
import 'package:blog_app/models/blog.dart';
|
||||
import 'package:blog_app/widget/search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -165,7 +165,7 @@ class _HomePageState extends State<HomePage> {
|
||||
return buildSearchResultList(context, _searchResults);
|
||||
}
|
||||
|
||||
void _onBottomNavTap(int index) {
|
||||
void _onTapNavbarItem(int index) {
|
||||
setState(() {
|
||||
_currentPageIndex = index;
|
||||
if (_isSearching) {
|
||||
@@ -194,37 +194,6 @@ class _HomePageState extends State<HomePage> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -235,12 +204,14 @@ class _HomePageState extends State<HomePage> {
|
||||
leading: _buildLeading(),
|
||||
actions: _buildAppBarActions(),
|
||||
),
|
||||
drawer: _buildDrawer(),
|
||||
drawer: AppDrawer(
|
||||
currentPageIndex: _currentPageIndex,
|
||||
onTapDrawerItem: onTapDrawerItem,
|
||||
),
|
||||
body: Padding(padding: EdgeInsets.all(10), child: _buildPageView()),
|
||||
bottomNavigationBar: buildBottomNavigationBar(
|
||||
context: context,
|
||||
index: _currentPageIndex,
|
||||
onTap: _onBottomNavTap,
|
||||
bottomNavigationBar: AppNavbar(
|
||||
currentPageIndex: _currentPageIndex,
|
||||
onTapNavbarItem: _onTapNavbarItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,10 +76,8 @@ class StatsPageState extends State<StatsPage> {
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: barChart(
|
||||
child: rankChart(
|
||||
context: context,
|
||||
xAxisName: '名称',
|
||||
yAxisName: '访问量',
|
||||
unit: '人次',
|
||||
data: provider.visitRankStats,
|
||||
),
|
||||
@@ -114,10 +112,8 @@ class StatsPageState extends State<StatsPage> {
|
||||
buildChartDivider(context),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: barChart(
|
||||
child: rankChart(
|
||||
context: context,
|
||||
xAxisName: '名称',
|
||||
yAxisName: '时长',
|
||||
unit: '分钟',
|
||||
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,
|
||||
children: [
|
||||
if (blog.isGreat)
|
||||
_buildBlogLabelItem('精品', Icons.workspace_premium, Colors.deepOrange),
|
||||
_buildBlogLabelItem(
|
||||
'精品',
|
||||
Icons.workspace_premium,
|
||||
Colors.deepOrange,
|
||||
),
|
||||
if (blog.topValue > 1)
|
||||
_buildBlogLabelItem('置顶', Icons.push_pin, Colors.red),
|
||||
_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(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
blog.summary,
|
||||
style: TextStyle(color: Colors.grey.shade700, height: 1.6),
|
||||
style: TextStyle(color: colors.secondary, height: 1.6),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
@@ -126,7 +132,7 @@ class BlogCard extends StatelessWidget {
|
||||
SizedBox(height: 16),
|
||||
Container(height: 1, width: 80, color: colors.primary),
|
||||
SizedBox(height: 16),
|
||||
_buildBlogSummary(),
|
||||
_buildBlogSummary(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -181,14 +187,16 @@ Widget buildCategoryCard(BuildContext context, BlogCategory category) {
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.primary.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(50),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
category.count.toString(),
|
||||
style: TextStyle(color: colors.primary, fontWeight: FontWeight.bold),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onTap: () => navigateToBlogList(context, category),
|
||||
@@ -220,11 +228,17 @@ Widget buildBlogListItem(BuildContext context, Blog blog, int index) {
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('$index.', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text(
|
||||
'$index.',
|
||||
style: TextStyle(
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
formatDateString(blog.createTime),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
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);
|
||||
|
||||
Widget buildTocPanel({
|
||||
required BuildContext context,
|
||||
required TocController tocController,
|
||||
required bool showToc,
|
||||
}) => Visibility(
|
||||
@@ -18,7 +19,7 @@ Widget buildTocPanel({
|
||||
width: 250,
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -34,7 +35,7 @@ Widget buildTocPanel({
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
|
||||
@@ -27,14 +27,13 @@ Widget buildSearchField({
|
||||
}
|
||||
|
||||
Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return BuildCard(
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
blog.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: colors.primary),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -42,7 +41,7 @@ Widget buildSearchResultItem(BuildContext context, BlogSearch blog) {
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
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(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user