fix:优化使用常用技术框架

This commit is contained in:
jingyun
2026-06-03 16:29:53 +08:00
parent e93356e849
commit e2554edfab
22 changed files with 1319 additions and 661 deletions
+144 -80
View File
@@ -1,6 +1,9 @@
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/wise_tokens.dart';
@@ -10,7 +13,7 @@ import '../../widgets/mini_player.dart';
import '../../widgets/states.dart';
import '../shared/report_card_widget.dart';
class ReportsPage extends StatefulWidget {
class ReportsPage extends HookConsumerWidget {
const ReportsPage({
required this.dataSource,
required this.onPlay,
@@ -25,29 +28,36 @@ class ReportsPage extends StatefulWidget {
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 void Function(String audioId, String reportId, String title, int durationSec)?
onStartModuleAudio;
final VoidCallback? onToggleAudio;
final void Function(int delta)? onSeekAudio;
final VoidCallback? onSpeed;
@override
State<ReportsPage> createState() => _ReportsPageState();
}
Widget build(BuildContext context, WidgetRef ref) {
final query = useState('');
final topic = useState('');
final hasAudio = useState(false);
final snapshot = ref.watch(reportsProvider);
class _ReportsPageState extends State<ReportsPage> {
late Future<List<ReportCardModel>> future = widget.dataSource.reports();
String query = '';
String topic = '';
bool hasAudio = false;
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,
);
@override
Widget build(BuildContext context) {
return FutureBuilder<List<ReportCardModel>>(
future: future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) return const LoadingState(label: '正在搜索研报');
if (snapshot.hasError) return ErrorState(message: snapshot.error.toString(), onRetry: () => setState(() => future = widget.dataSource.reports()));
final items = applyFilters(snapshot.data ?? const []);
return ListView(
padding: const EdgeInsets.all(WiseSpacing.x4),
children: [
@@ -55,50 +65,85 @@ class _ReportsPageState extends State<ReportsPage> {
decoration: InputDecoration(
hintText: '搜索标题、机构或主题',
prefixIcon: const Icon(Icons.search),
suffixIcon: query.isEmpty ? null : IconButton(onPressed: () => setState(() => query = ''), icon: const Icon(Icons.close)),
suffixIcon: currentQuery.isEmpty
? null
: IconButton(
onPressed: () => query.value = '',
icon: const Icon(Icons.close),
),
filled: true,
fillColor: WiseColors.surface,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(WiseRadius.pill), borderSide: BorderSide.none),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(WiseRadius.pill),
borderSide: BorderSide.none,
),
),
onChanged: (value) => setState(() => query = value.trim()),
onChanged: (value) => query.value = value.trim(),
),
const SizedBox(height: WiseSpacing.x3),
Row(
children: [
AppButton(label: '筛选', icon: Icons.tune, kind: AppButtonKind.ghost, onPressed: openFilterSheet),
AppButton(
label: '筛选',
icon: Icons.tune,
kind: AppButtonKind.ghost,
onPressed: items.isEmpty
? null
: () => _openFilterSheet(
context,
items: items,
topic: topic,
),
),
const SizedBox(width: WiseSpacing.x2),
AppChip(label: '有音频', selected: hasAudio, onTap: () => setState(() => hasAudio = !hasAudio)),
AppChip(
label: '有音频',
selected: currentHasAudio,
onTap: () => hasAudio.value = !currentHasAudio,
),
],
),
const SizedBox(height: WiseSpacing.x3),
Text('${items.length} 篇研报解读${query.isNotEmpty || topic.isNotEmpty || hasAudio ? '(已筛选)' : ''}', style: Theme.of(context).textTheme.bodySmall),
Text(
'${filtered.length} 篇研报解读${currentQuery.isNotEmpty || currentTopic.isNotEmpty || currentHasAudio ? '(已筛选)' : ''}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: WiseSpacing.x3),
if (items.isEmpty)
if (filtered.isEmpty)
EmptyState(
title: query.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报',
message: query.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试',
title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报',
message: currentQuery.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试',
actionLabel: '清除筛选',
onAction: () => setState(() {
query = '';
topic = '';
hasAudio = false;
}),
onAction: () {
query.value = '';
topic.value = '';
hasAudio.value = false;
},
)
else
for (final report in items) ...[
for (final report in filtered) ...[
ReportCardWidget(
report: report,
onTap: () => openReportDetail(
context,
widget.dataSource,
dataSource,
report,
player: widget.player,
onStartAudio: widget.onStartModuleAudio,
onToggleAudio: widget.onToggleAudio,
onSeekAudio: widget.onSeekAudio,
onSpeed: widget.onSpeed,
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,
),
),
onPlayTap: () => widget.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),
],
@@ -107,51 +152,70 @@ class _ReportsPageState extends State<ReportsPage> {
},
);
}
}
List<ReportCardModel> applyFilters(List<ReportCardModel> items) {
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();
}
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() {
widget.dataSource.reports().then((items) {
if (!mounted) return;
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,
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: [
Text('筛选研报', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: WiseSpacing.x3),
Wrap(
spacing: WiseSpacing.x2,
runSpacing: WiseSpacing.x2,
children: [
AppChip(label: '全部主题', selected: topic.isEmpty, onTap: () => selectTopic('')),
for (final t in topics) AppChip(label: t, selected: topic == t, onTap: () => selectTopic(t)),
],
AppChip(
label: '全部主题',
selected: topic.value.isEmpty,
onTap: () => topic.value = '',
),
const SizedBox(height: WiseSpacing.x4),
AppButton(label: '完成', expand: true, onPressed: () => Navigator.pop(context)),
for (final t in topics)
AppChip(
label: t,
selected: topic.value == t,
onTap: () => topic.value = t,
),
],
),
),
);
});
}
void selectTopic(String value) {
setState(() => topic = value);
}
const SizedBox(height: WiseSpacing.x4),
AppButton(
label: '完成',
expand: true,
onPressed: () => Navigator.pop(context),
),
],
),
),
);
}