typedef JsonMap = Map; String asString(Object? value, [String fallback = '']) => value == null ? fallback : value.toString(); int asInt(Object? value, [int fallback = 0]) { if (value is int) return value; if (value is num) return value.round(); return int.tryParse(asString(value)) ?? fallback; } bool asBool(Object? value) => value == true; List asStringList(Object? value) { if (value is List) return value.map((item) => item.toString()).toList(); return const []; } JsonMap asMap(Object? value) { if (value is Map) return value; if (value is Map) { return value.map((key, val) => MapEntry(key.toString(), val)); } return const {}; } List asMapList(Object? value) { if (value is List) { return value.map(asMap).where((item) => item.isNotEmpty).toList(); } return const []; } String formatDate(String? iso) { if (iso == null || iso.isEmpty) return ''; final parsed = DateTime.tryParse(iso); if (parsed == null) return iso; return '${parsed.year}年${parsed.month}月${parsed.day}日'; } String formatDuration(int seconds) { final safe = seconds < 0 ? 0 : seconds; final minutes = safe ~/ 60; final secs = safe % 60; return '$minutes:${secs.toString().padLeft(2, '0')}'; } class Institution { const Institution({ required this.id, required this.nameCn, this.nameEn = '', this.logoUrl = '', this.institutionType = '', this.sourceTier = '', this.websiteUrl = '', this.coveredTopics = const [], this.reportCount = 0, this.latestReportAt, this.credibilityNote = '', this.introCn = '', this.recentReports = const [], }); final String id; final String nameCn; final String nameEn; final String logoUrl; final String institutionType; final String sourceTier; final String websiteUrl; final List coveredTopics; final int reportCount; final String? latestReportAt; final String credibilityNote; final String introCn; final List recentReports; factory Institution.fromJson(JsonMap json) { return 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']), coveredTopics: asStringList(json['covered_topics']), reportCount: asInt(json['report_count']), 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(), ); } } class ReportCardModel { const ReportCardModel({ required this.id, required this.titleCn, required this.institution, this.subtitleCn = '', this.oneLiner = '', this.topics = const [], this.releasedAt, this.hasAudio = false, this.interpretationLabel = '研报解读', this.sourceTier = '', this.cacheVersion = '', }); final String id; final String titleCn; final String subtitleCn; final String oneLiner; final Institution institution; final List topics; final String? releasedAt; final bool hasAudio; final String interpretationLabel; final String sourceTier; final String cacheVersion; factory ReportCardModel.fromJson(JsonMap json) { final institution = Institution.fromJson(asMap(json['institution'])); return ReportCardModel( id: asString(json['report_id']), titleCn: asString(json['title_cn']), subtitleCn: asString(json['subtitle_cn']), oneLiner: asString(json['one_liner']), institution: institution, topics: asStringList(json['topics']), releasedAt: json['released_at']?.toString(), hasAudio: asBool(json['has_audio']), interpretationLabel: asString(json['interpretation_label'], '研报解读'), sourceTier: asString(json['source_tier']), cacheVersion: asString(json['cache_version']), ); } } class AudioItem { const AudioItem({ required this.audioId, required this.reportId, required this.titleCn, required this.reportTitleCn, required this.durationSec, required this.institution, this.releasedAt, this.cacheVersion = '', }); final String audioId; final String reportId; final String titleCn; final String reportTitleCn; final int durationSec; final Institution institution; final String? releasedAt; final String cacheVersion; factory AudioItem.fromJson(JsonMap json) { return 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']), ), durationSec: asInt(json['duration_sec']), institution: Institution.fromJson(asMap(json['institution'])), releasedAt: json['released_at']?.toString(), cacheVersion: asString(json['cache_version']), ); } } class DisplayModule { const DisplayModule({ required this.id, required this.type, required this.titleCn, required this.renderMode, this.layer = '', this.hasDetailPage = false, this.sortOrder = 0, this.content = const {}, this.preview = const {}, this.contentRef = '', this.contentEtag = '', }); final String id; final String type; final String titleCn; final String layer; final String renderMode; final bool hasDetailPage; final int sortOrder; final JsonMap content; final JsonMap preview; final String contentRef; final String contentEtag; factory DisplayModule.fromJson(JsonMap json) { return DisplayModule( id: asString(json['module_id']), type: asString(json['type']), titleCn: asString(json['title_cn'], asString(json['type'])), layer: asString(json['layer']), renderMode: asString(json['render_mode'], 'inline'), hasDetailPage: asBool(json['has_detail_page']), sortOrder: asInt(json['sort_order']), content: asMap(json['content']), preview: asMap(json['preview']), contentRef: asString(json['content_ref']), contentEtag: asString(json['content_etag']), ); } } class ReportDetail { const ReportDetail({ required this.id, required this.titleCn, required this.institution, this.subtitleCn = '', this.originalTitle = '', this.oneLiner = '', this.source = const {}, this.topics = const [], this.hasAudio = false, this.interpretationLabel = '研报解读', this.riskDisclaimer = '', this.releasedAt, this.cacheVersion = '', this.modules = const [], }); final String id; final String titleCn; final String subtitleCn; final String originalTitle; final String oneLiner; final Institution institution; final JsonMap source; final List topics; final bool hasAudio; final String interpretationLabel; final String riskDisclaimer; final String? releasedAt; final String cacheVersion; final List modules; factory ReportDetail.fromJson(JsonMap json) { return ReportDetail( id: asString(json['report_id']), titleCn: asString(json['title_cn']), subtitleCn: asString(json['subtitle_cn']), originalTitle: asString(json['original_title']), oneLiner: asString(json['one_liner']), institution: Institution.fromJson(asMap(json['institution'])), source: asMap(json['source']), topics: asStringList(json['topics']), hasAudio: asBool(json['has_audio']), interpretationLabel: asString(json['interpretation_label'], '研报解读'), riskDisclaimer: asString(json['risk_disclaimer']), releasedAt: json['released_at']?.toString(), cacheVersion: asString(json['cache_version']), modules: asMapList(json['modules']).map(DisplayModule.fromJson).toList(), ); } ReportCardModel asCard() { return ReportCardModel( id: id, titleCn: titleCn, subtitleCn: subtitleCn, oneLiner: oneLiner, institution: institution, topics: topics, releasedAt: releasedAt, hasAudio: hasAudio, interpretationLabel: interpretationLabel, sourceTier: asString(source['source_tier']), cacheVersion: cacheVersion, ); } } class ModuleDetail { const ModuleDetail({ required this.id, required this.type, required this.titleCn, this.content = const {}, this.contentEtag = '', this.cacheVersion = '', }); final String id; final String type; final String titleCn; final JsonMap content; final String contentEtag; final String cacheVersion; factory ModuleDetail.fromJson(JsonMap json) { return ModuleDetail( id: asString(json['module_id']), type: asString(json['type']), titleCn: asString(json['title_cn'], asString(json['type'])), content: asMap(json['content']), contentEtag: asString(json['content_etag']), cacheVersion: asString(json['cache_version']), ); } }