import 'package:flutter/material.dart'; import '../../data/api/report_data_source.dart'; import '../../data/models/models.dart'; import '../../theme/wise_tokens.dart'; import '../../widgets/app_buttons.dart'; import '../../widgets/app_card.dart'; import '../../widgets/badges.dart'; import '../../widgets/mini_player.dart'; import '../../widgets/sheets.dart'; import '../../widgets/states.dart'; import 'modules/renderer_registry.dart'; class ReportDetailPage extends StatefulWidget { const ReportDetailPage({ required this.reportId, required this.dataSource, this.player = const PlayerStateModel(), this.onStartAudio, this.onToggleAudio, this.onSeekAudio, this.onSpeed, super.key, }); final String reportId; final ReportDataSource dataSource; final PlayerStateModel player; final void Function( String audioId, String reportId, String title, int durationSec, )? onStartAudio; final VoidCallback? onToggleAudio; final void Function(int delta)? onSeekAudio; final VoidCallback? onSpeed; @override State createState() => _ReportDetailPageState(); } class _ReportDetailPageState extends State { static const registry = ModuleRendererRegistry(); late Future future = widget.dataSource.reportDetail( widget.reportId, ); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('研报详情')), body: FutureBuilder( future: future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const LoadingState(); } if (snapshot.hasError) { return ErrorState( message: snapshot.error.toString(), onRetry: () => setState( () => future = widget.dataSource.reportDetail(widget.reportId), ), ); } final detail = snapshot.data!; return ListView( padding: const EdgeInsets.all(WiseSpacing.x4), children: [ AppCard( color: WiseColors.secondary200, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: WiseSpacing.x2, runSpacing: WiseSpacing.x2, children: [ AppBadge( text: detail.interpretationLabel, kind: BadgeKind.brand, ), if (detail.hasAudio) const AppBadge( text: '音频', icon: Icons.graphic_eq, kind: BadgeKind.audio, ), AppBadge( text: asString(detail.source['source_tier']), icon: Icons.verified_outlined, kind: BadgeKind.tier, ), ], ), const SizedBox(height: WiseSpacing.x3), Text( detail.titleCn, maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.headlineSmall, ), if (detail.oneLiner.isNotEmpty) ...[ const SizedBox(height: WiseSpacing.x2), Text( detail.oneLiner, style: Theme.of(context).textTheme.bodyMedium, ), ], const SizedBox(height: WiseSpacing.x3), Text( '${detail.institution.nameCn} · ${formatDate(detail.releasedAt)}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), const SizedBox(height: WiseSpacing.x4), _ActionBar(detail: detail), const SizedBox(height: WiseSpacing.x4), _Toc(modules: detail.modules), const SizedBox(height: WiseSpacing.x4), for (final module in detail.modules) ...[ registry.card( context: context, module: module, report: detail, dataSource: widget.dataSource, player: widget.player, onStartAudio: widget.onStartAudio, onToggleAudio: widget.onToggleAudio, onSeekAudio: widget.onSeekAudio, onSpeed: widget.onSpeed, ), const SizedBox(height: WiseSpacing.x4), ], ], ); }, ), ); } } class _ActionBar extends StatelessWidget { const _ActionBar({required this.detail}); final ReportDetail detail; @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: AppButton( label: '收藏', icon: Icons.favorite_border, kind: AppButtonKind.ghost, onPressed: () => showLoginSheet(context, reason: '登录后保存到你的收藏'), ), ), const SizedBox(width: WiseSpacing.x2), Expanded( child: AppButton( label: '原文', icon: Icons.open_in_new, kind: AppButtonKind.ghost, onPressed: () => showOutboundSheet(context, title: detail.titleCn), ), ), ], ); } } class _Toc extends StatelessWidget { const _Toc({required this.modules}); final List modules; @override Widget build(BuildContext context) { if (modules.isEmpty) return const SizedBox.shrink(); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final module in modules) Padding( padding: const EdgeInsets.only(right: WiseSpacing.x2), child: AppBadge(text: module.titleCn, kind: BadgeKind.brand), ), ], ), ); } }