324 lines
9.1 KiB
Dart
324 lines
9.1 KiB
Dart
typedef JsonMap = Map<String, dynamic>;
|
|
|
|
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<String> asStringList(Object? value) {
|
|
if (value is List) return value.map((item) => item.toString()).toList();
|
|
return const [];
|
|
}
|
|
|
|
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));
|
|
}
|
|
return const {};
|
|
}
|
|
|
|
List<JsonMap> 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<String> coveredTopics;
|
|
final int reportCount;
|
|
final String? latestReportAt;
|
|
final String credibilityNote;
|
|
final String introCn;
|
|
final List<ReportCardModel> 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<String> 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<String> topics;
|
|
final bool hasAudio;
|
|
final String interpretationLabel;
|
|
final String riskDisclaimer;
|
|
final String? releasedAt;
|
|
final String cacheVersion;
|
|
final List<DisplayModule> 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']),
|
|
);
|
|
}
|
|
}
|