import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../models/video_item.dart'; import '../../providers/search_provider.dart'; /// 搜索页(《01-开发文档》2.5.8): /// 默认页:本地搜索历史(最近 10 条)+ 热搜榜 Top10 /// 结果页:热度/最新排序切换 + 双列封面网格,点击进单视频播放页 class SearchScreen extends ConsumerStatefulWidget { const SearchScreen({super.key}); @override ConsumerState createState() => _SearchScreenState(); } class _SearchScreenState extends ConsumerState { final _inputController = TextEditingController(); final _scrollController = ScrollController(); @override void initState() { super.initState(); ref.read(searchProvider.notifier).loadHot(); _scrollController.addListener(_onScroll); } @override void dispose() { _inputController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels > _scrollController.position.maxScrollExtent - 200) { ref.read(searchProvider.notifier).loadMore(); } } void _submit(String keyword) => ref.read(searchProvider.notifier).search(keyword); @override Widget build(BuildContext context) { final state = ref.watch(searchProvider); final notifier = ref.read(searchProvider.notifier); return Scaffold( body: SafeArea( child: Column( children: [ // 搜索输入栏 Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back_ios, size: 20), onPressed: () => context.pop(), ), Expanded( child: TextField( controller: _inputController, autofocus: true, maxLength: 30, textInputAction: TextInputAction.search, onSubmitted: _submit, style: const TextStyle(fontSize: 14), decoration: const InputDecoration( hintText: '搜索视频标题或标签', counterText: '', isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), ), ), ), TextButton( onPressed: () => _submit(_inputController.text), child: const Text( '搜索', style: TextStyle( color: AppColors.primary, fontWeight: FontWeight.w600, ), ), ), ], ), ), // 内容:默认页 / 结果页 Expanded( child: state.searched ? _buildResults(state, notifier) : _buildDefault(state, notifier), ), ], ), ), ); } /// 默认页:搜索历史 + 热搜榜 Widget _buildDefault(SearchState state, SearchNotifier notifier) { return ListView( padding: const EdgeInsets.symmetric(horizontal: 16), children: [ if (state.history.isNotEmpty) ...[ Row( children: [ const Expanded( child: Text( '搜索历史', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), ), IconButton( icon: const Icon(Icons.delete_outline, size: 20, color: AppColors.textSecondary), onPressed: notifier.clearHistory, ), ], ), Wrap( spacing: 8, runSpacing: 8, children: [ for (final word in state.history) GestureDetector( onTap: () { _inputController.text = word; _submit(word); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), ), child: Text( word, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, ), ), ), ), ], ), const SizedBox(height: 24), ], const Text( '热搜榜', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), if (state.hotWords.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center( child: Text( '暂无热搜', style: TextStyle(color: AppColors.textWeak), ), ), ) else for (var i = 0; i < state.hotWords.length; i++) ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: SizedBox( width: 24, child: Text( '${i + 1}', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, // 前三名品牌红,其余灰色 color: i < 3 ? AppColors.primary : AppColors.textWeak, ), ), ), title: Text(state.hotWords[i]), onTap: () { _inputController.text = state.hotWords[i]; _submit(state.hotWords[i]); }, ), ], ); } /// 结果页:排序切换 + 双列网格 Widget _buildResults(SearchState state, SearchNotifier notifier) { return Column( children: [ // 排序切换 Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ for (final s in SearchSort.values) ...[ GestureDetector( onTap: () => notifier.search(state.keyword, sort: s), child: Text( s.label, style: TextStyle( fontSize: 14, fontWeight: s == state.sort ? FontWeight.w700 : FontWeight.w400, color: s == state.sort ? Colors.white : AppColors.textSecondary, ), ), ), const SizedBox(width: 20), ], const Spacer(), if (state.total > 0) Text( '共 ${state.total} 条', style: const TextStyle( color: AppColors.textWeak, fontSize: 12, ), ), ], ), ), const SizedBox(height: 8), Expanded( child: state.isSearching ? const Center( child: CircularProgressIndicator(color: AppColors.primary), ) : state.items.isEmpty ? const Center( child: Text( '未找到相关视频', style: TextStyle(color: AppColors.textSecondary), ), ) : GridView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 0.68, ), itemCount: state.items.length + (state.isLoadingMore ? 2 : 0), itemBuilder: (context, index) { if (index >= state.items.length) { return const Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary, ), ), ); } return VideoGridCard(item: state.items[index]); }, ), ), ], ); } } /// 视频网格卡片:封面 + 标题 + 作者 + 播放量(搜索结果 / 标签视频页共用) class VideoGridCard extends StatelessWidget { const VideoGridCard({super.key, required this.item}); final VideoItem item; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => context.push('/search/play', extra: item), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 封面 Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( width: double.infinity, color: AppColors.surface, child: item.coverUrl.isNotEmpty ? CachedNetworkImage( imageUrl: item.coverUrl, fit: BoxFit.cover, errorWidget: (_, __, ___) => const Icon( Icons.play_circle_outline, color: AppColors.textWeak, size: 40, ), ) : const Icon( Icons.play_circle_outline, color: AppColors.textWeak, size: 40, ), ), ), ), const SizedBox(height: 4), Text( item.title.isNotEmpty ? item.title : '未命名视频', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13), ), Text( '${item.author.nickname} · ${_formatCount(item.viewCount)}次播放', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textWeak, fontSize: 11, ), ), ], ), ); } static String _formatCount(int n) { if (n >= 10000) return '${(n / 10000).toStringAsFixed(1)}w'; return n.toString(); } }