fix:按html的假数据demo

This commit is contained in:
jingyun
2026-06-05 11:12:55 +08:00
parent b4272b5ec9
commit 9727b906c6
28 changed files with 2159 additions and 711 deletions
+493
View File
@@ -0,0 +1,493 @@
import '../models/models.dart';
import 'report_data_source.dart';
class MockReportDataSource implements ReportDataSource {
MockReportDataSource();
static final Institution _wgcSummary = _institutionSummary(
id: 'wgc',
nameCn: '世界黄金协会',
nameEn: 'World Gold Council',
logoUrl: 'https://www.google.com/s2/favicons?domain=www.gold.org&sz=128',
institutionType: 'industry_org',
sourceTier: 'A',
websiteUrl: 'https://www.gold.org',
coveredTopics: ['贵金属', '央行', '跨资产'],
reportCount: 6,
introCn: '世界黄金协会致力于推动黄金市场研究与应用,常发布黄金需求、投资与央行购金相关分析。',
credibilityNote: '公开数据、央行储备与 ETF 流量交叉验证,适合跟踪黄金需求结构变化。',
);
static final Institution _bisSummary = _institutionSummary(
id: 'bis',
nameCn: '国际清算银行 BIS',
nameEn: 'Bank for International Settlements',
logoUrl: 'https://www.google.com/s2/favicons?domain=www.bis.org&sz=128',
institutionType: 'official',
sourceTier: 'A',
websiteUrl: 'https://www.bis.org',
coveredTopics: ['宏观', '货币政策'],
reportCount: 4,
introCn: 'BIS 关注全球金融稳定、资本流动和银行体系结构,是宏观与金融市场观察的重要来源。',
credibilityNote: '跨国监管机构,通常以统计和制度性框架解释市场变化。',
);
static final Institution _ieaSummary = _institutionSummary(
id: 'iea',
nameCn: '国际能源署 IEA',
nameEn: 'International Energy Agency',
logoUrl: 'https://www.google.com/s2/favicons?domain=www.iea.org&sz=128',
institutionType: 'official',
sourceTier: 'A',
websiteUrl: 'https://www.iea.org',
coveredTopics: ['能源', '大宗'],
reportCount: 5,
introCn: 'IEA 主要跟踪全球原油、天然气、电力与能源转型相关供需变化。',
credibilityNote: '统计频率高,适合配合库存、产量与政策节奏观察。',
);
static final Institution _worldBankSummary = _institutionSummary(
id: 'worldbank',
nameCn: '世界银行',
nameEn: 'World Bank',
logoUrl:
'https://www.google.com/s2/favicons?domain=www.worldbank.org&sz=128',
institutionType: 'official',
sourceTier: 'A',
websiteUrl: 'https://www.worldbank.org',
coveredTopics: ['大宗', '全球增长'],
reportCount: 3,
introCn: '世界银行常通过大宗商品展望、全球增长与贫困相关研究提供宏观视角。',
credibilityNote: '跨国公共机构,适合作为全球价格和增长基准参考。',
);
static final Institution _ssgaSummary = _institutionSummary(
id: 'ssga',
nameCn: '道富环球投资管理',
nameEn: 'State Street Global Advisors',
logoUrl: 'https://www.google.com/s2/favicons?domain=www.ssga.com&sz=128',
institutionType: 'asset_manager',
sourceTier: 'A',
websiteUrl: 'https://www.ssga.com',
coveredTopics: ['贵金属', '跨资产'],
reportCount: 4,
introCn: '道富的市场观点常围绕资产配置、黄金投资和 ETF 流向展开。',
credibilityNote: '资产管理机构视角偏市场化,适合观察投资者行为和资金流。',
);
static final ReportDetail _gold = ReportDetail(
id: 'gold-demand-q2-2026',
titleCn: '黄金需求趋势:央行与亚洲买家的双轮驱动',
institution: _wgcSummary,
oneLiner: '央行购金与亚洲投资需求,构成本季黄金需求的双轮。',
source: const {'source_tier': 'A', 'source_name': 'World Gold Council'},
topics: const ['贵金属', '央行', '跨资产'],
hasAudio: true,
releasedAt: '2026-05-30',
modules: [
_module('gold-basic', 'basic_info', '基本信息', {
'summary_cn': '央行持续买入黄金,叠加亚洲投资需求回暖,支撑本季金价中枢。',
'topics': ['贵金属', '央行', '跨资产'],
}),
_module('gold-insights', 'core_insights', '核心洞察', {
'points': [
{'kind': 'view', 'text': '央行购金从战术操作转向结构性配置,已成为黄金需求底层支撑。'},
{'kind': 'number', 'text': '金价重回高位后,ETF 与亚洲实物买盘接续承接,波动被更广泛的资金流吸收。'},
{'kind': 'risk', 'text': '若美元和实际利率同步抬升,黄金的防御属性仍会面临短期回撤。'},
],
}, hasDetailPage: true),
_module('gold-audio', 'audio', '音频解读', {
'audio_id': 'audio_gold_q2_2026',
'title_cn': '黄金需求趋势:央行与亚洲买家的双轮驱动',
'duration_sec': 860,
}),
_module('gold-timeline', 'timeline', '时间线', {
'events': [
{'date': '2026-03', 'event': '央行购金维持高位', 'impact': '储备资产配置继续偏向黄金。'},
{'date': '2026-04', 'event': '亚洲投资需求回升', 'impact': '零售和机构买盘共同抬升需求。'},
{
'date': '2026-05',
'event': '金价刷新阶段高位',
'impact': '高位波动加大,但下方买盘承接仍强。',
},
],
}, hasDetailPage: true),
_module('gold-key-data', 'key_data', '关键数据', {
'rows': [
{
'metric': '央行净买入',
'value': '800+',
'unit': '',
'judgment': '仍处高位,支撑黄金中期结构性需求。',
},
{
'metric': 'ETF 流向',
'value': '净流入',
'unit': '',
'judgment': '投资盘开始重新接力。',
},
{
'metric': '亚洲实物需求',
'value': '回升',
'unit': '',
'judgment': '节庆和资产配置需求叠加。',
},
],
}),
_module('gold-study', 'study_guide', '研读指南', {
'intro_cn': '如果你只想看结论,先读核心洞察;如果你想跟踪驱动因素,继续看时间线与关键数据。',
'faq_items': [
{
'question': '为什么央行购金重要?',
'answer': '央行购金往往具有中长期配置属性,可以给金价提供比短线投机更稳定的需求锚。',
},
{
'question': '亚洲买盘如何影响金价?',
'answer': '当亚洲需求在价格高位仍然保持韧性时,金价上方空间通常更容易被市场重新定价。',
},
],
'glossary': [
{'term': 'ETF', 'definition': '交易型开放式基金,常用于观察机构资金流向。'},
{'term': '央行购金', 'definition': '中央银行增持黄金储备的行为。'},
],
}, hasDetailPage: true),
_module('gold-sources', 'related_sources', '相关来源', {
'items': [
{'title': '世界黄金协会季度需求报告', 'summary_cn': '黄金首饰、央行和投资需求的总览。'},
{'title': '道富黄金配置观点', 'summary_cn': '从资产配置角度看黄金在组合中的角色。'},
],
}),
_module('gold-weaknesses', 'weaknesses', '风险与验证', {
'disclaimer_cn': '以下内容不构成投资建议,需结合自身风险承受能力判断。',
'items': [
{
'topic': '美元波动',
'weakness': '若美元在短期内持续走强,黄金可能承压。',
'counter_evidence': '实际利率仍处下行或中性,黄金下方支撑尚在。',
},
{
'topic': '投机拥挤',
'weakness': '当资金快速涌入时,行情容易出现脉冲式回调。',
'counter_evidence': '央行和长期配置资金可部分对冲短线拥挤。',
},
],
'verification_notes': ['关注央行月度购金变化。', '关注 ETF 净流入是否持续两周以上。'],
}),
_module('gold-compliance', 'source_compliance', '来源与合规', {
'source_note': '来源以公开研报、统计口径和机构原文为准,结构化内容用于中文解读。',
'copyright_cn': '版权归原始发布机构与作者所有。',
'disclaimer': '本内容为公开/授权研报的结构化解读,不构成投资建议。',
}),
_module('gold-institution', 'institution', '机构', {
'name_cn': '世界黄金协会',
'report_count': 6,
}),
],
);
static final ReportDetail _bis = ReportDetail(
id: 'bis-quarterly-2026-q1',
titleCn: 'BIS 季报 2026年3月:市场重新校准',
institution: _bisSummary,
oneLiner: '表面平静之下,全球金融市场正经历深刻的流向切换与重新校准。',
source: const {'source_tier': 'A', 'source_name': 'BIS'},
topics: const ['宏观', '货币政策'],
hasAudio: true,
releasedAt: '2026-03-05',
modules: [
_module('bis-basic', 'basic_info', '基本信息', {
'summary_cn': 'BIS 关注全球宏观流动、资本配置和金融体系的再平衡。',
'topics': ['宏观', '货币政策'],
}),
_module('bis-insights', 'core_insights', '核心洞察', {
'points': [
{'kind': 'view', 'text': '市场定价已从单一利率路径转向对增长、通胀和资本流动的综合校准。'},
{'kind': 'number', 'text': '跨境资本流向的再分配比利率本身更能解释当下的市场分化。'},
],
}, hasDetailPage: true),
_module('bis-audio', 'audio', '音频解读', {
'audio_id': 'audio_bis_q1_2026',
'title_cn': 'BIS 季报 2026年3月:市场重新校准',
'duration_sec': 1040,
}),
_module('bis-compliance', 'source_compliance', '来源与合规', {
'source_note': '该模块用于展示机构原文与结构化解读的对应关系。',
'copyright_cn': '版权归 BIS 原始报告与作者所有。',
'disclaimer': '本内容为公开/授权研报的结构化解读,不构成投资建议。',
}),
_module('bis-institution', 'institution', '机构', {
'name_cn': '国际清算银行 BIS',
'report_count': 4,
}),
],
);
static final ReportDetail _iea = ReportDetail(
id: 'iea-oil-monthly-2026-05',
titleCn: '石油市场月报:需求放缓,供给为何仍偏紧?',
institution: _ieaSummary,
oneLiner: '需求增速放缓,但减产纪律与库存偏低支撑油价下方。',
source: const {'source_tier': 'A', 'source_name': 'IEA'},
topics: const ['能源', '大宗'],
hasAudio: true,
releasedAt: '2026-05-20',
modules: [
_module('iea-basic', 'basic_info', '基本信息', {
'summary_cn': 'IEA 月报聚焦全球原油供需、库存和宏观风险。',
'topics': ['能源', '大宗'],
}),
_module('iea-insights', 'core_insights', '核心洞察', {
'points': [
{'kind': 'view', 'text': '需求放缓并不自动意味着价格回落,关键还是供给纪律和库存水平。'},
{'kind': 'number', 'text': '库存偏低时,价格对中短期扰动的敏感度明显提高。'},
],
}, hasDetailPage: true),
_module('iea-audio', 'audio', '音频解读', {
'audio_id': 'audio_iea_2026_05',
'title_cn': '石油市场月报:需求放缓,供给为何仍偏紧?',
'duration_sec': 1120,
}),
_module('iea-key-data', 'key_data', '关键数据', {
'rows': [
{
'metric': '需求增速',
'value': '放缓',
'unit': '',
'judgment': '不代表价格立刻下行。',
},
{
'metric': '供给纪律',
'value': '偏紧',
'unit': '',
'judgment': '减产执行仍在延续。',
},
{'metric': '库存', 'value': '偏低', 'unit': '', 'judgment': '对价格形成支撑。'},
],
}),
_module('iea-compliance', 'source_compliance', '来源与合规', {
'source_note': 'IEA 原文与统计框架保持一致,中文内容仅用于结构化阅读。',
'copyright_cn': '版权归原始发布机构与作者所有。',
'disclaimer': '本内容为公开/授权研报的结构化解读,不构成投资建议。',
}),
_module('iea-institution', 'institution', '机构', {
'name_cn': '国际能源署',
'report_count': 5,
}),
],
);
static final ReportDetail _worldBank = ReportDetail(
id: 'worldbank-commodities-2026-04',
titleCn: '世界银行大宗商品展望:价格见顶了吗?',
institution: _worldBankSummary,
oneLiner: '全球增长预期放缓,但价格路径仍受供给侧扰动影响。',
source: const {'source_tier': 'A', 'source_name': 'World Bank'},
topics: const ['大宗', '全球增长'],
hasAudio: true,
releasedAt: '2026-04-18',
modules: [
_module('wb-basic', 'basic_info', '基本信息', {
'summary_cn': '世界银行展望通常从全球增长、贸易和大宗商品价格三个层面展开。',
'topics': ['大宗', '全球增长'],
}),
_module('wb-insights', 'core_insights', '核心洞察', {
'points': [
{'kind': 'view', 'text': '价格是否见顶,往往取决于供给扰动能否被需求放缓完全吸收。'},
{'kind': 'risk', 'text': '若全球增长进一步放缓,大宗商品的边际弹性会显著下降。'},
],
}, hasDetailPage: true),
_module('wb-audio', 'audio', '音频解读', {
'audio_id': 'audio_worldbank_2026_04',
'title_cn': '世界银行大宗商品展望:价格见顶了吗?',
'duration_sec': 980,
}),
_module('wb-compliance', 'source_compliance', '来源与合规', {
'source_note': '世界银行报告适合作为全球商品价格的基准视角。',
'copyright_cn': '版权归原始发布机构与作者所有。',
'disclaimer': '本内容为公开/授权研报的结构化解读,不构成投资建议。',
}),
_module('wb-institution', 'institution', '机构', {
'name_cn': '世界银行',
'report_count': 3,
}),
],
);
static final List<ReportDetail> _details = [_gold, _bis, _iea, _worldBank];
static final Map<String, ReportDetail> _detailById = {
for (final detail in _details) detail.id: detail,
};
static final Map<String, Institution> _institutionDetails = {
'wgc': _institutionDetail(
base: _wgcSummary,
recentReports: [_gold.asCard()],
),
'bis': _institutionDetail(
base: _bisSummary,
recentReports: [_bis.asCard()],
),
'iea': _institutionDetail(
base: _ieaSummary,
recentReports: [_iea.asCard()],
),
'worldbank': _institutionDetail(
base: _worldBankSummary,
recentReports: [_worldBank.asCard()],
),
'ssga': _institutionDetail(
base: _ssgaSummary,
recentReports: [_gold.asCard()],
),
};
static final Map<String, ModuleDetail> _moduleDetails = {
'gold-insights': _moduleDetail(_gold.modules[1]),
'gold-timeline': _moduleDetail(_gold.modules[3]),
'gold-study': _moduleDetail(_gold.modules[5]),
'bis-insights': _moduleDetail(_bis.modules[1]),
'iea-insights': _moduleDetail(_iea.modules[1]),
'wb-insights': _moduleDetail(_worldBank.modules[1]),
};
@override
Future<List<ReportCardModel>> recommended() async => [
_gold.asCard(),
_bis.asCard(),
_iea.asCard(),
];
@override
Future<List<ReportCardModel>> reports() async =>
_details.map((d) => d.asCard()).toList();
@override
Future<List<Institution>> institutions() async {
return _institutionDetails.values.toList()
..sort((a, b) => b.reportCount.compareTo(a.reportCount));
}
@override
Future<Institution> institutionDetail(String institutionId) async {
return _institutionDetails[institutionId] ?? _institutionDetails['wgc']!;
}
@override
Future<List<AudioItem>> listen() async {
return [
_audioFromReport(_gold, 860),
_audioFromReport(_bis, 1040),
_audioFromReport(_iea, 1120),
_audioFromReport(_worldBank, 980),
];
}
@override
Future<ReportDetail> reportDetail(String reportId) async {
return _detailById[reportId] ?? _gold;
}
@override
Future<ModuleDetail> moduleDetail(String reportId, String moduleId) async {
return _moduleDetails[moduleId] ??
ModuleDetail(
id: moduleId,
type: 'basic_info',
titleCn: '模块详情',
content: const {'preview_summary': '该模块暂无单独详情数据。'},
);
}
static Institution _institutionSummary({
required String id,
required String nameCn,
required String nameEn,
required String logoUrl,
required String institutionType,
required String sourceTier,
required String websiteUrl,
required List<String> coveredTopics,
required int reportCount,
required String introCn,
required String credibilityNote,
}) {
return Institution(
id: id,
nameCn: nameCn,
nameEn: nameEn,
logoUrl: logoUrl,
institutionType: institutionType,
sourceTier: sourceTier,
websiteUrl: websiteUrl,
coveredTopics: coveredTopics,
reportCount: reportCount,
introCn: introCn,
credibilityNote: credibilityNote,
);
}
static Institution _institutionDetail({
required Institution base,
required List<ReportCardModel> recentReports,
}) {
return Institution(
id: base.id,
nameCn: base.nameCn,
nameEn: base.nameEn,
logoUrl: base.logoUrl,
institutionType: base.institutionType,
sourceTier: base.sourceTier,
websiteUrl: base.websiteUrl,
coveredTopics: base.coveredTopics,
reportCount: base.reportCount,
latestReportAt: base.latestReportAt,
credibilityNote: base.credibilityNote,
introCn: base.introCn,
recentReports: recentReports,
);
}
static DisplayModule _module(
String id,
String type,
String titleCn,
JsonMap content, {
bool hasDetailPage = false,
String renderMode = 'inline',
}) {
return DisplayModule(
id: id,
type: type,
titleCn: titleCn,
renderMode: renderMode,
hasDetailPage: hasDetailPage,
content: content,
preview: content,
);
}
static ModuleDetail _moduleDetail(DisplayModule module) {
return ModuleDetail(
id: module.id,
type: module.type,
titleCn: module.titleCn,
content: module.content,
contentEtag: 'mock',
cacheVersion: 'mock',
);
}
static AudioItem _audioFromReport(ReportDetail report, int duration) {
return AudioItem(
audioId: 'audio_${report.id}',
reportId: report.id,
titleCn: report.titleCn,
reportTitleCn: report.titleCn,
durationSec: duration,
institution: report.institution,
releasedAt: report.releasedAt,
cacheVersion: 'mock',
);
}
}
+16 -6
View File
@@ -18,12 +18,16 @@ List<String> asStringList(Object? value) {
JsonMap asMap(Object? value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) return value.map((key, val) => MapEntry(key.toString(), val));
if (value is Map) {
return value.map((key, val) => MapEntry(key.toString(), val));
}
return const {};
}
List<JsonMap> asMapList(Object? value) {
if (value is List) return value.map(asMap).where((item) => item.isNotEmpty).toList();
if (value is List) {
return value.map(asMap).where((item) => item.isNotEmpty).toList();
}
return const [];
}
@@ -46,6 +50,7 @@ class Institution {
required this.id,
required this.nameCn,
this.nameEn = '',
this.logoUrl = '',
this.institutionType = '',
this.sourceTier = '',
this.websiteUrl = '',
@@ -60,6 +65,7 @@ class Institution {
final String id;
final String nameCn;
final String nameEn;
final String logoUrl;
final String institutionType;
final String sourceTier;
final String websiteUrl;
@@ -75,6 +81,7 @@ class Institution {
id: asString(json['institution_id']),
nameCn: asString(json['name_cn']),
nameEn: asString(json['name_en']),
logoUrl: asString(json['logo_url']),
institutionType: asString(json['institution_type']),
sourceTier: asString(json['source_tier']),
websiteUrl: asString(json['website_url']),
@@ -83,9 +90,9 @@ class Institution {
latestReportAt: json['latest_report_at']?.toString(),
credibilityNote: asString(json['credibility_note']),
introCn: asString(json['intro_cn']),
recentReports: asMapList(json['recent_reports'])
.map(ReportCardModel.fromJson)
.toList(),
recentReports: asMapList(
json['recent_reports'],
).map(ReportCardModel.fromJson).toList(),
);
}
}
@@ -161,7 +168,10 @@ class AudioItem {
audioId: asString(json['audio_id']),
reportId: asString(json['report_id']),
titleCn: asString(json['title_cn']),
reportTitleCn: asString(json['report_title_cn'], asString(json['title_cn'])),
reportTitleCn: asString(
json['report_title_cn'],
asString(json['title_cn']),
),
durationSec: asInt(json['duration_sec']),
institution: Institution.fromJson(asMap(json['institution'])),
releasedAt: json['released_at']?.toString(),
+7 -2
View File
@@ -1,14 +1,19 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'api/mock_report_data_source.dart';
import 'api/report_data_source.dart';
import 'audio_player_controller.dart';
import '../widgets/mini_player.dart';
final reportDataSourceProvider = Provider<ReportDataSource>((ref) {
const useMock = bool.fromEnvironment('YANTING_USE_MOCK', defaultValue: true);
if (useMock) {
return MockReportDataSource();
}
return RnbApiDataSource();
});
final audioPlayerControllerProvider =
StateNotifierProvider<AudioPlayerController, PlayerStateModel>((ref) {
return AudioPlayerController();
});
return AudioPlayerController();
});