// 时间: 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 createState() => _ActiveSearchPageState(); } class _ActiveSearchPageState extends State 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 _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 _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.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) 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 ? 16 : 8, AppConstants.pageHorizontalPadding, 8, ), 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(() {}), ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, 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, ), ); }, ); } }