feat(app): 发现页动态化 — DiscoverBloc + API 驱动 + 下拉刷新

- 新增 DiscoverBloc (LoadData/Refresh) + DiscoverModels 4 个数据类
- DiscoverPage 改为 BlocBuilder 驱动: loading/loaded/error/empty 四态
- 替换全部硬编码占位数据为 API 实时数据
- 添加 RefreshIndicator 下拉刷新
- 离线异常时保留已有数据,友好错误提示
This commit is contained in:
iven
2026-06-07 10:43:23 +08:00
parent 3bc2ca7332
commit a5d2b0409f
5 changed files with 572 additions and 81 deletions

View File

@@ -0,0 +1,160 @@
// 发现页数据模型 — 手写不可变类(避免 build_runner 依赖)
/// 发现页聚合响应 — 一次 API 返回全部板块数据
class DiscoverData {
final InspirationItem? dailyInspiration;
final List<TagCount> hotTopics;
final List<DiscoverTemplateItem> featuredTemplates;
final List<ExpertDiaryItem> expertDiaries;
const DiscoverData({
this.dailyInspiration,
this.hotTopics = const [],
this.featuredTemplates = const [],
this.expertDiaries = const [],
});
factory DiscoverData.fromJson(Map<String, dynamic> json) => DiscoverData(
dailyInspiration: json['daily_inspiration'] != null
? InspirationItem.fromJson(
json['daily_inspiration'] as Map<String, dynamic>)
: null,
hotTopics: (json['hot_topics'] as List? ?? [])
.map((e) => TagCount.fromJson(e as Map<String, dynamic>))
.toList(),
featuredTemplates: (json['featured_templates'] as List? ?? [])
.map(
(e) => DiscoverTemplateItem.fromJson(e as Map<String, dynamic>))
.toList(),
expertDiaries: (json['expert_diaries'] as List? ?? [])
.map((e) => ExpertDiaryItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
/// 心情 → emoji 映射
static String moodToEmoji(String mood) => switch (mood) {
'happy' => '😊',
'calm' => '😌',
'sad' => '😢',
'angry' => '😤',
'thinking' => '🤔',
_ => '📝',
};
}
/// 每日推荐条目
class InspirationItem {
final String journalId;
final String title;
final String authorName;
final String mood;
final DateTime date;
const InspirationItem({
required this.journalId,
required this.title,
required this.authorName,
required this.mood,
required this.date,
});
factory InspirationItem.fromJson(Map<String, dynamic> json) =>
InspirationItem(
journalId: json['journal_id'] as String,
title: json['title'] as String,
authorName: json['author_name'] as String,
mood: json['mood'] as String,
date: DateTime.parse(json['date'] as String),
);
}
/// 热门话题
class TagCount {
final String tag;
final int count;
const TagCount({required this.tag, required this.count});
factory TagCount.fromJson(Map<String, dynamic> json) => TagCount(
tag: json['tag'] as String,
count: json['count'] as int,
);
}
/// 精选模板条目(轻量版,不含 layout_data
class DiscoverTemplateItem {
final String id;
final String name;
final String? previewUrl;
final String? category;
final bool isFree;
const DiscoverTemplateItem({
required this.id,
required this.name,
this.previewUrl,
this.category,
this.isFree = true,
});
factory DiscoverTemplateItem.fromJson(Map<String, dynamic> json) =>
DiscoverTemplateItem(
id: json['id'] as String,
name: json['name'] as String,
previewUrl: json['preview_url'] as String?,
category: json['category'] as String?,
isFree: json['is_free'] as bool? ?? true,
);
/// 分类 → emoji 映射
String get emoji => switch (category) {
'日常' => '📖',
'旅行' => '✈️',
'校园' => '🎓',
'节日' => '🎄',
'创意' => '',
'心情' => '🌿',
_ => '📝',
};
/// 使用人数展示文本
String get usageText => isFree ? '免费模板' : '精品模板';
}
/// 达人日记条目
class ExpertDiaryItem {
final String journalId;
final String title;
final String authorId;
final String authorName;
final String authorEmoji;
final String contentPreview;
final int likeCount;
final DateTime createdAt;
const ExpertDiaryItem({
required this.journalId,
required this.title,
required this.authorId,
required this.authorName,
required this.authorEmoji,
required this.contentPreview,
required this.likeCount,
required this.createdAt,
});
factory ExpertDiaryItem.fromJson(Map<String, dynamic> json) =>
ExpertDiaryItem(
journalId: json['journal_id'] as String,
title: json['title'] as String,
authorId: json['author_id'] as String,
authorName: json['author_name'] as String,
authorEmoji: json['author_emoji'] as String,
contentPreview: json['content_preview'] as String? ?? '',
likeCount: json['like_count'] as int? ?? 0,
createdAt: DateTime.parse(json['created_at'] as String),
);
/// 点赞数展示文本
String get likeText => '$likeCount';
}