Files
xianyan/lib/features/home/feed_model.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

1016 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — Feed信息流数据模型
// 创建时间: 2026-04-28
// 更新时间: 2026-06-09
// 作用: Feed API + SearchAll API 数据模型,覆盖信息流/频道/互动/搜索
// 上次更新: 新增platform_enabled字段容错解析null/非Map/key缺失/值类型错误均默认启用
// ============================================================
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import '../../../core/utils/data/extensions.dart';
/// Feed 类型主题色映射
///
/// 44种数据源对应不同色调增强视觉差异化。
class FeedTypeColor {
FeedTypeColor._();
static const Map<String, Color> _colorMap = {
'poetry': Color(0xFF4A90D9),
'wisdom': Color(0xFFD4A843),
'story': Color(0xFF4CAF50),
'hitokoto': Color(0xFF7C8DB5),
'riddle': Color(0xFF9C27B0),
'efs': Color(0xFFFF7043),
'brainteaser': Color(0xFF26A69A),
'saying': Color(0xFF8D6E63),
'lyric': Color(0xFFE91E63),
'why': Color(0xFF42A5F5),
'composition': Color(0xFFAB47BC),
'couplet': Color(0xFFEF5350),
'cs': Color(0xFF66BB6A),
'drug': Color(0xFF5C6BC0),
'herbal': Color(0xFF66BB6A),
'food': Color(0xFFFF9800),
'wine': Color(0xFFAD1457),
'article': Color(0xFF5C6BC0),
'chengyu': Color(0xFF7E57C2),
'hanzi': Color(0xFF29B6F6),
'cidian': Color(0xFF5C6BC0),
'prescription': Color(0xFF26A69A),
'tisana': Color(0xFF8D6E63),
'joke': Color(0xFFFFCA28),
'zgjm': Color(0xFF5C6BC0),
'illness': Color(0xFFEF5350),
'word': Color(0xFF29B6F6),
'abbr': Color(0xFF78909C),
'surname': Color(0xFF8D6E63),
'jieqi': Color(0xFF66BB6A),
'nation': Color(0xFF42A5F5),
'jiufang': Color(0xFFAD1457),
'lunyu': Color(0xFFD4A843),
'hdnj': Color(0xFF26A69A),
'jgj': Color(0xFF8D6E63),
'mz': Color(0xFF5C6BC0),
'zz': Color(0xFF7E57C2),
'zuozhuan': Color(0xFF4A90D9),
'sj': Color(0xFFD4A843),
'sgz': Color(0xFFEF5350),
'sbbf': Color(0xFF78909C),
'warring': Color(0xFFFF7043),
'wlyh': Color(0xFF8D6E63),
'bot': Color(0xFF66BB6A),
};
static const Color _defaultColor = Color(0xFF6C63FF);
static Color getColor(String? feedType) {
if (feedType == null) return _defaultColor;
return _colorMap[feedType] ?? _defaultColor;
}
static Color getColorWithAlpha(String? feedType, {double alpha = 0.15}) {
return getColor(feedType).withValues(alpha: alpha);
}
/// Feed类型 → Interaction API targetType 映射
/// Interaction API允许的targetType: article, tool, poetry, cy, story,
/// wisdom, hanzi, chengyu, cidian, saying, joke, feed, page, user
static const _interactionTypeMap = <String, String>{
'hitokoto': 'saying',
'sentence': 'saying',
'lyric': 'saying',
'riddle': 'feed',
'brainteaser': 'feed',
'efs': 'feed',
'article': 'article',
'poetry': 'poetry',
'cy': 'cy',
'story': 'story',
'wisdom': 'wisdom',
'hanzi': 'hanzi',
'chengyu': 'chengyu',
'cidian': 'cidian',
'saying': 'saying',
'joke': 'joke',
'tool': 'tool',
'page': 'page',
'user': 'user',
'feed': 'feed',
};
/// 将Feed类型转换为Interaction API可接受的targetType
static String toInteractionType(String? feedType) {
if (feedType == null) return 'feed';
return _interactionTypeMap[feedType] ?? 'feed';
}
}
class FeedTypeIcon {
FeedTypeIcon._();
static final Map<String, IconData> _iconMap = {
'poetry': CupertinoIcons.book_fill,
'wisdom': CupertinoIcons.lightbulb_fill,
'story': CupertinoIcons.book,
'hitokoto': CupertinoIcons.chat_bubble_2_fill,
'riddle': CupertinoIcons.question_circle_fill,
'efs': CupertinoIcons.smiley_fill,
'brainteaser': CupertinoIcons.lightbulb_fill,
'saying': CupertinoIcons.chat_bubble_fill,
'lyric': CupertinoIcons.music_note_2,
'why': CupertinoIcons.question_circle,
'composition': CupertinoIcons.pencil,
'couplet': CupertinoIcons.textformat,
'cs': CupertinoIcons.gear,
'drug': CupertinoIcons.heart_fill,
'herbal': CupertinoIcons.globe,
'food': CupertinoIcons.cart_fill,
'wine': CupertinoIcons.drop_fill,
'article': CupertinoIcons.doc_text_fill,
'chengyu': CupertinoIcons.star_fill,
'hanzi': CupertinoIcons.textformat,
'cidian': CupertinoIcons.textformat,
'prescription': CupertinoIcons.doc_text,
'tisana': CupertinoIcons.globe,
'joke': CupertinoIcons.smiley,
'zgjm': CupertinoIcons.moon_fill,
'illness': CupertinoIcons.heart_fill,
'word': CupertinoIcons.textformat,
'abbr': CupertinoIcons.textformat,
'surname': CupertinoIcons.person_2_fill,
'jieqi': CupertinoIcons.calendar,
'nation': CupertinoIcons.flag_fill,
'jiufang': CupertinoIcons.drop,
'lunyu': CupertinoIcons.book_fill,
'hdnj': CupertinoIcons.calendar,
'jgj': CupertinoIcons.book,
'mz': CupertinoIcons.flag,
'zz': CupertinoIcons.doc_text,
'zuozhuan': CupertinoIcons.doc_text,
'sj': CupertinoIcons.book_fill,
'sgz': CupertinoIcons.star_fill,
'sbbf': CupertinoIcons.trash_fill,
'warring': CupertinoIcons.shield_fill,
'wlyh': CupertinoIcons.globe,
'bot': CupertinoIcons.gear,
};
static const IconData _defaultIcon = CupertinoIcons.doc_text_fill;
static IconData getIcon(String? feedType) {
if (feedType == null) return _defaultIcon;
return _iconMap[feedType] ?? _defaultIcon;
}
}
int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
int? _parseIntOrNull(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value);
return null;
}
/// Feed信息流条目模型
class FeedItem {
const FeedItem({
required this.feedType,
required this.feedName,
required this.feedIcon,
required this.id,
required this.title,
required this.author,
required this.content,
required this.summary,
required this.views,
this.likeCount = 0,
this.favoriteCount = 0,
this.commentCount = 0,
this.shareCount = 0,
this.isLiked = false,
this.isFavorited = false,
this.imageUrl = '',
this.source = '',
this.extra,
this.myActions = const [],
this.createtime,
this.updatetime,
this.rawData,
});
final String feedType;
final String feedName;
final String feedIcon;
final int id;
final String title;
final String author;
final String content;
final String summary;
final int views;
/// 互动计数
final int likeCount;
final int favoriteCount;
final int commentCount;
final int shareCount;
/// 交互状态(服务端返回)
final bool isLiked;
final bool isFavorited;
final String imageUrl;
final String source;
final Map<String, dynamic>? extra;
final List<String> myActions;
/// 时间戳
final int? createtime;
final int? updatetime;
/// 全量详情原始数据(fullDetail接口)
final Map<String, dynamic>? rawData;
bool get isReadLater => myActions.contains('readlater');
FeedItem copyWith({
List<String>? myActions,
int? views,
int? likeCount,
int? favoriteCount,
int? commentCount,
int? shareCount,
bool? isLiked,
bool? isFavorited,
}) {
return FeedItem(
feedType: feedType,
feedName: feedName,
feedIcon: feedIcon,
id: id,
title: title,
author: author,
content: content,
summary: summary,
views: views ?? this.views,
likeCount: likeCount ?? this.likeCount,
favoriteCount: favoriteCount ?? this.favoriteCount,
commentCount: commentCount ?? this.commentCount,
shareCount: shareCount ?? this.shareCount,
isLiked: isLiked ?? this.isLiked,
isFavorited: isFavorited ?? this.isFavorited,
imageUrl: imageUrl,
source: source,
extra: extra,
myActions: myActions ?? this.myActions,
createtime: createtime,
updatetime: updatetime,
rawData: rawData,
);
}
factory FeedItem.fromJson(Map<String, dynamic> json) {
final extra = json['extra'] as Map<String, dynamic>?;
final rawTitle = (json['title'] as String? ?? '').cleanHtml;
final rawContent = (json['content'] as String? ?? '').cleanHtml;
final rawSummary = (json['summary'] as String? ?? '').cleanHtml;
String effectiveTitle = rawTitle;
if (effectiveTitle.isEmpty && extra != null) {
final feedType = json['feed_type'] as String? ?? '';
effectiveTitle = _extractTitleFromExtra(extra, feedType).cleanHtml;
}
String effectiveSummary = rawSummary;
if (effectiveSummary.isEmpty && rawContent.isNotEmpty) {
effectiveSummary = rawContent.length > 100
? '${rawContent.substring(0, 100)}...'
: rawContent;
}
return FeedItem(
feedType: json['feed_type'] as String? ?? '',
feedName: json['feed_name'] as String? ?? '',
feedIcon: json['feed_icon'] as String? ?? '',
id: _parseInt(json['id']),
title: effectiveTitle,
author: (json['author'] as String? ?? '').cleanHtml,
content: rawContent,
summary: effectiveSummary,
views: _parseInt(json['views']),
likeCount: _parseInt(json['like_count']),
favoriteCount: _parseInt(json['favorite_count']),
commentCount: _parseInt(json['comment_count']),
shareCount: _parseInt(json['share_count']),
isLiked: json['is_liked'] as bool? ?? false,
isFavorited: json['is_favorited'] as bool? ?? false,
imageUrl: extra?['image_url'] as String? ?? '',
source: json['source'] as String? ?? '',
extra: extra,
myActions:
(json['my_actions'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
createtime: _parseIntOrNull(json['createtime']),
updatetime: _parseIntOrNull(json['updatetime']),
rawData: json['raw_data'] as Map<String, dynamic>?,
);
}
}
/// Feed频道模型
String _extractTitleFromExtra(Map<String, dynamic> extra, String feedType) {
const typeFieldMap = <String, List<String>>{
'chengyu': ['cy', 'cyjs', 'cypy', 'cycc'],
'hanzi': ['pinyin', 'bushou'],
'cidian': ['zc', 'zcjs', 'zcpy'],
'illness': ['jb', 'zz'],
'drug': ['name', 'syz', 'goods_name', 'gg', 'cf'],
'herbal': ['name', 'effect', 'name_alias', 'spell'],
'food': ['sw', 'yh'],
'wine': ['name', 'ingredients', 'usage', 'source'],
'jiufang': [
'name',
'ingredients',
'usage',
'source',
'method',
'categories',
],
'prescription': ['title', 'content'],
'tisana': ['name', 'effect', 'recipe', 'source'],
'couplet': ['hp', 'sl', 'xl', 'yy'],
'composition': ['zw'],
'riddle': ['riddle', 'miidii'],
'brainteaser': ['topic', 'answer'],
'saying': ['cy'],
'why': ['wt'],
'efs': ['facet', 'undertone'],
'joke': ['xh'],
'story': ['gs'],
'lyric': ['gc'],
'poetry': ['ss'],
'wisdom': ['my'],
'hitokoto': ['hitokoto', 'type_name', 'from_source', 'from_who'],
'zgjm': ['jm'],
'word': ['dc'],
'abbr': ['js'],
'surname': ['xs'],
'jieqi': ['jq'],
'nation': ['mz'],
};
final fields = typeFieldMap[feedType] ?? [];
for (final field in fields) {
final val = extra[field];
if (val is String && val.isNotEmpty) return val;
}
for (final entry in extra.entries) {
final val = entry.value;
if (val is String && val.isNotEmpty && val.length < 100) return val;
}
return '';
}
/// 所有支持的平台标识列表(与服务端 platform_enabled 字段 key 对应)
const kAllPlatforms = [
'android',
'ios',
'harmony',
'macos',
'win',
'web',
'other',
];
/// 解析 platform_enabled 字段,全面容错处理
///
/// 容错策略:
/// - platform_enabled 为 null → 所有平台默认启用
/// - platform_enabled 不是 Map 类型 → 所有平台默认启用
/// - 某个平台 key 不存在 → 该平台默认启用
/// - 某个平台值不是 bool 类型 → 该平台默认启用
Map<String, bool> _parsePlatformEnabled(dynamic value) {
// 默认所有平台启用
final defaultMap = {for (final p in kAllPlatforms) p: true};
// null → 默认全部启用
if (value == null) return defaultMap;
// 非 Map 类型 → 默认全部启用
if (value is! Map) return defaultMap;
final result = <String, bool>{};
for (final p in kAllPlatforms) {
// key 不存在或值非 bool → 默认启用
final v = value[p];
result[p] = v is bool ? v : true;
}
return result;
}
/// Feed频道模型
class FeedChannel {
const FeedChannel({
required this.key,
required this.name,
required this.icon,
required this.count,
this.isEnabled = true,
this.platformEnabled = const {},
});
final String key;
final String name;
final String icon;
final int count;
/// 后台启用状态(来自推荐权重管理配置)
final bool isEnabled;
/// 各平台启用状态(来自 platform_enabled 字段)
/// key: 平台标识(android/ios/harmony/macos/win/web/other)
/// value: 是否启用null/缺失时默认启用
final Map<String, bool> platformEnabled;
/// 前端本地名称覆盖映射(修正后端返回的不准确名称)
static const Map<String, String> _nameOverrides = {'wlyh': '网络用语'};
factory FeedChannel.fromJson(Map<String, dynamic> json) {
final key = json['key'] as String? ?? '';
final remoteName = json['name'] as String? ?? '';
return FeedChannel(
key: key,
name: _nameOverrides[key] ?? remoteName,
icon: json['icon'] as String? ?? '',
count: _parseInt(json['count']),
isEnabled: json['is_enabled'] as bool? ?? true,
platformEnabled: _parsePlatformEnabled(json['platform_enabled']),
);
}
/// 检查指定平台是否启用
///
/// 容错策略platformEnabled 为空、key 不存在、值非预期 → 默认启用
bool isPlatformEnabled(String platform) {
if (platformEnabled.isEmpty) return true;
return platformEnabled[platform] ?? true;
}
Map<String, dynamic> toJson() {
return {
'key': key,
'name': name,
'icon': icon,
'count': count,
'is_enabled': isEnabled,
'platform_enabled': platformEnabled,
};
}
}
/// Feed互动操作类型
enum FeedAction {
like('like', '👍 点赞'),
unlike('unlike', '取消点赞'),
favorite('favorite', '⭐ 收藏'),
unfavorite('unfavorite', '取消收藏'),
share('share', '📤 分享'),
dislike('dislike', '👎 不感兴趣'),
readlater('readlater', '📖 稍后读'),
unreadlater('unreadlater', '取消稍后读'),
rating('rating', '⭐ 评分'),
block('block', '🚫 屏蔽'),
unblock('unblock', '取消屏蔽'),
report('report', '🚨 举报'),
readtime('readtime', '⏱️ 阅读时长'),
comment('comment', '💬 评论'),
commentLike('comment_like', '👍 评论点赞'),
commentUnlike('comment_unlike', '取消评论点赞');
const FeedAction(this.code, this.label);
final String code;
final String label;
}
/// Feed评论模型
class FeedComment {
const FeedComment({
required this.id,
required this.userId,
required this.username,
required this.content,
this.avatar = '',
this.likeCount = 0,
this.createtime,
});
final int id;
final int userId;
final String username;
final String content;
final String avatar;
final int likeCount;
final int? createtime;
factory FeedComment.fromJson(Map<String, dynamic> json) {
return FeedComment(
id: _parseInt(json['id']),
userId: _parseInt(json['user_id']),
username: json['username'] as String? ?? '',
content: json['content'] as String? ?? '',
avatar: json['avatar'] as String? ?? '',
likeCount: _parseInt(json['like_count']),
createtime: _parseIntOrNull(json['createtime']),
);
}
}
/// Feed评论列表结果
class FeedCommentResult {
const FeedCommentResult({
required this.list,
required this.total,
required this.feedType,
required this.feedId,
});
final List<FeedComment> list;
final int total;
final String feedType;
final int feedId;
static FeedCommentResult empty() =>
const FeedCommentResult(list: [], total: 0, feedType: '', feedId: 0);
}
/// Feed统计 — 单频道统计
class FeedChannelStat {
const FeedChannelStat({
required this.key,
required this.name,
required this.icon,
required this.count,
this.views = 0,
});
final String key;
final String name;
final String icon;
final int count;
final int views;
factory FeedChannelStat.fromJson(Map<String, dynamic> json) {
return FeedChannelStat(
key: json['key'] as String? ?? '',
name: json['name'] as String? ?? '',
icon: json['icon'] as String? ?? '',
count: _parseInt(json['count']),
views: _parseInt(json['views']),
);
}
}
/// Feed统计 — 互动统计条目
class FeedInteractionStat {
const FeedInteractionStat({required this.action, required this.count});
final String action;
final int count;
factory FeedInteractionStat.fromJson(Map<String, dynamic> json) {
return FeedInteractionStat(
action: json['action'] as String? ?? '',
count: _parseInt(json['cnt']),
);
}
}
/// Feed统计结果
class FeedStatsResult {
const FeedStatsResult({
this.totalContent = 0,
this.totalViews = 0,
this.channelCount = 0,
this.channels = const [],
this.interactions = const [],
});
final int totalContent;
final int totalViews;
final int channelCount;
final List<FeedChannelStat> channels;
final List<FeedInteractionStat> interactions;
static FeedStatsResult empty() => const FeedStatsResult();
}
/// 搜索建议条目
class SearchSuggestion {
const SearchSuggestion({required this.text, this.type = '', this.label = ''});
final String text;
final String type;
final String label;
factory SearchSuggestion.fromJson(Map<String, dynamic> json) {
return SearchSuggestion(
text: json['text'] as String? ?? '',
type: json['type'] as String? ?? '',
label: json['label'] as String? ?? '',
);
}
}
/// 搜索结果分类统计
class SearchTypeStat {
const SearchTypeStat({
required this.type,
required this.name,
required this.icon,
required this.count,
});
final String type;
final String name;
final String icon;
final int count;
factory SearchTypeStat.fromJson(Map<String, dynamic> json) {
return SearchTypeStat(
type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
icon: json['icon'] as String? ?? '',
count: _parseInt(json['count']),
);
}
}
/// 搜索高亮结果
class SearchHighlightItem {
const SearchHighlightItem({
required this.item,
this.titleHighlight = '',
this.contentHighlight = '',
this.authorHighlight = '',
});
final FeedItem item;
final String titleHighlight;
final String contentHighlight;
final String authorHighlight;
}
/// Feed列表查询参数
class FeedListParams {
const FeedListParams({
this.channel = 'all',
this.sort = 'newest',
this.page = 1,
this.limit = 20,
this.lastId,
this.lite = false,
this.seenIds,
this.seenHashes,
this.platform,
});
final String channel;
final String sort;
final int page;
final int limit;
final int? lastId;
final bool lite;
final List<String>? seenIds;
final List<String>? seenHashes;
/// 平台标识(android/ios/harmony/macos/win/web/other),服务端按平台过滤启用分类
final String? platform;
Map<String, dynamic> toQueryParameters() {
final params = <String, dynamic>{
'channel': channel,
'sort': sort,
'page': page,
'limit': limit,
};
if (lastId != null) params['last_id'] = lastId;
if (lite) params['lite'] = 1;
if (seenIds != null && seenIds!.isNotEmpty) {
params['seen_ids'] = seenIds!.join(',');
}
if (seenHashes != null && seenHashes!.isNotEmpty) {
params['seen_hashes'] = seenHashes!.join(',');
}
if (platform != null && platform!.isNotEmpty) {
params['platform'] = platform;
}
return params;
}
}
/// Feed列表结果
class FeedListResult {
const FeedListResult({
required this.list,
required this.total,
required this.page,
required this.hasMore,
});
final List<FeedItem> list;
final int total;
final int page;
final bool hasMore;
static FeedListResult empty() =>
const FeedListResult(list: [], total: 0, page: 1, hasMore: false);
}
/// Feed推荐结果
class FeedRecommendResult {
const FeedRecommendResult({required this.list, required this.personalized});
final List<FeedItem> list;
final bool personalized;
static FeedRecommendResult empty() =>
const FeedRecommendResult(list: [], personalized: false);
}
/// Feed混合信息流结果
class FeedMixResult {
const FeedMixResult({
required this.list,
required this.total,
required this.mode,
required this.channels,
required this.limit,
});
final List<FeedItem> list;
final int total;
final String mode;
final List<String> channels;
final int limit;
static FeedMixResult empty() => const FeedMixResult(
list: [],
total: 0,
mode: 'random',
channels: [],
limit: 0,
);
}
/// Feed混合规则配置
class FeedMixConfig {
const FeedMixConfig({
this.mode = 'random',
this.channels = const [],
this.ratios = const {},
this.groupSize = 3,
this.limit = 20,
this.sort = 'newest',
this.platform,
});
final String mode;
final List<String> channels;
final Map<String, int> ratios;
final int groupSize;
final int limit;
final String sort;
/// 平台标识,服务端按平台过滤启用分类
final String? platform;
Map<String, dynamic> toQueryParameters() {
final params = <String, dynamic>{
'mode': mode,
'limit': limit,
'sort': sort,
};
if (channels.isNotEmpty) {
params['channels'] = channels.join(',');
}
if (mode == 'ratio' && ratios.isNotEmpty) {
params['ratios'] = jsonEncode(ratios);
}
if (mode == 'group') {
params['group_size'] = groupSize;
}
if (platform != null && platform!.isNotEmpty) {
params['platform'] = platform;
}
return params;
}
FeedMixConfig copyWith({
String? mode,
List<String>? channels,
Map<String, int>? ratios,
int? groupSize,
int? limit,
String? sort,
String? platform,
}) {
return FeedMixConfig(
mode: mode ?? this.mode,
channels: channels ?? this.channels,
ratios: ratios ?? this.ratios,
groupSize: groupSize ?? this.groupSize,
limit: limit ?? this.limit,
sort: sort ?? this.sort,
platform: platform ?? this.platform,
);
}
Map<String, dynamic> toJson() => {
'mode': mode,
'channels': channels,
'ratios': ratios,
'groupSize': groupSize,
'limit': limit,
'sort': sort,
};
factory FeedMixConfig.fromJson(Map<String, dynamic> json) => FeedMixConfig(
mode: json['mode'] as String? ?? 'random',
channels: (json['channels'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList(),
ratios: Map<String, int>.from(json['ratios'] as Map? ?? {}),
groupSize: _parseInt(json['groupSize']),
limit: _parseInt(json['limit']),
sort: json['sort'] as String? ?? 'newest',
);
}
/// Feed刷新检查结果
class FeedRefreshResult {
const FeedRefreshResult({
required this.hasNew,
required this.newCount,
required this.latestId,
});
final bool hasNew;
final int newCount;
final int latestId;
}
/// Feed刷新内容结果
class FeedRefreshContentResult {
const FeedRefreshContentResult({
required this.list,
required this.total,
required this.channel,
required this.sort,
required this.limit,
});
final List<FeedItem> list;
final int total;
final String channel;
final String sort;
final int limit;
static FeedRefreshContentResult empty() => const FeedRefreshContentResult(
list: [],
total: 0,
channel: 'all',
sort: 'newest',
limit: 20,
);
}
/// Feed偏好设置
class FeedPreferences {
const FeedPreferences({
this.disabledChannels = const [],
this.mixMode = 'random',
this.mixChannels = const [],
this.mixRatios = const {},
this.groupSize = 3,
this.deduplicate = true,
this.sort = 'newest',
this.homeCardMode = 'random',
this.homeCardChannels = const [],
this.perPage = 20,
});
final List<String> disabledChannels;
final String mixMode;
final List<String> mixChannels;
final Map<String, int> mixRatios;
final int groupSize;
final bool deduplicate;
final String sort;
final String homeCardMode;
final List<String> homeCardChannels;
final int perPage;
factory FeedPreferences.fromJson(Map<String, dynamic> json) =>
FeedPreferences(
disabledChannels: (json['disabled_channels'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList(),
mixMode: json['mix_mode'] as String? ?? 'random',
mixChannels: (json['mix_channels'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList(),
mixRatios: Map<String, int>.from(json['mix_ratios'] as Map? ?? {}),
groupSize: _parseInt(json['group_size']),
deduplicate: json['deduplicate'] as bool? ?? true,
sort: json['sort'] as String? ?? 'newest',
homeCardMode: json['home_card_mode'] as String? ?? 'random',
homeCardChannels: (json['home_card_channels'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList(),
perPage: _parseInt(json['per_page']),
);
Map<String, dynamic> toJson() => {
'disabled_channels': disabledChannels,
'mix_mode': mixMode,
'mix_channels': mixChannels,
'mix_ratios': mixRatios,
'group_size': groupSize,
'deduplicate': deduplicate,
'sort': sort,
'home_card_mode': homeCardMode,
'home_card_channels': homeCardChannels,
'per_page': perPage,
};
}
/// SearchAll 聚合搜索结果
class SearchAllResult {
const SearchAllResult({
required this.list,
required this.total,
required this.typeStats,
this.keyword = '',
this.mode = 'fuzzy',
this.page = 1,
this.hasMore = false,
});
final List<FeedItem> list;
final int total;
final List<SearchTypeStat> typeStats;
final String keyword;
final String mode;
final int page;
final bool hasMore;
static SearchAllResult empty() =>
const SearchAllResult(list: [], total: 0, typeStats: []);
}
/// 数据源分类模型
///
/// 对应 /api/searchall/sources 返回的每个数据源。
class SourceCategory {
const SourceCategory({
required this.type,
required this.name,
required this.icon,
required this.searchFields,
this.count = 0,
});
final String type;
final String name;
final String icon;
final List<String> searchFields;
final int count;
factory SourceCategory.fromJson(Map<String, dynamic> json) {
return SourceCategory(
type: json['key'] as String? ?? json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
icon: json['icon'] as String? ?? '',
searchFields: (json['search_fields'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList(),
count: _parseInt(json['count']),
);
}
}
/// 热搜关键词(带计数)
///
/// 对应 /api/searchall/hot 返回的 hot_list 条目。
class HotKeyword {
const HotKeyword({required this.keyword, this.count = 0});
final String keyword;
final int count;
}