From 766c62f3c0fa49755a5db41fd0215870995affd8 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Thu, 29 Jan 2026 15:53:26 +0800 Subject: [PATCH] 1 --- keyBoard.xcodeproj/project.pbxproj | 6 + .../Class/AiTalk/M/KBAICompanionDetailModel.h | 63 ++++ .../Class/AiTalk/M/KBAICompanionDetailModel.m | 23 ++ .../Class/AiTalk/V/Comment/KBAICommentView.m | 241 +++++++++++++- keyBoard/Class/AiTalk/VC/AIPersonInfoVC.h | 25 ++ keyBoard/Class/AiTalk/VC/AIPersonInfoVC.m | 313 ++++++++++++++++++ keyBoard/Class/AiTalk/VC/AIReportVC.h | 19 ++ keyBoard/Class/AiTalk/VC/AIReportVC.m | 160 +++++++++ keyBoard/Class/AiTalk/VM/AiVM.h | 11 +- keyBoard/Class/AiTalk/VM/AiVM.m | 47 ++- 10 files changed, 890 insertions(+), 18 deletions(-) create mode 100644 keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.h create mode 100644 keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.m create mode 100644 keyBoard/Class/AiTalk/VC/AIPersonInfoVC.h create mode 100644 keyBoard/Class/AiTalk/VC/AIPersonInfoVC.m create mode 100644 keyBoard/Class/AiTalk/VC/AIReportVC.h create mode 100644 keyBoard/Class/AiTalk/VC/AIReportVC.m diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index fe96fc6..689f476 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 048FFD3C2F29F500005D62AE /* KBLikedCompanionModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD3B2F29F500005D62AE /* KBLikedCompanionModel.m */; }; 048FFD3F2F29F600005D62AE /* KBChattedCompanionModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD3E2F29F600005D62AE /* KBChattedCompanionModel.m */; }; 048FFD422F29F700005D62AE /* KBChatSessionResetModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD412F29F700005D62AE /* KBChatSessionResetModel.m */; }; + 048FFD472F2B45D4005D62AE /* AIPersonInfoVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD462F2B45D4005D62AE /* AIPersonInfoVC.m */; }; 0498BD622EDFFC12006CC1D5 /* KBMyVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */; }; 0498BD652EE0116D006CC1D5 /* KBEmailLoginVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD642EE0116D006CC1D5 /* KBEmailLoginVC.m */; }; 0498BD682EE01180006CC1D5 /* KBEmailRegistVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD672EE01180006CC1D5 /* KBEmailRegistVC.m */; }; @@ -577,6 +578,8 @@ 048FFD3E2F29F600005D62AE /* KBChattedCompanionModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChattedCompanionModel.m; sourceTree = ""; }; 048FFD402F29F700005D62AE /* KBChatSessionResetModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBChatSessionResetModel.h; sourceTree = ""; }; 048FFD412F29F700005D62AE /* KBChatSessionResetModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatSessionResetModel.m; sourceTree = ""; }; + 048FFD452F2B45D4005D62AE /* AIPersonInfoVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AIPersonInfoVC.h; sourceTree = ""; }; + 048FFD462F2B45D4005D62AE /* AIPersonInfoVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AIPersonInfoVC.m; sourceTree = ""; }; 0498BD5E2EDF2157006CC1D5 /* KBBizCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBizCode.h; sourceTree = ""; }; 0498BD602EDFFC12006CC1D5 /* KBMyVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyVM.h; sourceTree = ""; }; 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyVM.m; sourceTree = ""; }; @@ -1083,6 +1086,8 @@ 048FFD322F29F3D2005D62AE /* KBAIMessageChatingVC.m */, 048FFD352F29F400005D62AE /* KBAIMessageListVC.h */, 048FFD362F29F400005D62AE /* KBAIMessageListVC.m */, + 048FFD452F2B45D4005D62AE /* AIPersonInfoVC.h */, + 048FFD462F2B45D4005D62AE /* AIPersonInfoVC.m */, ); path = VC; sourceTree = ""; @@ -2393,6 +2398,7 @@ 04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */, 04122F912EC73AF700EF7AB3 /* KBVipPay.m in Sources */, 0477BE002EBC6A330055D639 /* HomeRankVC.m in Sources */, + 048FFD472F2B45D4005D62AE /* AIPersonInfoVC.m in Sources */, 047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */, 0450AC0A2EF11E4400B6AF06 /* StoreKitManager.swift in Sources */, 0450AC0B2EF11E4400B6AF06 /* StoreKitStateConverter.swift in Sources */, diff --git a/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.h b/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.h new file mode 100644 index 0000000..940c89a --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.h @@ -0,0 +1,63 @@ +// +// KBAICompanionDetailModel.h +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// AI 角色详情 Model +@interface KBAICompanionDetailModel : NSObject + +/// 角色 ID +@property (nonatomic, assign) NSInteger companionId; +/// 名称 +@property (nonatomic, copy, nullable) NSString *name; +/// 头像 URL +@property (nonatomic, copy, nullable) NSString *avatarUrl; +/// 封面图 URL +@property (nonatomic, copy, nullable) NSString *coverImageUrl; +/// 性别 +@property (nonatomic, copy, nullable) NSString *gender; +/// 年龄范围 +@property (nonatomic, copy, nullable) NSString *ageRange; +/// 简短描述 +@property (nonatomic, copy, nullable) NSString *shortDesc; +/// 介绍文本 +@property (nonatomic, copy, nullable) NSString *introText; +/// 性格标签 +@property (nonatomic, copy, nullable) NSString *personalityTags; +/// 说话风格 +@property (nonatomic, copy, nullable) NSString *speakingStyle; +/// 排序 +@property (nonatomic, assign) NSInteger sortOrder; +/// 热度分数 +@property (nonatomic, assign) NSInteger popularityScore; +/// 开场白 +@property (nonatomic, copy, nullable) NSString *prologue; +/// 开场白音频 +@property (nonatomic, copy, nullable) NSString *prologueAudio; +/// 点赞数 +@property (nonatomic, assign) NSInteger likeCount; +/// 评论数 +@property (nonatomic, assign) NSInteger commentCount; +/// 当前用户是否已点赞 +@property (nonatomic, assign) BOOL liked; +/// 创建时间 +@property (nonatomic, copy, nullable) NSString *createdAt; + +@end + +/// AI 角色详情响应 Model +@interface KBAICompanionDetailResponse : NSObject + +@property (nonatomic, assign) NSInteger code; +@property (nonatomic, strong, nullable) KBAICompanionDetailModel *data; +@property (nonatomic, copy, nullable) NSString *message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.m b/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.m new file mode 100644 index 0000000..c8989f4 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAICompanionDetailModel.m @@ -0,0 +1,23 @@ +// +// KBAICompanionDetailModel.m +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import "KBAICompanionDetailModel.h" +#import + +@implementation KBAICompanionDetailModel + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ + @"companionId": @"id" + }; +} + +@end + +@implementation KBAICompanionDetailResponse + +@end diff --git a/keyBoard/Class/AiTalk/V/Comment/KBAICommentView.m b/keyBoard/Class/AiTalk/V/Comment/KBAICommentView.m index 589a864..f8fa9f1 100644 --- a/keyBoard/Class/AiTalk/V/Comment/KBAICommentView.m +++ b/keyBoard/Class/AiTalk/V/Comment/KBAICommentView.m @@ -14,6 +14,8 @@ #import "KBAIReplyModel.h" #import "KBCommentModel.h" #import "AiVM.h" +#import "KBUserSessionManager.h" +#import "KBUser.h" #import #import #import @@ -56,6 +58,81 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; @implementation KBAICommentView +#pragma mark - Local Model Builders + +- (NSString *)currentUserName { + KBUser *user = [KBUserSessionManager shared].currentUser; + if (user.nickName.length > 0) { + return user.nickName; + } + return @"我"; +} + +- (NSString *)currentUserId { + KBUser *user = [KBUserSessionManager shared].currentUser; + return user.userId ?: @""; +} + +- (NSString *)currentUserAvatarUrl { + KBUser *user = [KBUserSessionManager shared].currentUser; + return user.avatarUrl ?: @""; +} + +- (NSString *)generateTempIdString { + long long ms = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); + // 使用负数避免与后端 ID 冲突 + long long tmp = -ms; + return [NSString stringWithFormat:@"%lld", tmp]; +} + +- (KBAICommentModel *)buildLocalNewCommentWithText:(NSString *)text + serverItem:(KBCommentItem *_Nullable)serverItem + tableWidth:(CGFloat)tableWidth { + KBAICommentModel *comment = [[KBAICommentModel alloc] init]; + NSString *cid = nil; + if (serverItem && serverItem.commentId > 0) { + cid = [NSString stringWithFormat:@"%ld", (long)serverItem.commentId]; + } else { + cid = [self generateTempIdString]; + } + comment.commentId = cid; + comment.userId = [self currentUserId]; + comment.userName = [self currentUserName]; + comment.avatarUrl = [self currentUserAvatarUrl]; + comment.content = text ?: @""; + comment.likeCount = 0; + comment.liked = NO; + comment.createTime = [[NSDate date] timeIntervalSince1970]; + comment.replies = @[]; + comment.cachedHeaderHeight = + [comment calculateHeaderHeightWithMaxWidth:tableWidth]; + return comment; +} + +- (KBAIReplyModel *)buildLocalNewReplyWithText:(NSString *)text + serverItem:(KBCommentItem *_Nullable)serverItem + replyToUserName:(NSString *)replyToUserName + tableWidth:(CGFloat)tableWidth { + KBAIReplyModel *reply = [[KBAIReplyModel alloc] init]; + NSString *rid = nil; + if (serverItem && serverItem.commentId > 0) { + rid = [NSString stringWithFormat:@"%ld", (long)serverItem.commentId]; + } else { + rid = [self generateTempIdString]; + } + reply.replyId = rid; + reply.userId = [self currentUserId]; + reply.userName = [self currentUserName]; + reply.avatarUrl = [self currentUserAvatarUrl]; + reply.content = text ?: @""; + reply.replyToUserName = replyToUserName ?: @""; + reply.likeCount = 0; + reply.liked = NO; + reply.createTime = [[NSDate date] timeIntervalSince1970]; + reply.cachedCellHeight = [reply calculateCellHeightWithMaxWidth:tableWidth]; + return reply; +} + #pragma mark - Lifecycle - (instancetype)initWithFrame:(CGRect)frame { @@ -289,7 +366,36 @@ static NSString *const kCommentFooterIdentifier = @"CommentFooter"; for (KBCommentItem *item in pageModel.records) { // 转换为 KBAICommentModel(使用 MJExtension) - KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[item mj_keyValues]]; + // 注意:KBCommentItem 通过 MJExtension 将后端字段 id 映射为了 commentId。 + // 这里如果直接用 mj_keyValues,会导致字典里只有 commentId,KBAICommentModel/KBAIReplyModel + // 的映射(commentId/replyId -> id)拿不到值,最终 commentId/replyId 为空,进而影响发送回复时的 parentId/rootId。 + NSMutableDictionary *itemKV = [[item mj_keyValues] mutableCopy]; + id commentIdVal = itemKV[@"commentId"]; + if (commentIdVal) { + itemKV[@"id"] = commentIdVal; + [itemKV removeObjectForKey:@"commentId"]; + } + + id repliesObj = itemKV[@"replies"]; + if ([repliesObj isKindOfClass:[NSArray class]]) { + NSArray *replies = (NSArray *)repliesObj; + NSMutableArray *fixedReplies = [NSMutableArray arrayWithCapacity:replies.count]; + for (id obj in replies) { + if (![obj isKindOfClass:[NSDictionary class]]) { + continue; + } + NSMutableDictionary *replyKV = [((NSDictionary *)obj) mutableCopy]; + id replyCommentIdVal = replyKV[@"commentId"]; + if (replyCommentIdVal) { + replyKV[@"id"] = replyCommentIdVal; + [replyKV removeObjectForKey:@"commentId"]; + } + [fixedReplies addObject:[replyKV copy]]; + } + itemKV[@"replies"] = [fixedReplies copy]; + } + + KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:[itemKV copy]]; // 预先计算并缓存 Header 高度 comment.cachedHeaderHeight = [comment calculateHeaderHeightWithMaxWidth:tableWidth]; @@ -805,8 +911,44 @@ static NSInteger const kRepliesLoadCount = 5; } - (void)sendNewCommentWithText:(NSString *)text tableWidth:(CGFloat)tableWidth { - // TODO: 调用网络接口发送一级评论 NSLog(@"[KBAICommentView] 发送一级评论:%@", text); + + __weak typeof(self) weakSelf = self; + [self.aiVM addCommentWithCompanionId:self.companionId + content:text + parentId:nil + rootId:nil + completion:^(KBCommentItem * _Nullable newItem, NSInteger code, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (error || code != 0) { + NSLog(@"[KBAICommentView] 发送一级评论失败:%@", error.localizedDescription ?: @""); + return; + } + + // 本地插入新评论到第一条,不再全量刷新 + KBAICommentModel *localComment = + [strongSelf buildLocalNewCommentWithText:text + serverItem:newItem + tableWidth:tableWidth]; + [strongSelf.comments insertObject:localComment atIndex:0]; + strongSelf.totalCommentCount += 1; + [strongSelf updateTitle]; + [strongSelf hideEmptyState]; + + [strongSelf.tableView beginUpdates]; + [strongSelf.tableView + insertSections:[NSIndexSet indexSetWithIndex:0] + withRowAnimation:UITableViewRowAnimationAutomatic]; + [strongSelf.tableView endUpdates]; + + [strongSelf.tableView setContentOffset:CGPointZero animated:YES]; + }); + }]; // 示例代码: // [self.aiVM sendCommentWithCompanionId:self.companionId @@ -840,8 +982,101 @@ static NSInteger const kRepliesLoadCount = 5; if (!comment) return; - // TODO: 调用网络接口发送二级评论(回复) NSLog(@"[KBAICommentView] 回复评论 %@:%@", comment.commentId, text); + + NSInteger root = [comment.commentId integerValue]; + NSNumber *rootId = @(root); + NSNumber *parentId = nil; + + if (self.replyToReply && self.replyToReply.replyId.length > 0) { + parentId = @([self.replyToReply.replyId integerValue]); + } else { + parentId = @(root); + } + + __weak typeof(self) weakSelf = self; + [self.aiVM addCommentWithCompanionId:self.companionId + content:text + parentId:parentId + rootId:rootId + completion:^(KBCommentItem * _Nullable newItem, NSInteger code, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (error || code != 0) { + NSLog(@"[KBAICommentView] 回复评论失败:%@", error.localizedDescription ?: @""); + return; + } + + NSInteger section = [strongSelf.comments indexOfObject:comment]; + if (section == NSNotFound) { + return; + } + + NSInteger oldTotalReplyCount = comment.totalReplyCount; + BOOL wasFooterHidden = (oldTotalReplyCount == 0); + BOOL wasFullyExpanded = + (comment.isRepliesExpanded && + comment.displayedReplies.count == oldTotalReplyCount); + + NSString *replyToUserName = @""; + if (strongSelf.replyToReply && strongSelf.replyToReply.userName.length > 0) { + replyToUserName = strongSelf.replyToReply.userName; + } else if (comment.userName.length > 0) { + replyToUserName = comment.userName; + } + + KBAIReplyModel *localReply = + [strongSelf buildLocalNewReplyWithText:text + serverItem:newItem + replyToUserName:replyToUserName + tableWidth:tableWidth]; + + NSArray *oldReplies = comment.replies ?: @[]; + NSMutableArray *newReplies = + [NSMutableArray arrayWithArray:oldReplies]; + [newReplies addObject:localReply]; + comment.replies = [newReplies copy]; + + strongSelf.totalCommentCount += 1; + [strongSelf updateTitle]; + + // 若当前已完整展开,则直接插入新行;否则保持 displayedReplies 为前缀,避免破坏 loadMoreReplies 逻辑 + if (wasFullyExpanded) { + [comment.displayedReplies addObject:localReply]; + NSInteger newRowIndex = comment.displayedReplies.count - 1; + NSIndexPath *indexPath = + [NSIndexPath indexPathForRow:newRowIndex inSection:section]; + [strongSelf.tableView beginUpdates]; + [strongSelf.tableView insertRowsAtIndexPaths:@[ indexPath ] + withRowAnimation:UITableViewRowAnimationAutomatic]; + [strongSelf.tableView endUpdates]; + + KBAICommentFooterView *footerView = + (KBAICommentFooterView *)[strongSelf.tableView footerViewForSection:section]; + if (footerView) { + [footerView configureWithComment:comment]; + } + } else { + if (wasFooterHidden) { + [strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] + withRowAnimation:UITableViewRowAnimationNone]; + } else { + KBAICommentFooterView *footerView = + (KBAICommentFooterView *)[strongSelf.tableView footerViewForSection:section]; + if (footerView) { + [footerView configureWithComment:comment]; + } else { + [strongSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] + withRowAnimation:UITableViewRowAnimationNone]; + } + } + } + }); + }]; // 示例代码: // NSInteger parentId = [comment.commentId integerValue]; diff --git a/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.h b/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.h new file mode 100644 index 0000000..a23ac87 --- /dev/null +++ b/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.h @@ -0,0 +1,25 @@ +// +// AIPersonInfoVC.h +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import "BaseViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface AIPersonInfoVC : BaseViewController + +/// 背景图 URL +@property (nonatomic, copy) NSString *backgroundImageURL; +/// 人设名称 +@property (nonatomic, copy) NSString *personaName; +/// 人设介绍 +@property (nonatomic, copy) NSString *personaIntroduction; +/// 人设 ID(用于举报等) +@property (nonatomic, assign) NSInteger personaId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.m b/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.m new file mode 100644 index 0000000..6ff6aae --- /dev/null +++ b/keyBoard/Class/AiTalk/VC/AIPersonInfoVC.m @@ -0,0 +1,313 @@ +// +// AIPersonInfoVC.m +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import "AIPersonInfoVC.h" +#import "AIReportVC.h" +#import + +@interface AIPersonInfoVC () + +/// 背景图 +@property (nonatomic, strong) UIImageView *backgroundImageView; +/// 底部磨砂渐变视图 +@property (nonatomic, strong) UIVisualEffectView *blurEffectView; +/// 渐变遮罩层 +@property (nonatomic, strong) CAGradientLayer *gradientMaskLayer; +/// 左上角关闭按钮 +@property (nonatomic, strong) UIButton *closeButton; +/// 右上角更多按钮 +@property (nonatomic, strong) UIButton *moreButton; +/// 举报弹窗视图 +@property (nonatomic, strong) UIView *reportPopView; +/// 名称标签 +@property (nonatomic, strong) UILabel *nameLabel; +/// 介绍标签 +@property (nonatomic, strong) UILabel *introLabel; +/// 底部 Go Chatting 按钮 +@property (nonatomic, strong) UIButton *goChatButton; + +@end + +@implementation AIPersonInfoVC + +#pragma mark - Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.kb_navView.hidden = YES; + self.view.backgroundColor = [UIColor blackColor]; + + /// 1:控件初始化 + [self setupUI]; + /// 2:绑定事件 + [self bindActions]; + /// 3:填充数据 + [self loadData]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + // 更新渐变遮罩 frame + self.gradientMaskLayer.frame = self.blurEffectView.bounds; +} + +#pragma mark - 1:控件初始化 + +- (void)setupUI { + // 背景图 + [self.view addSubview:self.backgroundImageView]; + [self.backgroundImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.view); + }]; + + // 底部磨砂渐变 + [self.view addSubview:self.blurEffectView]; + [self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.bottom.equalTo(self.view); + make.height.mas_equalTo(KB_SCREEN_HEIGHT * 0.75); + }]; + + // 左上角关闭按钮 + [self.view addSubview:self.closeButton]; + [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(16); + make.top.equalTo(self.view).offset(KB_STATUSBAR_HEIGHT + 10); + make.width.height.mas_equalTo(32); + }]; + + // 右上角更多按钮 + [self.view addSubview:self.moreButton]; + [self.moreButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.view).offset(-16); + make.centerY.equalTo(self.closeButton); + make.width.height.mas_equalTo(32); + }]; + + // 名称标签 + [self.view addSubview:self.nameLabel]; + [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(20); + make.right.equalTo(self.view).offset(-20); + make.centerY.equalTo(self.view).offset(20); + }]; + + // 介绍标签 + [self.view addSubview:self.introLabel]; + [self.introLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(20); + make.right.equalTo(self.view).offset(-20); + make.top.equalTo(self.nameLabel.mas_bottom).offset(16); + }]; + + // 底部 Go Chatting 按钮 + [self.view addSubview:self.goChatButton]; + [self.goChatButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(40); + make.right.equalTo(self.view).offset(-40); + make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM - 30); + make.height.mas_equalTo(50); + }]; + + // 举报弹窗(初始隐藏) + [self.view addSubview:self.reportPopView]; + self.reportPopView.hidden = YES; + [self.reportPopView mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.moreButton.mas_left).offset(8); + make.top.equalTo(self.moreButton.mas_bottom).offset(4); + make.width.mas_equalTo(100); + make.height.mas_equalTo(40); + }]; +} + +#pragma mark - 2:绑定事件 + +- (void)bindActions { + // 点击空白处隐藏弹窗 + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundTap:)]; + tap.cancelsTouchesInView = NO; + [self.view addGestureRecognizer:tap]; +} + +#pragma mark - 3:填充数据 + +- (void)loadData { + // 加载背景图 + if (self.backgroundImageURL.length > 0) { + [self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:self.backgroundImageURL] + placeholderImage:KBPlaceholderImage]; + } + + // 设置名称 + self.nameLabel.text = self.personaName ?: @""; + + // 设置介绍 + self.introLabel.text = self.personaIntroduction ?: @""; +} + +#pragma mark - Actions + +- (void)closeButtonTapped { + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)moreButtonTapped { + self.reportPopView.hidden = !self.reportPopView.hidden; +} + +- (void)reportButtonTapped { + self.reportPopView.hidden = YES; + + AIReportVC *vc = [[AIReportVC alloc] init]; + vc.personaId = self.personaId; + [self.navigationController pushViewController:vc animated:YES]; +} + +- (void)goChatButtonTapped { + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)handleBackgroundTap:(UITapGestureRecognizer *)tap { + CGPoint point = [tap locationInView:self.view]; + + // 如果点击的不是 moreButton 和 reportPopView 区域,则隐藏弹窗 + if (!CGRectContainsPoint(self.moreButton.frame, point) && + !CGRectContainsPoint(self.reportPopView.frame, point)) { + self.reportPopView.hidden = YES; + } +} + +#pragma mark - Lazy Load + +- (UIImageView *)backgroundImageView { + if (!_backgroundImageView) { + _backgroundImageView = [[UIImageView alloc] init]; + _backgroundImageView.contentMode = UIViewContentModeScaleAspectFill; + _backgroundImageView.clipsToBounds = YES; + } + return _backgroundImageView; +} + +- (UIVisualEffectView *)blurEffectView { + if (!_blurEffectView) { + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + _blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + _blurEffectView.layer.mask = self.gradientMaskLayer; + } + return _blurEffectView; +} + +- (CAGradientLayer *)gradientMaskLayer { + if (!_gradientMaskLayer) { + _gradientMaskLayer = [CAGradientLayer layer]; + // 从上到下:透明 -> 不透明 + _gradientMaskLayer.colors = @[ + (__bridge id)[UIColor clearColor].CGColor, + (__bridge id)[UIColor whiteColor].CGColor + ]; + _gradientMaskLayer.startPoint = CGPointMake(0.5, 0); + _gradientMaskLayer.endPoint = CGPointMake(0.5, 0.4); + _gradientMaskLayer.locations = @[@0, @1]; + } + return _gradientMaskLayer; +} + +- (UIButton *)closeButton { + if (!_closeButton) { + _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_closeButton setImage:[UIImage imageNamed:@"comment_close_icon"] forState:UIControlStateNormal]; + [_closeButton addTarget:self action:@selector(closeButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + } + return _closeButton; +} + +- (UIButton *)moreButton { + if (!_moreButton) { + _moreButton = [UIButton buttonWithType:UIButtonTypeCustom]; + // 使用三个点的图标,如果没有可以用系统的 + [_moreButton setImage:[UIImage imageNamed:@"ai_more_icon"] forState:UIControlStateNormal]; + if (![UIImage imageNamed:@"ai_more_icon"]) { + // 如果没有图标,使用文字 + [_moreButton setTitle:@"•••" forState:UIControlStateNormal]; + [_moreButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + _moreButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightBold]; + } + [_moreButton addTarget:self action:@selector(moreButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + } + return _moreButton; +} + +- (UIView *)reportPopView { + if (!_reportPopView) { + _reportPopView = [[UIView alloc] init]; + _reportPopView.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.9]; + _reportPopView.layer.cornerRadius = 8; + _reportPopView.clipsToBounds = YES; + + // 举报按钮 + UIButton *reportBtn = [UIButton buttonWithType:UIButtonTypeCustom]; + [reportBtn setImage:[UIImage imageNamed:@"ai_report_icon"] forState:UIControlStateNormal]; + [reportBtn setTitle:KBLocalized(@"Report") forState:UIControlStateNormal]; + [reportBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + reportBtn.titleLabel.font = [UIFont systemFontOfSize:14]; + reportBtn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + reportBtn.imageEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 0); + reportBtn.titleEdgeInsets = UIEdgeInsetsMake(0, 16, 0, 0); + [reportBtn addTarget:self action:@selector(reportButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + + [_reportPopView addSubview:reportBtn]; + [reportBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(_reportPopView); + }]; + } + return _reportPopView; +} + +- (UILabel *)nameLabel { + if (!_nameLabel) { + _nameLabel = [[UILabel alloc] init]; + _nameLabel.font = [UIFont systemFontOfSize:28 weight:UIFontWeightBold]; + _nameLabel.textColor = [UIColor whiteColor]; + _nameLabel.textAlignment = NSTextAlignmentLeft; + } + return _nameLabel; +} + +- (UILabel *)introLabel { + if (!_introLabel) { + _introLabel = [[UILabel alloc] init]; + _introLabel.font = [UIFont systemFontOfSize:14]; + _introLabel.textColor = [UIColor colorWithWhite:1.0 alpha:0.8]; + _introLabel.textAlignment = NSTextAlignmentLeft; + _introLabel.numberOfLines = 0; + } + return _introLabel; +} + +- (UIButton *)goChatButton { + if (!_goChatButton) { + _goChatButton = [UIButton buttonWithType:UIButtonTypeCustom]; + + // 设置背景图(如果有的话) + UIImage *bgImage = [UIImage imageNamed:@"ai_go_chat_bg"]; + if (bgImage) { + [_goChatButton setBackgroundImage:bgImage forState:UIControlStateNormal]; + } else { + // 没有背景图时使用渐变色 + _goChatButton.backgroundColor = [UIColor colorWithRed:0.8 green:1.0 blue:0.6 alpha:1.0]; + _goChatButton.layer.cornerRadius = 25; + } + + [_goChatButton setTitle:KBLocalized(@"Go Chatting") forState:UIControlStateNormal]; + [_goChatButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + _goChatButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + [_goChatButton addTarget:self action:@selector(goChatButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + } + return _goChatButton; +} + +@end diff --git a/keyBoard/Class/AiTalk/VC/AIReportVC.h b/keyBoard/Class/AiTalk/VC/AIReportVC.h new file mode 100644 index 0000000..2240341 --- /dev/null +++ b/keyBoard/Class/AiTalk/VC/AIReportVC.h @@ -0,0 +1,19 @@ +// +// AIReportVC.h +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import "BaseViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface AIReportVC : BaseViewController + +/// 被举报的人设 ID +@property (nonatomic, assign) NSInteger personaId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/VC/AIReportVC.m b/keyBoard/Class/AiTalk/VC/AIReportVC.m new file mode 100644 index 0000000..490b2d2 --- /dev/null +++ b/keyBoard/Class/AiTalk/VC/AIReportVC.m @@ -0,0 +1,160 @@ +// +// AIReportVC.m +// keyBoard +// +// Created by Mac on 2026/1/29. +// + +#import "AIReportVC.h" + +@interface AIReportVC () + +/// 举报原因列表 +@property (nonatomic, strong) NSArray *reportReasons; +/// 当前选中的索引 +@property (nonatomic, assign) NSInteger selectedIndex; +/// 举报原因 TableView +@property (nonatomic, strong) UITableView *tableView; +/// 提交按钮 +@property (nonatomic, strong) UIButton *submitButton; + +@end + +@implementation AIReportVC + +#pragma mark - Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0]; + self.kb_titleLabel.text = KBLocalized(@"Report"); + self.selectedIndex = -1; + + /// 1:初始化数据 + [self initData]; + /// 2:控件初始化 + [self setupUI]; +} + +#pragma mark - 1:初始化数据 + +- (void)initData { + self.reportReasons = @[ + KBLocalized(@"Inappropriate content"), + KBLocalized(@"Spam or advertising"), + KBLocalized(@"Harassment or bullying"), + KBLocalized(@"False information"), + KBLocalized(@"Intellectual property violation"), + KBLocalized(@"Other") + ]; +} + +#pragma mark - 2:控件初始化 + +- (void)setupUI { + [self.view addSubview:self.tableView]; + [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.kb_navView.mas_bottom).offset(20); + make.left.right.equalTo(self.view); + make.bottom.equalTo(self.submitButton.mas_top).offset(-20); + }]; + + [self.view addSubview:self.submitButton]; + [self.submitButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.view).offset(40); + make.right.equalTo(self.view).offset(-40); + make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM - 30); + make.height.mas_equalTo(50); + }]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.reportReasons.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ReportCell" forIndexPath:indexPath]; + cell.backgroundColor = [UIColor clearColor]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.textLabel.text = self.reportReasons[indexPath.row]; + cell.textLabel.textColor = [UIColor whiteColor]; + cell.textLabel.font = [UIFont systemFontOfSize:16]; + + // 选中状态 + if (indexPath.row == self.selectedIndex) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + cell.tintColor = [UIColor colorWithRed:0.8 green:1.0 blue:0.6 alpha:1.0]; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + + return cell; +} + +#pragma mark - UITableViewDelegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + self.selectedIndex = indexPath.row; + [tableView reloadData]; + + // 更新提交按钮状态 + self.submitButton.enabled = YES; + self.submitButton.alpha = 1.0; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 56; +} + +#pragma mark - Actions + +- (void)submitButtonTapped { + if (self.selectedIndex < 0) { + [KBHUD showError:KBLocalized(@"Please select a reason")]; + return; + } + + NSString *reason = self.reportReasons[self.selectedIndex]; + NSLog(@"[AIReportVC] 举报人设 ID: %ld, 原因: %@", (long)self.personaId, reason); + + // TODO: 调用举报接口 + [KBHUD showSuccess:KBLocalized(@"Report submitted")]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.navigationController popViewControllerAnimated:YES]; + }); +} + +#pragma mark - Lazy Load + +- (UITableView *)tableView { + if (!_tableView) { + _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.backgroundColor = [UIColor clearColor]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableView.delegate = self; + _tableView.dataSource = self; + [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"ReportCell"]; + } + return _tableView; +} + +- (UIButton *)submitButton { + if (!_submitButton) { + _submitButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _submitButton.backgroundColor = [UIColor colorWithRed:0.8 green:1.0 blue:0.6 alpha:1.0]; + _submitButton.layer.cornerRadius = 25; + [_submitButton setTitle:KBLocalized(@"Submit") forState:UIControlStateNormal]; + [_submitButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + _submitButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + _submitButton.enabled = NO; + _submitButton.alpha = 0.5; + [_submitButton addTarget:self action:@selector(submitButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + } + return _submitButton; +} + +@end diff --git a/keyBoard/Class/AiTalk/VM/AiVM.h b/keyBoard/Class/AiTalk/VM/AiVM.h index 18ba73b..8071792 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.h +++ b/keyBoard/Class/AiTalk/VM/AiVM.h @@ -12,6 +12,7 @@ #import "KBLikedCompanionModel.h" #import "KBChattedCompanionModel.h" #import "KBChatSessionResetModel.h" +#import "KBAICompanionDetailModel.h" NS_ASSUME_NONNULL_BEGIN @@ -116,12 +117,12 @@ typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nu /// @param content 评论内容 /// @param parentId 父评论 ID(一级评论传 NULL) /// @param rootId 根评论 ID(用于标识一级评论) -/// @param completion 完成回调(返回 code 200 表示成功) +/// @param completion 完成回调(newItem 可能为空,取决于后端是否返回 data) - (void)addCommentWithCompanionId:(NSInteger)companionId - content:(NSString *)content - parentId:(nullable NSNumber *)parentId - rootId:(NSInteger)rootId - completion:(void(^)(NSInteger code, NSError * _Nullable error))completion; + content:(NSString *)content + parentId:(nullable NSNumber *)parentId + rootId:(nullable NSNumber *)rootId + completion:(void(^)(KBCommentItem * _Nullable newItem, NSInteger code, NSError * _Nullable error))completion; /// 分页查询评论列表 /// @param companionId AI 陪聊角色 ID diff --git a/keyBoard/Class/AiTalk/VM/AiVM.m b/keyBoard/Class/AiTalk/VM/AiVM.m index 94d21c8..212952f 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.m +++ b/keyBoard/Class/AiTalk/VM/AiVM.m @@ -435,14 +435,14 @@ autoShowBusinessError:NO - (void)addCommentWithCompanionId:(NSInteger)companionId content:(NSString *)content parentId:(nullable NSNumber *)parentId - rootId:(NSInteger)rootId - completion:(void (^)(NSInteger, NSError * _Nullable))completion { + rootId:(nullable NSNumber *)rootId + completion:(void (^)(KBCommentItem * _Nullable, NSInteger, NSError * _Nullable))completion { if (content.length == 0) { NSError *error = [NSError errorWithDomain:@"AiVM" - code:-1 - userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}]; + code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}]; if (completion) { - completion(-1, error); + completion(nil, -1, error); } return; } @@ -450,7 +450,9 @@ autoShowBusinessError:NO NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"companionId"] = @(companionId); params[@"content"] = content; - params[@"rootId"] = @(rootId); + if (rootId) { + params[@"rootId"] = rootId; + } if (parentId) { params[@"parentId"] = parentId; } @@ -467,17 +469,42 @@ autoShowBusinessError:NO if (error) { NSLog(@"[AiVM] /ai-companion/comment/add failed: %@", error.localizedDescription ?: @""); if (completion) { - completion(-1, error); + completion(nil, -1, error); } return; } NSLog(@"[AiVM] /ai-companion/comment/add response: %@", json); NSInteger code = [json[@"code"] integerValue]; - if (completion) { - completion(code, nil); + if (code != 0) { + NSString *message = json[@"message"] ?: @"请求失败"; + NSError *bizError = [NSError errorWithDomain:@"AiVM" + code:code + userInfo:@{NSLocalizedDescriptionKey: message}]; + if (completion) { + completion(nil, code, bizError); + } + return; } - }]; + + KBCommentItem *newItem = nil; + id dataObj = json[@"data"]; + if ([dataObj isKindOfClass:[NSDictionary class]]) { + newItem = [KBCommentItem mj_objectWithKeyValues:(NSDictionary *)dataObj]; + } else if ([dataObj isKindOfClass:[NSNumber class]]) { + KBCommentItem *tmp = [[KBCommentItem alloc] init]; + tmp.commentId = [(NSNumber *)dataObj integerValue]; + tmp.companionId = companionId; + tmp.content = content; + tmp.parentId = parentId; + tmp.rootId = rootId; + newItem = tmp; + } + + if (completion) { + completion(newItem, code, nil); + } + }]; } - (void)fetchCommentsWithCompanionId:(NSInteger)companionId