diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index a909ccb..d91e7a6 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -69,6 +69,13 @@ 046086B92F19239B00757C95 /* AudioCaptureManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A02F19239B00757C95 /* AudioCaptureManager.m */; }; 046086BA2F19239B00757C95 /* AudioStreamPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A42F19239B00757C95 /* AudioStreamPlayer.m */; }; 046086BD2F1A039F00757C95 /* KBAICommentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086BC2F1A039F00757C95 /* KBAICommentView.m */; }; + 046086CB2F1A092500757C95 /* comments_mock.json in Resources */ = {isa = PBXBuildFile; fileRef = 046086C62F1A092500757C95 /* comments_mock.json */; }; + 046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086CA2F1A092500757C95 /* KBAIReplyModel.m */; }; + 046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086C82F1A092500757C95 /* KBAICommentModel.m */; }; + 046086D62F1A093400757C95 /* KBAICommentInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086D32F1A093400757C95 /* KBAICommentInputView.m */; }; + 046086D72F1A093400757C95 /* KBAICommentFooterView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086CF2F1A093400757C95 /* KBAICommentFooterView.m */; }; + 046086D82F1A093400757C95 /* KBAIReplyCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086D52F1A093400757C95 /* KBAIReplyCell.m */; }; + 046086D92F1A093400757C95 /* KBAICommentHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086D12F1A093400757C95 /* KBAICommentHeaderView.m */; }; 046131142ECF454500A6FADF /* KBKeyPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131132ECF454500A6FADF /* KBKeyPreviewView.m */; }; 0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; }; 0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF22EBB7B850055D639 /* KBDirectionIndicatorView.m */; }; @@ -374,6 +381,19 @@ 046086B02F19239B00757C95 /* TTSServiceClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TTSServiceClient.m; sourceTree = ""; }; 046086BB2F1A039F00757C95 /* KBAICommentView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentView.h; sourceTree = ""; }; 046086BC2F1A039F00757C95 /* KBAICommentView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentView.m; sourceTree = ""; }; + 046086C62F1A092500757C95 /* comments_mock.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = comments_mock.json; sourceTree = ""; }; + 046086C72F1A092500757C95 /* KBAICommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentModel.h; sourceTree = ""; }; + 046086C82F1A092500757C95 /* KBAICommentModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentModel.m; sourceTree = ""; }; + 046086C92F1A092500757C95 /* KBAIReplyModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIReplyModel.h; sourceTree = ""; }; + 046086CA2F1A092500757C95 /* KBAIReplyModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIReplyModel.m; sourceTree = ""; }; + 046086CE2F1A093400757C95 /* KBAICommentFooterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentFooterView.h; sourceTree = ""; }; + 046086CF2F1A093400757C95 /* KBAICommentFooterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentFooterView.m; sourceTree = ""; }; + 046086D02F1A093400757C95 /* KBAICommentHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentHeaderView.h; sourceTree = ""; }; + 046086D12F1A093400757C95 /* KBAICommentHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentHeaderView.m; sourceTree = ""; }; + 046086D22F1A093400757C95 /* KBAICommentInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentInputView.h; sourceTree = ""; }; + 046086D32F1A093400757C95 /* KBAICommentInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentInputView.m; sourceTree = ""; }; + 046086D42F1A093400757C95 /* KBAIReplyCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIReplyCell.h; sourceTree = ""; }; + 046086D52F1A093400757C95 /* KBAIReplyCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIReplyCell.m; sourceTree = ""; }; 046131122ECF454500A6FADF /* KBKeyPreviewView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyPreviewView.h; sourceTree = ""; }; 046131132ECF454500A6FADF /* KBKeyPreviewView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyPreviewView.m; sourceTree = ""; }; 0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = ""; }; @@ -899,6 +919,11 @@ 0460866C2F191A5100757C95 /* M */ = { isa = PBXGroup; children = ( + 046086C62F1A092500757C95 /* comments_mock.json */, + 046086C72F1A092500757C95 /* KBAICommentModel.h */, + 046086C82F1A092500757C95 /* KBAICommentModel.m */, + 046086C92F1A092500757C95 /* KBAIReplyModel.h */, + 046086CA2F1A092500757C95 /* KBAIReplyModel.m */, ); path = M; sourceTree = ""; @@ -914,6 +939,14 @@ 046086992F19238500757C95 /* KBAiWaveformView.m */, 046086BB2F1A039F00757C95 /* KBAICommentView.h */, 046086BC2F1A039F00757C95 /* KBAICommentView.m */, + 046086CE2F1A093400757C95 /* KBAICommentFooterView.h */, + 046086CF2F1A093400757C95 /* KBAICommentFooterView.m */, + 046086D02F1A093400757C95 /* KBAICommentHeaderView.h */, + 046086D12F1A093400757C95 /* KBAICommentHeaderView.m */, + 046086D22F1A093400757C95 /* KBAICommentInputView.h */, + 046086D32F1A093400757C95 /* KBAICommentInputView.m */, + 046086D42F1A093400757C95 /* KBAIReplyCell.h */, + 046086D52F1A093400757C95 /* KBAIReplyCell.m */, ); path = V; sourceTree = ""; @@ -1974,6 +2007,7 @@ 04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */, 04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */, 04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */, + 046086CB2F1A092500757C95 /* comments_mock.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2119,6 +2153,10 @@ 04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */, 04122F8B2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m in Sources */, 04B5A1A22EEFA12300AAAAAA /* KBPayProductModel.m in Sources */, + 046086D62F1A093400757C95 /* KBAICommentInputView.m in Sources */, + 046086D72F1A093400757C95 /* KBAICommentFooterView.m in Sources */, + 046086D82F1A093400757C95 /* KBAIReplyCell.m in Sources */, + 046086D92F1A093400757C95 /* KBAICommentHeaderView.m in Sources */, 04286A062ECC81B200CE730C /* KBSkinService.m in Sources */, 0479204A2EDDCE25004E8522 /* KBUserSessionManager.m in Sources */, 04122FAD2EC73C0100EF7AB3 /* KBVipSubscribeCell.m in Sources */, @@ -2132,6 +2170,8 @@ 04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */, 04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */, 04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */, + 046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */, + 046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */, 04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */, 047C65582EBCC06D0035E841 /* HomeRankCardCell.m in Sources */, 04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */, diff --git a/keyBoard/Class/AiTalk/M/KBAICommentModel.h b/keyBoard/Class/AiTalk/M/KBAICommentModel.h new file mode 100644 index 0000000..1a15a92 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAICommentModel.h @@ -0,0 +1,83 @@ +// +// KBAICommentModel.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +@class KBAIReplyModel; + +NS_ASSUME_NONNULL_BEGIN + +/// Footer 状态枚举 +typedef NS_ENUM(NSInteger, KBAIReplyFooterState) { + KBAIReplyFooterStateHidden, // 无二级评论,不显示 + KBAIReplyFooterStateExpand, // 显示"展开x条回复"(初始折叠状态) + KBAIReplyFooterStateLoadMore, // 显示"加载更多"(已展开部分) + KBAIReplyFooterStateCollapse // 显示"收起"(全部展开后) +}; + +/// 一级评论模型 +@interface KBAICommentModel : NSObject + +/// 评论ID +@property(nonatomic, copy) NSString *commentId; + +/// 用户ID +@property(nonatomic, copy) NSString *userId; + +/// 用户名 +@property(nonatomic, copy) NSString *userName; + +/// 用户头像 +@property(nonatomic, copy) NSString *avatarUrl; + +/// 评论内容 +@property(nonatomic, copy) NSString *content; + +/// 点赞数 +@property(nonatomic, assign) NSInteger likeCount; + +/// 是否已点赞 +@property(nonatomic, assign) BOOL isLiked; + +/// 创建时间(时间戳) +@property(nonatomic, assign) NSTimeInterval createTime; + +#pragma mark - 二级评论相关 + +/// 所有二级评论(从服务器获取的完整数据) +@property(nonatomic, strong) NSArray *replies; + +/// 当前显示的二级评论(分页展开用) +@property(nonatomic, strong) NSMutableArray *displayedReplies; + +/// 二级评论总数 +@property(nonatomic, assign) NSInteger totalReplyCount; + +/// 是否还有更多回复未展开 +@property(nonatomic, assign) BOOL hasMoreReplies; + +/// 回复是否已展开 +@property(nonatomic, assign) BOOL isRepliesExpanded; + +#pragma mark - Helper Methods + +/// 格式化后的时间字符串 +- (NSString *)formattedTime; + +/// 获取当前 Footer 状态 +- (KBAIReplyFooterState)footerState; + +/// 展开更多回复(每次加载指定数量) +/// @param count 本次加载的数量 +- (void)loadMoreReplies:(NSInteger)count; + +/// 收起所有回复 +- (void)collapseReplies; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/M/KBAICommentModel.m b/keyBoard/Class/AiTalk/M/KBAICommentModel.m new file mode 100644 index 0000000..40580f1 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAICommentModel.m @@ -0,0 +1,100 @@ +// +// KBAICommentModel.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAICommentModel.h" +#import "KBAIReplyModel.h" +#import + +@implementation KBAICommentModel + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ + @"commentId" : @"id", + @"userName" : @[ @"userName", @"nickname", @"name" ], + @"avatarUrl" : @[ @"avatarUrl", @"avatar" ], + }; +} + ++ (NSDictionary *)mj_objectClassInArray { + return @{@"replies" : [KBAIReplyModel class]}; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _displayedReplies = [NSMutableArray array]; + _isRepliesExpanded = NO; + _hasMoreReplies = NO; + } + return self; +} + +- (void)setReplies:(NSArray *)replies { + _replies = replies; + _totalReplyCount = replies.count; + _hasMoreReplies = replies.count > 0; +} + +- (NSString *)formattedTime { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.createTime]; + NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date]; + + if (interval < 60) { + return @"刚刚"; + } else if (interval < 3600) { + return [NSString stringWithFormat:@"%.0f分钟前", interval / 60]; + } else if (interval < 86400) { + return [NSString stringWithFormat:@"%.0f小时前", interval / 3600]; + } else if (interval < 86400 * 30) { + return [NSString stringWithFormat:@"%.0f天前", interval / 86400]; + } else { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"MM-dd"; + return [formatter stringFromDate:date]; + } +} + +- (KBAIReplyFooterState)footerState { + // 无二级评论 + if (self.totalReplyCount == 0) { + return KBAIReplyFooterStateHidden; + } + + // 未展开(初始状态) + if (!self.isRepliesExpanded) { + return KBAIReplyFooterStateExpand; + } + + // 已展开但还有更多 + if (self.displayedReplies.count < self.totalReplyCount) { + return KBAIReplyFooterStateLoadMore; + } + + // 全部展开,显示收起 + return KBAIReplyFooterStateCollapse; +} + +- (void)loadMoreReplies:(NSInteger)count { + self.isRepliesExpanded = YES; + + NSInteger currentCount = self.displayedReplies.count; + NSInteger endIndex = MIN(currentCount + count, self.replies.count); + + for (NSInteger i = currentCount; i < endIndex; i++) { + [self.displayedReplies addObject:self.replies[i]]; + } + + self.hasMoreReplies = (self.displayedReplies.count < self.totalReplyCount); +} + +- (void)collapseReplies { + self.isRepliesExpanded = NO; + [self.displayedReplies removeAllObjects]; + self.hasMoreReplies = self.totalReplyCount > 0; +} + +@end diff --git a/keyBoard/Class/AiTalk/M/KBAIReplyModel.h b/keyBoard/Class/AiTalk/M/KBAIReplyModel.h new file mode 100644 index 0000000..0c86f12 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAIReplyModel.h @@ -0,0 +1,47 @@ +// +// KBAIReplyModel.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 二级评论(回复)模型 +@interface KBAIReplyModel : NSObject + +/// 回复ID +@property(nonatomic, copy) NSString *replyId; + +/// 用户ID +@property(nonatomic, copy) NSString *userId; + +/// 用户名 +@property(nonatomic, copy) NSString *userName; + +/// 用户头像 +@property(nonatomic, copy) NSString *avatarUrl; + +/// 回复内容 +@property(nonatomic, copy) NSString *content; + +/// 被回复的用户名(@xxx) +@property(nonatomic, copy, nullable) NSString *replyToUserName; + +/// 点赞数 +@property(nonatomic, assign) NSInteger likeCount; + +/// 是否已点赞 +@property(nonatomic, assign) BOOL isLiked; + +/// 创建时间(时间戳) +@property(nonatomic, assign) NSTimeInterval createTime; + +/// 格式化后的时间字符串 +- (NSString *)formattedTime; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/M/KBAIReplyModel.m b/keyBoard/Class/AiTalk/M/KBAIReplyModel.m new file mode 100644 index 0000000..25093a7 --- /dev/null +++ b/keyBoard/Class/AiTalk/M/KBAIReplyModel.m @@ -0,0 +1,40 @@ +// +// KBAIReplyModel.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAIReplyModel.h" +#import + +@implementation KBAIReplyModel + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ + @"replyId" : @"id", + @"userName" : @[ @"userName", @"nickname", @"name" ], + @"avatarUrl" : @[ @"avatarUrl", @"avatar" ], + }; +} + +- (NSString *)formattedTime { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.createTime]; + NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date]; + + if (interval < 60) { + return @"刚刚"; + } else if (interval < 3600) { + return [NSString stringWithFormat:@"%.0f分钟前", interval / 60]; + } else if (interval < 86400) { + return [NSString stringWithFormat:@"%.0f小时前", interval / 3600]; + } else if (interval < 86400 * 30) { + return [NSString stringWithFormat:@"%.0f天前", interval / 86400]; + } else { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"MM-dd"; + return [formatter stringFromDate:date]; + } +} + +@end diff --git a/keyBoard/Class/AiTalk/M/comments_mock.json b/keyBoard/Class/AiTalk/M/comments_mock.json new file mode 100644 index 0000000..5ea2aaf --- /dev/null +++ b/keyBoard/Class/AiTalk/M/comments_mock.json @@ -0,0 +1,207 @@ +{ + "totalCount": 1258, + "comments": [ + { + "id": "comment_001", + "userId": "user_101", + "userName": "小明同学", + "avatarUrl": "https://picsum.photos/100/100?random=1", + "content": "这个功能太棒了!期待更多更新", + "likeCount": 328, + "isLiked": false, + "createTime": 1737010800, + "replies": [ + { + "id": "reply_001_1", + "userId": "user_102", + "userName": "科技达人", + "avatarUrl": "https://picsum.photos/100/100?random=2", + "content": "同意!这个设计真的很用心", + "likeCount": 45, + "isLiked": false, + "createTime": 1737014400 + }, + { + "id": "reply_001_2", + "userId": "user_103", + "userName": "程序员小李", + "avatarUrl": "https://picsum.photos/100/100?random=3", + "content": "感谢支持,会继续努力的!", + "replyToUserName": "科技达人", + "likeCount": 23, + "isLiked": true, + "createTime": 1737018000 + }, + { + "id": "reply_001_3", + "userId": "user_104", + "userName": "产品经理", + "avatarUrl": "https://picsum.photos/100/100?random=4", + "content": "下个版本会有更多惊喜", + "likeCount": 12, + "isLiked": false, + "createTime": 1737021600 + }, + { + "id": "reply_001_4", + "userId": "user_105", + "userName": "UI设计师", + "avatarUrl": "https://picsum.photos/100/100?random=5", + "content": "细节决定品质", + "likeCount": 8, + "isLiked": false, + "createTime": 1737025200 + }, + { + "id": "reply_001_5", + "userId": "user_106", + "userName": "测试工程师", + "avatarUrl": "https://picsum.photos/100/100?random=6", + "content": "已经测试通过了,稳定性很好", + "likeCount": 5, + "isLiked": false, + "createTime": 1737028800 + }, + { + "id": "reply_001_6", + "userId": "user_107", + "userName": "运营妹子", + "avatarUrl": "https://picsum.photos/100/100?random=7", + "content": "用户反馈都很正面", + "likeCount": 3, + "isLiked": false, + "createTime": 1737032400 + }, + { + "id": "reply_001_7", + "userId": "user_108", + "userName": "后端大神", + "avatarUrl": "https://picsum.photos/100/100?random=8", + "content": "接口性能优化了30%", + "likeCount": 15, + "isLiked": false, + "createTime": 1737036000 + } + ] + }, + { + "id": "comment_002", + "userId": "user_201", + "userName": "热心网友", + "avatarUrl": "https://picsum.photos/100/100?random=10", + "content": "请问这个键盘支持哪些语言输入?想知道有没有日语输入法。", + "likeCount": 89, + "isLiked": false, + "createTime": 1736924400, + "replies": [ + { + "id": "reply_002_1", + "userId": "user_202", + "userName": "官方客服", + "avatarUrl": "https://picsum.photos/100/100?random=11", + "content": "目前支持中文、英文,日语输入法正在开发中,预计下月上线", + "likeCount": 32, + "isLiked": false, + "createTime": 1736928000 + }, + { + "id": "reply_002_2", + "userId": "user_201", + "userName": "热心网友", + "avatarUrl": "https://picsum.photos/100/100?random=10", + "content": "太好了,期待!", + "replyToUserName": "官方客服", + "likeCount": 5, + "isLiked": false, + "createTime": 1736931600 + } + ] + }, + { + "id": "comment_003", + "userId": "user_301", + "userName": "设计爱好者", + "avatarUrl": "https://picsum.photos/100/100?random=12", + "content": "这个UI设计太精美了,配色方案很舒服,是哪位设计师的作品?", + "likeCount": 256, + "isLiked": true, + "createTime": 1736838000, + "replies": [] + }, + { + "id": "comment_004", + "userId": "user_401", + "userName": "效率达人", + "avatarUrl": "https://picsum.photos/100/100?random=13", + "content": "自从用了这个键盘,打字速度提升了一倍!强烈推荐给大家", + "likeCount": 512, + "isLiked": false, + "createTime": 1736751600, + "replies": [ + { + "id": "reply_004_1", + "userId": "user_402", + "userName": "新用户", + "avatarUrl": "https://picsum.photos/100/100?random=14", + "content": "真的吗?那我也要试试", + "likeCount": 18, + "isLiked": false, + "createTime": 1736755200 + } + ] + }, + { + "id": "comment_005", + "userId": "user_501", + "userName": "数码博主", + "avatarUrl": "https://picsum.photos/100/100?random=15", + "content": "做了一期评测视频,这款键盘在同类产品中确实是顶尖水平", + "likeCount": 1024, + "isLiked": false, + "createTime": 1736665200, + "replies": [ + { + "id": "reply_005_1", + "userId": "user_502", + "userName": "粉丝A", + "avatarUrl": "https://picsum.photos/100/100?random=16", + "content": "博主的评测视频太专业了", + "likeCount": 56, + "isLiked": false, + "createTime": 1736668800 + }, + { + "id": "reply_005_2", + "userId": "user_503", + "userName": "粉丝B", + "avatarUrl": "https://picsum.photos/100/100?random=17", + "content": "已经去看了,确实很详细", + "likeCount": 23, + "isLiked": false, + "createTime": 1736672400 + }, + { + "id": "reply_005_3", + "userId": "user_504", + "userName": "路人甲", + "avatarUrl": "https://picsum.photos/100/100?random=18", + "content": "求链接", + "likeCount": 8, + "isLiked": false, + "createTime": 1736676000 + } + ] + }, + { + "id": "comment_006", + "userId": "user_601", + "userName": "老用户", + "avatarUrl": "https://picsum.photos/100/100?random=19", + "content": "用了三年了,一直很稳定,希望能出个暗黑主题", + "likeCount": 178, + "isLiked": false, + "createTime": 1736578800, + "replies": [] + } + ] +} diff --git a/keyBoard/Class/AiTalk/V/KBAICommentFooterView.h b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.h new file mode 100644 index 0000000..ec65b33 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.h @@ -0,0 +1,25 @@ +// +// KBAICommentFooterView.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +@class KBAICommentModel; + +NS_ASSUME_NONNULL_BEGIN + +/// Section Footer - 展开/收起/加载更多按钮 +@interface KBAICommentFooterView : UITableViewHeaderFooterView + +/// 配置 Footer 数据 +- (void)configureWithComment:(KBAICommentModel *)comment; + +/// 操作按钮点击回调 +@property(nonatomic, copy, nullable) void (^onAction)(void); + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m new file mode 100644 index 0000000..5c2d6e1 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentFooterView.m @@ -0,0 +1,131 @@ +// +// KBAICommentFooterView.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAICommentFooterView.h" +#import "KBAICommentModel.h" +#import + +@interface KBAICommentFooterView () + +@property(nonatomic, strong) UIButton *actionButton; +@property(nonatomic, strong) UIView *lineView; + +@end + +@implementation KBAICommentFooterView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithReuseIdentifier:reuseIdentifier]; + if (self) { + [self setupUI]; + } + return self; +} + +#pragma mark - UI Setup + +- (void)setupUI { + self.contentView.backgroundColor = [UIColor whiteColor]; + + [self.contentView addSubview:self.actionButton]; + [self.contentView addSubview:self.lineView]; + + [self.actionButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(68); // 对齐二级评论 + make.centerY.equalTo(self.contentView); + make.height.mas_equalTo(24); + }]; + + [self.lineView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(16); + make.right.equalTo(self.contentView).offset(-16); + make.bottom.equalTo(self.contentView); + make.height.mas_equalTo(0.5); + }]; +} + +#pragma mark - Configuration + +- (void)configureWithComment:(KBAICommentModel *)comment { + KBAIReplyFooterState state = [comment footerState]; + NSString *title = @""; + + switch (state) { + case KBAIReplyFooterStateHidden: { + self.actionButton.hidden = YES; + break; + } + case KBAIReplyFooterStateExpand: { + self.actionButton.hidden = NO; + title = [NSString + stringWithFormat:@"展开%ld条回复", (long)comment.totalReplyCount]; + [self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"] + forState:UIControlStateNormal]; + break; + } + case KBAIReplyFooterStateLoadMore: { + self.actionButton.hidden = NO; + NSInteger remaining = + comment.totalReplyCount - comment.displayedReplies.count; + title = + [NSString stringWithFormat:@"展开更多回复 (%ld条)", (long)remaining]; + [self.actionButton setImage:[UIImage systemImageNamed:@"chevron.down"] + forState:UIControlStateNormal]; + break; + } + case KBAIReplyFooterStateCollapse: { + self.actionButton.hidden = NO; + title = @"收起"; + [self.actionButton setImage:[UIImage systemImageNamed:@"chevron.up"] + forState:UIControlStateNormal]; + break; + } + } + + [self.actionButton setTitle:title forState:UIControlStateNormal]; +} + +#pragma mark - Actions + +- (void)actionButtonTapped { + if (self.onAction) { + self.onAction(); + } +} + +#pragma mark - Lazy Loading + +- (UIButton *)actionButton { + if (!_actionButton) { + _actionButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _actionButton.titleLabel.font = [UIFont systemFontOfSize:13]; + [_actionButton setTitleColor:[UIColor secondaryLabelColor] + forState:UIControlStateNormal]; + _actionButton.tintColor = [UIColor secondaryLabelColor]; + + // 文字在左,图标在右 + _actionButton.semanticContentAttribute = + UISemanticContentAttributeForceLeftToRight; + _actionButton.imageEdgeInsets = UIEdgeInsetsMake(0, 4, 0, -4); + _actionButton.titleEdgeInsets = UIEdgeInsetsMake(0, -4, 0, 4); + + [_actionButton addTarget:self + action:@selector(actionButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + } + return _actionButton; +} + +- (UIView *)lineView { + if (!_lineView) { + _lineView = [[UIView alloc] init]; + _lineView.backgroundColor = [UIColor separatorColor]; + } + return _lineView; +} + +@end diff --git a/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.h b/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.h new file mode 100644 index 0000000..e4ece20 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.h @@ -0,0 +1,25 @@ +// +// KBAICommentHeaderView.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +@class KBAICommentModel; + +NS_ASSUME_NONNULL_BEGIN + +/// 一级评论 Section Header 视图 +@interface KBAICommentHeaderView : UITableViewHeaderFooterView + +/// 配置评论数据 +- (void)configureWithComment:(KBAICommentModel *)comment; + +/// 点赞按钮点击回调 +@property(nonatomic, copy, nullable) void (^onLikeAction)(void); + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.m b/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.m new file mode 100644 index 0000000..331a4a6 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentHeaderView.m @@ -0,0 +1,178 @@ +// +// KBAICommentHeaderView.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAICommentHeaderView.h" +#import "KBAICommentModel.h" +#import +#import + +@interface KBAICommentHeaderView () + +@property(nonatomic, strong) UIImageView *avatarImageView; +@property(nonatomic, strong) UILabel *userNameLabel; +@property(nonatomic, strong) UILabel *timeLabel; +@property(nonatomic, strong) UILabel *contentLabel; +@property(nonatomic, strong) UIButton *likeButton; + +@end + +@implementation KBAICommentHeaderView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithReuseIdentifier:reuseIdentifier]; + if (self) { + [self setupUI]; + } + return self; +} + +#pragma mark - UI Setup + +- (void)setupUI { + self.contentView.backgroundColor = [UIColor whiteColor]; + + [self.contentView addSubview:self.avatarImageView]; + [self.contentView addSubview:self.userNameLabel]; + [self.contentView addSubview:self.timeLabel]; + [self.contentView addSubview:self.contentLabel]; + [self.contentView addSubview:self.likeButton]; + + [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(16); + make.top.equalTo(self.contentView).offset(12); + make.width.height.mas_equalTo(40); + }]; + + [self.userNameLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.avatarImageView.mas_right).offset(12); + make.top.equalTo(self.avatarImageView); + make.right.lessThanOrEqualTo(self.likeButton.mas_left).offset(-10); + }]; + + [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.userNameLabel); + make.top.equalTo(self.userNameLabel.mas_bottom).offset(2); + }]; + + [self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.userNameLabel); + make.top.equalTo(self.timeLabel.mas_bottom).offset(8); + make.right.equalTo(self.contentView).offset(-50); + make.bottom.equalTo(self.contentView).offset(-12); + }]; + + [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.contentView).offset(-16); + make.top.equalTo(self.contentView).offset(12); + make.width.mas_equalTo(50); + make.height.mas_equalTo(40); + }]; +} + +#pragma mark - Configuration + +- (void)configureWithComment:(KBAICommentModel *)comment { + [self.avatarImageView + sd_setImageWithURL:[NSURL URLWithString:comment.avatarUrl] + placeholderImage:[UIImage imageNamed:@"default_avatar"]]; + self.userNameLabel.text = comment.userName; + self.timeLabel.text = [comment formattedTime]; + self.contentLabel.text = comment.content; + + // 点赞按钮 + NSString *likeText = + comment.likeCount > 0 ? [self formatLikeCount:comment.likeCount] : @"赞"; + [self.likeButton setTitle:likeText forState:UIControlStateNormal]; + + UIImage *likeImage = comment.isLiked + ? [UIImage systemImageNamed:@"heart.fill"] + : [UIImage systemImageNamed:@"heart"]; + [self.likeButton setImage:likeImage forState:UIControlStateNormal]; + self.likeButton.tintColor = + comment.isLiked ? [UIColor systemRedColor] : [UIColor grayColor]; +} + +- (NSString *)formatLikeCount:(NSInteger)count { + if (count >= 10000) { + return [NSString stringWithFormat:@"%.1fw", count / 10000.0]; + } else if (count >= 1000) { + return [NSString stringWithFormat:@"%.1fk", count / 1000.0]; + } + return [NSString stringWithFormat:@"%ld", (long)count]; +} + +#pragma mark - Actions + +- (void)likeButtonTapped { + if (self.onLikeAction) { + self.onLikeAction(); + } +} + +#pragma mark - Lazy Loading + +- (UIImageView *)avatarImageView { + if (!_avatarImageView) { + _avatarImageView = [[UIImageView alloc] init]; + _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; + _avatarImageView.layer.cornerRadius = 20; + _avatarImageView.layer.masksToBounds = YES; + _avatarImageView.backgroundColor = [UIColor systemGray5Color]; + } + return _avatarImageView; +} + +- (UILabel *)userNameLabel { + if (!_userNameLabel) { + _userNameLabel = [[UILabel alloc] init]; + _userNameLabel.font = [UIFont systemFontOfSize:14 + weight:UIFontWeightMedium]; + _userNameLabel.textColor = [UIColor labelColor]; + } + return _userNameLabel; +} + +- (UILabel *)timeLabel { + if (!_timeLabel) { + _timeLabel = [[UILabel alloc] init]; + _timeLabel.font = [UIFont systemFontOfSize:12]; + _timeLabel.textColor = [UIColor secondaryLabelColor]; + } + return _timeLabel; +} + +- (UILabel *)contentLabel { + if (!_contentLabel) { + _contentLabel = [[UILabel alloc] init]; + _contentLabel.font = [UIFont systemFontOfSize:15]; + _contentLabel.textColor = [UIColor labelColor]; + _contentLabel.numberOfLines = 0; + } + return _contentLabel; +} + +- (UIButton *)likeButton { + if (!_likeButton) { + _likeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _likeButton.titleLabel.font = [UIFont systemFontOfSize:12]; + [_likeButton setTitleColor:[UIColor grayColor] + forState:UIControlStateNormal]; + [_likeButton setImage:[UIImage systemImageNamed:@"heart"] + forState:UIControlStateNormal]; + _likeButton.tintColor = [UIColor grayColor]; + + // 图片在上,文字在下的布局 + _likeButton.contentHorizontalAlignment = + UIControlContentHorizontalAlignmentCenter; + [_likeButton addTarget:self + action:@selector(likeButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + } + return _likeButton; +} + +@end diff --git a/keyBoard/Class/AiTalk/V/KBAICommentInputView.h b/keyBoard/Class/AiTalk/V/KBAICommentInputView.h new file mode 100644 index 0000000..d4d5368 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentInputView.h @@ -0,0 +1,26 @@ +// +// KBAICommentInputView.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 底部评论输入框 +@interface KBAICommentInputView : UIView + +/// 发送回调,参数为输入的文本 +@property(nonatomic, copy, nullable) void (^onSend)(NSString *text); + +/// 设置占位文字 +@property(nonatomic, copy) NSString *placeholder; + +/// 清空输入框 +- (void)clearText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBAICommentInputView.m b/keyBoard/Class/AiTalk/V/KBAICommentInputView.m new file mode 100644 index 0000000..ade0f06 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAICommentInputView.m @@ -0,0 +1,176 @@ +// +// KBAICommentInputView.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAICommentInputView.h" +#import + +@interface KBAICommentInputView () + +@property(nonatomic, strong) UIView *containerView; +@property(nonatomic, strong) UIImageView *avatarImageView; +@property(nonatomic, strong) UITextField *textField; +@property(nonatomic, strong) UIButton *sendButton; +@property(nonatomic, strong) UIView *topLine; + +@end + +@implementation KBAICommentInputView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self setupUI]; + } + return self; +} + +#pragma mark - UI Setup + +- (void)setupUI { + self.backgroundColor = [UIColor whiteColor]; + + [self addSubview:self.topLine]; + [self addSubview:self.avatarImageView]; + [self addSubview:self.containerView]; + [self.containerView addSubview:self.textField]; + [self addSubview:self.sendButton]; + + [self.topLine mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.top.equalTo(self); + make.height.mas_equalTo(0.5); + }]; + + [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self).offset(12); + make.centerY.equalTo(self); + make.width.height.mas_equalTo(32); + }]; + + [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.avatarImageView.mas_right).offset(10); + make.right.equalTo(self.sendButton.mas_left).offset(-10); + make.centerY.equalTo(self); + make.height.mas_equalTo(36); + }]; + + [self.textField mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.containerView).offset(12); + make.right.equalTo(self.containerView).offset(-12); + make.centerY.equalTo(self.containerView); + }]; + + [self.sendButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self).offset(-12); + make.centerY.equalTo(self); + make.width.mas_equalTo(50); + make.height.mas_equalTo(30); + }]; +} + +#pragma mark - Public Methods + +- (void)setPlaceholder:(NSString *)placeholder { + _placeholder = placeholder; + self.textField.placeholder = placeholder; +} + +- (void)clearText { + self.textField.text = @""; + [self updateSendButtonState]; +} + +#pragma mark - Actions + +- (void)sendButtonTapped { + NSString *text = self.textField.text; + if (text.length > 0 && self.onSend) { + self.onSend(text); + } +} + +- (void)textFieldDidChange:(UITextField *)textField { + [self updateSendButtonState]; +} + +- (void)updateSendButtonState { + BOOL hasText = self.textField.text.length > 0; + self.sendButton.enabled = hasText; + self.sendButton.alpha = hasText ? 1.0 : 0.5; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [self sendButtonTapped]; + return YES; +} + +#pragma mark - Lazy Loading + +- (UIView *)topLine { + if (!_topLine) { + _topLine = [[UIView alloc] init]; + _topLine.backgroundColor = [UIColor separatorColor]; + } + return _topLine; +} + +- (UIImageView *)avatarImageView { + if (!_avatarImageView) { + _avatarImageView = [[UIImageView alloc] init]; + _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; + _avatarImageView.layer.cornerRadius = 16; + _avatarImageView.layer.masksToBounds = YES; + _avatarImageView.backgroundColor = [UIColor systemGray5Color]; + _avatarImageView.image = [UIImage systemImageNamed:@"person.circle.fill"]; + _avatarImageView.tintColor = [UIColor systemGray3Color]; + } + return _avatarImageView; +} + +- (UIView *)containerView { + if (!_containerView) { + _containerView = [[UIView alloc] init]; + _containerView.backgroundColor = [UIColor systemGray6Color]; + _containerView.layer.cornerRadius = 18; + _containerView.layer.masksToBounds = YES; + } + return _containerView; +} + +- (UITextField *)textField { + if (!_textField) { + _textField = [[UITextField alloc] init]; + _textField.placeholder = @"说点什么..."; + _textField.font = [UIFont systemFontOfSize:14]; + _textField.delegate = self; + _textField.returnKeyType = UIReturnKeySend; + [_textField addTarget:self + action:@selector(textFieldDidChange:) + forControlEvents:UIControlEventEditingChanged]; + } + return _textField; +} + +- (UIButton *)sendButton { + if (!_sendButton) { + _sendButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_sendButton setTitle:@"发送" forState:UIControlStateNormal]; + [_sendButton setTitleColor:[UIColor systemBlueColor] + forState:UIControlStateNormal]; + _sendButton.titleLabel.font = [UIFont systemFontOfSize:15 + weight:UIFontWeightMedium]; + _sendButton.enabled = NO; + _sendButton.alpha = 0.5; + [_sendButton addTarget:self + action:@selector(sendButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + } + return _sendButton; +} + +@end diff --git a/keyBoard/Class/AiTalk/V/KBAICommentView.h b/keyBoard/Class/AiTalk/V/KBAICommentView.h index 9fe0534..3e6e2e4 100644 --- a/keyBoard/Class/AiTalk/V/KBAICommentView.h +++ b/keyBoard/Class/AiTalk/V/KBAICommentView.h @@ -9,8 +9,15 @@ NS_ASSUME_NONNULL_BEGIN +/// 抖音风格评论视图 @interface KBAICommentView : UIView +/// 加载评论数据(从本地 JSON 文件) +- (void)loadComments; + +/// 评论总数 +@property(nonatomic, readonly) NSInteger totalCommentCount; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBAICommentView.m b/keyBoard/Class/AiTalk/V/KBAICommentView.m index 2dbb95a..df8eb80 100644 --- a/keyBoard/Class/AiTalk/V/KBAICommentView.m +++ b/keyBoard/Class/AiTalk/V/KBAICommentView.m @@ -6,15 +6,610 @@ // #import "KBAICommentView.h" +#import "KBAICommentFooterView.h" +#import "KBAICommentHeaderView.h" +#import "KBAICommentInputView.h" +#import "KBAICommentModel.h" +#import "KBAIReplyCell.h" +#import "KBAIReplyModel.h" +#import +#import + +static NSString *const kCommentHeaderIdentifier = @"CommentHeader"; +static NSString *const kReplyCellIdentifier = @"ReplyCell"; +static NSString *const kCommentFooterIdentifier = @"CommentFooter"; + +/// 每次展开的回复数量 +static NSInteger const kRepliesLoadCount = 3; + +@interface KBAICommentView () + +@property(nonatomic, strong) UIView *headerView; +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UIButton *closeButton; +@property(nonatomic, strong) UITableView *tableView; +@property(nonatomic, strong) KBAICommentInputView *inputView; + +@property(nonatomic, strong) NSMutableArray *comments; +@property(nonatomic, assign) NSInteger totalCommentCount; + +/// 键盘高度 +@property(nonatomic, assign) CGFloat keyboardHeight; +/// 输入框底部约束 +@property(nonatomic, strong) MASConstraint *inputBottomConstraint; + +@end @implementation KBAICommentView -/* -// Only override drawRect: if you perform custom drawing. -// An empty implementation adversely affects performance during animation. -- (void)drawRect:(CGRect)rect { - // Drawing code +#pragma mark - Lifecycle + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.comments = [NSMutableArray array]; + [self setupUI]; + [self setupKeyboardObservers]; + [self loadComments]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - UI Setup + +- (void)setupUI { + self.backgroundColor = [UIColor whiteColor]; + self.layer.cornerRadius = 12; + self.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner; + self.clipsToBounds = YES; + + [self addSubview:self.headerView]; + [self.headerView addSubview:self.titleLabel]; + [self.headerView addSubview:self.closeButton]; + [self addSubview:self.tableView]; + [self addSubview:self.inputView]; + + [self.headerView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.left.right.equalTo(self); + make.height.mas_equalTo(50); + }]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.headerView); + }]; + + [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.headerView).offset(-16); + make.centerY.equalTo(self.headerView); + make.width.height.mas_equalTo(30); + }]; + + [self.inputView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self); + make.height.mas_equalTo(50); + self.inputBottomConstraint = make.bottom.equalTo(self); + }]; + + [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.headerView.mas_bottom); + make.left.right.equalTo(self); + make.bottom.equalTo(self.inputView.mas_top); + }]; +} + +#pragma mark - Keyboard Observers + +- (void)setupKeyboardObservers { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; +} + +- (void)keyboardWillShow:(NSNotification *)notification { + CGRect keyboardFrame = + [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + NSTimeInterval duration = + [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] + doubleValue]; + + self.keyboardHeight = keyboardFrame.size.height; + + [self.inputBottomConstraint uninstall]; + [self.inputView mas_updateConstraints:^(MASConstraintMaker *make) { + self.inputBottomConstraint = + make.bottom.equalTo(self).offset(-self.keyboardHeight); + }]; + + [UIView animateWithDuration:duration + animations:^{ + [self layoutIfNeeded]; + }]; +} + +- (void)keyboardWillHide:(NSNotification *)notification { + NSTimeInterval duration = + [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] + doubleValue]; + + self.keyboardHeight = 0; + + [self.inputBottomConstraint uninstall]; + [self.inputView mas_updateConstraints:^(MASConstraintMaker *make) { + self.inputBottomConstraint = make.bottom.equalTo(self); + }]; + + [UIView animateWithDuration:duration + animations:^{ + [self layoutIfNeeded]; + }]; +} + +#pragma mark - Data Loading + +- (void)loadComments { + NSString *filePath = [[NSBundle mainBundle] pathForResource:@"comments_mock" + ofType:@"json"]; + if (!filePath) { + NSLog(@"[KBAICommentView] comments_mock.json not found"); + return; + } + + NSData *data = [NSData dataWithContentsOfFile:filePath]; + if (!data) { + NSLog(@"[KBAICommentView] Failed to read comments_mock.json"); + return; + } + + NSError *error; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&error]; + if (error) { + NSLog(@"[KBAICommentView] JSON parse error: %@", error); + return; + } + + self.totalCommentCount = [json[@"totalCount"] integerValue]; + NSArray *commentsArray = json[@"comments"]; + + [self.comments removeAllObjects]; + for (NSDictionary *dict in commentsArray) { + KBAICommentModel *comment = [KBAICommentModel mj_objectWithKeyValues:dict]; + [self.comments addObject:comment]; + } + + [self updateTitle]; + [self.tableView reloadData]; +} + +- (void)updateTitle { + NSString *countText; + if (self.totalCommentCount >= 10000) { + countText = [NSString + stringWithFormat:@"%.1fw条评论", self.totalCommentCount / 10000.0]; + } else { + countText = + [NSString stringWithFormat:@"%ld条评论", (long)self.totalCommentCount]; + } + self.titleLabel.text = countText; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return self.comments.count; +} + +- (NSInteger)tableView:(UITableView *)tableView + numberOfRowsInSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + return comment.displayedReplies.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath { + KBAIReplyCell *cell = + [tableView dequeueReusableCellWithIdentifier:kReplyCellIdentifier + forIndexPath:indexPath]; + + KBAICommentModel *comment = self.comments[indexPath.section]; + KBAIReplyModel *reply = comment.displayedReplies[indexPath.row]; + [cell configureWithReply:reply]; + + __weak typeof(self) weakSelf = self; + cell.onLikeAction = ^{ + // TODO: 处理点赞逻辑 + reply.isLiked = !reply.isLiked; + reply.likeCount += reply.isLiked ? 1 : -1; + [weakSelf.tableView reloadRowsAtIndexPaths:@[ indexPath ] + withRowAnimation:UITableViewRowAnimationNone]; + }; + + return cell; +} + +#pragma mark - UITableViewDelegate + +- (UIView *)tableView:(UITableView *)tableView + viewForHeaderInSection:(NSInteger)section { + KBAICommentHeaderView *header = [tableView + dequeueReusableHeaderFooterViewWithIdentifier:kCommentHeaderIdentifier]; + + KBAICommentModel *comment = self.comments[section]; + [header configureWithComment:comment]; + + __weak typeof(self) weakSelf = self; + header.onLikeAction = ^{ + // TODO: 处理点赞逻辑 + comment.isLiked = !comment.isLiked; + comment.likeCount += comment.isLiked ? 1 : -1; + [weakSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] + withRowAnimation:UITableViewRowAnimationNone]; + }; + + return header; +} + +- (UIView *)tableView:(UITableView *)tableView + viewForFooterInSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + KBAIReplyFooterState state = [comment footerState]; + + // 无二级评论时返回空视图 + if (state == KBAIReplyFooterStateHidden) { + NSLog(@"[KBAICommentView] footer hidden section=%ld", (long)section); + return nil; + } + + KBAICommentFooterView *footer = [tableView + dequeueReusableHeaderFooterViewWithIdentifier:kCommentFooterIdentifier]; + [footer configureWithComment:comment]; + + __weak typeof(self) weakSelf = self; + footer.onAction = ^{ + [weakSelf handleFooterActionForSection:section]; + }; + + return footer; +} + +- (CGFloat)tableView:(UITableView *)tableView + heightForHeaderInSection:(NSInteger)section { + return UITableViewAutomaticDimension; +} + +- (CGFloat)tableView:(UITableView *)tableView + heightForFooterInSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + KBAIReplyFooterState state = [comment footerState]; + + if (state == KBAIReplyFooterStateHidden) { + return CGFLOAT_MIN; + } + return 30; +} + +- (CGFloat)tableView:(UITableView *)tableView + estimatedHeightForHeaderInSection:(NSInteger)section { + return 100; +} + +- (CGFloat)tableView:(UITableView *)tableView + estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 60; +} + +#pragma mark - Footer Actions + +- (void)handleFooterActionForSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + KBAIReplyFooterState state = [comment footerState]; + + switch (state) { + case KBAIReplyFooterStateExpand: + case KBAIReplyFooterStateLoadMore: { + [self loadMoreRepliesForSection:section]; + break; + } + case KBAIReplyFooterStateCollapse: { + [self collapseRepliesForSection:section]; + break; + } + default: + break; + } +} + +- (void)loadMoreRepliesForSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + NSInteger currentCount = comment.displayedReplies.count; + NSDictionary *anchor = [self captureHeaderAnchorForSection:section]; + + NSLog(@"[KBAICommentView] loadMore(before) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f rows=%ld", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + (long)currentCount); + + // 加载更多 + [comment loadMoreReplies:kRepliesLoadCount]; + + // 计算新增的行 + NSInteger newCount = comment.displayedReplies.count; + NSMutableArray *insertIndexPaths = [NSMutableArray array]; + for (NSInteger i = currentCount; i < newCount; i++) { + [insertIndexPaths addObject:[NSIndexPath indexPathForRow:i + inSection:section]]; + } + + // 更新(避免动画导致跳动) + [UIView performWithoutAnimation:^{ + [self.tableView beginUpdates]; + if (insertIndexPaths.count > 0) { + [self.tableView insertRowsAtIndexPaths:insertIndexPaths + withRowAnimation:UITableViewRowAnimationNone]; + } + [self.tableView endUpdates]; + [self.tableView layoutIfNeeded]; + [self restoreHeaderAnchor:anchor]; + }]; + + NSLog(@"[KBAICommentView] loadMore(after) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f rows=%ld", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + (long)comment.displayedReplies.count); + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"[KBAICommentView] loadMore(next) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f viewBoundsH=%.2f", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + self.bounds.size.height); + }); + + // 手动刷新 Footer(避免使用 reloadSections 导致滚动) + KBAICommentFooterView *footerView = + (KBAICommentFooterView *)[self.tableView footerViewForSection:section]; + if (footerView) { + [footerView configureWithComment:comment]; + } +} + +- (void)collapseRepliesForSection:(NSInteger)section { + KBAICommentModel *comment = self.comments[section]; + NSInteger rowCount = comment.displayedReplies.count; + NSDictionary *anchor = [self captureHeaderAnchorForSection:section]; + + NSLog(@"[KBAICommentView] collapse(before) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f rows=%ld", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + (long)rowCount); + + // 计算要删除的行 + NSMutableArray *deleteIndexPaths = [NSMutableArray array]; + for (NSInteger i = 0; i < rowCount; i++) { + [deleteIndexPaths addObject:[NSIndexPath indexPathForRow:i + inSection:section]]; + } + + // 收起回复 + [comment collapseReplies]; + + // 删除行(避免动画导致跳动) + [UIView performWithoutAnimation:^{ + [self.tableView beginUpdates]; + if (deleteIndexPaths.count > 0) { + [self.tableView deleteRowsAtIndexPaths:deleteIndexPaths + withRowAnimation:UITableViewRowAnimationNone]; + } + [self.tableView endUpdates]; + [self.tableView layoutIfNeeded]; + [self restoreHeaderAnchor:anchor]; + }]; + + NSLog(@"[KBAICommentView] collapse(after) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f rows=%ld", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + (long)comment.displayedReplies.count); + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"[KBAICommentView] collapse(next) section=%ld offsetY=%.2f contentSizeH=%.2f boundsH=%.2f viewBoundsH=%.2f", + (long)section, + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height, + self.bounds.size.height); + }); + + // 手动刷新 Footer(避免使用 reloadSections 导致滚动) + KBAICommentFooterView *footerView = + (KBAICommentFooterView *)[self.tableView footerViewForSection:section]; + if (footerView) { + [footerView configureWithComment:comment]; + } +} + +#pragma mark - Actions + +- (void)closeButtonTapped { + // 关闭评论视图(由外部处理) + [[NSNotificationCenter defaultCenter] + postNotificationName:@"KBAICommentViewCloseNotification" + object:nil]; +} + +#pragma mark - Lazy Loading + +- (UIView *)headerView { + if (!_headerView) { + _headerView = [[UIView alloc] init]; + _headerView.backgroundColor = [UIColor whiteColor]; + } + return _headerView; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium]; + _titleLabel.textColor = [UIColor labelColor]; + _titleLabel.text = @"0条评论"; + } + return _titleLabel; +} + +- (UIButton *)closeButton { + if (!_closeButton) { + _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_closeButton setImage:[UIImage systemImageNamed:@"xmark"] + forState:UIControlStateNormal]; + _closeButton.tintColor = [UIColor labelColor]; + [_closeButton addTarget:self + action:@selector(closeButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + } + return _closeButton; +} + +- (UITableView *)tableView { + if (!_tableView) { + _tableView = [[UITableView alloc] initWithFrame:CGRectZero + style:UITableViewStyleGrouped]; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.backgroundColor = [UIColor whiteColor]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableView.estimatedRowHeight = 0; + _tableView.rowHeight = UITableViewAutomaticDimension; + _tableView.estimatedSectionHeaderHeight = 0; + _tableView.estimatedSectionFooterHeight = 0; + _tableView.sectionHeaderHeight = UITableViewAutomaticDimension; + _tableView.sectionFooterHeight = UITableViewAutomaticDimension; + _tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; + + // 注册 Header/Cell/Footer + [_tableView registerClass:[KBAICommentHeaderView class] + forHeaderFooterViewReuseIdentifier:kCommentHeaderIdentifier]; + [_tableView registerClass:[KBAIReplyCell class] + forCellReuseIdentifier:kReplyCellIdentifier]; + [_tableView registerClass:[KBAICommentFooterView class] + forHeaderFooterViewReuseIdentifier:kCommentFooterIdentifier]; + + // 去掉顶部间距 + if (@available(iOS 15.0, *)) { + _tableView.sectionHeaderTopPadding = 0; + } + } + return _tableView; +} + +- (KBAICommentInputView *)inputView { + if (!_inputView) { + _inputView = [[KBAICommentInputView alloc] init]; + _inputView.placeholder = @"说点什么..."; + + __weak typeof(self) weakSelf = self; + _inputView.onSend = ^(NSString *text) { + // TODO: 发送评论 + NSLog(@"[KBAICommentView] Send comment: %@", text); + [weakSelf.inputView clearText]; + }; + } + return _inputView; +} + +#pragma mark - Header Anchor + +- (NSDictionary *)captureHeaderAnchorForSection:(NSInteger)section { + CGRect rect = [self.tableView rectForHeaderInSection:section]; + NSLog(@"[KBAICommentView] anchor capture(header) section=%ld rect=%@ offsetY=%.2f contentSizeH=%.2f boundsH=%.2f", + (long)section, + NSStringFromCGRect(rect), + self.tableView.contentOffset.y, + self.tableView.contentSize.height, + self.tableView.bounds.size.height); + if (CGRectIsEmpty(rect) || CGRectIsNull(rect)) { + NSLog(@"[KBAICommentView] anchor capture(header) empty section=%ld", + (long)section); + return nil; + } + + CGFloat offset = self.tableView.contentOffset.y - rect.origin.y; + return @{ + @"section" : @(section), + @"offset" : @(offset), + @"fallbackOffset" : @(self.tableView.contentOffset.y) + }; +} + +- (void)restoreHeaderAnchor:(NSDictionary *)anchor { + if (!anchor) { + NSLog(@"[KBAICommentView] anchor restore(header) skipped (nil)"); + return; + } + + NSInteger section = [anchor[@"section"] integerValue]; + if (section < 0 || section >= self.comments.count) { + NSLog(@"[KBAICommentView] anchor restore(header) invalid section=%ld", + (long)section); + return; + } + + CGRect rect = [self.tableView rectForHeaderInSection:section]; + NSLog(@"[KBAICommentView] anchor restore(header) section=%ld rect=%@", + (long)section, + NSStringFromCGRect(rect)); + if (CGRectIsEmpty(rect) || CGRectIsNull(rect)) { + NSNumber *fallbackOffset = anchor[@"fallbackOffset"]; + if (!fallbackOffset) { + NSLog(@"[KBAICommentView] anchor restore(header) no fallback section=%ld", + (long)section); + return; + } + NSLog(@"[KBAICommentView] anchor restore(header) fallback section=%ld offsetY=%.2f", + (long)section, + [fallbackOffset doubleValue]); + [self setTableViewOffset:[fallbackOffset doubleValue]]; + return; + } + + CGFloat targetOffsetY = rect.origin.y + [anchor[@"offset"] doubleValue]; + NSLog(@"[KBAICommentView] anchor restore(header) target section=%ld targetY=%.2f", + (long)section, + targetOffsetY); + [self setTableViewOffset:targetOffsetY]; +} + +- (void)setTableViewOffset:(CGFloat)offsetY { + UIEdgeInsets inset = self.tableView.adjustedContentInset; + CGFloat minOffsetY = -inset.top; + CGFloat maxOffsetY = + MAX(minOffsetY, self.tableView.contentSize.height + inset.bottom - + self.tableView.bounds.size.height); + CGFloat targetOffsetY = MIN(MAX(offsetY, minOffsetY), maxOffsetY); + NSLog(@"[KBAICommentView] set offset target=%.2f min=%.2f max=%.2f", + targetOffsetY, + minOffsetY, + maxOffsetY); + [self.tableView setContentOffset:CGPointMake(0, targetOffsetY) + animated:NO]; } -*/ @end diff --git a/keyBoard/Class/AiTalk/V/KBAIReplyCell.h b/keyBoard/Class/AiTalk/V/KBAIReplyCell.h new file mode 100644 index 0000000..a2c3885 --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAIReplyCell.h @@ -0,0 +1,25 @@ +// +// KBAIReplyCell.h +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import + +@class KBAIReplyModel; + +NS_ASSUME_NONNULL_BEGIN + +/// 二级评论 Cell +@interface KBAIReplyCell : UITableViewCell + +/// 配置回复数据 +- (void)configureWithReply:(KBAIReplyModel *)reply; + +/// 点赞按钮点击回调 +@property(nonatomic, copy, nullable) void (^onLikeAction)(void); + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/AiTalk/V/KBAIReplyCell.m b/keyBoard/Class/AiTalk/V/KBAIReplyCell.m new file mode 100644 index 0000000..2cd1b1b --- /dev/null +++ b/keyBoard/Class/AiTalk/V/KBAIReplyCell.m @@ -0,0 +1,198 @@ +// +// KBAIReplyCell.m +// keyBoard +// +// Created by Mac on 2026/1/16. +// + +#import "KBAIReplyCell.h" +#import "KBAIReplyModel.h" +#import +#import + +@interface KBAIReplyCell () + +@property(nonatomic, strong) UIImageView *avatarImageView; +@property(nonatomic, strong) UILabel *contentLabel; +@property(nonatomic, strong) UILabel *timeLabel; +@property(nonatomic, strong) UIButton *likeButton; + +@end + +@implementation KBAIReplyCell + +- (instancetype)initWithStyle:(UITableViewCellStyle)style + reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + self.selectionStyle = UITableViewCellSelectionStyleNone; + self.backgroundColor = [UIColor whiteColor]; + [self setupUI]; + } + return self; +} + +#pragma mark - UI Setup + +- (void)setupUI { + [self.contentView addSubview:self.avatarImageView]; + [self.contentView addSubview:self.contentLabel]; + [self.contentView addSubview:self.timeLabel]; + [self.contentView addSubview:self.likeButton]; + + // 左侧缩进 48pt(对齐一级评论内容) + [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(68); // 16 + 40 + 12 = 68 + make.top.equalTo(self.contentView).offset(8); + make.width.height.mas_equalTo(28); + }]; + + [self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.avatarImageView.mas_right).offset(8); + make.top.equalTo(self.avatarImageView); + make.right.equalTo(self.likeButton.mas_left).offset(-8); + }]; + + [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentLabel); + make.top.equalTo(self.contentLabel.mas_bottom).offset(4); + make.bottom.equalTo(self.contentView).offset(-8); + }]; + + [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.contentView).offset(-16); + make.top.equalTo(self.contentView).offset(8); + make.width.mas_equalTo(40); + make.height.mas_equalTo(30); + }]; +} + +#pragma mark - Configuration + +- (void)configureWithReply:(KBAIReplyModel *)reply { + [self.avatarImageView + sd_setImageWithURL:[NSURL URLWithString:reply.avatarUrl] + placeholderImage:[UIImage imageNamed:@"default_avatar"]]; + + // 构建富文本:用户名(蓝色)+ @回复用户(蓝色)+ 内容 + NSMutableAttributedString *attrText = + [[NSMutableAttributedString alloc] init]; + + // 用户名 + NSDictionary *nameAttrs = @{ + NSFontAttributeName : [UIFont systemFontOfSize:14 + weight:UIFontWeightMedium], + NSForegroundColorAttributeName : [UIColor labelColor] + }; + [attrText appendAttributedString:[[NSAttributedString alloc] + initWithString:reply.userName + attributes:nameAttrs]]; + + // @回复用户 + if (reply.replyToUserName.length > 0) { + NSDictionary *replyAttrs = @{ + NSFontAttributeName : [UIFont systemFontOfSize:14], + NSForegroundColorAttributeName : [UIColor systemBlueColor] + }; + NSString *replyTo = + [NSString stringWithFormat:@" 回复 @%@", reply.replyToUserName]; + [attrText appendAttributedString:[[NSAttributedString alloc] + initWithString:replyTo + attributes:replyAttrs]]; + } + + // 内容 + NSDictionary *contentAttrs = @{ + NSFontAttributeName : [UIFont systemFontOfSize:14], + NSForegroundColorAttributeName : [UIColor labelColor] + }; + NSString *content = [NSString stringWithFormat:@":%@", reply.content]; + [attrText appendAttributedString:[[NSAttributedString alloc] + initWithString:content + attributes:contentAttrs]]; + + self.contentLabel.attributedText = attrText; + + // 时间 + self.timeLabel.text = [reply formattedTime]; + + // 点赞 + NSString *likeText = + reply.likeCount > 0 ? [self formatLikeCount:reply.likeCount] : @""; + [self.likeButton setTitle:likeText forState:UIControlStateNormal]; + + UIImage *likeImage = reply.isLiked ? [UIImage systemImageNamed:@"heart.fill"] + : [UIImage systemImageNamed:@"heart"]; + [self.likeButton setImage:likeImage forState:UIControlStateNormal]; + self.likeButton.tintColor = + reply.isLiked ? [UIColor systemRedColor] : [UIColor grayColor]; +} + +- (NSString *)formatLikeCount:(NSInteger)count { + if (count >= 10000) { + return [NSString stringWithFormat:@"%.1fw", count / 10000.0]; + } else if (count >= 1000) { + return [NSString stringWithFormat:@"%.1fk", count / 1000.0]; + } + return [NSString stringWithFormat:@"%ld", (long)count]; +} + +#pragma mark - Actions + +- (void)likeButtonTapped { + if (self.onLikeAction) { + self.onLikeAction(); + } +} + +#pragma mark - Lazy Loading + +- (UIImageView *)avatarImageView { + if (!_avatarImageView) { + _avatarImageView = [[UIImageView alloc] init]; + _avatarImageView.contentMode = UIViewContentModeScaleAspectFill; + _avatarImageView.layer.cornerRadius = 14; + _avatarImageView.layer.masksToBounds = YES; + _avatarImageView.backgroundColor = [UIColor systemGray5Color]; + } + return _avatarImageView; +} + +- (UILabel *)contentLabel { + if (!_contentLabel) { + _contentLabel = [[UILabel alloc] init]; + _contentLabel.numberOfLines = 0; + } + return _contentLabel; +} + +- (UILabel *)timeLabel { + if (!_timeLabel) { + _timeLabel = [[UILabel alloc] init]; + _timeLabel.font = [UIFont systemFontOfSize:11]; + _timeLabel.textColor = [UIColor secondaryLabelColor]; + } + return _timeLabel; +} + +- (UIButton *)likeButton { + if (!_likeButton) { + _likeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _likeButton.titleLabel.font = [UIFont systemFontOfSize:11]; + [_likeButton setTitleColor:[UIColor grayColor] + forState:UIControlStateNormal]; + [_likeButton setImage:[UIImage systemImageNamed:@"heart"] + forState:UIControlStateNormal]; + _likeButton.tintColor = [UIColor grayColor]; + [_likeButton addTarget:self + action:@selector(likeButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + + // 设置图片和文字间距 + _likeButton.imageEdgeInsets = UIEdgeInsetsMake(0, -2, 0, 2); + _likeButton.titleEdgeInsets = UIEdgeInsetsMake(0, 2, 0, -2); + } + return _likeButton; +} + +@end diff --git a/keyBoard/Class/AiTalk/VC/KBAiMainVC.m b/keyBoard/Class/AiTalk/VC/KBAiMainVC.m index 4633713..071c0d0 100644 --- a/keyBoard/Class/AiTalk/VC/KBAiMainVC.m +++ b/keyBoard/Class/AiTalk/VC/KBAiMainVC.m @@ -7,18 +7,20 @@ #import "KBAiMainVC.h" #import "ConversationOrchestrator.h" +#import "KBAICommentView.h" #import "KBAiChatView.h" #import "KBAiRecordButton.h" -#import "KBAICommentView.h" #import "LSTPopView.h" @interface KBAiMainVC () -@property (nonatomic,weak) LSTPopView *popView; +@property(nonatomic, weak) LSTPopView *popView; // UI @property(nonatomic, strong) KBAiChatView *chatView; @property(nonatomic, strong) KBAiRecordButton *recordButton; @property(nonatomic, strong) UILabel *statusLabel; +@property(nonatomic, strong) UIButton *commentButton; +@property(nonatomic, strong) KBAICommentView *commentView; // 核心模块 @property(nonatomic, strong) ConversationOrchestrator *orchestrator; @@ -73,6 +75,23 @@ self.recordButton.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.recordButton]; + // 评论按钮(聊天视图右侧居中) + self.commentButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.commentButton setImage:[UIImage systemImageNamed:@"bubble.right.fill"] + forState:UIControlStateNormal]; + self.commentButton.tintColor = [UIColor whiteColor]; + self.commentButton.backgroundColor = [UIColor systemBlueColor]; + self.commentButton.layer.cornerRadius = 25; + self.commentButton.layer.shadowColor = [UIColor blackColor].CGColor; + self.commentButton.layer.shadowOffset = CGSizeMake(0, 2); + self.commentButton.layer.shadowOpacity = 0.3; + self.commentButton.layer.shadowRadius = 4; + self.commentButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.commentButton addTarget:self + action:@selector(showComment) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.commentButton]; + // 布局约束 [NSLayoutConstraint activateConstraints:@[ // 状态标签 @@ -108,6 +127,15 @@ constraintEqualToAnchor:safeArea.bottomAnchor constant:-16], [self.recordButton.heightAnchor constraintEqualToConstant:50], + + // 评论按钮(右侧居中) + [self.commentButton.trailingAnchor + constraintEqualToAnchor:safeArea.trailingAnchor + constant:-16], + [self.commentButton.centerYAnchor + constraintEqualToAnchor:self.chatView.centerYAnchor], + [self.commentButton.widthAnchor constraintEqualToConstant:50], + [self.commentButton.heightAnchor constraintEqualToConstant:50], ]]; } @@ -211,24 +239,44 @@ } #pragma mark - 事件 -- (void)showComment{ - CGFloat customViewHeight = KB_SCREEN_HEIGHT*(0.8); - KBAICommentView *customView = [[KBAICommentView alloc] initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; - LSTPopView *popView = [LSTPopView initWithCustomView:customView - parentView:self.view - popStyle:LSTPopStyleSmoothFromBottom - dismissStyle:LSTDismissStyleSmoothToBottom]; - self.popView = popView; - popView.priority = 1000; - popView.hemStyle = LSTHemStyleBottom; - popView.dragStyle = LSTDragStyleY_Positive; - popView.dragDistance = customViewHeight*0.5; - popView.sweepStyle = LSTSweepStyleY_Positive; - popView.swipeVelocity = 1600; - popView.sweepDismissStyle = LSTSweepDismissStyleSmooth; +- (void)showComment { + CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8); + KBAICommentView *customView = [[KBAICommentView alloc] + initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)]; + LSTPopView *popView = + [LSTPopView initWithCustomView:customView + parentView:self.view + popStyle:LSTPopStyleSmoothFromBottom + dismissStyle:LSTDismissStyleSmoothToBottom]; + self.popView = popView; + popView.priority = 1000; + popView.hemStyle = LSTHemStyleBottom; + popView.dragStyle = LSTDragStyleY_Positive; + popView.dragDistance = customViewHeight * 0.5; + popView.sweepStyle = LSTSweepStyleY_Positive; + popView.swipeVelocity = 1600; + popView.sweepDismissStyle = LSTSweepDismissStyleSmooth; - - [popView pop]; + [popView pop]; +} + +- (void)showCommentDirectly { + if (self.commentView.superview) { + [self.view bringSubviewToFront:self.commentView]; + return; + } + + CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8); + KBAICommentView *customView = [[KBAICommentView alloc] initWithFrame:CGRectZero]; + customView.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:customView]; + [NSLayoutConstraint activateConstraints:@[ + [customView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [customView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [customView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [customView.heightAnchor constraintEqualToConstant:customViewHeight], + ]]; + self.commentView = customView; } #pragma mark - UI Updates diff --git a/keyBoard/Class/Base/VC/BaseTabBarController.m b/keyBoard/Class/Base/VC/BaseTabBarController.m index 34bd280..2c84301 100644 --- a/keyBoard/Class/Base/VC/BaseTabBarController.m +++ b/keyBoard/Class/Base/VC/BaseTabBarController.m @@ -60,7 +60,7 @@ image:@"tab_my" selectedImg:@"tab_my_selected"]; - self.viewControllers = @[navHome, navShop,aiMainVC, navMy]; + self.viewControllers = @[aiMainVC,navHome, navShop, navMy]; // 测试储存Token // [[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];