941 lines
25 KiB
Dart
941 lines
25 KiB
Dart
// ============================================================
|
||
// 闲言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;
|
||
}
|