chore(client): 归档原型阶段新增文件(配置、主题、模型与测试)

为什么:上一批归档只覆盖了「已跟踪文件的修改」,原型阶段新增的文件(app_config、theme、
home_feed/play_ticket 模型、catalog_api、测试目录等)还是未入库状态 —— 而已入库的代码会 import 它们,
留在工作区会导致 HEAD 处的代码编译不过。一并归档,让 HEAD 自洽。

纯归档,无行为改动。
This commit is contained in:
Vicky committed 2026-10-15 17:53:45 +08:00
1 parent e1b98df509
commit feecc349a0
19 files changed
+2210

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
import 'drama.dart';
/// 首页/发现页聚合数据,一次请求拿到轮播与分区。
class HomeFeed {
const HomeFeed({this.banners = const [], this.sections = const []});
final List<FeedBanner> banners;
final List<FeedSection> sections;
bool get isEmpty => sections.every((section) => section.dramas.isEmpty);
/// 分区打平后的短剧列表,供现有 UI 的横滑/竖滑复用;按 id 去重。
List<Drama> get dramas {
final seen = <String>{};
final result = <Drama>[];
for (final section in sections) {
for (final drama in section.dramas) {
if (seen.add(drama.id)) result.add(drama);
}
}
return result;
}
factory HomeFeed.fromJson(Map<String, dynamic> json) {
final banners = (json['banners'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
.map(FeedBanner.fromJson)
.toList();
final sections = (json['sections'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
.map(FeedSection.fromJson)
.toList();
return HomeFeed(banners: banners, sections: sections);
}
}
class FeedSection {
const FeedSection({
required this.code,
required this.title,
this.dramas = const [],
});
final String code;
final String title;
final List<Drama> dramas;
factory FeedSection.fromJson(Map<String, dynamic> json) {
return FeedSection(
code: json['code'] as String? ?? '',
title: json['title'] as String? ?? '',
dramas: (json['dramas'] as List? ?? const [])
.whereType<Map<String, dynamic>>()
.map(Drama.fromCardJson)
.toList(),
);
}
}
class FeedBanner {
const FeedBanner({
required this.id,
required this.imageUrl,
this.linkType = 'NONE',
this.linkValue = '',
});
final String id;
final String imageUrl;
final String linkType;
final String linkValue;
/// 点击轮播要打开的短剧 id,非短剧跳转返回 null。
String? get dramaId => linkType == 'DRAMA' && linkValue.isNotEmpty
? linkValue
: null;
factory FeedBanner.fromJson(Map<String, dynamic> json) {
return FeedBanner(
id: (json['id'] ?? '').toString(),
imageUrl: json['imageUrl'] as String? ?? '',
linkType: json['linkType'] as String? ?? 'NONE',
linkValue: json['linkValue'] as String? ?? '',
);
}
}
+37
View File
@@ -0,0 +1,37 @@
/// 播放票据:客户端据此决定直接播放、提示解锁还是提示暂无片源。
class PlayTicket {
const PlayTicket({
required this.episodeId,
required this.accessible,
required this.accessType,
this.playUrl = '',
this.durationSec = 0,
this.unlockCoin = 0,
});
static const String free = 'FREE';
static const String coinRequired = 'COIN_REQUIRED';
static const String vipRequired = 'VIP_REQUIRED';
static const String noMedia = 'NO_MEDIA';
final String episodeId;
final bool accessible;
final String accessType;
final String playUrl;
final int durationSec;
final int unlockCoin;
bool get needsUnlock =>
accessType == coinRequired || accessType == vipRequired;
factory PlayTicket.fromJson(Map<String, dynamic> json) {
return PlayTicket(
episodeId: (json['episodeId'] ?? '').toString(),
accessible: json['accessible'] == true,
accessType: json['accessType'] as String? ?? noMedia,
playUrl: json['playUrl'] as String? ?? '',
durationSec: (json['durationSec'] as num?)?.toInt() ?? 0,
unlockCoin: (json['unlockCoin'] as num?)?.toInt() ?? 0,
);
}
}
+36
View File
@@ -0,0 +1,36 @@
import '../models/drama.dart';
import '../models/home_feed.dart';
import '../models/play_ticket.dart';
import 'api_client.dart';
/// 客户端目录接口,对应后端 `/app/**`。
class CatalogApi {
const CatalogApi(this._client);
final ApiClient _client;
Future<HomeFeed> homeFeed() async {
return HomeFeed.fromJson(_asMap(await _client.get('/app/home/feed')));
}
Future<HomeFeed> discoverFeed() async {
return HomeFeed.fromJson(_asMap(await _client.get('/app/discover/feed')));
}
Future<Drama> dramaDetail(String dramaId) async {
return Drama.fromDetailJson(
_asMap(await _client.get('/app/dramas/$dramaId')),
);
}
Future<PlayTicket> playTicket(String episodeId) async {
return PlayTicket.fromJson(
_asMap(await _client.get('/app/episodes/$episodeId/play')),
);
}
Map<String, dynamic> _asMap(Object? data) {
if (data is Map<String, dynamic>) return data;
throw const ApiException(-1, '返回数据格式异常');
}
}