feat(app): 客户端展示多语言并支持筛选分页

This commit is contained in:
Zhou Weidong committed 2026-10-24 17:20:24 +08:00
1 parent 8481611372
commit 8b50b5bd06
68 files changed
+2742 -1120

No files matched your search

+150 -9
View File
@@ -1,6 +1,9 @@
// Author: fruit.com.cn
import 'dart:ui';
import 'json_numbers.dart';
import 'locale_text.dart';
/// 短剧与剧集,字段与后端 `/app/**` 的下发结构对应。
///
@@ -76,7 +79,14 @@ class Drama {
required this.synopsis,
required this.posterUrl,
this.episodes = const [],
this.titleLocales = const {},
this.synopsisLocales = const {},
this.tag = '',
this.tagLocales = const {},
this.tagColor = '',
this.category = '',
this.categoryLocales = const {},
this.types = const [],
this.genre = '',
this.totalEpisodes = 0,
this.freeEpisodes = 0,
@@ -94,9 +104,16 @@ class Drama {
final String id;
final String title;
final String synopsis;
final Map<String, String> titleLocales;
final Map<String, String> synopsisLocales;
final String tag;
final Map<String, String> tagLocales;
final String tagColor;
final String posterUrl;
final List<Episode> episodes;
final String category;
final Map<String, String> categoryLocales;
final List<Map<String, String>> types;
final String genre;
final int totalEpisodes;
final int freeEpisodes;
@@ -127,19 +144,26 @@ class Drama {
/// 列表卡片:带一集可播预览(首页直接起播),完整剧集进详情再取。
factory Drama.fromCardJson(Map<String, dynamic> json) {
final preview = json['preview'];
final text = _DramaText.fromJson(json);
return Drama(
id: (json['id'] ?? '').toString(),
title: json['title'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
title: text.title,
synopsis: text.synopsis,
titleLocales: text.titleLocales,
synopsisLocales: text.synopsisLocales,
tag: text.tag,
tagLocales: text.tagLocales,
tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '',
episodes: preview is Map<String, dynamic>
? [Episode.fromJson(preview)]
: const [],
category: json['category'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: text.category,
categoryLocales: text.categoryLocales,
types: text.types,
genre: text.genre,
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
isHot: json['isHot'] == true,
// viewCount 后端是 Long,按字符串下发(见 json_numbers.dart)
viewCount: jsonToInt(json['viewCount']),
likeCount: _optionalInt(json['likeCount']),
favoriteCount: _optionalInt(json['favoriteCount']),
@@ -152,13 +176,21 @@ class Drama {
/// 详情:带剧集列表。
factory Drama.fromDetailJson(Map<String, dynamic> json) {
final text = _DramaText.fromJson(json);
return Drama(
id: (json['id'] ?? '').toString(),
title: json['title'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
title: text.title,
synopsis: text.synopsis,
titleLocales: text.titleLocales,
synopsisLocales: text.synopsisLocales,
tag: text.tag,
tagLocales: text.tagLocales,
tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '',
category: json['category'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: text.category,
categoryLocales: text.categoryLocales,
types: text.types,
genre: text.genre,
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0,
isHot: json['isHot'] == true,
@@ -176,6 +208,37 @@ class Drama {
.toList(),
);
}
String localizedTitle(String languageCode) {
return localizedText(languageCode, title, titleLocales);
}
String localizedSynopsis(String languageCode) {
return localizedText(languageCode, synopsis, synopsisLocales);
}
String localizedTag(String languageCode) {
return localizedText(languageCode, tag, tagLocales);
}
/// 标签角标背景色。接口没给合法色值时用 fallback。
Color tagRibbonColor(Color fallback) {
final parsed = _colorFromHex(tagColor);
return parsed ?? fallback;
}
/// 按当前语言取类型名称。接口没带语言表时返回空,调用方继续用中文 genre。
List<String> localizedTypeLabels(String languageCode) {
final labels = <String>[];
for (final item in types) {
final label = localizedText(languageCode, item['zh'] ?? '', item);
if (label.isNotEmpty && !labels.contains(label)) labels.add(label);
}
if (labels.isNotEmpty) return labels;
final primary = localizedText(languageCode, '', categoryLocales);
if (primary.isNotEmpty) return [primary];
return const [];
}
}
int? _optionalInt(dynamic value) => value == null ? null : jsonToInt(value);
@@ -184,3 +247,81 @@ String? _optionalString(dynamic value) {
final text = value is String ? value.trim() : '';
return text.isEmpty ? null : text;
}
Color? _colorFromHex(String raw) {
final text = raw.trim();
if (text.length != 7 || !text.startsWith('#')) return null;
final value = int.tryParse(text.substring(1), radix: 16);
if (value == null) return null;
return Color(0xFF000000 | value);
}
List<Map<String, String>> _typeLocales(Object? raw) {
if (raw is! List) return const [];
return raw.whereType<Map>().map(localeMap).toList(growable: false);
}
/// 从语言表还原中文展示字段。列表接口不再单独下发 title / synopsis / tag。
class _DramaText {
const _DramaText({
required this.title,
required this.synopsis,
required this.titleLocales,
required this.synopsisLocales,
required this.tag,
required this.tagLocales,
required this.category,
required this.categoryLocales,
required this.types,
required this.genre,
});
final String title;
final String synopsis;
final Map<String, String> titleLocales;
final Map<String, String> synopsisLocales;
final String tag;
final Map<String, String> tagLocales;
final String category;
final Map<String, String> categoryLocales;
final List<Map<String, String>> types;
final String genre;
factory _DramaText.fromJson(Map<String, dynamic> json) {
final titleLocales = localeMap(json['titleLocales']);
final synopsisLocales = localeMap(json['synopsisLocales']);
final tagLocales = localeMap(json['tagLocales']);
final categoryLocales = localeMap(json['categoryLocales']);
final types = _typeLocales(json['types']);
final category = _firstText(categoryLocales, json['category'], types);
return _DramaText(
title: _firstText(titleLocales, json['title']),
synopsis: _firstText(synopsisLocales, json['synopsis'] ?? json['description']),
titleLocales: titleLocales,
synopsisLocales: synopsisLocales,
tag: _firstText(tagLocales, json['tag']),
tagLocales: tagLocales,
category: category,
categoryLocales: categoryLocales,
types: types,
genre: _genreText(types, json['genre']),
);
}
}
String _firstText(Map<String, String> locales, Object? fallback, [List<Map<String, String>>? types]) {
final chinese = locales['zh'] ?? '';
if (chinese.isNotEmpty) return chinese;
if (types != null && types.isNotEmpty) {
final label = types.first['zh'] ?? '';
if (label.isNotEmpty) return label;
}
return fallback is String ? fallback : '';
}
String _genreText(List<Map<String, String>> types, Object? fallback) {
final names = types.map((item) => item['zh'] ?? '').where((value) => value.isNotEmpty);
final joined = names.join(' / ');
if (joined.isNotEmpty) return joined;
return fallback is String ? fallback : '';
}