Files
wushu/lib/views/active/active_search_page.dart
2026-04-02 07:06:55 +08:00

568 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 时间: 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<ActiveSearchPage> createState() => _ActiveSearchPageState();
}
class _ActiveSearchPageState extends State<ActiveSearchPage>
with NetworkListenerMixin {
static const String _loadKey = 'active_search';
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
final ThemeController _themeController = Get.find<ThemeController>();
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);
bool isInTabBarView = false;
try {
final ancestor = context.findAncestorWidgetOfExactType<TabBarView>();
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: 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,
),
),
),
],
),
),
Expanded(
child: RefreshIndicator(
color: AppConstants.primaryColor,
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: AppConstants.primaryColor,
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,
),
);
},
);
}
}