Files
wushu/lib/views/active/active_search_page.dart
2026-03-31 20:15:16 +08:00

461 lines
17 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 上报加载与搜索完成事件
// 最新变化: 初始版本
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,
),
);
},
);
}
}