import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; import '../../data/models/models.dart'; import '../../routing/app_routes.dart'; import '../../theme/yanting_text.dart'; import '../../theme/yanting_tokens.dart'; import '../../widgets/mini_player.dart'; import '../../widgets/page_header.dart'; import '../../widgets/states.dart'; import '../shared/report_card_widget.dart'; class ReportsPage extends HookConsumerWidget { const ReportsPage({ required this.dataSource, required this.onPlay, this.player = const PlayerStateModel(), this.onStartModuleAudio, this.onToggleAudio, this.onSeekAudio, this.onSpeed, super.key, }); final ReportDataSource dataSource; final void Function(AudioItem item) onPlay; final PlayerStateModel player; final void Function( String audioId, String reportId, String title, int durationSec, )? onStartModuleAudio; final VoidCallback? onToggleAudio; final void Function(int delta)? onSeekAudio; final VoidCallback? onSpeed; @override Widget build(BuildContext context, WidgetRef ref) { final theme = ShadTheme.of(context); final searchController = useTextEditingController(); final query = useState(''); final topic = useState(''); final hasAudio = useState(false); final snapshot = ref.watch(reportsProvider); return snapshot.when( loading: () => const LoadingState(label: '正在搜索研报'), error: (error, _) => ErrorState( message: error.toString(), onRetry: () => ref.invalidate(reportsProvider), ), data: (items) { final currentQuery = query.value; final currentTopic = topic.value; final currentHasAudio = hasAudio.value; final filtered = _applyFilters( items, query: currentQuery, topic: currentTopic, hasAudio: currentHasAudio, ); return SingleChildScrollView( padding: const EdgeInsets.fromLTRB( YantingSpacing.screenX, 4, YantingSpacing.screenX, 16, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const PageHeader(title: '研报', subtitle: '全部已发布研报解读'), ShadInput( controller: searchController, placeholder: const Text('搜索标题、机构或主题'), leading: const Padding( padding: EdgeInsets.only(right: 8), child: Icon(LucideIcons.search, size: 16), ), trailing: currentQuery.isEmpty ? null : Padding( padding: const EdgeInsets.only(left: 8), child: ShadButton.ghost( size: ShadButtonSize.sm, onPressed: () { searchController.clear(); query.value = ''; }, child: const Icon(LucideIcons.x, size: 16), ), ), onChanged: (value) => query.value = value.trim(), ), const SizedBox(height: YantingSpacing.cardGap), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Wrap( spacing: YantingSpacing.x2, runSpacing: YantingSpacing.x2, children: [ ShadButton.outline( onPressed: items.isEmpty ? null : () => _openFilterSheet( context, items: items, topic: topic, ), leading: const Icon( LucideIcons.slidersHorizontal, size: 16, ), child: const Text('筛选'), ), ShadButton.outline( onPressed: () {}, leading: const Icon( LucideIcons.arrowUpDown, size: 16, ), child: const Text('最新'), ), ShadBadge.secondary( onPressed: () => hasAudio.value = !currentHasAudio, backgroundColor: currentHasAudio ? theme.colorScheme.foreground : theme.colorScheme.secondary, foregroundColor: currentHasAudio ? theme.colorScheme.background : theme.colorScheme.secondaryForeground, hoverBackgroundColor: currentHasAudio ? theme.colorScheme.foreground.withValues( alpha: 0.9, ) : theme.colorScheme.border, child: const Text('音频'), ), ], ), ), const SizedBox(width: YantingSpacing.x2), Padding( padding: const EdgeInsets.only(top: 10), child: Text( '共 ${filtered.length} 篇', style: YantingText.meta, ), ), ], ), const SizedBox(height: YantingSpacing.cardGap), if (filtered.isEmpty) EmptyState( title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报', message: currentQuery.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试', actionLabel: '清除筛选', onAction: () { searchController.clear(); query.value = ''; topic.value = ''; hasAudio.value = false; }, ) else for (final report in filtered) ...[ ReportCardWidget( report: report, onTap: () => openReportDetail( context, dataSource, report, player: player, onStartAudio: onStartModuleAudio, onToggleAudio: onToggleAudio, onSeekAudio: onSeekAudio, onSpeed: onSpeed, ), onPlayTap: () => onPlay( AudioItem( audioId: 'local_${report.id}', reportId: report.id, titleCn: report.titleCn, reportTitleCn: report.titleCn, durationSec: 180, institution: report.institution, ), ), ), const SizedBox(height: YantingSpacing.x3), ], ], ), ); }, ); } } List _applyFilters( List items, { required String query, required String topic, required bool hasAudio, }) { return items.where((item) { final hay = '${item.titleCn} ${item.institution.nameCn} ${item.topics.join(' ')}' .toLowerCase(); if (query.isNotEmpty && !hay.contains(query.toLowerCase())) return false; if (topic.isNotEmpty && !item.topics.contains(topic)) return false; if (hasAudio && !item.hasAudio) return false; return true; }).toList(); } void _openFilterSheet( BuildContext context, { required List items, required ValueNotifier topic, }) { final topics = {for (final item in items) ...item.topics}.toList(); showShadSheet( context: context, side: ShadSheetSide.bottom, builder: (context) { final theme = ShadTheme.of(context); final selectedBackground = theme.colorScheme.foreground; final selectedForeground = theme.colorScheme.background; final unselectedBackground = theme.colorScheme.secondary; final unselectedForeground = theme.colorScheme.secondaryForeground; return ShadSheet( title: const Text('筛选研报'), description: const Text('按主题快速收窄列表。'), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: YantingSpacing.x2, runSpacing: YantingSpacing.x2, children: [ ShadBadge.secondary( onPressed: () => topic.value = '', backgroundColor: topic.value.isEmpty ? selectedBackground : unselectedBackground, foregroundColor: topic.value.isEmpty ? selectedForeground : unselectedForeground, hoverBackgroundColor: topic.value.isEmpty ? selectedBackground.withValues(alpha: 0.9) : theme.colorScheme.border, child: const Text('全部主题'), ), for (final t in topics) ShadBadge.secondary( onPressed: () => topic.value = t, backgroundColor: topic.value == t ? selectedBackground : unselectedBackground, foregroundColor: topic.value == t ? selectedForeground : unselectedForeground, hoverBackgroundColor: topic.value == t ? selectedBackground.withValues(alpha: 0.9) : theme.colorScheme.border, child: Text(t), ), ], ), const SizedBox(height: 12), ShadButton( width: double.infinity, onPressed: () => Navigator.pop(context), child: const Text('完成'), ), ], ), ); }, ); }