fix:按html的假数据demo
This commit is contained in:
@@ -4,6 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../data/api/report_data_source.dart';
|
||||
import '../../../data/models/models.dart';
|
||||
import '../../../theme/app_icons.dart';
|
||||
import '../../../theme/yanting_text.dart';
|
||||
import '../../../theme/yanting_tokens.dart';
|
||||
import '../../../theme/wise_tokens.dart';
|
||||
import '../../../widgets/app_card.dart';
|
||||
import '../../../widgets/badges.dart';
|
||||
@@ -50,7 +53,7 @@ class ModuleRendererRegistry {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ModuleHeader(module: module),
|
||||
const SizedBox(height: WiseSpacing.x4),
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
_contentFor(
|
||||
context,
|
||||
type: module.type,
|
||||
@@ -66,12 +69,12 @@ class ModuleRendererRegistry {
|
||||
compact: module.renderMode != 'inline',
|
||||
),
|
||||
if (module.hasDetailPage) ...[
|
||||
const SizedBox(height: WiseSpacing.x4),
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: openDetail,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
icon: const Icon(AppIcons.externalLink),
|
||||
label: const Text('查看详情'),
|
||||
),
|
||||
),
|
||||
@@ -183,20 +186,20 @@ class ModuleDetailPage extends HookConsumerWidget {
|
||||
body: snapshot.connectionState != ConnectionState.done
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: snapshot.hasError
|
||||
? Center(
|
||||
child: TextButton(
|
||||
onPressed: () => retryCount.value++,
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _ModuleDetailContent(
|
||||
detail: snapshot.data!,
|
||||
report: report,
|
||||
registry: registry,
|
||||
? Center(
|
||||
child: TextButton(
|
||||
onPressed: () => retryCount.value++,
|
||||
child: Text(
|
||||
snapshot.error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _ModuleDetailContent(
|
||||
detail: snapshot.data!,
|
||||
report: report,
|
||||
registry: registry,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,8 +218,13 @@ class _ModuleDetailContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(WiseSpacing.x4),
|
||||
padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16),
|
||||
children: [
|
||||
Text(
|
||||
detail.titleCn,
|
||||
style: YantingText.sectionTitle.copyWith(fontSize: 21),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
AppCard(
|
||||
child: registry.page(
|
||||
context,
|
||||
@@ -226,10 +234,7 @@ class _ModuleDetailContent extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
Text(
|
||||
'缓存版本 ${detail.cacheVersion}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text('缓存版本 ${detail.cacheVersion}', style: YantingText.meta),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -247,7 +252,7 @@ class _ModuleHeader extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
module.titleCn,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
),
|
||||
if (module.layer.isNotEmpty)
|
||||
@@ -276,7 +281,7 @@ class _BasicInfo extends StatelessWidget {
|
||||
payload['summary_cn'],
|
||||
asString(payload['scope_cn'], report?.oneLiner ?? ''),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: YantingText.body,
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
Wrap(
|
||||
@@ -306,8 +311,14 @@ class _CoreInsights extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final point in points)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x3),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: WiseSpacing.x3),
|
||||
padding: const EdgeInsets.all(WiseSpacing.x3),
|
||||
decoration: BoxDecoration(
|
||||
color: YantingColors.background,
|
||||
border: Border.all(color: YantingColors.border),
|
||||
borderRadius: BorderRadius.circular(YantingRadius.md),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -316,10 +327,7 @@ class _CoreInsights extends StatelessWidget {
|
||||
kind: _kindBadge(asString(point['kind'])),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(point['text']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(asString(point['text']), style: YantingText.body),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -341,13 +349,10 @@ class _SourceCompliance extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (asString(payload['source_note']).isNotEmpty)
|
||||
Text(
|
||||
asString(payload['source_note']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(asString(payload['source_note']), style: YantingText.body),
|
||||
if (institution != null) ...[
|
||||
const SizedBox(height: WiseSpacing.x4),
|
||||
Text('发布机构', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text('发布机构', style: YantingText.cardTitle.copyWith(fontSize: 17)),
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
_InfoLine(label: '机构名称', value: institution.nameCn),
|
||||
if (institution.nameEn.isNotEmpty)
|
||||
@@ -373,24 +378,20 @@ class _SourceCompliance extends StatelessWidget {
|
||||
],
|
||||
if (asString(payload['copyright_cn']).isNotEmpty) ...[
|
||||
const SizedBox(height: WiseSpacing.x4),
|
||||
Text(
|
||||
asString(payload['copyright_cn']),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(asString(payload['copyright_cn']), style: YantingText.meta),
|
||||
],
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x109A6500),
|
||||
borderRadius: BorderRadius.circular(WiseRadius.sm),
|
||||
color: YantingColors.background,
|
||||
border: Border.all(color: YantingColors.border),
|
||||
borderRadius: BorderRadius.circular(YantingRadius.md),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(WiseSpacing.x3),
|
||||
child: Text(
|
||||
asString(payload['disclaimer'], '本内容为公开/授权研报的结构化解读,不构成投资建议。'),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: WiseColors.warning),
|
||||
style: YantingText.meta.copyWith(color: YantingColors.warning),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -415,12 +416,12 @@ class _InfoLine extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: WiseColors.ink700),
|
||||
style: YantingText.badge.copyWith(
|
||||
color: YantingColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(value, style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(value, style: YantingText.body),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -478,14 +479,12 @@ class _InstitutionModule extends StatelessWidget {
|
||||
final name = asString(payload['name_cn'], report?.institution.nameCn ?? '');
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.account_balance_outlined, color: WiseColors.primary),
|
||||
const Icon(AppIcons.bank, color: YantingColors.foreground),
|
||||
const SizedBox(width: WiseSpacing.x2),
|
||||
Expanded(
|
||||
child: Text(name, style: Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
Expanded(child: Text(name, style: YantingText.body)),
|
||||
Text(
|
||||
'${asInt(payload['report_count'], report?.institution.reportCount ?? 0)} 份',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: YantingText.meta,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -509,19 +508,15 @@ class _SectionsModule extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (summary.isNotEmpty)
|
||||
Text(summary, style: Theme.of(context).textTheme.bodyMedium),
|
||||
if (summary.isNotEmpty) Text(summary, style: YantingText.body),
|
||||
for (final section in sections) ...[
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
Text(
|
||||
asString(section['heading']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(section['body']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(asString(section['body']), style: YantingText.body),
|
||||
],
|
||||
],
|
||||
);
|
||||
@@ -542,39 +537,40 @@ class _KeyDataModule extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final row in rows)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x4),
|
||||
child: Column(
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: WiseSpacing.x3),
|
||||
padding: const EdgeInsets.all(WiseSpacing.x3),
|
||||
decoration: BoxDecoration(
|
||||
color: YantingColors.secondary,
|
||||
borderRadius: BorderRadius.circular(YantingRadius.md),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
asString(row['metric']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(asString(row['metric']), style: YantingText.meta),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
asString(row['judgment'], asString(row['importance'])),
|
||||
style: YantingText.body.copyWith(
|
||||
color: YantingColors.foreground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
const SizedBox(width: WiseSpacing.x2),
|
||||
Text(
|
||||
_valueWithUnit(row),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.right,
|
||||
style: YantingText.cardTitle.copyWith(
|
||||
fontSize: 17,
|
||||
fontFeatures: YantingTypographyFeatures.tabularNums,
|
||||
),
|
||||
),
|
||||
if (asString(
|
||||
row['judgment'],
|
||||
asString(row['importance']),
|
||||
).isNotEmpty) ...[
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(row['judgment'], asString(row['importance'])),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
if (asString(row['importance']).isNotEmpty &&
|
||||
asString(row['importance']) !=
|
||||
asString(row['judgment'])) ...[
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(row['importance']),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -596,37 +592,81 @@ class _TimelineModule extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final event in events)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (asString(event['date']).isNotEmpty)
|
||||
Text(
|
||||
asString(event['date']),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: WiseColors.primary),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(event['event']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(event['impact']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
for (var index = 0; index < events.length; index++)
|
||||
_TimelineEntry(
|
||||
event: events[index],
|
||||
isLast: index == events.length - 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimelineEntry extends StatelessWidget {
|
||||
const _TimelineEntry({required this.event, required this.isLast});
|
||||
|
||||
final JsonMap event;
|
||||
final bool isLast;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 9,
|
||||
height: 9,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
decoration: const BoxDecoration(
|
||||
color: YantingColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 1,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
color: YantingColors.border,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: WiseSpacing.x2),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x3),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (asString(event['date']).isNotEmpty)
|
||||
Text(
|
||||
asString(event['date']),
|
||||
style: YantingText.meta.copyWith(
|
||||
color: YantingColors.foreground,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(event['event']),
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(asString(event['impact']), style: YantingText.body),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StudyGuideModule extends StatelessWidget {
|
||||
const _StudyGuideModule({required this.payload, required this.compact});
|
||||
|
||||
@@ -642,21 +682,15 @@ class _StudyGuideModule extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (asString(payload['intro_cn']).isNotEmpty)
|
||||
Text(
|
||||
asString(payload['intro_cn']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(asString(payload['intro_cn']), style: YantingText.body),
|
||||
for (final item in faqs)
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: Text(asString(item['question'])),
|
||||
title: Text(asString(item['question']), style: YantingText.body),
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
asString(item['answer']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
child: Text(asString(item['answer']), style: YantingText.body),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -694,7 +728,7 @@ class _StructureGraphModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
asString(payload['root']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
for (final node in nodes)
|
||||
@@ -705,16 +739,13 @@ class _StructureGraphModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
asString(node['label']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
for (final child in asStringList(node['children']))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x1),
|
||||
child: Text(
|
||||
child,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
child: Text(child, style: YantingText.body),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -745,12 +776,12 @@ class _RelatedSourcesModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
asString(item['title']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(item['summary_cn'], asString(item['source_name'])),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: YantingText.body,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -784,34 +815,34 @@ class _DifferentiatedViewModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
asString(item['topic']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
if (asString(item['consensus_view']).isNotEmpty) ...[
|
||||
Text(
|
||||
'常见观点',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: WiseColors.ink700),
|
||||
style: YantingText.badge.copyWith(
|
||||
color: YantingColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(item['consensus_view']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: YantingText.body,
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
],
|
||||
if (asString(item['report_position']).isNotEmpty) ...[
|
||||
Text(
|
||||
'报告观点',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: WiseColors.primary),
|
||||
style: YantingText.badge.copyWith(
|
||||
color: YantingColors.foreground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(item['report_position']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: YantingText.body,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -842,10 +873,7 @@ class _WeaknessesModule extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (asString(payload['disclaimer_cn']).isNotEmpty)
|
||||
Text(
|
||||
asString(payload['disclaimer_cn']),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(asString(payload['disclaimer_cn']), style: YantingText.meta),
|
||||
for (final item in items)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -857,13 +885,10 @@ class _WeaknessesModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
asString(item['topic']),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: YantingText.cardTitle.copyWith(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
Text(
|
||||
asString(item['weakness']),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(asString(item['weakness']), style: YantingText.body),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -881,9 +906,9 @@ class _WeaknessesModule extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'需要继续验证',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: WiseColors.warning),
|
||||
style: YantingText.badge.copyWith(
|
||||
color: YantingColors.warning,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: WiseSpacing.x1),
|
||||
for (final note
|
||||
@@ -892,10 +917,7 @@ class _WeaknessesModule extends StatelessWidget {
|
||||
: counterEvidence)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: WiseSpacing.x1),
|
||||
child: Text(
|
||||
note,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
child: Text(note, style: YantingText.meta),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -922,15 +944,11 @@ class _Preview extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (headline.isNotEmpty)
|
||||
Text(headline, style: Theme.of(context).textTheme.bodyMedium),
|
||||
if (headline.isNotEmpty) Text(headline, style: YantingText.body),
|
||||
for (final item in highlights.take(3))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: WiseSpacing.x1),
|
||||
child: Text(
|
||||
'• $item',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
child: Text('• $item', style: YantingText.meta),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -953,7 +971,7 @@ class _TextLines extends StatelessWidget {
|
||||
.join('\n');
|
||||
return Text(
|
||||
values.isEmpty ? '该模块暂无可展示内容。' : values,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: YantingText.body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../data/api/report_data_source.dart';
|
||||
import '../../data/models/models.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/app_card.dart';
|
||||
@@ -42,10 +45,11 @@ class ReportDetailPage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final retryCount = useState(0);
|
||||
final detailFuture = useMemoized(
|
||||
() => dataSource.reportDetail(reportId),
|
||||
[dataSource, reportId, retryCount.value],
|
||||
);
|
||||
final detailFuture = useMemoized(() => dataSource.reportDetail(reportId), [
|
||||
dataSource,
|
||||
reportId,
|
||||
retryCount.value,
|
||||
]);
|
||||
final snapshot = useFuture(detailFuture);
|
||||
const registry = ModuleRendererRegistry();
|
||||
|
||||
@@ -54,20 +58,20 @@ class ReportDetailPage extends HookConsumerWidget {
|
||||
body: snapshot.connectionState != ConnectionState.done
|
||||
? const LoadingState()
|
||||
: snapshot.hasError
|
||||
? ErrorState(
|
||||
message: snapshot.error.toString(),
|
||||
onRetry: () => retryCount.value++,
|
||||
)
|
||||
: _ReportDetailContent(
|
||||
detail: snapshot.data!,
|
||||
dataSource: dataSource,
|
||||
player: player,
|
||||
onStartAudio: onStartAudio,
|
||||
onToggleAudio: onToggleAudio,
|
||||
onSeekAudio: onSeekAudio,
|
||||
onSpeed: onSpeed,
|
||||
registry: registry,
|
||||
),
|
||||
? ErrorState(
|
||||
message: snapshot.error.toString(),
|
||||
onRetry: () => retryCount.value++,
|
||||
)
|
||||
: _ReportDetailContent(
|
||||
detail: snapshot.data!,
|
||||
dataSource: dataSource,
|
||||
player: player,
|
||||
onStartAudio: onStartAudio,
|
||||
onToggleAudio: onToggleAudio,
|
||||
onSeekAudio: onSeekAudio,
|
||||
onSpeed: onSpeed,
|
||||
registry: registry,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -102,10 +106,11 @@ class _ReportDetailContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(WiseSpacing.x4),
|
||||
padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16),
|
||||
children: [
|
||||
AppCard(
|
||||
color: WiseColors.secondary200,
|
||||
color: YantingColors.brandSoft,
|
||||
borderColor: YantingColors.brandSoftBorder,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -120,12 +125,11 @@ class _ReportDetailContent extends StatelessWidget {
|
||||
if (detail.hasAudio)
|
||||
const AppBadge(
|
||||
text: '音频',
|
||||
icon: Icons.graphic_eq,
|
||||
icon: AppIcons.playCircle,
|
||||
kind: BadgeKind.audio,
|
||||
),
|
||||
AppBadge(
|
||||
text: asString(detail.source['source_tier']),
|
||||
icon: Icons.verified_outlined,
|
||||
kind: BadgeKind.tier,
|
||||
),
|
||||
],
|
||||
@@ -135,19 +139,16 @@ class _ReportDetailContent extends StatelessWidget {
|
||||
detail.titleCn,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
style: YantingText.sectionTitle.copyWith(fontSize: 21),
|
||||
),
|
||||
if (detail.oneLiner.isNotEmpty) ...[
|
||||
const SizedBox(height: WiseSpacing.x2),
|
||||
Text(
|
||||
detail.oneLiner,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(detail.oneLiner, style: YantingText.body),
|
||||
],
|
||||
const SizedBox(height: WiseSpacing.x3),
|
||||
Text(
|
||||
'${detail.institution.nameCn} · ${formatDate(detail.releasedAt)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: YantingText.meta,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -188,17 +189,16 @@ class _ActionBar extends StatelessWidget {
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: '收藏',
|
||||
icon: Icons.favorite_border,
|
||||
icon: AppIcons.heart,
|
||||
kind: AppButtonKind.ghost,
|
||||
onPressed: () =>
|
||||
showLoginSheet(context, reason: '登录后保存到你的收藏'),
|
||||
onPressed: () => showLoginSheet(context, reason: '登录后保存到你的收藏'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: WiseSpacing.x2),
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
label: '原文',
|
||||
icon: Icons.open_in_new,
|
||||
icon: AppIcons.externalLink,
|
||||
kind: AppButtonKind.ghost,
|
||||
onPressed: () => showOutboundSheet(context, title: detail.titleCn),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user