新增接口,界面
This commit is contained in:
@@ -136,6 +136,10 @@
|
|||||||
04890A042EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A012EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m */; };
|
04890A042EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A012EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m */; };
|
||||||
04890A052EC0BBBB00FABA60 /* KBCategoryTitleImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */; };
|
04890A052EC0BBBB00FABA60 /* KBCategoryTitleImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */; };
|
||||||
04890B122EC2F00000FABA60 /* KBMyHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890B112EC2F00000FABA60 /* KBMyHeaderView.m */; };
|
04890B122EC2F00000FABA60 /* KBMyHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890B112EC2F00000FABA60 /* KBMyHeaderView.m */; };
|
||||||
|
048FFD0B2F273BFC005D62AE /* KBAIHomeVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD0A2F273BFC005D62AE /* KBAIHomeVC.m */; };
|
||||||
|
048FFD102F27432D005D62AE /* KBPersonaModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD0D2F27432D005D62AE /* KBPersonaModel.m */; };
|
||||||
|
048FFD112F27432D005D62AE /* KBPersonaPageModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD0F2F27432D005D62AE /* KBPersonaPageModel.m */; };
|
||||||
|
048FFD142F274342005D62AE /* KBPersonaChatCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048FFD132F274342005D62AE /* KBPersonaChatCell.m */; };
|
||||||
0498BD622EDFFC12006CC1D5 /* KBMyVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */; };
|
0498BD622EDFFC12006CC1D5 /* KBMyVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD612EDFFC12006CC1D5 /* KBMyVM.m */; };
|
||||||
0498BD652EE0116D006CC1D5 /* KBEmailLoginVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD642EE0116D006CC1D5 /* KBEmailLoginVC.m */; };
|
0498BD652EE0116D006CC1D5 /* KBEmailLoginVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD642EE0116D006CC1D5 /* KBEmailLoginVC.m */; };
|
||||||
0498BD682EE01180006CC1D5 /* KBEmailRegistVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD672EE01180006CC1D5 /* KBEmailRegistVC.m */; };
|
0498BD682EE01180006CC1D5 /* KBEmailRegistVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0498BD672EE01180006CC1D5 /* KBEmailRegistVC.m */; };
|
||||||
@@ -518,6 +522,14 @@
|
|||||||
04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCategoryTitleImageView.m; sourceTree = "<group>"; };
|
04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCategoryTitleImageView.m; sourceTree = "<group>"; };
|
||||||
04890B102EC2F00000FABA60 /* KBMyHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyHeaderView.h; sourceTree = "<group>"; };
|
04890B102EC2F00000FABA60 /* KBMyHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyHeaderView.h; sourceTree = "<group>"; };
|
||||||
04890B112EC2F00000FABA60 /* KBMyHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyHeaderView.m; sourceTree = "<group>"; };
|
04890B112EC2F00000FABA60 /* KBMyHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyHeaderView.m; sourceTree = "<group>"; };
|
||||||
|
048FFD092F273BFC005D62AE /* KBAIHomeVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAIHomeVC.h; sourceTree = "<group>"; };
|
||||||
|
048FFD0A2F273BFC005D62AE /* KBAIHomeVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAIHomeVC.m; sourceTree = "<group>"; };
|
||||||
|
048FFD0C2F27432D005D62AE /* KBPersonaModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPersonaModel.h; sourceTree = "<group>"; };
|
||||||
|
048FFD0D2F27432D005D62AE /* KBPersonaModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPersonaModel.m; sourceTree = "<group>"; };
|
||||||
|
048FFD0E2F27432D005D62AE /* KBPersonaPageModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPersonaPageModel.h; sourceTree = "<group>"; };
|
||||||
|
048FFD0F2F27432D005D62AE /* KBPersonaPageModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPersonaPageModel.m; sourceTree = "<group>"; };
|
||||||
|
048FFD122F274342005D62AE /* KBPersonaChatCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPersonaChatCell.h; sourceTree = "<group>"; };
|
||||||
|
048FFD132F274342005D62AE /* KBPersonaChatCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPersonaChatCell.m; sourceTree = "<group>"; };
|
||||||
0498BD5E2EDF2157006CC1D5 /* KBBizCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBizCode.h; sourceTree = "<group>"; };
|
0498BD5E2EDF2157006CC1D5 /* KBBizCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBizCode.h; sourceTree = "<group>"; };
|
||||||
0498BD602EDFFC12006CC1D5 /* KBMyVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyVM.h; sourceTree = "<group>"; };
|
0498BD602EDFFC12006CC1D5 /* KBMyVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBMyVM.h; sourceTree = "<group>"; };
|
||||||
0498BD612EDFFC12006CC1D5 /* KBMyVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyVM.m; sourceTree = "<group>"; };
|
0498BD612EDFFC12006CC1D5 /* KBMyVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBMyVM.m; sourceTree = "<group>"; };
|
||||||
@@ -965,6 +977,10 @@
|
|||||||
046086CA2F1A092500757C95 /* KBAIReplyModel.m */,
|
046086CA2F1A092500757C95 /* KBAIReplyModel.m */,
|
||||||
04E039502F2387D2002CA5A0 /* KBAiChatMessage.h */,
|
04E039502F2387D2002CA5A0 /* KBAiChatMessage.h */,
|
||||||
04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */,
|
04E039512F2387D2002CA5A0 /* KBAiChatMessage.m */,
|
||||||
|
048FFD0C2F27432D005D62AE /* KBPersonaModel.h */,
|
||||||
|
048FFD0D2F27432D005D62AE /* KBPersonaModel.m */,
|
||||||
|
048FFD0E2F27432D005D62AE /* KBPersonaPageModel.h */,
|
||||||
|
048FFD0F2F27432D005D62AE /* KBPersonaPageModel.m */,
|
||||||
);
|
);
|
||||||
path = M;
|
path = M;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -997,6 +1013,8 @@
|
|||||||
04E039482F236E75002CA5A0 /* KBChatTimeCell.m */,
|
04E039482F236E75002CA5A0 /* KBChatTimeCell.m */,
|
||||||
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */,
|
04E039492F236E75002CA5A0 /* KBChatUserMessageCell.h */,
|
||||||
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */,
|
04E0394A2F236E75002CA5A0 /* KBChatUserMessageCell.m */,
|
||||||
|
048FFD122F274342005D62AE /* KBPersonaChatCell.h */,
|
||||||
|
048FFD132F274342005D62AE /* KBPersonaChatCell.m */,
|
||||||
);
|
);
|
||||||
path = V;
|
path = V;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1006,6 +1024,8 @@
|
|||||||
children = (
|
children = (
|
||||||
046086712F191B6900757C95 /* KBAiMainVC.h */,
|
046086712F191B6900757C95 /* KBAiMainVC.h */,
|
||||||
046086722F191B6900757C95 /* KBAiMainVC.m */,
|
046086722F191B6900757C95 /* KBAiMainVC.m */,
|
||||||
|
048FFD092F273BFC005D62AE /* KBAIHomeVC.h */,
|
||||||
|
048FFD0A2F273BFC005D62AE /* KBAIHomeVC.m */,
|
||||||
);
|
);
|
||||||
path = VC;
|
path = VC;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2241,6 +2261,7 @@
|
|||||||
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 */,
|
||||||
|
048FFD0B2F273BFC005D62AE /* KBAIHomeVC.m in Sources */,
|
||||||
046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */,
|
046086CC2F1A092500757C95 /* KBAIReplyModel.m in Sources */,
|
||||||
046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */,
|
046086CD2F1A092500757C95 /* KBAICommentModel.m in Sources */,
|
||||||
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */,
|
||||||
@@ -2289,6 +2310,7 @@
|
|||||||
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */,
|
04E038E92F20E877002CA5A0 /* DeepgramStreamingManager.m in Sources */,
|
||||||
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */,
|
048908E32EBF821700FABA60 /* KBSkinDetailVC.m in Sources */,
|
||||||
0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */,
|
0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */,
|
||||||
|
048FFD142F274342005D62AE /* KBPersonaChatCell.m in Sources */,
|
||||||
049FB21A2EC20A9E00FAB05D /* KBMyKeyBoardVC.m in Sources */,
|
049FB21A2EC20A9E00FAB05D /* KBMyKeyBoardVC.m in Sources */,
|
||||||
049FB22C2EC31F8800FAB05D /* KBGenderPickerPopView.m in Sources */,
|
049FB22C2EC31F8800FAB05D /* KBGenderPickerPopView.m in Sources */,
|
||||||
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */,
|
048908D22EBF611D00FABA60 /* KBHistoryMoreCell.m in Sources */,
|
||||||
@@ -2330,6 +2352,8 @@
|
|||||||
04FC970E2EB334F8007BD342 /* UIImageView+KBWebImage.m in Sources */,
|
04FC970E2EB334F8007BD342 /* UIImageView+KBWebImage.m in Sources */,
|
||||||
049FB2232EC311F900FAB05D /* KBPersonInfoVC.m in Sources */,
|
049FB2232EC311F900FAB05D /* KBPersonInfoVC.m in Sources */,
|
||||||
04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */,
|
04E038EF2F21F0EC002CA5A0 /* AiVM.m in Sources */,
|
||||||
|
048FFD102F27432D005D62AE /* KBPersonaModel.m in Sources */,
|
||||||
|
048FFD112F27432D005D62AE /* KBPersonaPageModel.m in Sources */,
|
||||||
0498BD6B2EE025FC006CC1D5 /* KBForgetPwdVC.m in Sources */,
|
0498BD6B2EE025FC006CC1D5 /* KBForgetPwdVC.m in Sources */,
|
||||||
046086B12F19239B00757C95 /* SubtitleSync.m in Sources */,
|
046086B12F19239B00757C95 /* SubtitleSync.m in Sources */,
|
||||||
046086B22F19239B00757C95 /* TTSServiceClient.m in Sources */,
|
046086B22F19239B00757C95 /* TTSServiceClient.m in Sources */,
|
||||||
|
|||||||
96
keyBoard/Class/AiTalk/M/KBPersonaModel.h
Normal file
96
keyBoard/Class/AiTalk/M/KBPersonaModel.h
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaModel.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 人设状态
|
||||||
|
typedef NS_ENUM(NSInteger, KBPersonaStatus) {
|
||||||
|
KBPersonaStatusDisabled = 0, // 禁用
|
||||||
|
KBPersonaStatusEnabled = 1 // 启用
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 人设可见性
|
||||||
|
typedef NS_ENUM(NSInteger, KBPersonaVisibility) {
|
||||||
|
KBPersonaVisibilityPrivate = 0, // 私有
|
||||||
|
KBPersonaVisibilityPublic = 1 // 公开
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 人设模型
|
||||||
|
@interface KBPersonaModel : NSObject
|
||||||
|
|
||||||
|
/// 人设 ID
|
||||||
|
@property (nonatomic, assign) NSInteger personaId;
|
||||||
|
|
||||||
|
/// 人设名称
|
||||||
|
@property (nonatomic, copy) NSString *name;
|
||||||
|
|
||||||
|
/// 头像 URL
|
||||||
|
@property (nonatomic, copy) NSString *avatarUrl;
|
||||||
|
|
||||||
|
/// 封面图 URL
|
||||||
|
@property (nonatomic, copy) NSString *coverImageUrl;
|
||||||
|
|
||||||
|
/// 性别
|
||||||
|
@property (nonatomic, copy) NSString *gender;
|
||||||
|
|
||||||
|
/// 年龄范围
|
||||||
|
@property (nonatomic, copy) NSString *ageRange;
|
||||||
|
|
||||||
|
/// 简短描述
|
||||||
|
@property (nonatomic, copy) NSString *shortDesc;
|
||||||
|
|
||||||
|
/// 介绍文本
|
||||||
|
@property (nonatomic, copy) NSString *introText;
|
||||||
|
|
||||||
|
/// 性格标签(逗号分隔的字符串)
|
||||||
|
@property (nonatomic, copy) NSString *personalityTags;
|
||||||
|
|
||||||
|
/// 说话风格
|
||||||
|
@property (nonatomic, copy) NSString *speakingStyle;
|
||||||
|
|
||||||
|
/// 系统提示词
|
||||||
|
@property (nonatomic, copy) NSString *systemPrompt;
|
||||||
|
|
||||||
|
/// 状态(0-禁用,1-启用)
|
||||||
|
@property (nonatomic, assign) KBPersonaStatus status;
|
||||||
|
|
||||||
|
/// 可见性(0-私有,1-公开)
|
||||||
|
@property (nonatomic, assign) KBPersonaVisibility visibility;
|
||||||
|
|
||||||
|
/// 排序顺序
|
||||||
|
@property (nonatomic, assign) NSInteger sortOrder;
|
||||||
|
|
||||||
|
/// 热度分数
|
||||||
|
@property (nonatomic, assign) NSInteger popularityScore;
|
||||||
|
|
||||||
|
/// 创建时间
|
||||||
|
@property (nonatomic, copy) NSString *createdAt;
|
||||||
|
|
||||||
|
/// 更新时间
|
||||||
|
@property (nonatomic, copy) NSString *updatedAt;
|
||||||
|
|
||||||
|
#pragma mark - 扩展属性
|
||||||
|
|
||||||
|
/// 性格标签数组(从 personalityTags 解析)
|
||||||
|
@property (nonatomic, strong, readonly) NSArray<NSString *> *tagsArray;
|
||||||
|
|
||||||
|
/// 是否启用
|
||||||
|
@property (nonatomic, assign, readonly) BOOL isEnabled;
|
||||||
|
|
||||||
|
/// 是否公开
|
||||||
|
@property (nonatomic, assign, readonly) BOOL isPublic;
|
||||||
|
|
||||||
|
/// 开场白
|
||||||
|
@property (nonatomic, copy) NSString *prologue;
|
||||||
|
/// 开场白URL
|
||||||
|
@property (nonatomic, copy) NSString *prologueAudio;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
68
keyBoard/Class/AiTalk/M/KBPersonaModel.m
Normal file
68
keyBoard/Class/AiTalk/M/KBPersonaModel.m
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaModel.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBPersonaModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
@implementation KBPersonaModel
|
||||||
|
|
||||||
|
#pragma mark - MJExtension 配置
|
||||||
|
|
||||||
|
// 字段映射
|
||||||
|
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
|
||||||
|
return @{
|
||||||
|
@"personaId": @"id"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换完成后的处理
|
||||||
|
- (void)mj_keyValuesDidFinishConvertingToObject {
|
||||||
|
// 容错处理:如果字段为 null,设置默认值
|
||||||
|
if (!self.name) self.name = @"";
|
||||||
|
if (!self.avatarUrl) self.avatarUrl = @"";
|
||||||
|
if (!self.coverImageUrl) self.coverImageUrl = @"";
|
||||||
|
if (!self.gender) self.gender = @"";
|
||||||
|
if (!self.ageRange) self.ageRange = @"";
|
||||||
|
if (!self.shortDesc) self.shortDesc = @"";
|
||||||
|
if (!self.introText) self.introText = @"";
|
||||||
|
if (!self.personalityTags) self.personalityTags = @"";
|
||||||
|
if (!self.speakingStyle) self.speakingStyle = @"";
|
||||||
|
if (!self.systemPrompt) self.systemPrompt = @"";
|
||||||
|
if (!self.createdAt) self.createdAt = @"";
|
||||||
|
if (!self.updatedAt) self.updatedAt = @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 扩展属性
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)tagsArray {
|
||||||
|
if (self.personalityTags.length == 0) {
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
|
// 去除空格并分割
|
||||||
|
NSString *trimmed = [self.personalityTags stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
|
NSArray *tags = [trimmed componentsSeparatedByString:@","];
|
||||||
|
|
||||||
|
// 过滤空字符串
|
||||||
|
NSMutableArray *result = [NSMutableArray array];
|
||||||
|
for (NSString *tag in tags) {
|
||||||
|
NSString *trimmedTag = [tag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
|
if (trimmedTag.length > 0) {
|
||||||
|
[result addObject:trimmedTag];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEnabled {
|
||||||
|
return self.status == KBPersonaStatusEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isPublic {
|
||||||
|
return self.visibility == KBPersonaVisibilityPublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
45
keyBoard/Class/AiTalk/M/KBPersonaPageModel.h
Normal file
45
keyBoard/Class/AiTalk/M/KBPersonaPageModel.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaPageModel.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "KBPersonaModel.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 排序规则
|
||||||
|
@interface KBOrderRule : NSObject
|
||||||
|
@property (nonatomic, copy) NSString *column; // 排序字段
|
||||||
|
@property (nonatomic, assign) BOOL asc; // 是否升序
|
||||||
|
@end
|
||||||
|
|
||||||
|
/// 分页数据模型
|
||||||
|
@interface KBPersonaPageModel : NSObject
|
||||||
|
|
||||||
|
/// 人设列表
|
||||||
|
@property (nonatomic, strong) NSArray<KBPersonaModel *> *records;
|
||||||
|
|
||||||
|
/// 总记录数
|
||||||
|
@property (nonatomic, assign) NSInteger total;
|
||||||
|
|
||||||
|
/// 每页大小
|
||||||
|
@property (nonatomic, assign) NSInteger size;
|
||||||
|
|
||||||
|
/// 当前页码
|
||||||
|
@property (nonatomic, assign) NSInteger current;
|
||||||
|
|
||||||
|
/// 排序规则
|
||||||
|
@property (nonatomic, strong, nullable) NSArray<KBOrderRule *> *orders;
|
||||||
|
|
||||||
|
/// 总页数
|
||||||
|
@property (nonatomic, assign) NSInteger pages;
|
||||||
|
|
||||||
|
/// 是否还有更多数据
|
||||||
|
@property (nonatomic, assign, readonly) BOOL hasMore;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
39
keyBoard/Class/AiTalk/M/KBPersonaPageModel.m
Normal file
39
keyBoard/Class/AiTalk/M/KBPersonaPageModel.m
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaPageModel.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBPersonaPageModel.h"
|
||||||
|
#import <MJExtension/MJExtension.h>
|
||||||
|
|
||||||
|
@implementation KBOrderRule
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBPersonaPageModel
|
||||||
|
|
||||||
|
#pragma mark - MJExtension 配置
|
||||||
|
|
||||||
|
// 数组内元素类型
|
||||||
|
+ (NSDictionary *)mj_objectClassInArray {
|
||||||
|
return @{
|
||||||
|
@"records": [KBPersonaModel class],
|
||||||
|
@"orders": [KBOrderRule class]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换完成后的处理
|
||||||
|
- (void)mj_keyValuesDidFinishConvertingToObject {
|
||||||
|
if (!self.records) {
|
||||||
|
self.records = @[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 扩展属性
|
||||||
|
|
||||||
|
- (BOOL)hasMore {
|
||||||
|
return self.current < self.pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
24
keyBoard/Class/AiTalk/V/KBPersonaChatCell.h
Normal file
24
keyBoard/Class/AiTalk/V/KBPersonaChatCell.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaChatCell.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import "KBPersonaModel.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 人设聊天 Cell
|
||||||
|
@interface KBPersonaChatCell : UICollectionViewCell
|
||||||
|
|
||||||
|
/// 人设数据
|
||||||
|
@property (nonatomic, strong) KBPersonaModel *persona;
|
||||||
|
|
||||||
|
/// 预加载数据
|
||||||
|
- (void)preloadDataIfNeeded;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
224
keyBoard/Class/AiTalk/V/KBPersonaChatCell.m
Normal file
224
keyBoard/Class/AiTalk/V/KBPersonaChatCell.m
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
//
|
||||||
|
// KBPersonaChatCell.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Kiro on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBPersonaChatCell.h"
|
||||||
|
#import "KBChatTableView.h"
|
||||||
|
#import "KBAiChatMessage.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
|
||||||
|
@interface KBPersonaChatCell () <UITableViewDelegate, UITableViewDataSource>
|
||||||
|
|
||||||
|
/// 背景图
|
||||||
|
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
||||||
|
|
||||||
|
/// 头像
|
||||||
|
@property (nonatomic, strong) UIImageView *avatarImageView;
|
||||||
|
|
||||||
|
/// 人设名称
|
||||||
|
@property (nonatomic, strong) UILabel *nameLabel;
|
||||||
|
|
||||||
|
/// 开场白
|
||||||
|
@property (nonatomic, strong) UILabel *openingLabel;
|
||||||
|
|
||||||
|
/// 聊天列表
|
||||||
|
@property (nonatomic, strong) UITableView *tableView;
|
||||||
|
|
||||||
|
/// 聊天消息
|
||||||
|
@property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages;
|
||||||
|
|
||||||
|
/// 是否已加载数据
|
||||||
|
@property (nonatomic, assign) BOOL hasLoadedData;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBPersonaChatCell
|
||||||
|
|
||||||
|
#pragma mark - Lifecycle
|
||||||
|
|
||||||
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
|
if (self = [super initWithFrame:frame]) {
|
||||||
|
[self setupUI];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 1:控件初始化
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
// 背景图
|
||||||
|
[self.contentView addSubview:self.backgroundImageView];
|
||||||
|
[self.backgroundImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 半透明遮罩
|
||||||
|
UIView *maskView = [[UIView alloc] init];
|
||||||
|
maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3];
|
||||||
|
[self.contentView addSubview:maskView];
|
||||||
|
[maskView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 头像
|
||||||
|
[self.contentView addSubview:self.avatarImageView];
|
||||||
|
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.contentView).offset(80);
|
||||||
|
make.centerX.equalTo(self.contentView);
|
||||||
|
make.size.mas_equalTo(CGSizeMake(80, 80));
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 人设名称
|
||||||
|
[self.contentView addSubview:self.nameLabel];
|
||||||
|
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.avatarImageView.mas_bottom).offset(12);
|
||||||
|
make.centerX.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 开场白
|
||||||
|
[self.contentView addSubview:self.openingLabel];
|
||||||
|
[self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.nameLabel.mas_bottom).offset(8);
|
||||||
|
make.left.equalTo(self.contentView).offset(40);
|
||||||
|
make.right.equalTo(self.contentView).offset(-40);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 聊天列表
|
||||||
|
[self.contentView addSubview:self.tableView];
|
||||||
|
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(self.openingLabel.mas_bottom).offset(30);
|
||||||
|
make.left.right.bottom.equalTo(self.contentView);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Setter
|
||||||
|
|
||||||
|
- (void)setPersona:(KBPersonaModel *)persona {
|
||||||
|
_persona = persona;
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
self.hasLoadedData = NO;
|
||||||
|
self.messages = [NSMutableArray array];
|
||||||
|
|
||||||
|
// 设置 UI
|
||||||
|
[self.backgroundImageView sd_setImageWithURL:[NSURL URLWithString:persona.coverImageUrl]
|
||||||
|
placeholderImage:[UIImage imageNamed:@"placeholder_bg"]];
|
||||||
|
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
||||||
|
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
||||||
|
self.nameLabel.text = persona.name;
|
||||||
|
self.openingLabel.text = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
|
||||||
|
|
||||||
|
[self.tableView reloadData];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 2:数据加载
|
||||||
|
|
||||||
|
- (void)preloadDataIfNeeded {
|
||||||
|
if (self.hasLoadedData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 这里后续需要用 chatId 去请求聊天记录
|
||||||
|
// 目前先添加开场白作为第一条消息
|
||||||
|
self.hasLoadedData = YES;
|
||||||
|
|
||||||
|
if (self.persona.introText.length > 0) {
|
||||||
|
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText];
|
||||||
|
openingMsg.isComplete = YES;
|
||||||
|
[self.messages addObject:openingMsg];
|
||||||
|
[self.tableView reloadData];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UITableViewDataSource
|
||||||
|
|
||||||
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||||
|
return self.messages.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
|
||||||
|
if (!cell) {
|
||||||
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
|
||||||
|
cell.backgroundColor = [UIColor clearColor];
|
||||||
|
cell.textLabel.textColor = [UIColor whiteColor];
|
||||||
|
cell.textLabel.numberOfLines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBAiChatMessage *message = self.messages[indexPath.row];
|
||||||
|
cell.textLabel.text = message.text;
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
return UITableViewAutomaticDimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy Load
|
||||||
|
|
||||||
|
- (UIImageView *)backgroundImageView {
|
||||||
|
if (!_backgroundImageView) {
|
||||||
|
_backgroundImageView = [[UIImageView alloc] init];
|
||||||
|
_backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||||
|
_backgroundImageView.clipsToBounds = YES;
|
||||||
|
}
|
||||||
|
return _backgroundImageView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIImageView *)avatarImageView {
|
||||||
|
if (!_avatarImageView) {
|
||||||
|
_avatarImageView = [[UIImageView alloc] init];
|
||||||
|
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||||
|
_avatarImageView.layer.cornerRadius = 40;
|
||||||
|
_avatarImageView.layer.borderWidth = 3;
|
||||||
|
_avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||||
|
_avatarImageView.clipsToBounds = YES;
|
||||||
|
}
|
||||||
|
return _avatarImageView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)nameLabel {
|
||||||
|
if (!_nameLabel) {
|
||||||
|
_nameLabel = [[UILabel alloc] init];
|
||||||
|
_nameLabel.font = [UIFont boldSystemFontOfSize:20];
|
||||||
|
_nameLabel.textColor = [UIColor whiteColor];
|
||||||
|
_nameLabel.textAlignment = NSTextAlignmentCenter;
|
||||||
|
}
|
||||||
|
return _nameLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)openingLabel {
|
||||||
|
if (!_openingLabel) {
|
||||||
|
_openingLabel = [[UILabel alloc] init];
|
||||||
|
_openingLabel.font = [UIFont systemFontOfSize:14];
|
||||||
|
_openingLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
|
||||||
|
_openingLabel.textAlignment = NSTextAlignmentCenter;
|
||||||
|
_openingLabel.numberOfLines = 2;
|
||||||
|
}
|
||||||
|
return _openingLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UITableView *)tableView {
|
||||||
|
if (!_tableView) {
|
||||||
|
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||||
|
_tableView.delegate = self;
|
||||||
|
_tableView.dataSource = self;
|
||||||
|
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||||
|
_tableView.backgroundColor = [UIColor clearColor];
|
||||||
|
_tableView.showsVerticalScrollIndicator = NO;
|
||||||
|
_tableView.estimatedRowHeight = 60;
|
||||||
|
_tableView.rowHeight = UITableViewAutomaticDimension;
|
||||||
|
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _tableView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
16
keyBoard/Class/AiTalk/VC/KBAIHomeVC.h
Normal file
16
keyBoard/Class/AiTalk/VC/KBAIHomeVC.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// KBAIHomeVC.h
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBAIHomeVC : BaseViewController
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
244
keyBoard/Class/AiTalk/VC/KBAIHomeVC.m
Normal file
244
keyBoard/Class/AiTalk/VC/KBAIHomeVC.m
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
//
|
||||||
|
// KBAIHomeVC.m
|
||||||
|
// keyBoard
|
||||||
|
//
|
||||||
|
// Created by Mac on 2026/1/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBAIHomeVC.h"
|
||||||
|
#import "KBPersonaChatCell.h"
|
||||||
|
#import "KBPersonaModel.h"
|
||||||
|
#import "AiVM.h"
|
||||||
|
#import <Masonry/Masonry.h>
|
||||||
|
|
||||||
|
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource>
|
||||||
|
|
||||||
|
/// 人设列表容器
|
||||||
|
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||||
|
|
||||||
|
/// 人设数据
|
||||||
|
@property (nonatomic, strong) NSMutableArray<KBPersonaModel *> *personas;
|
||||||
|
|
||||||
|
/// 当前页码
|
||||||
|
@property (nonatomic, assign) NSInteger currentPage;
|
||||||
|
|
||||||
|
/// 是否还有更多数据
|
||||||
|
@property (nonatomic, assign) BOOL hasMore;
|
||||||
|
|
||||||
|
/// 是否正在加载
|
||||||
|
@property (nonatomic, assign) BOOL isLoading;
|
||||||
|
|
||||||
|
/// 当前显示的索引
|
||||||
|
@property (nonatomic, assign) NSInteger currentIndex;
|
||||||
|
|
||||||
|
/// 已预加载的索引集合
|
||||||
|
@property (nonatomic, strong) NSMutableSet<NSNumber *> *preloadedIndexes;
|
||||||
|
|
||||||
|
/// AiVM 实例
|
||||||
|
@property (nonatomic, strong) AiVM *aiVM;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBAIHomeVC
|
||||||
|
|
||||||
|
#pragma mark - Lifecycle
|
||||||
|
|
||||||
|
- (void)viewDidLoad {
|
||||||
|
[super viewDidLoad];
|
||||||
|
self.view.backgroundColor = [UIColor whiteColor];
|
||||||
|
self.kb_navView.hidden = true;
|
||||||
|
// 初始化数据
|
||||||
|
self.personas = [NSMutableArray array];
|
||||||
|
self.currentPage = 1;
|
||||||
|
self.hasMore = YES;
|
||||||
|
self.isLoading = NO;
|
||||||
|
self.currentIndex = 0;
|
||||||
|
self.preloadedIndexes = [NSMutableSet set];
|
||||||
|
self.aiVM = [[AiVM alloc] init];
|
||||||
|
|
||||||
|
[self setupUI];
|
||||||
|
[self loadPersonas];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 1:控件初始化
|
||||||
|
|
||||||
|
- (void)setupUI {
|
||||||
|
[self.view addSubview:self.collectionView];
|
||||||
|
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.edges.equalTo(self.view);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 2:数据加载
|
||||||
|
|
||||||
|
- (void)loadPersonas {
|
||||||
|
if (self.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = YES;
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[self.aiVM fetchPersonasWithPageNum:self.currentPage
|
||||||
|
pageSize:10
|
||||||
|
completion:^(KBPersonaPageModel * _Nullable pageModel, NSError * _Nullable error) {
|
||||||
|
weakSelf.isLoading = NO;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"加载人设列表失败:%@", error.localizedDescription);
|
||||||
|
// TODO: 显示错误提示
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pageModel || !pageModel.records) {
|
||||||
|
NSLog(@"人设列表数据为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加数据
|
||||||
|
[weakSelf.personas addObjectsFromArray:pageModel.records];
|
||||||
|
weakSelf.hasMore = pageModel.hasMore;
|
||||||
|
|
||||||
|
// 刷新 UI
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf.collectionView reloadData];
|
||||||
|
|
||||||
|
// 首次加载,预加载前 3 个
|
||||||
|
if (weakSelf.currentPage == 1) {
|
||||||
|
[weakSelf preloadDataForIndexes:@[@0, @1, @2]];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NSLog(@"加载成功:当前 %ld 条,总共 %ld 条,还有更多:%@",
|
||||||
|
weakSelf.personas.count,
|
||||||
|
pageModel.total,
|
||||||
|
pageModel.hasMore ? @"是" : @"否");
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadMorePersonas {
|
||||||
|
if (!self.hasMore || self.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentPage++;
|
||||||
|
[self loadPersonas];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - 3:预加载逻辑
|
||||||
|
|
||||||
|
- (void)preloadAdjacentCellsForIndex:(NSInteger)index {
|
||||||
|
if (index < 0 || index >= self.personas.count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray *indexesToPreload = [NSMutableArray array];
|
||||||
|
|
||||||
|
// 上一个
|
||||||
|
if (index > 0) {
|
||||||
|
[indexesToPreload addObject:@(index - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前
|
||||||
|
[indexesToPreload addObject:@(index)];
|
||||||
|
|
||||||
|
// 下一个
|
||||||
|
if (index < self.personas.count - 1) {
|
||||||
|
[indexesToPreload addObject:@(index + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self preloadDataForIndexes:indexesToPreload];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)preloadDataForIndexes:(NSArray<NSNumber *> *)indexes {
|
||||||
|
for (NSNumber *indexNum in indexes) {
|
||||||
|
if ([self.preloadedIndexes containsObject:indexNum]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.preloadedIndexes addObject:indexNum];
|
||||||
|
|
||||||
|
NSInteger index = [indexNum integerValue];
|
||||||
|
if (index >= self.personas.count) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
|
||||||
|
KBPersonaChatCell *cell = (KBPersonaChatCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||||
|
|
||||||
|
if (cell) {
|
||||||
|
[cell preloadDataIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"预加载第 %ld 个人设", (long)index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UICollectionViewDataSource
|
||||||
|
|
||||||
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||||
|
return self.personas.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
KBPersonaChatCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBPersonaChatCell" forIndexPath:indexPath];
|
||||||
|
cell.persona = self.personas[indexPath.item];
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UIScrollViewDelegate
|
||||||
|
|
||||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
||||||
|
CGFloat offsetY = scrollView.contentOffset.y;
|
||||||
|
NSInteger currentPage = offsetY / pageHeight;
|
||||||
|
|
||||||
|
// 滑动超过 30% 就预加载
|
||||||
|
if (fmod(offsetY, pageHeight) > pageHeight * 0.3) {
|
||||||
|
[self preloadAdjacentCellsForIndex:currentPage + 1];
|
||||||
|
} else {
|
||||||
|
[self preloadAdjacentCellsForIndex:currentPage];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接近底部时加载更多
|
||||||
|
if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) {
|
||||||
|
[self loadMorePersonas];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||||
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
||||||
|
NSInteger currentPage = scrollView.contentOffset.y / pageHeight;
|
||||||
|
self.currentIndex = currentPage;
|
||||||
|
|
||||||
|
if (currentPage < self.personas.count) {
|
||||||
|
NSLog(@"当前在第 %ld 个人设:%@", (long)currentPage, self.personas[currentPage].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Lazy Load
|
||||||
|
|
||||||
|
- (UICollectionView *)collectionView {
|
||||||
|
if (!_collectionView) {
|
||||||
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
||||||
|
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
||||||
|
layout.minimumLineSpacing = 0;
|
||||||
|
layout.minimumInteritemSpacing = 0;
|
||||||
|
layout.itemSize = [UIScreen mainScreen].bounds.size;
|
||||||
|
|
||||||
|
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
||||||
|
_collectionView.pagingEnabled = YES;
|
||||||
|
_collectionView.showsVerticalScrollIndicator = NO;
|
||||||
|
_collectionView.backgroundColor = [UIColor whiteColor];
|
||||||
|
_collectionView.delegate = self;
|
||||||
|
_collectionView.dataSource = self;
|
||||||
|
[_collectionView registerClass:[KBPersonaChatCell class] forCellWithReuseIdentifier:@"KBPersonaChatCell"];
|
||||||
|
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
_collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _collectionView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "KBPersonaPageModel.h"
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@@ -55,6 +56,16 @@ typedef void (^AiVMAudioURLCompletion)(NSString *_Nullable audioURL,
|
|||||||
- (void)requestAudioWithAudioId:(NSString *)audioId
|
- (void)requestAudioWithAudioId:(NSString *)audioId
|
||||||
completion:(AiVMAudioURLCompletion)completion;
|
completion:(AiVMAudioURLCompletion)completion;
|
||||||
|
|
||||||
|
#pragma mark - 人设相关接口
|
||||||
|
|
||||||
|
/// 分页查询人设列表
|
||||||
|
/// @param pageNum 页码(从 1 开始)
|
||||||
|
/// @param pageSize 每页大小
|
||||||
|
/// @param completion 完成回调
|
||||||
|
- (void)fetchPersonasWithPageNum:(NSInteger)pageNum
|
||||||
|
pageSize:(NSInteger)pageSize
|
||||||
|
completion:(void(^)(KBPersonaPageModel * _Nullable pageModel, NSError * _Nullable error))completion;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -212,4 +212,64 @@ autoShowBusinessError:NO
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - 人设相关接口
|
||||||
|
|
||||||
|
- (void)fetchPersonasWithPageNum:(NSInteger)pageNum
|
||||||
|
pageSize:(NSInteger)pageSize
|
||||||
|
completion:(void (^)(KBPersonaPageModel * _Nullable, NSError * _Nullable))completion {
|
||||||
|
NSDictionary *params = @{
|
||||||
|
@"pageNum": @(pageNum),
|
||||||
|
@"pageSize": @(pageSize)
|
||||||
|
};
|
||||||
|
|
||||||
|
NSLog(@"[AiVM] /ai-companion/page request: %@", params);
|
||||||
|
[[KBNetworkManager shared]
|
||||||
|
POST:@"/ai-companion/page"
|
||||||
|
jsonBody:params
|
||||||
|
headers:nil
|
||||||
|
autoShowBusinessError:NO
|
||||||
|
completion:^(NSDictionary *_Nullable json,
|
||||||
|
NSURLResponse *_Nullable response,
|
||||||
|
NSError *_Nullable error) {
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"[AiVM] /ai-companion/page failed: %@", error.localizedDescription ?: @"");
|
||||||
|
if (completion) {
|
||||||
|
completion(nil, error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"[AiVM] /ai-companion/page response: %@", json);
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
NSInteger code = [json[@"code"] integerValue];
|
||||||
|
if (code != 0) {
|
||||||
|
NSString *message = json[@"message"] ?: @"请求失败";
|
||||||
|
NSError *bizError = [NSError errorWithDomain:@"AiVM"
|
||||||
|
code:code
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||||
|
if (completion) {
|
||||||
|
completion(nil, bizError);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为模型
|
||||||
|
id dataObj = json[@"data"];
|
||||||
|
if ([dataObj isKindOfClass:[NSDictionary class]]) {
|
||||||
|
KBPersonaPageModel *pageModel = [KBPersonaPageModel mj_objectWithKeyValues:dataObj];
|
||||||
|
if (completion) {
|
||||||
|
completion(pageModel, nil);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSError *parseError = [NSError errorWithDomain:@"AiVM"
|
||||||
|
code:-1
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}];
|
||||||
|
if (completion) {
|
||||||
|
completion(nil, parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
260
keyBoard/Class/AiTalk/人设列表实现说明.md
Normal file
260
keyBoard/Class/AiTalk/人设列表实现说明.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# 人设列表实现说明
|
||||||
|
|
||||||
|
## 📦 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
KBAIHomeVC (人设列表容器)
|
||||||
|
└─ UICollectionView (竖向分页滚动)
|
||||||
|
└─ KBPersonaChatCell (每个人设占满屏)
|
||||||
|
├─ 背景图(coverImageUrl)
|
||||||
|
├─ 头像(avatarUrl)
|
||||||
|
├─ 人设名称(name)
|
||||||
|
├─ 简介(shortDesc)
|
||||||
|
└─ UITableView (聊天记录 - 待实现)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 文件结构
|
||||||
|
|
||||||
|
### Model 层(keyBoard/Class/AiTalk/M/)
|
||||||
|
- **KBPersonaModel.h/m**:人设模型
|
||||||
|
- 包含:personaId、name、avatarUrl、coverImageUrl、shortDesc、introText 等
|
||||||
|
- 扩展属性:tagsArray、isEnabled、isPublic
|
||||||
|
|
||||||
|
- **KBPersonaPageModel.h/m**:分页数据模型
|
||||||
|
- 包含:records(人设数组)、total、current、pages、hasMore
|
||||||
|
|
||||||
|
### View 层(keyBoard/Class/AiTalk/V/)
|
||||||
|
- **KBPersonaChatCell.h/m**:人设聊天 Cell
|
||||||
|
- 展示:背景图、头像、名称、简介
|
||||||
|
- 支持:预加载数据
|
||||||
|
|
||||||
|
### VM 层(keyBoard/Class/AiTalk/VM/)
|
||||||
|
- **AiVM.h/m**:网络请求管理
|
||||||
|
- 新增接口:`fetchPersonasWithPageNum:pageSize:completion:`
|
||||||
|
- 请求地址:`POST /ai-companion/page`
|
||||||
|
|
||||||
|
### VC 层(keyBoard/Class/AiTalk/VC/)
|
||||||
|
- **KBAIHomeVC.h/m**:人设列表容器
|
||||||
|
- 功能:分页加载、竖向翻页、预加载相邻 Cell
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 核心功能
|
||||||
|
|
||||||
|
### 1. 竖向分页滚动
|
||||||
|
```objc
|
||||||
|
// UICollectionView 配置
|
||||||
|
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
||||||
|
layout.itemSize = [UIScreen mainScreen].bounds.size; // 每个 cell 占满屏
|
||||||
|
_collectionView.pagingEnabled = YES; // 开启分页
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 预加载机制
|
||||||
|
- **预加载 3 个 Cell**:上一个、当前、下一个
|
||||||
|
- **触发时机**:滑动超过 30% 时开始预加载
|
||||||
|
- **避免重复**:用 `preloadedIndexes` 记录已加载的索引
|
||||||
|
|
||||||
|
```objc
|
||||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
CGFloat pageHeight = scrollView.bounds.size.height;
|
||||||
|
CGFloat offsetY = scrollView.contentOffset.y;
|
||||||
|
NSInteger currentPage = offsetY / pageHeight;
|
||||||
|
|
||||||
|
// 滑动超过 30% 就预加载
|
||||||
|
if (fmod(offsetY, pageHeight) > pageHeight * 0.3) {
|
||||||
|
[self preloadAdjacentCellsForIndex:currentPage + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分页加载
|
||||||
|
- **首次加载**:pageNum=1, pageSize=10
|
||||||
|
- **加载更多**:接近底部时自动加载下一页
|
||||||
|
- **防重复**:用 `isLoading` 标记防止重复请求
|
||||||
|
|
||||||
|
```objc
|
||||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
// 接近底部时加载更多
|
||||||
|
if (offsetY + scrollView.bounds.size.height >= scrollView.contentSize.height - pageHeight) {
|
||||||
|
[self loadMorePersonas];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 网络请求
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
```
|
||||||
|
POST /ai-companion/page
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "温柔小姐姐",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"coverImageUrl": "https://...",
|
||||||
|
"shortDesc": "温柔体贴的聊天伙伴",
|
||||||
|
"introText": "你好呀,今天过得怎么样?",
|
||||||
|
"personalityTags": "温柔,体贴,善解人意",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 100,
|
||||||
|
"current": 1,
|
||||||
|
"pages": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
```objc
|
||||||
|
[self.aiVM fetchPersonasWithPageNum:1
|
||||||
|
pageSize:10
|
||||||
|
completion:^(KBPersonaPageModel *pageModel, NSError *error) {
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"加载失败:%@", error.localizedDescription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.personas addObjectsFromArray:pageModel.records];
|
||||||
|
self.hasMore = pageModel.hasMore;
|
||||||
|
[self.collectionView reloadData];
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI 布局
|
||||||
|
|
||||||
|
### KBPersonaChatCell 布局
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 背景图(coverImageUrl) │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ 半透明遮罩(黑色 0.3) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌───┐ │ │
|
||||||
|
│ │ │头像│ │ │
|
||||||
|
│ │ └───┘ │ │
|
||||||
|
│ │ 人设名称 │ │
|
||||||
|
│ │ 简短描述 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌───────────────┐ │ │
|
||||||
|
│ │ │ 聊天记录列表 │ │ │
|
||||||
|
│ │ │ (待实现) │ │ │
|
||||||
|
│ │ └───────────────┘ │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 已完成功能
|
||||||
|
|
||||||
|
1. ✅ Model 层:KBPersonaModel、KBPersonaPageModel
|
||||||
|
2. ✅ VM 层:AiVM 新增人设列表接口
|
||||||
|
3. ✅ View 层:KBPersonaChatCell(基础 UI)
|
||||||
|
4. ✅ VC 层:KBAIHomeVC(分页加载、竖向翻页、预加载)
|
||||||
|
5. ✅ MJExtension 配置:JSON 自动转 Model
|
||||||
|
6. ✅ 懒加载:所有控件使用懒加载
|
||||||
|
7. ✅ Masonry 布局:纯代码布局
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚧 待实现功能
|
||||||
|
|
||||||
|
### 1. 聊天记录展示
|
||||||
|
- 需要后端提供 `chatId` 字段
|
||||||
|
- 用 `chatId` 请求聊天记录接口
|
||||||
|
- 在 `KBPersonaChatCell` 的 `tableView` 中展示聊天记录
|
||||||
|
|
||||||
|
### 2. 聊天功能
|
||||||
|
- 点击输入框发送消息
|
||||||
|
- 接收 AI 回复
|
||||||
|
- 支持语音消息
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
- 网络请求失败提示
|
||||||
|
- 空数据占位图
|
||||||
|
- 加载中状态
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
- Cell 高度缓存
|
||||||
|
- 图片缓存策略
|
||||||
|
- 内存管理(清理不可见 Cell 的数据)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 使用方式
|
||||||
|
|
||||||
|
### 1. 在其他 VC 中跳转
|
||||||
|
```objc
|
||||||
|
KBAIHomeVC *homeVC = [[KBAIHomeVC alloc] init];
|
||||||
|
[self.navigationController pushViewController:homeVC animated:YES];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 作为 TabBar 的一个页面
|
||||||
|
```objc
|
||||||
|
KBAIHomeVC *homeVC = [[KBAIHomeVC alloc] init];
|
||||||
|
homeVC.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"AI聊天"
|
||||||
|
image:[UIImage imageNamed:@"tab_ai"]
|
||||||
|
selectedImage:[UIImage imageNamed:@"tab_ai_sel"]];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 调试日志
|
||||||
|
|
||||||
|
### 网络请求日志
|
||||||
|
```
|
||||||
|
[AiVM] /ai-companion/page request: {pageNum: 1, pageSize: 10}
|
||||||
|
[AiVM] /ai-companion/page response: {...}
|
||||||
|
加载成功:当前 10 条,总共 100 条,还有更多:是
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预加载日志
|
||||||
|
```
|
||||||
|
预加载第 0 个人设
|
||||||
|
预加载第 1 个人设
|
||||||
|
预加载第 2 个人设
|
||||||
|
当前在第 1 个人设:温柔小姐姐
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 注意事项
|
||||||
|
|
||||||
|
1. **接口地址**:需要在 `KBAPI.h` 中定义 `/ai-companion/page`
|
||||||
|
2. **图片占位图**:需要添加 `placeholder_bg` 和 `placeholder_avatar` 图片资源
|
||||||
|
3. **线程安全**:网络回调后需要回到主线程更新 UI
|
||||||
|
4. **内存管理**:注意使用 `weakSelf` 避免循环引用
|
||||||
|
5. **字段兼容**:后端可能返回 `null`,已做容错处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 下一步计划
|
||||||
|
|
||||||
|
1. 接入真实的聊天记录接口
|
||||||
|
2. 实现聊天输入和发送功能
|
||||||
|
3. 优化 Cell 的聊天记录展示(使用现有的 KBChatTableView)
|
||||||
|
4. 添加下拉刷新和上拉加载更多
|
||||||
|
5. 添加错误提示和空数据占位图
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#import "KBAiMainVC.h"
|
#import "KBAiMainVC.h"
|
||||||
#import "KBShopVC.h"
|
#import "KBShopVC.h"
|
||||||
#import "MyVC.h"
|
#import "MyVC.h"
|
||||||
|
#import "KBAIHomeVC.h"
|
||||||
|
|
||||||
// #import "KBCommunityVC.h"
|
// #import "KBCommunityVC.h"
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
selectedImg:@"tab_shop_selected"];
|
selectedImg:@"tab_shop_selected"];
|
||||||
|
|
||||||
// AI
|
// AI
|
||||||
KBAiMainVC *aiMainVC = [[KBAiMainVC alloc] init];
|
KBAIHomeVC *aiMainVC = [[KBAIHomeVC alloc] init];
|
||||||
// community.title = KBLocalized(@"Circle");
|
// community.title = KBLocalized(@"Circle");
|
||||||
BaseNavigationController *navCommunity =
|
BaseNavigationController *navCommunity =
|
||||||
[[BaseNavigationController alloc] initWithRootViewController:aiMainVC];
|
[[BaseNavigationController alloc] initWithRootViewController:aiMainVC];
|
||||||
|
|||||||
Reference in New Issue
Block a user