feat(app): 发现页动态化 — DiscoverBloc + API 驱动 + 下拉刷新
- 新增 DiscoverBloc (LoadData/Refresh) + DiscoverModels 4 个数据类 - DiscoverPage 改为 BlocBuilder 驱动: loading/loaded/error/empty 四态 - 替换全部硬编码占位数据为 API 实时数据 - 添加 RefreshIndicator 下拉刷新 - 离线异常时保留已有数据,友好错误提示
This commit is contained in:
160
app/lib/features/discover/models/discover_models.dart
Normal file
160
app/lib/features/discover/models/discover_models.dart
Normal 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 赞';
|
||||
}
|
||||
Reference in New Issue
Block a user