Files
xianyan/lib/features/home/models/feed_model.dart
2026-06-07 18:20:26 +08:00

941 lines
25 KiB
Dart
Raw 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-05-14
// 作用: Feed API + SearchAll API 数据模型,覆盖信息流/频道/互动/搜索
// 上次更新: 修复_extractTitleFromExtra字段映射对齐API实际返回字段名
// ============================================================
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 '';
}
class FeedChannel {
const FeedChannel({
required this.key,
required this.name,
required this.icon,
required this.count,
this.isEnabled = true,
});
final String key;
final String name;
final String icon;
final int count;
/// 后台启用状态(来自推荐权重管理配置)
final bool isEnabled;
factory FeedChannel.fromJson(Map<String, dynamic> json) {
return FeedChannel(
key: json['key'] as String? ?? '',
name: json['name'] as String? ?? '',
icon: json['icon'] as String? ?? '',
count: _parseInt(json['count']),
isEnabled: json['is_enabled'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'key': key,
'name': name,
'icon': icon,
'count': count,
'is_enabled': isEnabled,
};
}
}
/// 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,
});
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;
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(',');
}
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',
});
final String mode;
final List<String> channels;
final Map<String, int> ratios;
final int groupSize;
final int limit;
final String sort;
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;
}
return params;
}
FeedMixConfig copyWith({
String? mode,
List<String>? channels,
Map<String, int>? ratios,
int? groupSize,
int? limit,
String? sort,
}) {
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,
);
}
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;
}