深色模式、首页设置页面和功能优化

This commit is contained in:
Developer
2026-04-02 07:06:55 +08:00
parent f0a62ed68b
commit 954d173329
88 changed files with 12157 additions and 7578 deletions

View File

@@ -1,19 +1,20 @@
// 时间: 2026-03-22
// 功能: 全站诗词搜索页(收藏入口可跳转)
// 介绍: 调用 search.php配合 NetworkListenerService 上报加载与搜索完成事件
// 最新变化: 初始版本
// 最新变化: 2026-04-02 支持深色模式
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/app_constants.dart';
import '../../services/network_listener_service.dart';
import '../../services/get/theme_controller.dart';
import '../../utils/http/poetry_api.dart';
import '../main_navigation.dart';
import '../../widgets/main_navigation.dart';
/// 诗词搜索页(独立路由栈页面)
class ActiveSearchPage extends StatefulWidget {
const ActiveSearchPage({super.key, this.initialQuery});
/// 从收藏页带入的预填关键词
final String? initialQuery;
@override
@@ -26,8 +27,8 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
final ThemeController _themeController = Get.find<ThemeController>();
/// 空=不限字段name / keywords / introduce 见 API 文档
String _field = '';
int _page = 1;
final int _pageSize = 20;
@@ -106,258 +107,336 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
Widget build(BuildContext context) {
final loading = isNetworkLoading(_loadKey);
// 检查是否在 TabBarView 中显示(通过上下文判断)
final bool isInTabBarView = ModalRoute.of(context)?.settings.name == null;
bool isInTabBarView = false;
try {
final ancestor = context.findAncestorWidgetOfExactType<TabBarView>();
isInTabBarView = ancestor != null;
} catch (e) {
isInTabBarView = false;
}
return Scaffold(
backgroundColor: Colors.grey[50],
// 当在 TabBarView 中时显示自定义标题栏,在单独页面中不显示
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 自定义标题栏
if (isInTabBarView)
SafeArea(
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: Colors.white,
child: Row(
return Obx(() {
final isDark = _themeController.isDarkModeRx.value;
return Scaffold(
backgroundColor: isDark ? const Color(0xFF121212) : Colors.grey[50],
appBar: !isInTabBarView
? AppBar(
title: Row(
children: [
// 返回按钮
//todo
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () {
// 检查是否可以返回,避免黑屏
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
// 如果无法返回(如在 TabBarView 中),跳转到主页
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const MainNavigation(),
),
);
}
},
tooltip: '返回上一页',
Icon(
Icons.travel_explore,
size: 20,
color: isDark ? Colors.white : Colors.black87,
),
// 标题
Expanded(
child: Row(
children: [
const Icon(
Icons.travel_explore,
size: 20,
color: Colors.black87,
const SizedBox(width: 8),
Text(
'诗词搜索',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
backgroundColor: isDark ? Colors.grey[900] : Colors.white,
elevation: 1,
actions: [
IconButton(
icon: Icon(
Icons.more_vert,
color: isDark ? Colors.white : Colors.black87,
),
onPressed: () {
showModalBottomSheet(
context: context,
backgroundColor: isDark
? Colors.grey[850]
: Colors.white,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
Icons.history,
color: isDark
? Colors.grey[300]
: Colors.black87,
),
title: Text(
'搜索历史(开发中)',
style: TextStyle(
color: isDark
? Colors.white
: Colors.black87,
),
),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: Icon(
Icons.settings,
color: isDark
? Colors.grey[300]
: Colors.black87,
),
title: Text(
'搜索设置(开发中)',
style: TextStyle(
color: isDark
? Colors.white
: Colors.black87,
),
),
onTap: () {
Navigator.pop(context);
},
),
],
),
const SizedBox(width: 8),
const Text(
'诗词搜索',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
);
},
tooltip: '更多',
),
],
)
: null,
body: SafeArea(
top: !isInTabBarView,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.fromLTRB(
AppConstants.pageHorizontalPadding,
8,
AppConstants.pageHorizontalPadding,
4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Material(
color: isDark ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(12),
child: TextField(
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _runSearch(reset: true),
style: TextStyle(
color: isDark ? Colors.white : Colors.black87,
),
decoration: InputDecoration(
hintText: '输入关键词,搜标题 / 标签 / 译文…',
hintStyle: TextStyle(
color: isDark
? Colors.grey[500]
: Colors.grey,
),
prefixIcon: Icon(
Icons.search,
color: isDark
? Colors.grey[400]
: Colors.grey,
),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: isDark
? Colors.grey[400]
: Colors.grey,
),
onPressed: () {
_controller.clear();
setState(() {
_items = [];
_total = 0;
_error = null;
});
},
)
: IconButton(
icon: Icon(
Icons.arrow_forward,
color: isDark
? Colors.grey[400]
: Colors.grey,
),
tooltip: '搜索',
onPressed: () =>
_runSearch(reset: true),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
onChanged: (_) => setState(() {}),
),
),
Wrap(
spacing: 4,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
ChoiceChip(
label: Text(
'全部',
style: TextStyle(
color: _field.isEmpty
? Colors.white
: (isDark
? Colors.grey[300]
: Colors.black87),
),
),
selected: _field.isEmpty,
selectedColor: AppConstants.primaryColor,
backgroundColor: isDark
? Colors.grey[800]
: Colors.grey[200],
onSelected: (_) {
setState(() => _field = '');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: Text(
'标题',
style: TextStyle(
color: _field == 'name'
? Colors.white
: (isDark
? Colors.grey[300]
: Colors.black87),
),
),
selected: _field == 'name',
selectedColor: AppConstants.primaryColor,
backgroundColor: isDark
? Colors.grey[800]
: Colors.grey[200],
onSelected: (_) {
setState(() => _field = 'name');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: Text(
'标签',
style: TextStyle(
color: _field == 'keywords'
? Colors.white
: (isDark
? Colors.grey[300]
: Colors.black87),
),
),
selected: _field == 'keywords',
selectedColor: AppConstants.primaryColor,
backgroundColor: isDark
? Colors.grey[800]
: Colors.grey[200],
onSelected: (_) {
setState(() => _field = 'keywords');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: Text(
'译文',
style: TextStyle(
color: _field == 'introduce'
? Colors.white
: (isDark
? Colors.grey[300]
: Colors.black87),
),
),
selected: _field == 'introduce',
selectedColor: AppConstants.primaryColor,
backgroundColor: isDark
? Colors.grey[800]
: Colors.grey[200],
onSelected: (_) {
setState(() => _field = 'introduce');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
],
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
_error!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
),
),
),
],
),
),
// 更多按钮
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.black87),
onPressed: () {
// 更多按钮的点击事件
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.history),
title: const Text('搜索历史(开发中)'),
onTap: () {
Navigator.pop(context);
// 实现搜索历史功能
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('搜索设置(开发中)'),
onTap: () {
Navigator.pop(context);
// 实现搜索设置功能
},
),
],
),
),
);
},
tooltip: '更多',
Expanded(
child: RefreshIndicator(
color: AppConstants.primaryColor,
onRefresh: () async {
await _runSearch(reset: true);
},
child: _buildListBody(loading, isDark),
),
),
],
),
),
),
// 搜索内容区域
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.fromLTRB(
AppConstants.pageHorizontalPadding,
isInTabBarView ? 8 : 4,
AppConstants.pageHorizontalPadding,
4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Material(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
child: TextField(
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _runSearch(reset: true),
decoration: InputDecoration(
hintText: '输入关键词,搜标题 / 标签 / 译文…',
prefixIcon: const Icon(
Icons.search,
color: Colors.grey,
),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
setState(() {
_items = [];
_total = 0;
_error = null;
});
},
)
: IconButton(
icon: const Icon(Icons.arrow_forward),
tooltip: '搜索',
onPressed: () => _runSearch(reset: true),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
onChanged: (_) => setState(() {}),
),
),
Wrap(
spacing: 4,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const Text('', style: TextStyle(fontSize: 13)),
// const Text('范围:', style: TextStyle(fontSize: 13)),
ChoiceChip(
label: const Text('全部'),
selected: _field.isEmpty,
onSelected: (_) {
setState(() => _field = '');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: const Text('标题'),
selected: _field == 'name',
onSelected: (_) {
setState(() => _field = 'name');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: const Text('标签'),
selected: _field == 'keywords',
onSelected: (_) {
setState(() => _field = 'keywords');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
ChoiceChip(
label: const Text('译文'),
selected: _field == 'introduce',
onSelected: (_) {
setState(() => _field = 'introduce');
if (_controller.text.trim().isNotEmpty) {
_runSearch(reset: true);
}
},
),
],
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
_error!,
style: const TextStyle(
color: Colors.red,
fontSize: 13,
),
),
),
],
),
),
Expanded(
child: RefreshIndicator(
color: AppConstants.primaryColor,
onRefresh: () async {
await _runSearch(reset: true);
},
child: _buildListBody(loading),
),
),
],
),
],
),
],
),
// 始终显示返回按钮
// todo 二次黑屏处理 标记
floatingActionButton: FloatingActionButton(
onPressed: () {
// 检查是否可以返回,避免黑屏
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
// 如果无法返回,跳转到主页
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainNavigation()),
);
}
},
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
child: const Icon(Icons.arrow_back),
tooltip: '返回上一页',
),
);
),
floatingActionButton: !isInTabBarView
? FloatingActionButton(
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainNavigation()),
);
}
},
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
tooltip: '返回上一页',
child: const Icon(Icons.arrow_back),
)
: null,
);
});
}
Widget _buildListBody(bool loading) {
Widget _buildListBody(bool loading, bool isDark) {
if (loading && _items.isEmpty) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -373,12 +452,19 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.15),
Icon(Icons.manage_search, size: 64, color: Colors.grey[400]),
Icon(
Icons.manage_search,
size: 64,
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
const SizedBox(height: 12),
Center(
child: Text(
_controller.text.trim().isEmpty ? '输入关键词开始搜索' : '暂无结果',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
style: TextStyle(
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontSize: 16,
),
),
),
],
@@ -402,8 +488,16 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
? const CircularProgressIndicator()
: TextButton.icon(
onPressed: () => _runSearch(reset: false),
icon: const Icon(Icons.expand_more),
label: const Text('加载更多'),
icon: Icon(
Icons.expand_more,
color: isDark ? Colors.grey[400] : Colors.black87,
),
label: Text(
'加载更多',
style: TextStyle(
color: isDark ? Colors.grey[400] : Colors.black87,
),
),
),
),
);
@@ -412,6 +506,7 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 1,
color: isDark ? Colors.grey[850] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
@@ -424,7 +519,10 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
p.name.isNotEmpty ? p.name : p.url,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
style: TextStyle(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
@@ -434,19 +532,28 @@ class _ActiveSearchPageState extends State<ActiveSearchPage>
if (p.alias.isNotEmpty)
Text(
'📜 ${p.alias}',
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[700],
),
),
if (p.introduce.isNotEmpty)
Text(
p.introduce,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey[300] : Colors.grey[800],
),
),
const SizedBox(height: 4),
Text(
'👍 ${p.like} · 🔥 ${p.hitsTotal}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
],
),

View File

@@ -1,156 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/app_constants.dart';
import 'tags/corr_page.dart';
import '../../services/get/category_controller.dart';
import '../../services/get/theme_controller.dart';
/// 时间: 2026-04-01
/// 功能: 分类页面
/// 介绍: 展示诗词分类,包括场景分类和朝代分类
/// 最新变化: 重新设计iOS风格布局减少间距加大字体显示分类数量
/// 最新变化: 2026-04-02 支持深色模式
class CategoryPage extends StatefulWidget {
class CategoryPage extends StatelessWidget {
const CategoryPage({super.key});
@override
State<CategoryPage> createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<Map<String, dynamic>> _tabCategories = [
{'label': '场景分类', 'icon': Icons.category},
{'label': '朝代分类', 'icon': Icons.history},
];
static const sceneData = {
"节日": ["七夕节", "中秋节", "元宵节", "寒食节", "清明节", "端午节", "重阳节", "春节", "节日"],
"季节": ["三月", "二月", "冬天", "夏天", "春天", "春季", "秋天"],
"古籍": [
"三国志",
"三国演义",
"三字经",
"中庸",
"列子",
"史记",
"后汉书",
"吕氏春秋",
"商君书",
"围炉夜话",
"增广贤文",
"墨子",
"孙子兵法",
"孟子",
"小窗幽记",
"尚书",
"左传",
"幼学琼林",
"庄子",
"战国策",
"文心雕龙",
"易传",
"晋书",
"汉书",
"淮南子",
"礼记",
"管子",
"红楼梦",
"老子",
"荀子",
"菜根谭",
"警世通言",
"论语",
"资治通鉴",
"韩非子",
"鬼谷子",
"古籍",
"格言联璧",
],
"情感": ["伤感", "励志", "友情", "思乡", "思念", "感恩", "爱国", "爱情", "离别"],
"景物": ["庐山", "泰山", "西湖", "长江", "黄河", "边塞", "田园", "山水", "夜景"],
"天文气象": ["写云", "写雨", "写雪", "写风", "星星", "月亮", "流星"],
"动植物": ["写鸟", "柳树", "桃花", "梅花", "竹子", "荷花", "菊花"],
"语言文学": ["对联", "谚语", "一言", "读书", "哲理"],
"其他": ["母亲", "老师", "户外", "礼物", ""],
};
static const dynastyData = {
"主要朝代": ["唐代", "宋代", "元代", "明代", "清代"],
"古代朝代": ["南北朝", "五代", "隋代"],
"近现代": ["近现代", "用户投稿", "管理员测试"],
"其他": ["暂无朝代"],
};
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabCategories.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Tab栏
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TabBar(
controller: _tabController,
tabs: _tabCategories
.map(
(category) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(category['icon'], size: 18),
const SizedBox(width: 6),
Text(category['label']),
],
final controller = Get.put(CategoryController());
final themeController = Get.find<ThemeController>();
return Obx(() {
final isDark = themeController.isDarkModeRx.value;
return Column(
children: [
Container(
color: isDark ? Colors.grey[900] : Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TabBar(
controller: controller.tabController,
tabs: controller.tabCategories
.map(
(category) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(category['icon'] as IconData, size: 18),
const SizedBox(width: 6),
Text(category['label'] as String),
],
),
),
)
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: isDark
? Colors.grey[400]
: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
),
),
),
Container(
height: 0.5,
color: isDark ? Colors.grey[800] : const Color(0xFFE5E5EA),
),
Expanded(
child: Container(
color: isDark ? const Color(0xFF121212) : const Color(0xFFF2F2F7),
child: TabBarView(
controller: controller.tabController,
children: [
_buildCategoryList(
controller.sceneData,
controller.tabCategories[0]['label'] as String,
isDark,
),
)
.toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
_buildCategoryList(
controller.dynastyData,
controller.tabCategories[1]['label'] as String,
isDark,
),
],
),
),
),
),
Container(height: 0.5, color: const Color(0xFFE5E5EA)),
// 内容区域
Expanded(
child: Container(
color: const Color(0xFFF2F2F7),
child: TabBarView(
controller: _tabController,
children: [
_buildCategoryList(sceneData, _tabCategories[0]['label']),
_buildCategoryList(dynastyData, _tabCategories[1]['label']),
],
),
),
),
],
);
],
);
});
}
Widget _buildCategoryList(
Map<String, List<String>> data,
String categoryType,
bool isDark,
) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -163,11 +102,11 @@ class _CategoryPageState extends State<CategoryPage>
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
color: Colors.black.withAlpha(isDark ? 0 : 10),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -186,10 +125,10 @@ class _CategoryPageState extends State<CategoryPage>
Expanded(
child: Text(
category,
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black,
color: isDark ? Colors.white : Colors.black,
),
),
),
@@ -199,9 +138,7 @@ class _CategoryPageState extends State<CategoryPage>
vertical: 4,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
color: AppConstants.primaryColor.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
@@ -220,7 +157,7 @@ class _CategoryPageState extends State<CategoryPage>
spacing: 10,
runSpacing: 10,
children: items.map((item) {
return _buildCategoryChip(item, categoryType);
return _buildCategoryChip(item, categoryType, isDark);
}).toList(),
),
],
@@ -232,24 +169,20 @@ class _CategoryPageState extends State<CategoryPage>
);
}
Widget _buildCategoryChip(String label, String categoryType) {
Widget _buildCategoryChip(String label, String categoryType, bool isDark) {
final controller = Get.find<CategoryController>();
return GestureDetector(
onTap: () {
final searchType = categoryType == '朝代分类' ? 'alias' : 'keywords';
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CorrPage(label: label, searchType: searchType),
),
);
controller.navigateToCategoryDetail(label, categoryType);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(alpha: 0.1),
color: AppConstants.primaryColor.withAlpha(26),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppConstants.primaryColor.withValues(alpha: 0.3),
color: AppConstants.primaryColor.withAlpha(77),
width: 1,
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../constants/app_constants.dart';
import '../../config/app_config.dart';
import '../../utils/http/http_client.dart';
import '../../models/poetry_model.dart';
import '../../controllers/load/locally.dart';
@@ -156,7 +157,13 @@ class _PopularPageState extends State<PopularPage>
return false;
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
// 添加底部内边距,让内容延伸到导航栏下方,实现玻璃效果
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: AppConfig.liquidGlassTotalHeight + 16,
),
itemCount: _rankList.length + (_showBottomIndicator ? 1 : 0),
itemBuilder: (context, index) {
if (index == _rankList.length) {

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/app_constants.dart';
import '../../services/get/theme_controller.dart';
/// 时间: 2026-03-26
/// 功能: 活跃页面
/// 介绍: 展示用户活跃度热力图,参考 GitHub 贡献图样式
/// 最新变化: 添加调试选项,修复布局溢出,调整活跃阈值颜色
/// 最新变化: 2026-04-02 支持深色模式
class RatePage extends StatefulWidget {
const RatePage({super.key});
@@ -17,13 +19,12 @@ class _RatePageState extends State<RatePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _tabs = ['周活跃', '月活跃'];
final ThemeController _themeController = Get.find<ThemeController>();
// 调试参数
int _debugDayCount = 7;
int _debugBarCount = 7;
bool _showDebugPanel = false;
// 模拟活跃度数据 (实际条数)
List<int> _weekData = [];
List<List<int>> _monthData = [];
@@ -40,10 +41,8 @@ class _RatePageState extends State<RatePage>
super.dispose();
}
// 生成模拟数据
void _generateMockData() {
_weekData = List.generate(_debugDayCount, (index) {
// 生成 0-150 的随机活跃值
return [0, 3, 8, 15, 35, 80, 150][index % 7];
});
@@ -56,46 +55,45 @@ class _RatePageState extends State<RatePage>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
// Tab 切换栏
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(bottom: BorderSide(color: Colors.grey[200]!)),
return Obx(() {
final isDark = _themeController.isDarkModeRx.value;
return Scaffold(
backgroundColor: isDark ? const Color(0xFF121212) : Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
Container(
decoration: BoxDecoration(
color: isDark ? Colors.grey[900] : Colors.white,
border: Border(bottom: BorderSide(color: isDark ? Colors.grey[800]! : Colors.grey[200]!)),
),
child: TabBar(
controller: _tabController,
tabs: _tabs.map((tab) => Tab(text: tab)).toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: isDark ? Colors.grey[400] : Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 2,
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
),
),
child: TabBar(
controller: _tabController,
tabs: _tabs.map((tab) => Tab(text: tab)).toList(),
labelColor: AppConstants.primaryColor,
unselectedLabelColor: Colors.grey[600],
indicatorColor: AppConstants.primaryColor,
indicatorWeight: 2,
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
_buildDebugToggle(isDark),
if (_showDebugPanel) _buildDebugPanel(isDark),
Expanded(
child: TabBarView(
controller: _tabController,
children: [_buildWeekView(isDark), _buildMonthView(isDark)],
),
),
),
// 调试开关
_buildDebugToggle(),
// 调试面板
if (_showDebugPanel) _buildDebugPanel(),
// 内容区域
Expanded(
child: TabBarView(
controller: _tabController,
children: [_buildWeekView(), _buildMonthView()],
),
),
],
),
);
],
),
);
});
}
// 调试开关
Widget _buildDebugToggle() {
Widget _buildDebugToggle(bool isDark) {
return Container(
color: Colors.orange[50],
color: isDark ? Colors.orange[900]!.withAlpha(50) : Colors.orange[50],
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
@@ -130,21 +128,20 @@ class _RatePageState extends State<RatePage>
);
}
// 调试面板
Widget _buildDebugPanel() {
Widget _buildDebugPanel(bool isDark) {
return Container(
color: Colors.orange[50],
color: isDark ? Colors.orange[900]!.withAlpha(50) : Colors.orange[50],
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 天数调节
_buildDebugSlider(
label: '天数',
value: _debugDayCount.toDouble(),
min: 3,
max: 31,
isDark: isDark,
onChanged: (value) {
setState(() {
_debugDayCount = value.round();
@@ -153,12 +150,12 @@ class _RatePageState extends State<RatePage>
},
),
const SizedBox(height: 12),
// 条数调节
_buildDebugSlider(
label: '条数',
value: _debugBarCount.toDouble(),
min: 3,
max: 14,
isDark: isDark,
onChanged: (value) {
setState(() {
_debugBarCount = value.round();
@@ -167,11 +164,10 @@ class _RatePageState extends State<RatePage>
},
),
const SizedBox(height: 16),
// 活跃阈值说明
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange[200]!),
),
@@ -189,19 +185,22 @@ class _RatePageState extends State<RatePage>
_buildThresholdItem(
'1-5',
'浅色',
AppConstants.primaryColor.withValues(alpha: 0.3),
AppConstants.primaryColor.withAlpha(77),
isDark,
),
_buildThresholdItem(
'6-20',
'中浅色',
AppConstants.primaryColor.withValues(alpha: 0.5),
AppConstants.primaryColor.withAlpha(128),
isDark,
),
_buildThresholdItem(
'21-100',
'中深色',
AppConstants.primaryColor.withValues(alpha: 0.7),
AppConstants.primaryColor.withAlpha(179),
isDark,
),
_buildThresholdItem('100+', '最深色', AppConstants.primaryColor),
_buildThresholdItem('100+', '最深色', AppConstants.primaryColor, isDark),
],
),
),
@@ -210,12 +209,12 @@ class _RatePageState extends State<RatePage>
);
}
// 调试滑块
Widget _buildDebugSlider({
required String label,
required double value,
required double min,
required double max,
required bool isDark,
required ValueChanged<double> onChanged,
}) {
return Row(
@@ -253,8 +252,7 @@ class _RatePageState extends State<RatePage>
);
}
// 阈值项
Widget _buildThresholdItem(String range, String label, Color color) {
Widget _buildThresholdItem(String range, String label, Color color, bool isDark) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
@@ -270,15 +268,14 @@ class _RatePageState extends State<RatePage>
const SizedBox(width: 8),
Text(
'$range: $label',
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
style: TextStyle(fontSize: 12, color: isDark ? Colors.grey[300] : Colors.grey[700]),
),
],
),
);
}
// 周活跃视图
Widget _buildWeekView() {
Widget _buildWeekView(bool isDark) {
final weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return SingleChildScrollView(
@@ -286,17 +283,16 @@ class _RatePageState extends State<RatePage>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('本周活跃趋势(生成图片分享)'),
_buildSectionTitle('本周活跃趋势(生成图片分享)', isDark),
const SizedBox(height: 16),
// 热力图 - 使用 Wrap 防止溢出
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: Colors.black.withAlpha(isDark ? 0 : 13),
blurRadius: 10,
offset: const Offset(0, 2),
),
@@ -304,7 +300,6 @@ class _RatePageState extends State<RatePage>
),
child: Column(
children: [
// 星期标签
Wrap(
spacing: 8,
runSpacing: 8,
@@ -317,7 +312,7 @@ class _RatePageState extends State<RatePage>
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
@@ -325,44 +320,42 @@ class _RatePageState extends State<RatePage>
}),
),
const SizedBox(height: 12),
// 活跃度方块
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: List.generate(_weekData.length, (index) {
return _buildActivityBlock(_weekData[index], size: 40);
return _buildActivityBlock(_weekData[index], size: 40, isDark: isDark);
}),
),
],
),
),
const SizedBox(height: 16),
_buildStatsSection(),
_buildStatsSection(isDark),
const SizedBox(height: 16),
_buildLegend(),
_buildLegend(isDark),
],
),
);
}
// 月活跃视图
Widget _buildMonthView() {
Widget _buildMonthView(bool isDark) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('本月活跃热力图'),
_buildSectionTitle('本月活跃热力图', isDark),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: Colors.black.withAlpha(isDark ? 0 : 13),
blurRadius: 10,
offset: const Offset(0, 2),
),
@@ -370,7 +363,6 @@ class _RatePageState extends State<RatePage>
),
child: Column(
children: [
// 星期标签
Wrap(
spacing: 4,
alignment: WrapAlignment.center,
@@ -383,7 +375,7 @@ class _RatePageState extends State<RatePage>
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
color: isDark ? Colors.grey[500] : Colors.grey[500],
),
),
),
@@ -391,19 +383,17 @@ class _RatePageState extends State<RatePage>
.toList(),
),
const SizedBox(height: 8),
// 四周热力图 - 使用 Wrap 防止溢出
...List.generate(4, (weekIndex) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Wrap(
spacing: 4,
alignment: WrapAlignment.center,
children: List.generate(_monthData[weekIndex].length, (
dayIndex,
) {
children: List.generate(_monthData[weekIndex].length, (dayIndex) {
return _buildActivityBlock(
_monthData[weekIndex][dayIndex],
size: 28,
isDark: isDark,
);
}),
),
@@ -413,16 +403,15 @@ class _RatePageState extends State<RatePage>
),
),
const SizedBox(height: 16),
_buildStatsSection(),
_buildStatsSection(isDark),
const SizedBox(height: 16),
_buildLegend(),
_buildLegend(isDark),
],
),
);
}
// 活跃度方块
Widget _buildActivityBlock(int value, {required double size}) {
Widget _buildActivityBlock(int value, {required double size, required bool isDark}) {
return Tooltip(
message: '活跃度: $value',
child: Container(
@@ -431,14 +420,14 @@ class _RatePageState extends State<RatePage>
decoration: BoxDecoration(
color: _getActivityColor(value),
borderRadius: BorderRadius.circular(4),
border: value == 0 ? Border.all(color: Colors.grey[200]!) : null,
border: value == 0 ? Border.all(color: isDark ? Colors.grey[700]! : Colors.grey[200]!) : null,
),
child: value > 0
? Center(
child: value >= 100
? Icon(
Icons.local_fire_department,
color: Colors.white.withValues(alpha: 0.9),
color: Colors.white.withAlpha(230),
size: size * 0.6,
)
: null,
@@ -448,27 +437,21 @@ class _RatePageState extends State<RatePage>
);
}
// 获取活跃度颜色 (根据新的阈值)
Color _getActivityColor(int value) {
if (value == 0) {
return Colors.grey[100]!;
} else if (value >= 1 && value <= 5) {
// 1-5: 浅色
return AppConstants.primaryColor.withValues(alpha: 0.3);
return AppConstants.primaryColor.withAlpha(77);
} else if (value >= 6 && value <= 20) {
// 6-20: 中浅色
return AppConstants.primaryColor.withValues(alpha: 0.5);
return AppConstants.primaryColor.withAlpha(128);
} else if (value >= 21 && value <= 100) {
// 21-100: 中深色
return AppConstants.primaryColor.withValues(alpha: 0.7);
return AppConstants.primaryColor.withAlpha(179);
} else {
// 100+: 最深色
return AppConstants.primaryColor;
}
}
// 标题组件
Widget _buildSectionTitle(String title) {
Widget _buildSectionTitle(String title, bool isDark) {
return Row(
children: [
Container(
@@ -482,22 +465,21 @@ class _RatePageState extends State<RatePage>
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
],
);
}
// 统计信息
Widget _buildStatsSection() {
Widget _buildStatsSection(bool isDark) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: Colors.black.withAlpha(isDark ? 0 : 13),
blurRadius: 10,
offset: const Offset(0, 2),
),
@@ -506,64 +488,53 @@ class _RatePageState extends State<RatePage>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('活跃天数', '12', Icons.calendar_today),
_buildStatItem('连续活跃', '5', Icons.local_fire_department),
_buildStatItem('总活跃度', '89%', Icons.trending_up),
_buildStatItem('活跃天数', '12', Icons.calendar_today, isDark),
_buildStatItem('连续活跃', '5', Icons.local_fire_department, isDark),
_buildStatItem('总活跃度', '89%', Icons.trending_up, isDark),
],
),
);
}
// 统计项
Widget _buildStatItem(String label, String value, IconData icon) {
Widget _buildStatItem(String label, String value, IconData icon, bool isDark) {
return Column(
children: [
Icon(icon, color: AppConstants.primaryColor, size: 24),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black),
),
const SizedBox(height: 4),
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
Text(label, style: TextStyle(fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey[600])),
],
);
}
// 图例说明
Widget _buildLegend() {
Widget _buildLegend(bool isDark) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'活跃度说明',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
_buildLegendItem('无活跃', Colors.grey[100]!),
_buildLegendItem(
'1-5',
AppConstants.primaryColor.withValues(alpha: 0.3),
),
_buildLegendItem(
'6-20',
AppConstants.primaryColor.withValues(alpha: 0.5),
),
_buildLegendItem(
'21-100',
AppConstants.primaryColor.withValues(alpha: 0.7),
),
_buildLegendItem('100+', AppConstants.primaryColor),
_buildLegendItem('无活跃', Colors.grey[100]!, isDark),
_buildLegendItem('1-5', AppConstants.primaryColor.withAlpha(77), isDark),
_buildLegendItem('6-20', AppConstants.primaryColor.withAlpha(128), isDark),
_buildLegendItem('21-100', AppConstants.primaryColor.withAlpha(179), isDark),
_buildLegendItem('100+', AppConstants.primaryColor, isDark),
],
),
],
@@ -571,8 +542,7 @@ class _RatePageState extends State<RatePage>
);
}
// 图例项
Widget _buildLegendItem(String label, Color color) {
Widget _buildLegendItem(String label, Color color, bool isDark) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -582,13 +552,11 @@ class _RatePageState extends State<RatePage>
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
border: label == '无活跃'
? Border.all(color: Colors.grey[300]!)
: null,
border: label == '无活跃' ? Border.all(color: isDark ? Colors.grey[600]! : Colors.grey[300]!) : null,
),
),
const SizedBox(width: 4),
Text(label, style: TextStyle(fontSize: 11, color: Colors.grey[600])),
Text(label, style: TextStyle(fontSize: 11, color: isDark ? Colors.grey[400] : Colors.grey[600])),
],
);
}

View File

@@ -1,15 +1,17 @@
/// 时间: 2026-04-01
/// 功能: 标签/朝代诗词列表页面
/// 介绍: 展示指定标签或朝代相关的诗词列表,支持搜索和浏览
/// 最新变化: 新建页面iOS风格设计集成搜索API
/// 最新变化: 2026-04-02 支持深色模式
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/http_client.dart';
import '../../../controllers/history_controller.dart';
import '../../../services/network_listener_service.dart';
import '../../../services/get/theme_controller.dart';
/// 标签诗词列表页面
/// [label] 标签名称或朝代名称
@@ -42,6 +44,7 @@ class _CorrPageState extends State<CorrPage>
final ScrollController _scrollController = ScrollController();
late AnimationController _skeletonAnimationController;
late Animation<double> _skeletonAnimation;
final ThemeController _themeController = Get.find<ThemeController>();
@override
void initState() {
@@ -207,33 +210,36 @@ class _CorrPageState extends State<CorrPage>
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: const Color(0xFFF2F2F7),
appBar: _buildAppBar(),
body: _buildBody(),
),
);
return Obx(() {
final isDark = _themeController.isDarkModeRx.value;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: isDark
? const Color(0xFF121212)
: const Color(0xFFF2F2F7),
appBar: _buildAppBar(isDark),
body: _buildBody(isDark),
),
);
});
}
PreferredSizeWidget _buildAppBar() {
PreferredSizeWidget _buildAppBar(bool isDark) {
return AppBar(
backgroundColor: Colors.white,
backgroundColor: isDark ? Colors.grey[900] : Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: AppConstants.primaryColor,
),
icon: Icon(Icons.arrow_back_ios, color: AppConstants.primaryColor),
onPressed: () => Navigator.pop(context),
),
title: Column(
children: [
Text(
widget.label,
style: const TextStyle(
color: Colors.black,
style: TextStyle(
color: isDark ? Colors.white : Colors.black,
fontSize: 17,
fontWeight: FontWeight.w600,
),
@@ -241,7 +247,7 @@ class _CorrPageState extends State<CorrPage>
Text(
widget.searchType == 'alias' ? '朝代' : '标签',
style: TextStyle(
color: Colors.grey[600],
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.normal,
),
@@ -257,7 +263,7 @@ class _CorrPageState extends State<CorrPage>
child: Text(
'$_totalCount 篇 / $_totalPages',
style: TextStyle(
color: Colors.grey[600],
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontSize: 13,
fontWeight: FontWeight.w500,
),
@@ -267,22 +273,25 @@ class _CorrPageState extends State<CorrPage>
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0.5),
child: Container(height: 0.5, color: const Color(0xFFE5E5EA)),
child: Container(
height: 0.5,
color: isDark ? Colors.grey[800] : const Color(0xFFE5E5EA),
),
),
);
}
Widget _buildBody() {
Widget _buildBody(bool isDark) {
if (_isLoading) {
return _buildSkeletonView();
return _buildSkeletonView(isDark);
}
if (_errorMessage != null) {
return _buildErrorView();
return _buildErrorView(isDark);
}
if (_poetryList.isEmpty) {
return _buildEmptyView();
return _buildEmptyView(isDark);
}
return AnimatedSwitcher(
@@ -297,33 +306,33 @@ class _CorrPageState extends State<CorrPage>
itemCount: _poetryList.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _poetryList.length) {
return _buildLoadingMoreIndicator();
return _buildLoadingMoreIndicator(isDark);
}
return _buildPoetryCard(_poetryList[index]);
return _buildPoetryCard(_poetryList[index], isDark);
},
),
),
);
}
Widget _buildSkeletonView() {
Widget _buildSkeletonView(bool isDark) {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: 5,
itemBuilder: (context, index) {
return _buildSkeletonCard();
return _buildSkeletonCard(isDark);
},
);
}
Widget _buildSkeletonCard() {
Widget _buildSkeletonCard(bool isDark) {
return AnimatedBuilder(
animation: _skeletonAnimation,
builder: (context, child) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
@@ -331,23 +340,44 @@ class _CorrPageState extends State<CorrPage>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSkeletonLine(180, 20, _skeletonAnimation.value),
_buildSkeletonLine(180, 20, _skeletonAnimation.value, isDark),
const SizedBox(height: 8),
_buildSkeletonLine(
double.infinity,
16,
_skeletonAnimation.value * 0.9,
isDark,
),
const SizedBox(height: 6),
_buildSkeletonLine(250, 16, _skeletonAnimation.value * 0.8),
_buildSkeletonLine(
250,
16,
_skeletonAnimation.value * 0.8,
isDark,
),
const SizedBox(height: 6),
_buildSkeletonLine(200, 16, _skeletonAnimation.value * 0.7),
_buildSkeletonLine(
200,
16,
_skeletonAnimation.value * 0.7,
isDark,
),
const SizedBox(height: 12),
Row(
children: [
_buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.6),
_buildSkeletonLine(
60,
16,
_skeletonAnimation.value * 0.6,
isDark,
),
const SizedBox(width: 16),
_buildSkeletonLine(60, 16, _skeletonAnimation.value * 0.5),
_buildSkeletonLine(
60,
16,
_skeletonAnimation.value * 0.5,
isDark,
),
],
),
],
@@ -358,20 +388,26 @@ class _CorrPageState extends State<CorrPage>
);
}
Widget _buildSkeletonLine(double width, double height, double opacity) {
Widget _buildSkeletonLine(
double width,
double height,
double opacity,
bool isDark,
) {
final baseColor = isDark
? Colors.grey[700] ?? Colors.grey
: const Color(0xFFE5E5EA);
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: const Color(
0xFFE5E5EA,
).withValues(alpha: opacity.clamp(0.3, 0.8)),
color: baseColor.withAlpha((opacity.clamp(0.3, 0.8) * 255).toInt()),
borderRadius: BorderRadius.circular(4),
),
);
}
Widget _buildErrorView() {
Widget _buildErrorView(bool isDark) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
@@ -382,7 +418,10 @@ class _CorrPageState extends State<CorrPage>
const SizedBox(height: 16),
Text(
_errorMessage ?? '加载失败',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
style: TextStyle(
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
@@ -407,28 +446,38 @@ class _CorrPageState extends State<CorrPage>
);
}
Widget _buildEmptyView() {
Widget _buildEmptyView(bool isDark) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, color: Colors.grey[400], size: 64),
Icon(
Icons.inbox_outlined,
color: isDark ? Colors.grey[500] : Colors.grey[400],
size: 64,
),
const SizedBox(height: 16),
Text(
'暂无相关诗词',
style: TextStyle(color: Colors.grey[600], fontSize: 14),
style: TextStyle(
color: isDark ? Colors.grey[400] : Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 8),
Text(
'试试其他标签或朝代',
style: TextStyle(color: Colors.grey[400], fontSize: 12),
style: TextStyle(
color: isDark ? Colors.grey[500] : Colors.grey[400],
fontSize: 12,
),
),
],
),
);
}
Widget _buildLoadingMoreIndicator() {
Widget _buildLoadingMoreIndicator(bool isDark) {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
@@ -444,7 +493,7 @@ class _CorrPageState extends State<CorrPage>
);
}
Widget _buildPoetryCard(Map<String, dynamic> poetry) {
Widget _buildPoetryCard(Map<String, dynamic> poetry, bool isDark) {
final name = poetry['name']?.toString() ?? '未知标题';
final url = poetry['url']?.toString() ?? '';
final alias = poetry['alias']?.toString() ?? '';
@@ -455,11 +504,11 @@ class _CorrPageState extends State<CorrPage>
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[850] : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
color: Colors.black.withAlpha(isDark ? 0 : 10),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -470,7 +519,7 @@ class _CorrPageState extends State<CorrPage>
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _showPoetryDetail(poetry),
onTap: () => _showPoetryDetail(poetry, isDark),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -481,10 +530,10 @@ class _CorrPageState extends State<CorrPage>
Expanded(
child: Text(
name,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
color: isDark ? Colors.white : Colors.black,
),
),
),
@@ -495,9 +544,7 @@ class _CorrPageState extends State<CorrPage>
vertical: 4,
),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(
alpha: 0.1,
),
color: AppConstants.primaryColor.withAlpha(26),
borderRadius: BorderRadius.circular(8),
),
child: Text(
@@ -515,7 +562,10 @@ class _CorrPageState extends State<CorrPage>
const SizedBox(height: 4),
Text(
url,
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -526,7 +576,7 @@ class _CorrPageState extends State<CorrPage>
introduce,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
color: isDark ? Colors.grey[300] : Colors.grey[700],
height: 1.5,
),
maxLines: 3,
@@ -539,23 +589,29 @@ class _CorrPageState extends State<CorrPage>
Icon(
Icons.remove_red_eye_outlined,
size: 16,
color: Colors.grey[400],
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
const SizedBox(width: 4),
Text(
'$hitsTotal',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
),
const SizedBox(width: 16),
Icon(
Icons.favorite_outline,
size: 16,
color: Colors.grey[400],
color: isDark ? Colors.grey[500] : Colors.grey[400],
),
const SizedBox(width: 4),
Text(
'$like',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
),
const Spacer(),
FutureBuilder<bool>(
@@ -569,7 +625,11 @@ class _CorrPageState extends State<CorrPage>
child: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
size: 20,
color: isLiked ? Colors.red : Colors.grey[400],
color: isLiked
? Colors.red
: (isDark
? Colors.grey[500]
: Colors.grey[400]),
),
);
},
@@ -634,7 +694,7 @@ class _CorrPageState extends State<CorrPage>
}
}
void _showPoetryDetail(Map<String, dynamic> poetry) {
void _showPoetryDetail(Map<String, dynamic> poetry, bool isDark) {
final drtime = poetry['drtime']?.toString() ?? '';
final introduce = poetry['introduce']?.toString() ?? '';
@@ -644,22 +704,22 @@ class _CorrPageState extends State<CorrPage>
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
decoration: BoxDecoration(
color: isDark ? Colors.grey[900] : Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isDark ? Colors.grey[900] : Colors.white,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: Colors.black.withAlpha(isDark ? 0 : 13),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -670,14 +730,18 @@ class _CorrPageState extends State<CorrPage>
Expanded(
child: Text(
poetry['name']?.toString() ?? '诗词详情',
style: const TextStyle(
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
),
IconButton(
icon: const Icon(Icons.close),
icon: Icon(
Icons.close,
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
onPressed: () => Navigator.pop(context),
),
],
@@ -685,91 +749,92 @@ class _CorrPageState extends State<CorrPage>
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (poetry['url']?.toString().isNotEmpty == true) ...[
if (drtime.isNotEmpty) ...[
Text(
poetry['url'].toString(),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
'朝代',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.grey[300] : Colors.grey[700],
),
),
const SizedBox(height: 4),
Text(
drtime,
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.white : Colors.black,
),
),
const SizedBox(height: 16),
],
if (drtime.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF8F8F8),
borderRadius: BorderRadius.circular(12),
),
child: Text(
drtime,
style: const TextStyle(
fontSize: 16,
height: 1.8,
fontFamily: 'serif',
),
),
),
const SizedBox(height: 20),
],
if (introduce.isNotEmpty) ...[
const Text(
Text(
'译文',
style: TextStyle(
fontSize: 16,
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.grey[300] : Colors.grey[700],
),
),
const SizedBox(height: 12),
const SizedBox(height: 4),
Text(
introduce,
style: TextStyle(
fontSize: 15,
color: Colors.grey[700],
height: 1.8,
fontSize: 16,
height: 1.6,
color: isDark ? Colors.grey[200] : Colors.black87,
),
),
],
const SizedBox(height: 20),
],
),
),
),
SafeArea(
child: Container(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, -2),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
Navigator.pop(context);
_createNoteFromPoetry(poetry);
},
icon: Icon(
Icons.note_add,
color: AppConstants.primaryColor,
),
label: Text(
'创建笔记',
style: TextStyle(color: AppConstants.primaryColor),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppConstants.primaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _toggleLike(poetry),
icon: const Icon(Icons.favorite, color: Colors.white),
label: const Text('点赞'),
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_createNoteFromPoetry(poetry);
},
icon: const Icon(Icons.note_add, size: 18),
label: const Text('创建笔记'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
),
),
),
],