chore: prepare yanting monorepo handoff

This commit is contained in:
2026-06-03 10:39:03 +09:00
commit fde51468c6
106 changed files with 8171 additions and 0 deletions
@@ -0,0 +1,313 @@
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.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 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']),
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']),
);
}
}