feat(flutter): 接入互动评论点赞举报

This commit is contained in:
Zhou Weidong committed 2026-10-17 15:26:42 +08:00
1 parent 45f5f25640
commit d361034226
39 files changed
+3298 -462

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 评论游标分页大小,契约 §1.7 使用游标而非页码。
const int commentPageSize = 20;
/// 评论列表页。
class CommentPage {
const CommentPage({
this.comments = const [],
this.nextCursor,
this.hasMore = false,
});
final List<DramaComment> comments;
final String? nextCursor;
final bool hasMore;
/// 解析后端 `CursorResult{list,nextCursor,hasMore}`。
factory CommentPage.fromJson(Map<String, dynamic> json) {
final rawList = json['list'];
return CommentPage(
comments: [
if (rawList is List)
for (final item in rawList)
if (item is Map<String, dynamic>) DramaComment.fromJson(item),
],
nextCursor: json['nextCursor'] == null
? null
: jsonToId(json['nextCursor']),
hasMore: json['hasMore'] == true,
);
}
}
/// 服务端评论视图,字段对应 `CommentRespVO`。
class DramaComment {
const DramaComment({
required this.id,
required this.dramaId,
required this.episodeId,
required this.parentId,
required this.rootId,
required this.userId,
required this.uid,
required this.nickname,
required this.avatarUrl,
required this.content,
required this.status,
required this.auditReason,
required this.likeCount,
required this.replyCount,
required this.reportCount,
required this.createdAt,
required this.mine,
required this.liked,
});
final String id;
final String dramaId;
final String episodeId;
final String parentId;
final String rootId;
final String userId;
final String uid;
final String nickname;
final String avatarUrl;
final String content;
final String status;
final String auditReason;
final int likeCount;
final int replyCount;
final int reportCount;
final DateTime? createdAt;
final bool mine;
final bool liked;
/// 待人工审核的评论仅本人可见。
bool get isPending => status == 'pending';
/// 顶层评论没有父评论。
bool get isTopLevel => parentId.isEmpty || parentId == '0';
/// 解析 `CommentRespVO`,所有 ID 字段只走字符串解析。
factory DramaComment.fromJson(Map<String, dynamic> json) => DramaComment(
id: jsonToId(json['id']),
dramaId: jsonToId(json['dramaId']),
episodeId: jsonToId(json['episodeId'], fallback: '0'),
parentId: jsonToId(json['parentId'], fallback: '0'),
rootId: jsonToId(json['rootId'], fallback: '0'),
userId: jsonToId(json['userId']),
uid: json['uid']?.toString() ?? '',
nickname: json['nickname']?.toString() ?? '',
avatarUrl: json['avatarUrl']?.toString() ?? '',
content: json['content']?.toString() ?? '',
status: json['status']?.toString() ?? '',
auditReason: json['auditReason']?.toString() ?? '',
likeCount: jsonToInt(json['likeCount']),
replyCount: jsonToInt(json['replyCount']),
reportCount: jsonToInt(json['reportCount']),
createdAt: DateTime.tryParse(json['createdAt']?.toString() ?? ''),
mine: json['mine'] == true,
liked: json['liked'] == true,
);
/// 局部更新点赞或回复数,列表中的其他字段保持不变。
DramaComment copyWith({
int? likeCount,
int? replyCount,
bool? liked,
}) => DramaComment(
id: id,
dramaId: dramaId,
episodeId: episodeId,
parentId: parentId,
rootId: rootId,
userId: userId,
uid: uid,
nickname: nickname,
avatarUrl: avatarUrl,
content: content,
status: status,
auditReason: auditReason,
likeCount: likeCount ?? this.likeCount,
replyCount: replyCount ?? this.replyCount,
reportCount: reportCount,
createdAt: createdAt,
mine: mine,
liked: liked ?? this.liked,
);
}
@@ -1,13 +0,0 @@
// Author: fruit.com.cn
class EngagementSnapshot {
const EngagementSnapshot({
this.savedDramaIds = const {},
this.likedDramaIds = const {},
this.commentsByDrama = const {},
});
final Set<String> savedDramaIds;
final Set<String> likedDramaIds;
final Map<String, List<String>> commentsByDrama;
}
+70
View File
@@ -0,0 +1,70 @@
// Author: fruit.com.cn
import 'json_numbers.dart';
/// 互动目标类型,契约只允许短剧与评论。
enum InteractionTargetType {
drama('DRAMA'),
comment('COMMENT');
const InteractionTargetType(this.apiValue);
final String apiValue;
}
/// 客户端固定举报原因,后端当前未限制枚举值。
enum ReportReason {
spam('SPAM'),
abuse('ABUSE'),
porn('PORN'),
ad('AD'),
other('OTHER');
const ReportReason(this.apiValue);
final String apiValue;
}
/// 一次性互动请求,供仓库和测试断言使用。
class ReportRequest {
const ReportRequest({
required this.targetType,
required this.targetId,
required this.reasonCode,
this.description = '',
});
final InteractionTargetType targetType;
final String targetId;
final String reasonCode;
final String description;
}
/// 点赞切换后的服务端状态。
class LikeToggleResult {
const LikeToggleResult({required this.liked, required this.count});
final bool liked;
final int count;
/// 计数按 Long 字符串兼容解析。
factory LikeToggleResult.fromJson(Map<String, dynamic> json) =>
LikeToggleResult(
liked: json['liked'] == true,
count: jsonToInt(json['count']),
);
}
/// 控制器统一返回结果;失败消息可直接展示后端 `message`。
class OperationResult<T> {
const OperationResult._(this.data, this.errorMessage);
const OperationResult.success([T? data]) : this._(data, null);
const OperationResult.failure(this.errorMessage) : data = null;
final T? data;
final String? errorMessage;
bool get isSuccess => errorMessage == null;
}
+7
View File
@@ -11,3 +11,10 @@ int jsonToInt(dynamic value, {int fallback = 0}) {
if (value is String) return int.tryParse(value.trim()) ?? fallback;
return fallback;
}
/// ID 字段统一转字符串;后端按契约下发字符串,同时兼容测试或旧数据的数字形式。
String jsonToId(dynamic value, {String fallback = ''}) {
if (value is String) return value;
if (value is num) return value.toString();
return fallback;
}