feat(category): 分类频道接入榜单快照
This commit is contained in:
1 parent
8c1662475a
commit
59fbd33059
8 files changed
+234
-11
No files matched your search
@@ -0,0 +1,67 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'drama.dart';
|
||||
import 'locale_text.dart';
|
||||
|
||||
/// 服务端推荐榜快照;顺序和排名均不可在客户端重算。
|
||||
class CategoryRanking {
|
||||
const CategoryRanking({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.revision,
|
||||
required this.entries,
|
||||
this.locales = const {},
|
||||
this.generatedAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String code;
|
||||
final String revision;
|
||||
final Map<String, String> locales;
|
||||
final DateTime? generatedAt;
|
||||
final List<CategoryRankingEntry> entries;
|
||||
|
||||
/// 解析 `GET /app/categories/{id}/ranking`。
|
||||
factory CategoryRanking.fromJson(Map<String, dynamic> json) {
|
||||
final rawList = json['list'];
|
||||
final entries = rawList is List
|
||||
? rawList
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) =>
|
||||
CategoryRankingEntry.fromJson(item.cast<String, dynamic>()),
|
||||
)
|
||||
.toList(growable: false)
|
||||
: const <CategoryRankingEntry>[];
|
||||
return CategoryRanking(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
code: json['code'] as String? ?? '',
|
||||
revision: (json['revision'] ?? '1').toString(),
|
||||
locales: localeMap(json['locales']),
|
||||
generatedAt: DateTime.tryParse(json['generatedAt'] as String? ?? ''),
|
||||
entries: entries,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 推荐榜中的一部短剧及其服务端排名。
|
||||
class CategoryRankingEntry {
|
||||
const CategoryRankingEntry({required this.rank, required this.drama});
|
||||
|
||||
final int rank;
|
||||
final Drama drama;
|
||||
|
||||
factory CategoryRankingEntry.fromJson(Map<String, dynamic> json) {
|
||||
final rawDrama = json['drama'];
|
||||
return CategoryRankingEntry(
|
||||
rank: (json['rank'] as num?)?.toInt() ?? 0,
|
||||
drama: Drama.fromCardJson(
|
||||
rawDrama is Map<String, dynamic>
|
||||
? rawDrama
|
||||
: rawDrama is Map
|
||||
? rawDrama.cast<String, dynamic>()
|
||||
: const <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,19 @@ class ContentCategory {
|
||||
const ContentCategory({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.code = '',
|
||||
this.layoutType = gridLayout,
|
||||
this.revision = '1',
|
||||
this.locales = const {},
|
||||
});
|
||||
|
||||
static const String gridLayout = 'GRID';
|
||||
static const String rankingLayout = 'RANKING';
|
||||
|
||||
final String id;
|
||||
final String code;
|
||||
final String layoutType;
|
||||
final String revision;
|
||||
|
||||
/// 中文名称,与 locales 里的 zh 相同。
|
||||
final String name;
|
||||
@@ -20,9 +29,18 @@ class ContentCategory {
|
||||
factory ContentCategory.fromJson(Map<String, dynamic> json) {
|
||||
final locales = localeMap(json['locales']);
|
||||
final name = json['name'] as String? ?? locales['zh'] ?? '';
|
||||
return ContentCategory(id: (json['id'] ?? '').toString(), name: name, locales: locales);
|
||||
return ContentCategory(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
name: name,
|
||||
code: json['code'] as String? ?? '',
|
||||
layoutType: json['layoutType'] as String? ?? gridLayout,
|
||||
revision: (json['revision'] ?? '1').toString(),
|
||||
locales: locales,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isRanking => layoutType == rankingLayout;
|
||||
|
||||
/// 按界面语言取名称。缺对应语言时回落到中文。
|
||||
String localizedName(String languageCode) {
|
||||
return localizedText(languageCode, name, locales);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../models/content_category.dart';
|
||||
import '../models/category_ranking.dart';
|
||||
import '../models/drama.dart';
|
||||
import '../models/page_result.dart';
|
||||
import 'api_client.dart';
|
||||
@@ -30,11 +31,14 @@ class CategoryApi {
|
||||
}) async {
|
||||
final data = await _client.get(
|
||||
'/app/categories/$categoryId/dramas',
|
||||
query: {
|
||||
'pageIndex': '$pageIndex',
|
||||
'pageSize': '$pageSize',
|
||||
},
|
||||
query: {'pageIndex': '$pageIndex', 'pageSize': '$pageSize'},
|
||||
);
|
||||
return PageResult.parse(data, Drama.fromCardJson);
|
||||
}
|
||||
|
||||
/// 推荐榜快照,服务端已确定内容顺序和连续排名。
|
||||
Future<CategoryRanking> ranking(String categoryId) async {
|
||||
final data = await _client.get('/app/categories/$categoryId/ranking');
|
||||
return CategoryRanking.fromJson(ResponseDecoders.asMap(data));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../config/app_config.dart';
|
||||
import '../models/content_category.dart';
|
||||
import '../models/category_ranking.dart';
|
||||
import '../models/drama.dart';
|
||||
import '../models/home_feed.dart';
|
||||
import '../models/page_result.dart';
|
||||
@@ -21,10 +22,15 @@ class CategoryController extends ChangeNotifier {
|
||||
String categoryId, {
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
})? dramaPageLoader;
|
||||
})?
|
||||
dramaPageLoader;
|
||||
|
||||
/// 生产环境推荐榜加载器,测试与本地数据模式可不注入。
|
||||
Future<CategoryRanking> Function(String categoryId)? rankingLoader;
|
||||
|
||||
List<ContentCategory> categories = const [];
|
||||
final Map<String, CategoryDramaSlot> _slots = {};
|
||||
final Map<String, CategoryRankingSlot> _rankingSlots = {};
|
||||
HomeFeed feed = const HomeFeed();
|
||||
bool loading = false;
|
||||
String? error;
|
||||
@@ -36,6 +42,29 @@ class CategoryController extends ChangeNotifier {
|
||||
return _slots.putIfAbsent(categoryId, CategoryDramaSlot.new);
|
||||
}
|
||||
|
||||
CategoryRankingSlot rankingSlotOf(String categoryId) {
|
||||
return _rankingSlots.putIfAbsent(categoryId, CategoryRankingSlot.new);
|
||||
}
|
||||
|
||||
/// 打开推荐榜并获取当前服务端快照。
|
||||
Future<void> openRanking(String categoryId) async {
|
||||
final loader = rankingLoader;
|
||||
if (loader == null) return;
|
||||
final slot = rankingSlotOf(categoryId);
|
||||
slot.loading = true;
|
||||
slot.error = null;
|
||||
_notify();
|
||||
try {
|
||||
slot.ranking = await loader(categoryId);
|
||||
} catch (exception) {
|
||||
slot.error = exception;
|
||||
if (kDebugMode) debugPrint('[category-ranking] $exception');
|
||||
} finally {
|
||||
slot.loading = false;
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开分类并加载第一页短剧。
|
||||
Future<void> openCategory(String categoryId) async {
|
||||
final loader = dramaPageLoader;
|
||||
@@ -126,8 +155,14 @@ class CategoryController extends ChangeNotifier {
|
||||
loading = false;
|
||||
_notify();
|
||||
}
|
||||
if (error == null && dramaPageLoader != null && categories.isNotEmpty) {
|
||||
await openCategory(categories.first.id);
|
||||
final hasRemoteLoader = dramaPageLoader != null || rankingLoader != null;
|
||||
if (error == null && hasRemoteLoader && categories.isNotEmpty) {
|
||||
final first = categories.first;
|
||||
if (first.isRanking) {
|
||||
await openRanking(first.id);
|
||||
} else {
|
||||
await openCategory(first.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,3 +187,10 @@ class CategoryDramaSlot {
|
||||
|
||||
bool get hasMore => dramas.length < total;
|
||||
}
|
||||
|
||||
/// 单个推荐榜的快照状态。
|
||||
class CategoryRankingSlot {
|
||||
CategoryRanking? ranking;
|
||||
Object? error;
|
||||
bool loading = false;
|
||||
}
|
||||
Reference in new issue
Block a user