// 时间: 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 '../../widgets/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(); final ThemeController _themeController = Get.find(); 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); bool isInTabBarView = false; try { final ancestor = context.findAncestorWidgetOfExactType(); isInTabBarView = ancestor != null; } catch (e) { isInTabBarView = false; } return Obx(() { final isDark = _themeController.isDarkModeRx.value; return Scaffold( backgroundColor: isDark ? const Color(0xFF121212) : Colors.grey[50], appBar: !isInTabBarView ? AppBar( title: Row( children: [ Icon( Icons.travel_explore, size: 20, color: isDark ? Colors.white : 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); }, ), ], ), ), ); }, 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: _themeController.currentThemeColor, 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: _themeController.currentThemeColor, 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: _themeController.currentThemeColor, 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: _themeController.currentThemeColor, 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, ), ), ), ], ), ), Expanded( child: RefreshIndicator( color: _themeController.currentThemeColor, onRefresh: () async { await _runSearch(reset: true); }, child: _buildListBody(loading, isDark), ), ), ], ), ), ], ), ), floatingActionButton: !isInTabBarView ? FloatingActionButton( onPressed: () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } else { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const MainNavigation()), ); } }, backgroundColor: _themeController.currentThemeColor, foregroundColor: Colors.white, tooltip: '返回上一页', child: const Icon(Icons.arrow_back), ) : null, ); }); } Widget _buildListBody(bool loading, bool isDark) { 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: isDark ? Colors.grey[500] : Colors.grey[400], ), const SizedBox(height: 12), Center( child: Text( _controller.text.trim().isEmpty ? '输入关键词开始搜索' : '暂无结果', style: TextStyle( color: isDark ? Colors.grey[400] : 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: Icon( Icons.expand_more, color: isDark ? Colors.grey[400] : Colors.black87, ), label: Text( '加载更多', style: TextStyle( color: isDark ? Colors.grey[400] : Colors.black87, ), ), ), ), ); } final p = _items[index]; return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 1, color: isDark ? Colors.grey[850] : Colors.white, 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: TextStyle( fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), 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: isDark ? Colors.grey[400] : Colors.grey[700], ), ), if (p.introduce.isNotEmpty) Text( p.introduce, maxLines: 3, overflow: TextOverflow.ellipsis, 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: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ], ), ), isThreeLine: true, ), ); }, ); } }