461 lines
17 KiB
Dart
461 lines
17 KiB
Dart
// 时间: 2026-03-22
|
||
// 功能: 全站诗词搜索页(收藏入口可跳转)
|
||
// 介绍: 调用 search.php,配合 NetworkListenerService 上报加载与搜索完成事件
|
||
// 最新变化: 初始版本
|
||
|
||
import 'package:flutter/material.dart';
|
||
import '../../constants/app_constants.dart';
|
||
import '../../services/network_listener_service.dart';
|
||
import '../../utils/http/poetry_api.dart';
|
||
import '../main_navigation.dart';
|
||
|
||
/// 诗词搜索页(独立路由栈页面)
|
||
class ActiveSearchPage extends StatefulWidget {
|
||
const ActiveSearchPage({super.key, this.initialQuery});
|
||
|
||
/// 从收藏页带入的预填关键词
|
||
final String? initialQuery;
|
||
|
||
@override
|
||
State<ActiveSearchPage> createState() => _ActiveSearchPageState();
|
||
}
|
||
|
||
class _ActiveSearchPageState extends State<ActiveSearchPage>
|
||
with NetworkListenerMixin {
|
||
static const String _loadKey = 'active_search';
|
||
|
||
final TextEditingController _controller = TextEditingController();
|
||
final FocusNode _focusNode = FocusNode();
|
||
|
||
/// 空=不限字段;name / keywords / introduce 见 API 文档
|
||
String _field = '';
|
||
int _page = 1;
|
||
final int _pageSize = 20;
|
||
List<PoetryData> _items = [];
|
||
int _total = 0;
|
||
String? _error;
|
||
bool _initialized = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller.addListener(() => setState(() {}));
|
||
if (widget.initialQuery != null && widget.initialQuery!.trim().isNotEmpty) {
|
||
_controller.text = widget.initialQuery!.trim();
|
||
}
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!_initialized && _controller.text.trim().isNotEmpty) {
|
||
_initialized = true;
|
||
_runSearch(reset: true);
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
_focusNode.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _runSearch({required bool reset}) async {
|
||
final q = _controller.text.trim();
|
||
if (q.isEmpty) {
|
||
setState(() {
|
||
_error = '请输入关键词';
|
||
_items = [];
|
||
_total = 0;
|
||
});
|
||
return;
|
||
}
|
||
|
||
final nextPage = reset ? 1 : _page + 1;
|
||
if (!reset && !_hasMore) return;
|
||
|
||
setState(() => _error = null);
|
||
startNetworkLoading(_loadKey);
|
||
try {
|
||
final result = await PoetryApi.searchPoetry(
|
||
q: q,
|
||
field: _field,
|
||
page: nextPage,
|
||
limit: _pageSize,
|
||
);
|
||
if (!mounted) return;
|
||
setState(() {
|
||
if (reset) {
|
||
_items = List<PoetryData>.from(result.list);
|
||
} else {
|
||
_items = [..._items, ...result.list];
|
||
}
|
||
_page = result.page;
|
||
_total = result.total;
|
||
});
|
||
sendSearchEvent(keyword: q);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
setState(() => _error = e.toString());
|
||
} finally {
|
||
endNetworkLoading(_loadKey);
|
||
}
|
||
}
|
||
|
||
bool get _hasMore => _items.length < _total;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final loading = isNetworkLoading(_loadKey);
|
||
|
||
// 检查是否在 TabBarView 中显示(通过上下文判断)
|
||
final bool isInTabBarView = ModalRoute.of(context)?.settings.name == null;
|
||
|
||
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(
|
||
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: '返回上一页',
|
||
),
|
||
// 标题
|
||
Expanded(
|
||
child: Row(
|
||
children: [
|
||
const Icon(
|
||
Icons.travel_explore,
|
||
size: 20,
|
||
color: Colors.black87,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'诗词搜索',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 更多按钮
|
||
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: 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: '返回上一页',
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildListBody(bool loading) {
|
||
if (loading && _items.isEmpty) {
|
||
return ListView(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
children: const [
|
||
SizedBox(height: 120),
|
||
Center(child: CircularProgressIndicator()),
|
||
],
|
||
);
|
||
}
|
||
|
||
if (_items.isEmpty && !loading) {
|
||
return ListView(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
children: [
|
||
SizedBox(height: MediaQuery.of(context).size.height * 0.15),
|
||
Icon(Icons.manage_search, size: 64, color: Colors.grey[400]),
|
||
const SizedBox(height: 12),
|
||
Center(
|
||
child: Text(
|
||
_controller.text.trim().isEmpty ? '输入关键词开始搜索' : '暂无结果',
|
||
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
return ListView.builder(
|
||
padding: EdgeInsets.fromLTRB(
|
||
AppConstants.pageHorizontalPadding,
|
||
0,
|
||
AppConstants.pageHorizontalPadding,
|
||
24,
|
||
),
|
||
itemCount: _items.length + (_hasMore ? 1 : 0),
|
||
itemBuilder: (context, index) {
|
||
if (index >= _items.length) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
child: Center(
|
||
child: loading
|
||
? const CircularProgressIndicator()
|
||
: TextButton.icon(
|
||
onPressed: () => _runSearch(reset: false),
|
||
icon: const Icon(Icons.expand_more),
|
||
label: const Text('加载更多'),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
final p = _items[index];
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
elevation: 1,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: ListTile(
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 8,
|
||
),
|
||
title: Text(
|
||
p.name.isNotEmpty ? p.name : p.url,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||
),
|
||
subtitle: Padding(
|
||
padding: const EdgeInsets.only(top: 6),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (p.alias.isNotEmpty)
|
||
Text(
|
||
'📜 ${p.alias}',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||
),
|
||
if (p.introduce.isNotEmpty)
|
||
Text(
|
||
p.introduce,
|
||
maxLines: 3,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'👍 ${p.like} · 🔥 ${p.hitsTotal}',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
isThreeLine: true,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|