This commit is contained in:
2026-01-28 16:35:47 +08:00
parent 22f77d56ea
commit b4db79eba8
23 changed files with 1185 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
//
// KBChattedCompanionModel.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 聊过天的 AI 角色模型Chatting 列表)
@interface KBChattedCompanionModel : NSObject
/// 角色 ID
@property (nonatomic, assign) NSInteger companionId;
/// 角色名称
@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, assign) NSInteger sortOrder;
/// 热度分数
@property (nonatomic, assign) NSInteger popularityScore;
/// 开场白
@property (nonatomic, copy) NSString *prologue;
/// 开场白音频
@property (nonatomic, copy) NSString *prologueAudio;
/// 点赞数
@property (nonatomic, assign) NSInteger likeCount;
/// 评论数
@property (nonatomic, assign) NSInteger commentCount;
/// 是否已点赞
@property (nonatomic, assign) BOOL liked;
/// 创建时间
@property (nonatomic, copy) NSString *createdAt;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,19 @@
//
// KBChattedCompanionModel.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBChattedCompanionModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBChattedCompanionModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"companionId": @"id"
};
}
@end

View File

@@ -0,0 +1,54 @@
//
// KBLikedCompanionModel.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 点赞过的 AI 角色模型Thumbs Up 列表)
@interface KBLikedCompanionModel : NSObject
/// 角色 ID
@property (nonatomic, assign) NSInteger companionId;
/// 角色名称
@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, assign) NSInteger sortOrder;
/// 热度分数
@property (nonatomic, assign) NSInteger popularityScore;
/// 开场白
@property (nonatomic, copy) NSString *prologue;
/// 开场白音频
@property (nonatomic, copy) NSString *prologueAudio;
/// 点赞数
@property (nonatomic, assign) NSInteger likeCount;
/// 评论数
@property (nonatomic, assign) NSInteger commentCount;
/// 是否已点赞
@property (nonatomic, assign) BOOL liked;
/// 创建时间
@property (nonatomic, copy) NSString *createdAt;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,19 @@
//
// KBLikedCompanionModel.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBLikedCompanionModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBLikedCompanionModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"companionId": @"id"
};
}
@end

View File

@@ -0,0 +1,47 @@
//
// KBAIMessageCell.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class KBAIMessageCell;
@protocol KBAIMessageCellDelegate <NSObject>
@optional
/// 点击删除按钮
- (void)messageCell:(KBAIMessageCell *)cell didTapDeleteAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface KBAIMessageCell : UITableViewCell
@property (nonatomic, weak) id<KBAIMessageCellDelegate> delegate;
@property (nonatomic, strong) NSIndexPath *indexPath;
/// 头像
@property (nonatomic, strong, readonly) UIImageView *avatarImageView;
/// 昵称
@property (nonatomic, strong, readonly) UILabel *nameLabel;
/// 消息内容
@property (nonatomic, strong, readonly) UILabel *contentLabel;
/// 时间
@property (nonatomic, strong, readonly) UILabel *timeLabel;
/// 置顶图标(只显示,不做功能)
@property (nonatomic, strong, readonly) UIImageView *pinIconView;
/// 配置数据
- (void)configWithAvatar:(NSString *)avatarUrl
name:(NSString *)name
content:(NSString *)content
time:(NSString *)time
isPinned:(BOOL)isPinned;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,157 @@
//
// KBAIMessageCell.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageCell.h"
#import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h>
@interface KBAIMessageCell ()
@property (nonatomic, strong, readwrite) UIImageView *avatarImageView;
@property (nonatomic, strong, readwrite) UILabel *nameLabel;
@property (nonatomic, strong, readwrite) UILabel *contentLabel;
@property (nonatomic, strong, readwrite) UILabel *timeLabel;
@property (nonatomic, strong, readwrite) UIImageView *pinIconView;
@end
@implementation KBAIMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.backgroundColor = [UIColor whiteColor];
[self setupSubviews];
}
return self;
}
- (void)setupSubviews {
[self.contentView addSubview:self.avatarImageView];
[self.contentView addSubview:self.nameLabel];
[self.contentView addSubview:self.contentLabel];
[self.contentView addSubview:self.timeLabel];
[self.contentView addSubview:self.pinIconView];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(50);
}];
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.avatarImageView.mas_right).offset(12);
make.top.equalTo(self.avatarImageView).offset(2);
make.right.lessThanOrEqualTo(self.timeLabel.mas_left).offset(-8);
}];
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.nameLabel);
make.top.equalTo(self.nameLabel.mas_bottom).offset(4);
make.right.lessThanOrEqualTo(self.timeLabel.mas_left).offset(-8);
}];
[self.pinIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.top.equalTo(self.nameLabel);
make.width.height.mas_equalTo(16);
}];
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.bottom.equalTo(self.contentLabel);
}];
}
- (void)configWithAvatar:(NSString *)avatarUrl
name:(NSString *)name
content:(NSString *)content
time:(NSString *)time
isPinned:(BOOL)isPinned {
if (avatarUrl.length > 0) {
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:avatarUrl]
placeholderImage:KBAvatarPlaceholderImage];
} else {
self.avatarImageView.image = KBAvatarPlaceholderImage;
}
self.nameLabel.text = name;
self.contentLabel.text = content;
self.timeLabel.text = time;
self.pinIconView.hidden = !isPinned;
//
if (isPinned) {
[self.timeLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.bottom.equalTo(self.contentLabel);
}];
} else {
[self.timeLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.bottom.equalTo(self.contentLabel);
}];
}
}
#pragma mark - Lazy Load
- (UIImageView *)avatarImageView {
if (!_avatarImageView) {
_avatarImageView = [[UIImageView alloc] init];
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
_avatarImageView.layer.cornerRadius = 25;
_avatarImageView.layer.masksToBounds = YES;
_avatarImageView.backgroundColor = [UIColor colorWithHex:0xF5F5F5];
}
return _avatarImageView;
}
- (UILabel *)nameLabel {
if (!_nameLabel) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
_nameLabel.textColor = [UIColor colorWithHex:0x1B1F1A];
}
return _nameLabel;
}
- (UILabel *)contentLabel {
if (!_contentLabel) {
_contentLabel = [[UILabel alloc] init];
_contentLabel.font = [UIFont systemFontOfSize:14];
_contentLabel.textColor = [UIColor colorWithHex:0x9F9F9F];
_contentLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
return _contentLabel;
}
- (UILabel *)timeLabel {
if (!_timeLabel) {
_timeLabel = [[UILabel alloc] init];
_timeLabel.font = [UIFont systemFontOfSize:12];
_timeLabel.textColor = [UIColor colorWithHex:0x9F9F9F];
_timeLabel.textAlignment = NSTextAlignmentRight;
}
return _timeLabel;
}
- (UIImageView *)pinIconView {
if (!_pinIconView) {
_pinIconView = [[UIImageView alloc] init];
_pinIconView.contentMode = UIViewContentModeScaleAspectFit;
// 使
if (@available(iOS 13.0, *)) {
_pinIconView.image = [UIImage systemImageNamed:@"pin.fill"];
_pinIconView.tintColor = [UIColor colorWithHex:0x9F9F9F];
}
_pinIconView.hidden = YES;
}
return _pinIconView;
}
@end

View File

@@ -17,6 +17,7 @@
#import "KBVipPay.h"
#import "KBUserSessionManager.h"
#import "LSTPopView.h"
#import "KBAIMessageVC.h"
#import <Masonry/Masonry.h>
@interface KBAIHomeVC () <UICollectionViewDelegate, UICollectionViewDataSource, KBVoiceToTextManagerDelegate, KBVoiceRecordManagerDelegate, UIGestureRecognizerDelegate, KBChatLimitPopViewDelegate>
@@ -68,6 +69,9 @@
/// AI
@property (nonatomic, assign) BOOL isWaitingForAIResponse;
///
@property (nonatomic, strong) UIButton *messageButton;
@end
@implementation KBAIHomeVC
@@ -113,6 +117,14 @@
make.edges.equalTo(self.view);
}];
//
[self.view addSubview:self.messageButton];
[self.messageButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(KB_STATUSBAR_HEIGHT + 10);
make.right.equalTo(self.view).offset(-16);
make.width.height.mas_equalTo(32);
}];
//
[self.view addSubview:self.bottomBackgroundView];
[self.bottomBackgroundView addSubview:self.bottomBlurEffectView];
@@ -540,6 +552,22 @@
return _bottomMaskLayer;
}
- (UIButton *)messageButton {
if (!_messageButton) {
_messageButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_messageButton setImage:[UIImage imageNamed:@"ai_message_icon"] forState:UIControlStateNormal];
[_messageButton addTarget:self action:@selector(messageButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _messageButton;
}
#pragma mark - Actions
- (void)messageButtonTapped {
KBAIMessageVC *vc = [[KBAIMessageVC alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
#pragma mark - KBVoiceToTextManagerDelegate
- (void)voiceToTextManager:(KBVoiceToTextManager *)manager

View File

@@ -0,0 +1,17 @@
//
// KBAIMessageChatingVC.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageListVC.h"
NS_ASSUME_NONNULL_BEGIN
/// Chatting 消息列表
@interface KBAIMessageChatingVC : KBAIMessageListVC
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,99 @@
//
// KBAIMessageChatingVC.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageChatingVC.h"
#import "AiVM.h"
#import "KBChattedCompanionModel.h"
#import "KBHUD.h"
@interface KBAIMessageChatingVC ()
@property (nonatomic, strong) AiVM *viewModel;
@property (nonatomic, strong) NSMutableArray<KBChattedCompanionModel *> *chattedList;
@end
@implementation KBAIMessageChatingVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
self.listType = 1; // Chatting
[super viewDidLoad];
}
#pragma mark - 2
- (void)loadData {
[KBHUD show];
__weak typeof(self) weakSelf = self;
[self.viewModel fetchChattedCompanionsWithCompletion:^(NSArray<KBChattedCompanionModel *> * _Nullable list, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
if (error) {
[KBHUD showError:error.localizedDescription];
return;
}
[weakSelf.chattedList removeAllObjects];
if (list.count > 0) {
[weakSelf.chattedList addObjectsFromArray:list];
}
[weakSelf.dataArray removeAllObjects];
//
for (KBChattedCompanionModel *model in weakSelf.chattedList) {
NSMutableDictionary *item = [NSMutableDictionary dictionary];
item[@"avatar"] = model.avatarUrl ?: @"";
item[@"name"] = model.name ?: @"";
item[@"content"] = model.shortDesc ?: @"";
item[@"time"] = model.createdAt ?: @"";
item[@"isPinned"] = @NO;
item[@"companionId"] = @(model.companionId);
[weakSelf.dataArray addObject:item];
}
[weakSelf.tableView reloadData];
});
}];
}
#pragma mark -
- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= self.chattedList.count) {
return;
}
// TODO:
//
if (indexPath.row < self.chattedList.count) {
[self.chattedList removeObjectAtIndex:indexPath.row];
}
if (indexPath.row < self.dataArray.count) {
[self.dataArray removeObjectAtIndex:indexPath.row];
}
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
}
#pragma mark - Lazy Load
- (AiVM *)viewModel {
if (!_viewModel) {
_viewModel = [[AiVM alloc] init];
}
return _viewModel;
}
- (NSMutableArray<KBChattedCompanionModel *> *)chattedList {
if (!_chattedList) {
_chattedList = [NSMutableArray array];
}
return _chattedList;
}
@end

View File

@@ -0,0 +1,33 @@
//
// KBAIMessageListVC.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import <UIKit/UIKit.h>
#import <JXCategoryView/JXCategoryListContainerView.h>
NS_ASSUME_NONNULL_BEGIN
/// 消息列表基类,供 ZanVC 和 ChatingVC 继承
@interface KBAIMessageListVC : UIViewController <JXCategoryListContentViewDelegate>
/// 列表类型0 = Thumbs Up, 1 = Chatting
@property (nonatomic, assign) NSInteger listType;
/// 数据源
@property (nonatomic, strong) NSMutableArray *dataArray;
/// TableView
@property (nonatomic, strong) UITableView *tableView;
/// 加载数据(子类重写)
- (void)loadData;
/// 删除某条数据(子类重写)
- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,142 @@
//
// KBAIMessageListVC.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageListVC.h"
#import "KBAIMessageCell.h"
#import <Masonry/Masonry.h>
@interface KBAIMessageListVC () <UITableViewDelegate, UITableViewDataSource>
@end
@implementation KBAIMessageListVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
/// 1
[self setupUI];
/// 2
[self loadData];
}
#pragma mark - 1
- (void)setupUI {
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
}
#pragma mark - 2
- (void)loadData {
//
}
- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath {
//
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KBAIMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KBAIMessageCell" forIndexPath:indexPath];
cell.indexPath = indexPath;
//
if (indexPath.row < self.dataArray.count) {
NSDictionary *item = self.dataArray[indexPath.row];
[cell configWithAvatar:item[@"avatar"]
name:item[@"name"]
content:item[@"content"]
time:item[@"time"]
isPinned:[item[@"isPinned"] boolValue]];
}
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 76;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
//
}
///
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(11.0)) {
__weak typeof(self) weakSelf = self;
//
UIContextualAction *deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive
title:nil
handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
[weakSelf deleteItemAtIndexPath:indexPath];
completionHandler(YES);
}];
deleteAction.backgroundColor = [UIColor colorWithHex:0xF44336];
if (@available(iOS 13.0, *)) {
deleteAction.image = [UIImage systemImageNamed:@"trash.fill"];
}
UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteAction]];
config.performsFirstActionWithFullSwipe = NO;
return config;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
#pragma mark - JXCategoryListContentViewDelegate
- (UIView *)listView {
return self.view;
}
#pragma mark - Lazy Load
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.backgroundColor = [UIColor whiteColor];
_tableView.showsVerticalScrollIndicator = NO;
[_tableView registerClass:[KBAIMessageCell class] forCellReuseIdentifier:@"KBAIMessageCell"];
if (@available(iOS 15.0, *)) {
_tableView.sectionHeaderTopPadding = 0;
}
}
return _tableView;
}
- (NSMutableArray *)dataArray {
if (!_dataArray) {
_dataArray = [NSMutableArray array];
}
return _dataArray;
}
@end

View File

@@ -0,0 +1,17 @@
//
// KBAIMessageVC.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "BaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
/// AI 消息主页面Thumbs Up / Chatting 分页)
@interface KBAIMessageVC : BaseViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,170 @@
//
// KBAIMessageVC.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageVC.h"
#import <JXCategoryView/JXCategoryView.h>
#import "KBAIMessageZanVC.h"
#import "KBAIMessageChatingVC.h"
#import <Masonry/Masonry.h>
@interface KBAIMessageVC () <JXCategoryViewDelegate, JXCategoryListContainerViewDelegate>
///
@property (nonatomic, strong) JXCategoryTitleView *categoryView;
///
@property (nonatomic, strong) JXCategoryListContainerView *listContainerView;
///
@property (nonatomic, strong) UIButton *searchButton;
///
@property (nonatomic, strong) NSArray<NSString *> *titles;
@end
@implementation KBAIMessageVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
/// 1
[self setupUI];
/// 2
[self bindActions];
}
#pragma mark - 1
- (void)setupUI {
//
self.kb_titleLabel.hidden = YES;
//
[self.kb_navView addSubview:self.categoryView];
[self.categoryView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.kb_backButton.mas_right).offset(0);
make.centerY.equalTo(self.kb_backButton);
make.height.mas_equalTo(44);
make.width.mas_equalTo(180);
}];
//
[self.kb_navView addSubview:self.searchButton];
[self.searchButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.kb_navView).offset(-16);
make.centerY.equalTo(self.kb_backButton);
make.width.height.mas_equalTo(24);
}];
//
[self.view addSubview:self.listContainerView];
[self.listContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.kb_navView.mas_bottom);
make.left.right.bottom.equalTo(self.view);
}];
// categoryView listContainerView
self.categoryView.listContainer = self.listContainerView;
}
#pragma mark - 2
- (void)bindActions {
[self.searchButton addTarget:self action:@selector(searchButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark - Actions
- (void)searchButtonTapped {
// TODO:
NSLog(@"搜索按钮点击");
}
#pragma mark - JXCategoryViewDelegate
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index {
NSLog(@"选中分类:%@", self.titles[index]);
}
#pragma mark - JXCategoryListContainerViewDelegate
- (NSInteger)numberOfListsInlistContainerView:(JXCategoryListContainerView *)listContainerView {
return self.titles.count;
}
- (id<JXCategoryListContentViewDelegate>)listContainerView:(JXCategoryListContainerView *)listContainerView initListForIndex:(NSInteger)index {
if (index == 0) {
return [[KBAIMessageZanVC alloc] init];
} else {
return [[KBAIMessageChatingVC alloc] init];
}
}
#pragma mark - Lazy Load
- (NSArray<NSString *> *)titles {
if (!_titles) {
_titles = @[KBLocalized(@"Thumbs Up"), KBLocalized(@"Chatting")];
}
return _titles;
}
- (JXCategoryTitleView *)categoryView {
if (!_categoryView) {
_categoryView = [[JXCategoryTitleView alloc] init];
_categoryView.backgroundColor = [UIColor clearColor];
_categoryView.titles = self.titles;
_categoryView.delegate = self;
//
_categoryView.titleFont = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
_categoryView.titleSelectedFont = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
_categoryView.titleColor = [UIColor colorWithHex:0x9F9F9F];
_categoryView.titleSelectedColor = [UIColor colorWithHex:0x1B1F1A];
//
_categoryView.indicators = @[];
//
_categoryView.cellSpacing = 20;
_categoryView.contentEdgeInsetLeft = 0;
_categoryView.contentEdgeInsetRight = 0;
_categoryView.averageCellSpacingEnabled = NO;
//
_categoryView.cellWidthZoomEnabled = NO;
_categoryView.titleColorGradientEnabled = NO;
}
return _categoryView;
}
- (JXCategoryListContainerView *)listContainerView {
if (!_listContainerView) {
_listContainerView = [[JXCategoryListContainerView alloc] initWithType:JXCategoryListContainerType_ScrollView delegate:self];
_listContainerView.scrollView.bounces = NO;
}
return _listContainerView;
}
- (UIButton *)searchButton {
if (!_searchButton) {
_searchButton = [UIButton buttonWithType:UIButtonTypeCustom];
if (@available(iOS 13.0, *)) {
UIImage *searchImage = [UIImage systemImageNamed:@"magnifyingglass"];
[_searchButton setImage:searchImage forState:UIControlStateNormal];
_searchButton.tintColor = [UIColor colorWithHex:0x1B1F1A];
}
}
return _searchButton;
}
@end

View File

@@ -0,0 +1,17 @@
//
// KBAIMessageZanVC.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageListVC.h"
NS_ASSUME_NONNULL_BEGIN
/// Thumbs Up 消息列表
@interface KBAIMessageZanVC : KBAIMessageListVC
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,110 @@
//
// KBAIMessageZanVC.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageZanVC.h"
#import "AiVM.h"
#import "KBLikedCompanionModel.h"
#import "KBHUD.h"
@interface KBAIMessageZanVC ()
@property (nonatomic, strong) AiVM *viewModel;
@property (nonatomic, strong) NSMutableArray<KBLikedCompanionModel *> *likedList;
@end
@implementation KBAIMessageZanVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
self.listType = 0; // Thumbs Up
[super viewDidLoad];
}
#pragma mark - 2
- (void)loadData {
[KBHUD show];
__weak typeof(self) weakSelf = self;
[self.viewModel fetchLikedCompanionsWithCompletion:^(NSArray<KBLikedCompanionModel *> * _Nullable list, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
if (error) {
[KBHUD showError:error.localizedDescription];
return;
}
[weakSelf.likedList removeAllObjects];
if (list.count > 0) {
[weakSelf.likedList addObjectsFromArray:list];
}
[weakSelf.dataArray removeAllObjects];
//
for (KBLikedCompanionModel *model in weakSelf.likedList) {
NSMutableDictionary *item = [NSMutableDictionary dictionary];
item[@"avatar"] = model.avatarUrl ?: @"";
item[@"name"] = model.name ?: @"";
item[@"content"] = model.shortDesc ?: @"";
item[@"time"] = model.createdAt ?: @"";
item[@"isPinned"] = @NO;
item[@"companionId"] = @(model.companionId);
[weakSelf.dataArray addObject:item];
}
[weakSelf.tableView reloadData];
});
}];
}
#pragma mark -
- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= self.likedList.count) {
return;
}
KBLikedCompanionModel *model = self.likedList[indexPath.row];
__weak typeof(self) weakSelf = self;
[self.viewModel likeCompanionWithCompanionId:model.companionId completion:^(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
[KBHUD showError:error.localizedDescription];
return;
}
//
if (indexPath.row < weakSelf.likedList.count) {
[weakSelf.likedList removeObjectAtIndex:indexPath.row];
}
if (indexPath.row < weakSelf.dataArray.count) {
[weakSelf.dataArray removeObjectAtIndex:indexPath.row];
}
[weakSelf.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
});
}];
}
#pragma mark - Lazy Load
- (AiVM *)viewModel {
if (!_viewModel) {
_viewModel = [[AiVM alloc] init];
}
return _viewModel;
}
- (NSMutableArray<KBLikedCompanionModel *> *)likedList {
if (!_likedList) {
_likedList = [NSMutableArray array];
}
return _likedList;
}
@end

View File

@@ -0,0 +1,16 @@
//
// AIMessageVM.h
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AIMessageVM : NSObject
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,12 @@
//
// AIMessageVM.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "AIMessageVM.h"
@implementation AIMessageVM
@end

View File

@@ -9,6 +9,8 @@
#import "KBPersonaPageModel.h"
#import "KBChatHistoryPageModel.h"
#import "KBCommentModel.h"
#import "KBLikedCompanionModel.h"
#import "KBChattedCompanionModel.h"
NS_ASSUME_NONNULL_BEGIN
@@ -142,6 +144,16 @@ typedef void (^AiVMSpeechTranscribeCompletion)(KBAiSpeechTranscribeResponse *_Nu
- (void)likeCompanionWithCompanionId:(NSInteger)companionId
completion:(void(^)(KBCommentLikeResponse * _Nullable response, NSError * _Nullable error))completion;
#pragma mark - 点赞列表接口
/// 获取当前用户点赞过的 AI 角色列表Thumbs Up
/// @param completion 完成回调(返回点赞角色数组)
- (void)fetchLikedCompanionsWithCompletion:(void(^)(NSArray<KBLikedCompanionModel *> * _Nullable list, NSError * _Nullable error))completion;
/// 获取当前用户聊过天的 AI 角色列表Chatting
/// @param completion 完成回调(返回聊天角色数组)
- (void)fetchChattedCompanionsWithCompletion:(void(^)(NSArray<KBChattedCompanionModel *> * _Nullable list, NSError * _Nullable error))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -9,6 +9,8 @@
#import "KBAPI.h"
#import "KBNetworkManager.h"
#import "KBCommentModel.h"
#import "KBLikedCompanionModel.h"
#import "KBChattedCompanionModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBAiSyncData
@@ -599,4 +601,94 @@ autoShowBusinessError:NO
}];
}
#pragma mark -
- (void)fetchLikedCompanionsWithCompletion:(void (^)(NSArray<KBLikedCompanionModel *> * _Nullable, NSError * _Nullable))completion {
NSLog(@"[AiVM] /ai-companion/liked request");
[[KBNetworkManager shared]
GET:@"/ai-companion/liked"
parameters:nil
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary *_Nullable json,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error) {
NSLog(@"[AiVM] /ai-companion/liked failed: %@", error.localizedDescription ?: @"");
if (completion) {
completion(nil, error);
}
return;
}
NSLog(@"[AiVM] /ai-companion/liked 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;
}
NSArray *dataArray = json[@"data"];
if (![dataArray isKindOfClass:[NSArray class]]) {
dataArray = @[];
}
NSArray<KBLikedCompanionModel *> *list = [KBLikedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray];
if (completion) {
completion(list, nil);
}
}];
}
- (void)fetchChattedCompanionsWithCompletion:(void (^)(NSArray<KBChattedCompanionModel *> * _Nullable, NSError * _Nullable))completion {
NSLog(@"[AiVM] /ai-companion/chatted request");
[[KBNetworkManager shared]
GET:@"/ai-companion/chatted"
parameters:nil
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary *_Nullable json,
NSURLResponse *_Nullable response,
NSError *_Nullable error) {
if (error) {
NSLog(@"[AiVM] /ai-companion/chatted failed: %@", error.localizedDescription ?: @"");
if (completion) {
completion(nil, error);
}
return;
}
NSLog(@"[AiVM] /ai-companion/chatted 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;
}
NSArray *dataArray = json[@"data"];
if (![dataArray isKindOfClass:[NSArray class]]) {
dataArray = @[];
}
NSArray<KBChattedCompanionModel *> *list = [KBChattedCompanionModel mj_objectArrayWithKeyValuesArray:dataArray];
if (completion) {
completion(list, nil);
}
}];
}
@end