diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index cc591c5..182d8fb 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 048FFD1E2F277486005D62AE /* KBChatHistoryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD1A2F277486005D62AE /* KBChatHistoryModel.m */; }; 048FFD242F28A836005D62AE /* KBChatLimitPopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */; }; 048FFD272F28C6CF005D62AE /* KBImagePositionButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD262F28C6CF005D62AE /* KBImagePositionButton.m */; }; + 048FFD2A2F28E99A005D62AE /* KBCommentModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD292F28E99A005D62AE /* KBCommentModel.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 */; }; @@ -547,6 +548,8 @@ 048FFD232F28A836005D62AE /* KBChatLimitPopView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBChatLimitPopView.m; sourceTree = ""; }; 048FFD252F28C6CF005D62AE /* KBImagePositionButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBImagePositionButton.h; sourceTree = ""; }; 048FFD262F28C6CF005D62AE /* KBImagePositionButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBImagePositionButton.m; sourceTree = ""; }; + 048FFD282F28E99A005D62AE /* KBCommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCommentModel.h; sourceTree = ""; }; + 048FFD292F28E99A005D62AE /* KBCommentModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCommentModel.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 = ""; }; @@ -1006,6 +1009,8 @@ 048FFD1A2F277486005D62AE /* KBChatHistoryModel.m */, 048FFD1B2F277486005D62AE /* KBChatHistoryPageModel.h */, 048FFD1C2F277486005D62AE /* KBChatHistoryPageModel.m */, + 048FFD282F28E99A005D62AE /* KBCommentModel.h */, + 048FFD292F28E99A005D62AE /* KBCommentModel.m */, ); path = M; sourceTree = ""; @@ -2418,6 +2423,7 @@ 048908DA2EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m in Sources */, 04C6EABF2EAF86530089C901 /* main.m in Sources */, 0498BD6E2EE0285D006CC1D5 /* KBForgetVerPwdVC.m in Sources */, + 048FFD2A2F28E99A005D62AE /* KBCommentModel.m in Sources */, 0498BDE12EEA87C9006CC1D5 /* KBShopStyleModel.m in Sources */, 49B63DBAEE9076C591E13D68 /* KBShopThemeTagModel.m in Sources */, EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */, diff --git a/keyBoard/Class/AiTalk/M/KBCommentModel.h b/keyBoard/Class/AiTalk/M/KBCommentModel.h new file mode 100644 index 0000000..a87132d --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBCommentModel.h @@ -0,0 +1,75 @@ +// +// KBCommentModel.h +// keyBoard +// +// Created by Kiro on 2026/1/27. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - 评论项模型 + +@interface KBCommentItem : NSObject + +/// 评论 ID +@property (nonatomic, assign) NSInteger commentId; +/// AI 陪聊角色 ID +@property (nonatomic, assign) NSInteger companionId; +/// 评论内容 +@property (nonatomic, copy, nullable) NSString *content; +/// 创建时间 +@property (nonatomic, copy, nullable) NSString *createdAt; +/// 点赞数 +@property (nonatomic, assign) NSInteger likeCount; +/// 是否已点赞(0=未点赞,1=已点赞) +@property (nonatomic, assign) NSInteger liked; +/// 父评论 ID(一级评论为 null) +@property (nonatomic, strong, nullable) NSNumber *parentId; +/// 根评论 ID(用于标识评论线程) +@property (nonatomic, strong, nullable) NSNumber *rootId; +/// 回复列表 +@property (nonatomic, strong, nullable) NSArray *replies; +/// 回复数量 +@property (nonatomic, assign) NSInteger replyCount; +/// 用户头像 +@property (nonatomic, copy, nullable) NSString *userAvatar; +/// 用户 ID +@property (nonatomic, assign) NSInteger userId; +/// 用户名 +@property (nonatomic, copy, nullable) NSString *userName; + +@end + +#pragma mark - 评论分页模型 + +@interface KBCommentPageModel : NSObject + +/// 当前页码 +@property (nonatomic, assign) NSInteger current; +/// 总页数 +@property (nonatomic, assign) NSInteger pages; +/// 每页大小 +@property (nonatomic, assign) NSInteger size; +/// 总记录数 +@property (nonatomic, assign) NSInteger total; +/// 评论列表 +@property (nonatomic, strong, nullable) NSArray *records; + +@end + +#pragma mark - 点赞响应模型 + +@interface KBCommentLikeResponse : NSObject + +/// 响应码(0=成功) +@property (nonatomic, assign) NSInteger code; +/// 响应消息 +@property (nonatomic, copy, nullable) NSString *message; +/// 点赞结果(true=点赞成功,false=取消点赞成功) +@property (nonatomic, assign) BOOL data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/M/KBCommentModel.m b/keyBoard/Class/AiTalk/M/KBCommentModel.m new file mode 100644 index 0000000..5de6178 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBCommentModel.m @@ -0,0 +1,42 @@ +// +// KBCommentModel.m +// keyBoard +// +// Created by Kiro on 2026/1/27. +// + +#import "KBCommentModel.h" +#import + +@implementation KBCommentItem + ++ (void)load { + // id 是 OC 关键字,需要映射 + [self mj_setupReplacedKeyFromPropertyName:^NSDictionary *{ + return @{ + @"commentId": @"id" + }; + }]; +} + ++ (NSDictionary *)mj_objectClassInArray { + return @{ + @"replies": [KBCommentItem class] + }; +} + +@end + +@implementation KBCommentPageModel + ++ (NSDictionary *)mj_objectClassInArray { + return @{ + @"records": [KBCommentItem class] + }; +} + +@end + +@implementation KBCommentLikeResponse + +@end diff --git a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m index 724974d..6e8ac35 100644 --- a/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m +++ b/keyBoard/Class/AiTalk/V/KBPersonaChatCell.m @@ -11,8 +11,10 @@ #import "KBChatHistoryPageModel.h" #import "AiVM.h" #import "KBImagePositionButton.h" +#import "KBAICommentView.h" #import #import +#import @interface KBPersonaChatCell () @@ -55,6 +57,9 @@ /// 喜欢按钮 @property (nonatomic, strong) KBImagePositionButton *likeButton; +/// 评论弹窗 +@property (nonatomic, weak) LSTPopView *popView; + @end @implementation KBPersonaChatCell @@ -159,7 +164,6 @@ self.currentPage = 1; self.hasMoreHistory = YES; self.messages = [NSMutableArray array]; - self.aiVM = [[AiVM alloc] init]; // 设置 UI [self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl] @@ -452,17 +456,106 @@ #pragma mark - Button Actions - (void)commentButtonTapped:(KBImagePositionButton *)sender { - sender.selected = !sender.selected; - NSLog(@"[KBPersonaChatCell] 评论按钮点击,选中状态:%d", sender.selected); + NSLog(@"[KBPersonaChatCell] 评论按钮点击"); - // TODO: 在这里添加评论逻辑 + // 弹出评论视图 + [self showComment]; } - (void)likeButtonTapped:(KBImagePositionButton *)sender { - sender.selected = !sender.selected; - NSLog(@"[KBPersonaChatCell] 喜欢按钮点击,选中状态:%d", sender.selected); + NSLog(@"[KBPersonaChatCell] 喜欢按钮点击"); - // TODO: 在这里添加喜欢逻辑 + NSInteger personaId = self.persona.personaId; + + // 禁用按钮,防止重复点击 + sender.enabled = NO; + + __weak typeof(self) weakSelf = self; + [self.aiVM likeCompanionWithCompanionId:personaId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + // 恢复按钮可用状态 + sender.enabled = YES; + + if (error) { + NSLog(@"[KBPersonaChatCell] 点赞失败:%@", error.localizedDescription); + // TODO: 显示错误提示 + return; + } + + if (response && response.code == 0) { + // 获取当前喜欢数 + NSInteger currentLikeCount = [strongSelf.persona.likeCount integerValue]; + + // response.data: true 表示点赞成功,false 表示取消点赞成功 + if (response.data) { + // 点赞成功,喜欢数加1 + currentLikeCount += 1; + sender.selected = YES; + NSLog(@"[KBPersonaChatCell] 点赞成功,新喜欢数:%ld", (long)currentLikeCount); + } else { + // 取消点赞成功,喜欢数减1(但不能小于0) + currentLikeCount = MAX(0, currentLikeCount - 1); + sender.selected = NO; + NSLog(@"[KBPersonaChatCell] 取消点赞成功,新喜欢数:%ld", (long)currentLikeCount); + } + + // 更新模型数据 + strongSelf.persona.likeCount = [NSString stringWithFormat:@"%ld", (long)currentLikeCount]; + strongSelf.persona.liked = sender.selected; + + // 更新按钮显示文字 + [sender setTitle:strongSelf.persona.likeCount forState:UIControlStateNormal]; + } else { + NSLog(@"[KBPersonaChatCell] 点赞失败:%@", response.message ?: @"未知错误"); + // TODO: 显示错误提示 + } + }); + }]; } +#pragma mark - Comment View + +- (void)showComment { + // 关闭之前的弹窗 + if (self.popView) { + [self.popView dismiss]; + } + + CGFloat customViewHeight = KB_SCREEN_HEIGHT * 0.8; + KBAICommentView *customView = [[KBAICommentView alloc] + initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; + + // 设置评论视图的人设 ID + // customView.companionId = self.persona.personaId; + + LSTPopView *popView = [LSTPopView initWithCustomView:customView + parentView:nil + popStyle:LSTPopStyleSmoothFromBottom + dismissStyle:LSTDismissStyleSmoothToBottom]; + self.popView = popView; + popView.priority = 1000; + popView.isAvoidKeyboard = NO; + popView.hemStyle = LSTHemStyleBottom; + popView.dragStyle = LSTDragStyleY_Positive; + popView.dragDistance = customViewHeight * 0.5; + popView.sweepStyle = LSTSweepStyleY_Positive; + popView.swipeVelocity = 1600; + popView.sweepDismissStyle = LSTSweepDismissStyleSmooth; + + [popView pop]; +} + + + +- (AiVM *)aiVM{ + if (!_aiVM) { + _aiVM = [[AiVM alloc] init]; + } + return _aiVM; +} @end diff --git a/keyBoard/Class/AiTalk/VM/AiVM.h b/keyBoard/Class/AiTalk/VM/AiVM.h index bb18cf7..cf8e59c 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.h +++ b/keyBoard/Class/AiTalk/VM/AiVM.h @@ -8,6 +8,7 @@ #import #import "KBPersonaPageModel.h" #import "KBChatHistoryPageModel.h" +#import "KBCommentModel.h" NS_ASSUME_NONNULL_BEGIN @@ -105,6 +106,42 @@ typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nu pageSize:(NSInteger)pageSize completion:(void(^)(KBChatHistoryPageModel * _Nullable pageModel, NSError * _Nullable error))completion; +#pragma mark - 评论相关接口 + +/// 发表评论 +/// @param companionId AI 陪聊角色 ID +/// @param content 评论内容 +/// @param parentId 父评论 ID(一级评论传 NULL) +/// @param rootId 根评论 ID(用于标识一级评论) +/// @param completion 完成回调(返回 code 200 表示成功) +- (void)addCommentWithCompanionId:(NSInteger)companionId + content:(NSString *)content + parentId:(nullable NSNumber *)parentId + rootId:(NSInteger)rootId + completion:(void(^)(NSInteger code, NSError * _Nullable error))completion; + +/// 分页查询评论列表 +/// @param companionId AI 陪聊角色 ID +/// @param pageNum 页码(从 1 开始,默认 1) +/// @param pageSize 每页大小(默认 20) +/// @param completion 完成回调(返回评论分页模型) +- (void)fetchCommentsWithCompanionId:(NSInteger)companionId + pageNum:(NSInteger)pageNum + pageSize:(NSInteger)pageSize + completion:(void(^)(KBCommentPageModel * _Nullable pageModel, NSError * _Nullable error))completion; + +/// 点赞/取消点赞评论 +/// @param commentId 评论 ID +/// @param completion 完成回调(返回点赞响应模型) +- (void)likeCommentWithCommentId:(NSInteger)commentId + completion:(void(^)(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error))completion; + +/// 点赞/取消点赞 AI 角色 +/// @param companionId AI 角色 ID +/// @param completion 完成回调(返回点赞响应模型) +- (void)likeCompanionWithCompanionId:(NSInteger)companionId + completion:(void(^)(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/VM/AiVM.m b/keyBoard/Class/AiTalk/VM/AiVM.m index ff42f71..03be096 100644 --- a/keyBoard/Class/AiTalk/VM/AiVM.m +++ b/keyBoard/Class/AiTalk/VM/AiVM.m @@ -8,6 +8,7 @@ #import "AiVM.h" #import "KBAPI.h" #import "KBNetworkManager.h" +#import "KBCommentModel.h" #import @implementation KBAiSyncData @@ -426,4 +427,176 @@ autoShowBusinessError:NO }]; } +#pragma mark - 评论相关接口 + +- (void)addCommentWithCompanionId:(NSInteger)companionId + content:(NSString *)content + parentId:(nullable NSNumber *)parentId + rootId:(NSInteger)rootId + completion:(void (^)(NSInteger, NSError * _Nullable))completion { + if (content.length == 0) { + NSError *error = [NSError errorWithDomain:@"AiVM" + code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"评论内容不能为空"}]; + if (completion) { + completion(-1, error); + } + return; + } + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + params[@"companionId"] = @(companionId); + params[@"content"] = content; + params[@"rootId"] = @(rootId); + if (parentId) { + params[@"parentId"] = parentId; + } + + NSLog(@"[AiVM] /ai-companion/comment/add request: %@", params); + [[KBNetworkManager shared] + POST:@"/ai-companion/comment/add" + jsonBody:[params copy] + headers:nil +autoShowBusinessError:NO + completion:^(NSDictionary *_Nullable json, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + NSLog(@"[AiVM] /ai-companion/comment/add failed: %@", error.localizedDescription ?: @""); + if (completion) { + completion(-1, error); + } + return; + } + + NSLog(@"[AiVM] /ai-companion/comment/add response: %@", json); + NSInteger code = [json[@"code"] integerValue]; + if (completion) { + completion(code, nil); + } + }]; +} + +- (void)fetchCommentsWithCompanionId:(NSInteger)companionId + pageNum:(NSInteger)pageNum + pageSize:(NSInteger)pageSize + completion:(void (^)(KBCommentPageModel * _Nullable, NSError * _Nullable))completion { + NSDictionary *params = @{ + @"companionId": @(companionId), + @"pageNum": @(pageNum > 0 ? pageNum : 1), + @"pageSize": @(pageSize > 0 ? pageSize : 20) + }; + + NSLog(@"[AiVM] /ai-companion/comment/page request: %@", params); + [[KBNetworkManager shared] + POST:@"/ai-companion/comment/page" + jsonBody:params + headers:nil +autoShowBusinessError:NO + completion:^(NSDictionary *_Nullable json, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + NSLog(@"[AiVM] /ai-companion/comment/page failed: %@", error.localizedDescription ?: @""); + if (completion) { + completion(nil, error); + } + return; + } + + NSLog(@"[AiVM] /ai-companion/comment/page response: %@", json); + + NSInteger code = [json[@"code"] integerValue]; + if (code != 0) { + NSString *message = json[@"message"] ?: @"请求失败"; + NSError *bizError = [NSError errorWithDomain:@"AiVM" + code:code + userInfo:@{NSLocalizedDescriptionKey: message}]; + if (completion) { + completion(nil, bizError); + } + return; + } + + id dataObj = json[@"data"]; + if ([dataObj isKindOfClass:[NSDictionary class]]) { + KBCommentPageModel *pageModel = [KBCommentPageModel mj_objectWithKeyValues:dataObj]; + if (completion) { + completion(pageModel, nil); + } + } else { + NSError *parseError = [NSError errorWithDomain:@"AiVM" + code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}]; + if (completion) { + completion(nil, parseError); + } + } + }]; +} + +- (void)likeCommentWithCommentId:(NSInteger)commentId + completion:(void (^)(KBCommentLikeResponse * _Nullable, NSError * _Nullable))completion { + NSDictionary *params = @{ + @"commentId": @(commentId) + }; + + NSLog(@"[AiVM] /ai-companion/comment/like request: %@", params); + [[KBNetworkManager shared] + POST:@"/ai-companion/comment/like" + jsonBody:params + headers:nil +autoShowBusinessError:NO + completion:^(NSDictionary *_Nullable json, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + NSLog(@"[AiVM] /ai-companion/comment/like failed: %@", error.localizedDescription ?: @""); + if (completion) { + completion(nil, error); + } + return; + } + + NSLog(@"[AiVM] /ai-companion/comment/like response: %@", json); + + KBCommentLikeResponse *likeResponse = [KBCommentLikeResponse mj_objectWithKeyValues:json]; + if (completion) { + completion(likeResponse, nil); + } + }]; +} + +- (void)likeCompanionWithCompanionId:(NSInteger)companionId + completion:(void (^)(KBCommentLikeResponse * _Nullable, NSError * _Nullable))completion { + NSDictionary *params = @{ + @"companionId": @(companionId) + }; + + NSLog(@"[AiVM] /ai-companion/like request: %@", params); + [[KBNetworkManager shared] + POST:@"/ai-companion/like" + jsonBody:params + headers:nil +autoShowBusinessError:NO + completion:^(NSDictionary *_Nullable json, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + NSLog(@"[AiVM] /ai-companion/like failed: %@", error.localizedDescription ?: @""); + if (completion) { + completion(nil, error); + } + return; + } + + NSLog(@"[AiVM] /ai-companion/like response: %@", json); + + KBCommentLikeResponse *likeResponse = [KBCommentLikeResponse mj_objectWithKeyValues:json]; + if (completion) { + completion(likeResponse, nil); + } + }]; +} + @end