添加评论
This commit is contained in:
@@ -69,6 +69,13 @@
|
|||||||
046086B92F19239B00757C95 /* AudioCaptureManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A02F19239B00757C95 /* AudioCaptureManager.m */; };
|
046086B92F19239B00757C95 /* AudioCaptureManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A02F19239B00757C95 /* AudioCaptureManager.m */; };
|
||||||
046086BA2F19239B00757C95 /* AudioStreamPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A42F19239B00757C95 /* AudioStreamPlayer.m */; };
|
046086BA2F19239B00757C95 /* AudioStreamPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086A42F19239B00757C95 /* AudioStreamPlayer.m */; };
|
||||||
046086BD2F1A039F00757C95 /* KBAICommentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046086BC2F1A039F00757C95 /* KBAICommentView.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 */; };
|
046131142ECF454500A6FADF /* KBKeyPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131132ECF454500A6FADF /* KBKeyPreviewView.m */; };
|
||||||
0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; };
|
0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; };
|
||||||
0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF22EBB7B850055D639 /* KBDirectionIndicatorView.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 = "<group>"; };
|
046086B02F19239B00757C95 /* TTSServiceClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TTSServiceClient.m; sourceTree = "<group>"; };
|
||||||
046086BB2F1A039F00757C95 /* KBAICommentView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentView.h; sourceTree = "<group>"; };
|
046086BB2F1A039F00757C95 /* KBAICommentView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentView.h; sourceTree = "<group>"; };
|
||||||
046086BC2F1A039F00757C95 /* KBAICommentView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentView.m; sourceTree = "<group>"; };
|
046086BC2F1A039F00757C95 /* KBAICommentView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentView.m; sourceTree = "<group>"; };
|
||||||
|
046086C62F1A092500757C95 /* comments_mock.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = comments_mock.json; sourceTree = "<group>"; };
|
||||||
|
046086C72F1A092500757C95 /* KBAICommentModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentModel.h; sourceTree = "<group>"; };
|
||||||
|
046086C82F1A092500757C95 /* KBAICommentModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentModel.m; sourceTree = "<group>"; };
|
||||||
|
046086C92F1A092500757C95 /* KBAIReplyModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIReplyModel.h; sourceTree = "<group>"; };
|
||||||
|
046086CA2F1A092500757C95 /* KBAIReplyModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIReplyModel.m; sourceTree = "<group>"; };
|
||||||
|
046086CE2F1A093400757C95 /* KBAICommentFooterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentFooterView.h; sourceTree = "<group>"; };
|
||||||
|
046086CF2F1A093400757C95 /* KBAICommentFooterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentFooterView.m; sourceTree = "<group>"; };
|
||||||
|
046086D02F1A093400757C95 /* KBAICommentHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentHeaderView.h; sourceTree = "<group>"; };
|
||||||
|
046086D12F1A093400757C95 /* KBAICommentHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentHeaderView.m; sourceTree = "<group>"; };
|
||||||
|
046086D22F1A093400757C95 /* KBAICommentInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAICommentInputView.h; sourceTree = "<group>"; };
|
||||||
|
046086D32F1A093400757C95 /* KBAICommentInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAICommentInputView.m; sourceTree = "<group>"; };
|
||||||
|
046086D42F1A093400757C95 /* KBAIReplyCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIReplyCell.h; sourceTree = "<group>"; };
|
||||||
|
046086D52F1A093400757C95 /* KBAIReplyCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIReplyCell.m; sourceTree = "<group>"; };
|
||||||
046131122ECF454500A6FADF /* KBKeyPreviewView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyPreviewView.h; sourceTree = "<group>"; };
|
046131122ECF454500A6FADF /* KBKeyPreviewView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyPreviewView.h; sourceTree = "<group>"; };
|
||||||
046131132ECF454500A6FADF /* KBKeyPreviewView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyPreviewView.m; sourceTree = "<group>"; };
|
046131132ECF454500A6FADF /* KBKeyPreviewView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyPreviewView.m; sourceTree = "<group>"; };
|
||||||
0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = "<group>"; };
|
0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = "<group>"; };
|
||||||
@@ -899,6 +919,11 @@
|
|||||||
0460866C2F191A5100757C95 /* M */ = {
|
0460866C2F191A5100757C95 /* M */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
046086C62F1A092500757C95 /* comments_mock.json */,
|
||||||
|
046086C72F1A092500757C95 /* KBAICommentModel.h */,
|
||||||
|
046086C82F1A092500757C95 /* KBAICommentModel.m */,
|
||||||
|
046086C92F1A092500757C95 /* KBAIReplyModel.h */,
|
||||||
|
046086CA2F1A092500757C95 /* KBAIReplyModel.m */,
|
||||||
);
|
);
|
||||||
path = M;
|
path = M;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -914,6 +939,14 @@
|
|||||||
046086992F19238500757C95 /* KBAiWaveformView.m */,
|
046086992F19238500757C95 /* KBAiWaveformView.m */,
|
||||||
046086BB2F1A039F00757C95 /* KBAICommentView.h */,
|
046086BB2F1A039F00757C95 /* KBAICommentView.h */,
|
||||||
046086BC2F1A039F00757C95 /* KBAICommentView.m */,
|
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;
|
path = V;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1974,6 +2007,7 @@
|
|||||||
04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */,
|
04C6EABC2EAF86530089C901 /* LaunchScreen.storyboard in Resources */,
|
||||||
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */,
|
04286A132ECDEBF900CE730C /* KBSkinIconMap.strings in Resources */,
|
||||||
04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */,
|
04C6EABD2EAF86530089C901 /* Main.storyboard in Resources */,
|
||||||
|
046086CB2F1A092500757C95 /* comments_mock.json in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -2119,6 +2153,10 @@
|
|||||||
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */,
|
04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */,
|
||||||
04122F8B2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m in Sources */,
|
04122F8B2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m in Sources */,
|
||||||
04B5A1A22EEFA12300AAAAAA /* KBPayProductModel.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 */,
|
04286A062ECC81B200CE730C /* KBSkinService.m in Sources */,
|
||||||
0479204A2EDDCE25004E8522 /* KBUserSessionManager.m in Sources */,
|
0479204A2EDDCE25004E8522 /* KBUserSessionManager.m in Sources */,
|
||||||
04122FAD2EC73C0100EF7AB3 /* KBVipSubscribeCell.m in Sources */,
|
04122FAD2EC73C0100EF7AB3 /* KBVipSubscribeCell.m in Sources */,
|
||||||
@@ -2132,6 +2170,8 @@
|
|||||||
04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */,
|
04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */,
|
||||||
04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */,
|
04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */,
|
||||||
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */,
|
04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */,
|
||||||
|
046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */,
|
||||||
|
046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */,
|
||||||
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
||||||
047C65582EBCC06D0035E841 /* HomeRankCardCell.m in Sources */,
|
047C65582EBCC06D0035E841 /* HomeRankCardCell.m in Sources */,
|
||||||
04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */,
|
04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */,
|
||||||
|
|||||||
83
keyBoard/Class/AiTalk/M/KBAICommentModel.h
Normal file
83
keyBoard/Class/AiTalk/M/KBAICommentModel.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentModel.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@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<KBAIReplyModel *> *replies;
|
||||||
|
|
||||||
|
/// 当前显示的二级评论(分页展开用)
|
||||||
|
@property(nonatomic, strong) NSMutableArray<KBAIReplyModel *> *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
|
||||||
100
keyBoard/Class/AiTalk/M/KBAICommentModel.m
Normal file
100
keyBoard/Class/AiTalk/M/KBAICommentModel.m
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentModel.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAICommentModel.h"
|
||||||
|
#import "KBAIReplyModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
@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<KBAIReplyModel *> *)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
|
||||||
47
keyBoard/Class/AiTalk/M/KBAIReplyModel.h
Normal file
47
keyBoard/Class/AiTalk/M/KBAIReplyModel.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// KBAIReplyModel.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
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
|
||||||
40
keyBoard/Class/AiTalk/M/KBAIReplyModel.m
Normal file
40
keyBoard/Class/AiTalk/M/KBAIReplyModel.m
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// KBAIReplyModel.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAIReplyModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
@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
|
||||||
207
keyBoard/Class/AiTalk/M/comments_mock.json
Normal file
207
keyBoard/Class/AiTalk/M/comments_mock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
keyBoard/Class/AiTalk/V/KBAICommentFooterView.h
Normal file
25
keyBoard/Class/AiTalk/V/KBAICommentFooterView.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentFooterView.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@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
|
||||||
131
keyBoard/Class/AiTalk/V/KBAICommentFooterView.m
Normal file
131
keyBoard/Class/AiTalk/V/KBAICommentFooterView.m
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentFooterView.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAICommentFooterView.h"
|
||||||
|
#import "KBAICommentModel.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
@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
|
||||||
25
keyBoard/Class/AiTalk/V/KBAICommentHeaderView.h
Normal file
25
keyBoard/Class/AiTalk/V/KBAICommentHeaderView.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentHeaderView.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@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
|
||||||
178
keyBoard/Class/AiTalk/V/KBAICommentHeaderView.m
Normal file
178
keyBoard/Class/AiTalk/V/KBAICommentHeaderView.m
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentHeaderView.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAICommentHeaderView.h"
|
||||||
|
#import "KBAICommentModel.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
|
@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
|
||||||
26
keyBoard/Class/AiTalk/V/KBAICommentInputView.h
Normal file
26
keyBoard/Class/AiTalk/V/KBAICommentInputView.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentInputView.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
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
|
||||||
176
keyBoard/Class/AiTalk/V/KBAICommentInputView.m
Normal file
176
keyBoard/Class/AiTalk/V/KBAICommentInputView.m
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//
|
||||||
|
// KBAICommentInputView.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAICommentInputView.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
@interface KBAICommentInputView () <UITextFieldDelegate>
|
||||||
|
|
||||||
|
@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
|
||||||
@@ -9,8 +9,15 @@
|
|||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 抖音风格评论视图
|
||||||
@interface KBAICommentView : UIView
|
@interface KBAICommentView : UIView
|
||||||
|
|
||||||
|
/// 加载评论数据(从本地 JSON 文件)
|
||||||
|
- (void)loadComments;
|
||||||
|
|
||||||
|
/// 评论总数
|
||||||
|
@property(nonatomic, readonly) NSInteger totalCommentCount;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -6,15 +6,610 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "KBAICommentView.h"
|
#import "KBAICommentView.h"
|
||||||
|
#import "KBAICommentFooterView.h"
|
||||||
|
#import "KBAICommentHeaderView.h"
|
||||||
|
#import "KBAICommentInputView.h"
|
||||||
|
#import "KBAICommentModel.h"
|
||||||
|
#import "KBAIReplyCell.h"
|
||||||
|
#import "KBAIReplyModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
static NSString *const kCommentHeaderIdentifier = @"CommentHeader";
|
||||||
|
static NSString *const kReplyCellIdentifier = @"ReplyCell";
|
||||||
|
static NSString *const kCommentFooterIdentifier = @"CommentFooter";
|
||||||
|
|
||||||
|
/// 每次展开的回复数量
|
||||||
|
static NSInteger const kRepliesLoadCount = 3;
|
||||||
|
|
||||||
|
@interface KBAICommentView () <UITableViewDataSource, UITableViewDelegate>
|
||||||
|
|
||||||
|
@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<KBAICommentModel *> *comments;
|
||||||
|
@property(nonatomic, assign) NSInteger totalCommentCount;
|
||||||
|
|
||||||
|
/// 键盘高度
|
||||||
|
@property(nonatomic, assign) CGFloat keyboardHeight;
|
||||||
|
/// 输入框底部约束
|
||||||
|
@property(nonatomic, strong) MASConstraint *inputBottomConstraint;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation KBAICommentView
|
@implementation KBAICommentView
|
||||||
|
|
||||||
/*
|
#pragma mark - Lifecycle
|
||||||
// Only override drawRect: if you perform custom drawing.
|
|
||||||
// An empty implementation adversely affects performance during animation.
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
- (void)drawRect:(CGRect)rect {
|
self = [super initWithFrame:frame];
|
||||||
// Drawing code
|
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
|
@end
|
||||||
|
|||||||
25
keyBoard/Class/AiTalk/V/KBAIReplyCell.h
Normal file
25
keyBoard/Class/AiTalk/V/KBAIReplyCell.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// KBAIReplyCell.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@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
|
||||||
198
keyBoard/Class/AiTalk/V/KBAIReplyCell.m
Normal file
198
keyBoard/Class/AiTalk/V/KBAIReplyCell.m
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
//
|
||||||
|
// KBAIReplyCell.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAIReplyCell.h"
|
||||||
|
#import "KBAIReplyModel.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
|
@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
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
#import "KBAiMainVC.h"
|
#import "KBAiMainVC.h"
|
||||||
#import "ConversationOrchestrator.h"
|
#import "ConversationOrchestrator.h"
|
||||||
|
#import "KBAICommentView.h"
|
||||||
#import "KBAiChatView.h"
|
#import "KBAiChatView.h"
|
||||||
#import "KBAiRecordButton.h"
|
#import "KBAiRecordButton.h"
|
||||||
#import "KBAICommentView.h"
|
|
||||||
#import "LSTPopView.h"
|
#import "LSTPopView.h"
|
||||||
|
|
||||||
@interface KBAiMainVC () <KBAiRecordButtonDelegate>
|
@interface KBAiMainVC () <KBAiRecordButtonDelegate>
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
@property(nonatomic, strong) KBAiChatView *chatView;
|
@property(nonatomic, strong) KBAiChatView *chatView;
|
||||||
@property(nonatomic, strong) KBAiRecordButton *recordButton;
|
@property(nonatomic, strong) KBAiRecordButton *recordButton;
|
||||||
@property(nonatomic, strong) UILabel *statusLabel;
|
@property(nonatomic, strong) UILabel *statusLabel;
|
||||||
|
@property(nonatomic, strong) UIButton *commentButton;
|
||||||
|
@property(nonatomic, strong) KBAICommentView *commentView;
|
||||||
|
|
||||||
// 核心模块
|
// 核心模块
|
||||||
@property(nonatomic, strong) ConversationOrchestrator *orchestrator;
|
@property(nonatomic, strong) ConversationOrchestrator *orchestrator;
|
||||||
@@ -73,6 +75,23 @@
|
|||||||
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
[self.view addSubview:self.recordButton];
|
[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:@[
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
// 状态标签
|
// 状态标签
|
||||||
@@ -108,6 +127,15 @@
|
|||||||
constraintEqualToAnchor:safeArea.bottomAnchor
|
constraintEqualToAnchor:safeArea.bottomAnchor
|
||||||
constant:-16],
|
constant:-16],
|
||||||
[self.recordButton.heightAnchor constraintEqualToConstant:50],
|
[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],
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +241,10 @@
|
|||||||
#pragma mark - 事件
|
#pragma mark - 事件
|
||||||
- (void)showComment {
|
- (void)showComment {
|
||||||
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
|
CGFloat customViewHeight = KB_SCREEN_HEIGHT * (0.8);
|
||||||
KBAICommentView *customView = [[KBAICommentView alloc] initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)];
|
KBAICommentView *customView = [[KBAICommentView alloc]
|
||||||
LSTPopView *popView = [LSTPopView initWithCustomView:customView
|
initWithFrame:CGRectMake(0, 0, KB_SCREEN_WIDTH, customViewHeight)];
|
||||||
|
LSTPopView *popView =
|
||||||
|
[LSTPopView initWithCustomView:customView
|
||||||
parentView:self.view
|
parentView:self.view
|
||||||
popStyle:LSTPopStyleSmoothFromBottom
|
popStyle:LSTPopStyleSmoothFromBottom
|
||||||
dismissStyle:LSTDismissStyleSmoothToBottom];
|
dismissStyle:LSTDismissStyleSmoothToBottom];
|
||||||
@@ -227,10 +257,28 @@
|
|||||||
popView.swipeVelocity = 1600;
|
popView.swipeVelocity = 1600;
|
||||||
popView.sweepDismissStyle = LSTSweepDismissStyleSmooth;
|
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
|
#pragma mark - UI Updates
|
||||||
|
|
||||||
- (void)updateStatusForState:(ConversationState)state {
|
- (void)updateStatusForState:(ConversationState)state {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
image:@"tab_my"
|
image:@"tab_my"
|
||||||
selectedImg:@"tab_my_selected"];
|
selectedImg:@"tab_my_selected"];
|
||||||
|
|
||||||
self.viewControllers = @[navHome, navShop,aiMainVC, navMy];
|
self.viewControllers = @[aiMainVC,navHome, navShop, navMy];
|
||||||
|
|
||||||
// 测试储存Token
|
// 测试储存Token
|
||||||
// [[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];
|
// [[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil];
|
||||||
|
|||||||
Reference in New Issue
Block a user