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

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],
),
),
],
),