239 lines
7.8 KiB
Dart
239 lines
7.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.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/app_icons.dart';
|
|
import '../../theme/yanting_text.dart';
|
|
import '../../theme/yanting_tokens.dart';
|
|
import '../../theme/wise_tokens.dart';
|
|
import '../../widgets/app_buttons.dart';
|
|
import '../../widgets/badges.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 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 ListView(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
WiseSpacing.x4,
|
|
4,
|
|
WiseSpacing.x4,
|
|
16,
|
|
),
|
|
children: [
|
|
const PageHeader(title: '研报', subtitle: '全部已发布研报解读'),
|
|
TextField(
|
|
decoration: InputDecoration(
|
|
hintText: '搜索标题、机构或主题',
|
|
prefixIcon: const Icon(AppIcons.search),
|
|
suffixIcon: currentQuery.isEmpty
|
|
? null
|
|
: IconButton(
|
|
onPressed: () => query.value = '',
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(YantingRadius.md),
|
|
borderSide: const BorderSide(color: YantingColors.input),
|
|
),
|
|
),
|
|
onChanged: (value) => query.value = value.trim(),
|
|
),
|
|
const SizedBox(height: WiseSpacing.x3),
|
|
Row(
|
|
children: [
|
|
AppButton(
|
|
label: '筛选',
|
|
icon: AppIcons.filter,
|
|
kind: AppButtonKind.ghost,
|
|
onPressed: items.isEmpty
|
|
? null
|
|
: () => _openFilterSheet(
|
|
context,
|
|
items: items,
|
|
topic: topic,
|
|
),
|
|
),
|
|
const SizedBox(width: WiseSpacing.x2),
|
|
AppButton(
|
|
label: '最新',
|
|
icon: AppIcons.sort,
|
|
kind: AppButtonKind.ghost,
|
|
onPressed: () {},
|
|
),
|
|
const SizedBox(width: WiseSpacing.x2),
|
|
AppChip(
|
|
label: '音频',
|
|
selected: currentHasAudio,
|
|
onTap: () => hasAudio.value = !currentHasAudio,
|
|
),
|
|
const Spacer(),
|
|
Text('共 ${filtered.length} 篇', style: YantingText.meta),
|
|
],
|
|
),
|
|
const SizedBox(height: WiseSpacing.x3),
|
|
if (filtered.isEmpty)
|
|
EmptyState(
|
|
title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报',
|
|
message: currentQuery.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试',
|
|
actionLabel: '清除筛选',
|
|
onAction: () {
|
|
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: WiseSpacing.x3),
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
List<ReportCardModel> _applyFilters(
|
|
List<ReportCardModel> 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<ReportCardModel> items,
|
|
required ValueNotifier<String> topic,
|
|
}) {
|
|
final topics = {for (final item in items) ...item.topics}.toList();
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
showDragHandle: true,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)),
|
|
),
|
|
builder: (context) => Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 28),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('筛选研报', style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: WiseSpacing.x3),
|
|
Wrap(
|
|
spacing: WiseSpacing.x2,
|
|
runSpacing: WiseSpacing.x2,
|
|
children: [
|
|
AppChip(
|
|
label: '全部主题',
|
|
selected: topic.value.isEmpty,
|
|
onTap: () => topic.value = '',
|
|
),
|
|
for (final t in topics)
|
|
AppChip(
|
|
label: t,
|
|
selected: topic.value == t,
|
|
onTap: () => topic.value = t,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: WiseSpacing.x4),
|
|
AppButton(
|
|
label: '完成',
|
|
expand: true,
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|