添加侧边栏
This commit is contained in:
51
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h
Normal file
51
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.h
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// KBAIPersonaSidebarView.h
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/2/3.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class KBPersonaModel;
|
||||
@class KBAIPersonaSidebarView;
|
||||
|
||||
@protocol KBAIPersonaSidebarViewDelegate <NSObject>
|
||||
@optional
|
||||
/// 侧边栏请求人设数据
|
||||
/// page 从 1 开始
|
||||
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
|
||||
requestPersonasAtPage:(NSInteger)page;
|
||||
/// 选择某个人设
|
||||
- (void)personaSidebarView:(KBAIPersonaSidebarView *)view
|
||||
didSelectPersona:(KBPersonaModel *)persona;
|
||||
@end
|
||||
|
||||
/// 人设侧边栏(LSTPopView 内容视图)
|
||||
@interface KBAIPersonaSidebarView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<KBAIPersonaSidebarViewDelegate> delegate;
|
||||
@property (nonatomic, assign) NSInteger selectedPersonaId;
|
||||
@property (nonatomic, assign, readonly) NSInteger currentPage;
|
||||
|
||||
/// 更新人设列表
|
||||
/// reset=YES 表示重置并替换数据;reset=NO 表示追加
|
||||
/// currentPage 为当前已加载页数(从 1 开始)
|
||||
- (void)updatePersonas:(NSArray<KBPersonaModel *> *)personas
|
||||
reset:(BOOL)reset
|
||||
hasMore:(BOOL)hasMore
|
||||
currentPage:(NSInteger)currentPage;
|
||||
/// 请求数据(若为空)
|
||||
- (void)requestPersonasIfNeeded;
|
||||
/// 更新选中态
|
||||
- (void)updateSelectedPersonaId:(NSInteger)personaId;
|
||||
/// 结束加载更多
|
||||
- (void)endLoadingMore;
|
||||
/// 重置加载更多状态
|
||||
- (void)resetLoadMore;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
417
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m
Normal file
417
keyBoard/Class/AiTalk/V/PopView/KBAIPersonaSidebarView.m
Normal file
@@ -0,0 +1,417 @@
|
||||
//
|
||||
// KBAIPersonaSidebarView.m
|
||||
// keyBoard
|
||||
//
|
||||
// Created by Codex on 2026/2/3.
|
||||
//
|
||||
|
||||
#import "KBAIPersonaSidebarView.h"
|
||||
#import "KBPersonaModel.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
#import <MJRefresh/MJRefresh.h>
|
||||
|
||||
#pragma mark - Cell
|
||||
|
||||
@interface KBAIPersonaSidebarCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) UIImageView *avatarImageView;
|
||||
@property (nonatomic, strong) UILabel *nameLabel;
|
||||
@property (nonatomic, strong) UILabel *descLabel;
|
||||
@property (nonatomic, strong) UIImageView *checkImageView;
|
||||
@property (nonatomic, strong) UIImageView *arrowImageView;
|
||||
@property (nonatomic, strong) UIView *lineView;
|
||||
|
||||
- (void)configureWithPersona:(KBPersonaModel *)persona selected:(BOOL)selected;
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBAIPersonaSidebarCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style
|
||||
reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
[self.contentView addSubview:self.avatarImageView];
|
||||
[self.contentView addSubview:self.nameLabel];
|
||||
[self.contentView addSubview:self.descLabel];
|
||||
[self.contentView addSubview:self.checkImageView];
|
||||
[self.contentView addSubview:self.arrowImageView];
|
||||
[self.contentView addSubview:self.lineView];
|
||||
|
||||
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.contentView).offset(16);
|
||||
make.centerY.equalTo(self.contentView);
|
||||
make.width.height.mas_equalTo(44);
|
||||
}];
|
||||
|
||||
[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.contentView).offset(-60);
|
||||
}];
|
||||
|
||||
[self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.nameLabel);
|
||||
make.top.equalTo(self.nameLabel.mas_bottom).offset(4);
|
||||
make.right.lessThanOrEqualTo(self.contentView).offset(-60);
|
||||
}];
|
||||
|
||||
[self.checkImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.contentView);
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.width.height.mas_equalTo(22);
|
||||
}];
|
||||
|
||||
[self.arrowImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.centerY.equalTo(self.contentView);
|
||||
make.right.equalTo(self.contentView).offset(-18);
|
||||
make.width.height.mas_equalTo(16);
|
||||
}];
|
||||
|
||||
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.nameLabel);
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.bottom.equalTo(self.contentView);
|
||||
make.height.mas_equalTo(0.5);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)configureWithPersona:(KBPersonaModel *)persona selected:(BOOL)selected {
|
||||
if (!persona) {
|
||||
return;
|
||||
}
|
||||
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:persona.avatarUrl]
|
||||
placeholderImage:[UIImage imageNamed:@"placeholder_avatar"]];
|
||||
self.nameLabel.text = persona.name ?: @"";
|
||||
NSString *desc = persona.shortDesc.length > 0 ? persona.shortDesc : persona.introText;
|
||||
self.descLabel.text = desc ?: @"";
|
||||
self.checkImageView.hidden = !selected;
|
||||
self.arrowImageView.hidden = selected;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIImageView *)avatarImageView {
|
||||
if (!_avatarImageView) {
|
||||
_avatarImageView = [[UIImageView alloc] init];
|
||||
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
_avatarImageView.layer.cornerRadius = 22;
|
||||
_avatarImageView.clipsToBounds = YES;
|
||||
_avatarImageView.layer.borderWidth = 1;
|
||||
_avatarImageView.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6].CGColor;
|
||||
}
|
||||
return _avatarImageView;
|
||||
}
|
||||
|
||||
- (UILabel *)nameLabel {
|
||||
if (!_nameLabel) {
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||||
_nameLabel.textColor = [UIColor whiteColor];
|
||||
}
|
||||
return _nameLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)descLabel {
|
||||
if (!_descLabel) {
|
||||
_descLabel = [[UILabel alloc] init];
|
||||
_descLabel.font = [UIFont systemFontOfSize:12];
|
||||
_descLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
|
||||
_descLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
}
|
||||
return _descLabel;
|
||||
}
|
||||
|
||||
- (UIImageView *)checkImageView {
|
||||
if (!_checkImageView) {
|
||||
_checkImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ai_role_sel"]];
|
||||
}
|
||||
return _checkImageView;
|
||||
}
|
||||
|
||||
- (UIImageView *)arrowImageView {
|
||||
if (!_arrowImageView) {
|
||||
_arrowImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"right_arrow_icon"]];
|
||||
}
|
||||
return _arrowImageView;
|
||||
}
|
||||
|
||||
- (UIView *)lineView {
|
||||
if (!_lineView) {
|
||||
_lineView = [[UIView alloc] init];
|
||||
_lineView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||
}
|
||||
return _lineView;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - View
|
||||
|
||||
@interface KBAIPersonaSidebarView () <UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIVisualEffectView *blurView;
|
||||
@property (nonatomic, strong) UIView *contentView;
|
||||
@property (nonatomic, strong) UIView *searchContainer;
|
||||
@property (nonatomic, strong) UIImageView *searchIconView;
|
||||
@property (nonatomic, strong) UITextField *searchField;
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
|
||||
@property (nonatomic, strong) NSArray<KBPersonaModel *> *personas;
|
||||
@property (nonatomic, strong) NSArray<KBPersonaModel *> *displayPersonas;
|
||||
@property (nonatomic, assign) NSInteger currentPage;
|
||||
@property (nonatomic, assign) BOOL hasMore;
|
||||
|
||||
@end
|
||||
|
||||
@implementation KBAIPersonaSidebarView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setupUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
[self addSubview:self.blurView];
|
||||
[self addSubview:self.contentView];
|
||||
|
||||
[self.contentView addSubview:self.searchContainer];
|
||||
[self.searchContainer addSubview:self.searchIconView];
|
||||
[self.searchContainer addSubview:self.searchField];
|
||||
[self.contentView addSubview:self.tableView];
|
||||
|
||||
[self.blurView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.equalTo(self);
|
||||
}];
|
||||
|
||||
[self.searchContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.contentView).offset(14);
|
||||
make.left.equalTo(self.contentView).offset(16);
|
||||
make.right.equalTo(self.contentView).offset(-16);
|
||||
make.height.mas_equalTo(36);
|
||||
}];
|
||||
|
||||
[self.searchIconView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.searchContainer).offset(12);
|
||||
make.centerY.equalTo(self.searchContainer);
|
||||
make.width.height.mas_equalTo(16);
|
||||
}];
|
||||
|
||||
[self.searchField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.left.equalTo(self.searchIconView.mas_right).offset(8);
|
||||
make.right.equalTo(self.searchContainer).offset(-12);
|
||||
make.centerY.equalTo(self.searchContainer);
|
||||
make.height.mas_equalTo(28);
|
||||
}];
|
||||
|
||||
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.equalTo(self.searchContainer.mas_bottom).offset(10);
|
||||
make.left.right.bottom.equalTo(self.contentView);
|
||||
}];
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
if (!strongSelf.hasMore) {
|
||||
[strongSelf.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
return;
|
||||
}
|
||||
strongSelf.currentPage += 1;
|
||||
if ([strongSelf.delegate respondsToSelector:@selector(personaSidebarView:requestPersonasAtPage:)]) {
|
||||
[strongSelf.delegate personaSidebarView:strongSelf requestPersonasAtPage:strongSelf.currentPage];
|
||||
}
|
||||
}];
|
||||
footer.stateLabel.hidden = YES;
|
||||
footer.backgroundColor = [UIColor clearColor];
|
||||
footer.automaticallyHidden = YES;
|
||||
self.tableView.mj_footer = footer;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)requestPersonasIfNeeded {
|
||||
if (self.personas.count == 0) {
|
||||
self.currentPage = 1;
|
||||
self.hasMore = YES;
|
||||
if ([self.delegate respondsToSelector:@selector(personaSidebarView:requestPersonasAtPage:)]) {
|
||||
[self.delegate personaSidebarView:self requestPersonasAtPage:self.currentPage];
|
||||
}
|
||||
return;
|
||||
}
|
||||
[self applyFilterAndReload];
|
||||
}
|
||||
|
||||
- (void)updatePersonas:(NSArray<KBPersonaModel *> *)personas
|
||||
reset:(BOOL)reset
|
||||
hasMore:(BOOL)hasMore
|
||||
currentPage:(NSInteger)currentPage {
|
||||
self.hasMore = hasMore;
|
||||
NSInteger safePage = MAX(1, currentPage);
|
||||
// HomeVC 传入的是全量列表,这里直接替换,避免重复
|
||||
self.personas = personas ?: @[];
|
||||
self.currentPage = safePage;
|
||||
[self applyFilterAndReload];
|
||||
[self endLoadingMore];
|
||||
}
|
||||
|
||||
- (void)updateSelectedPersonaId:(NSInteger)personaId {
|
||||
self.selectedPersonaId = personaId;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)endLoadingMore {
|
||||
if ([self.tableView.mj_footer isRefreshing]) {
|
||||
if (self.hasMore) {
|
||||
[self.tableView.mj_footer endRefreshing];
|
||||
} else {
|
||||
[self.tableView.mj_footer endRefreshingWithNoMoreData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)resetLoadMore {
|
||||
[self.tableView.mj_footer resetNoMoreData];
|
||||
}
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)searchFieldChanged:(UITextField *)textField {
|
||||
[self applyFilterAndReload];
|
||||
}
|
||||
|
||||
- (void)applyFilterAndReload {
|
||||
NSString *keyword = [self.searchField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
if (keyword.length == 0) {
|
||||
self.displayPersonas = self.personas;
|
||||
} else {
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
for (KBPersonaModel *persona in self.personas) {
|
||||
NSString *name = persona.name ?: @"";
|
||||
NSString *desc = persona.shortDesc ?: persona.introText ?: @"";
|
||||
if ([name localizedCaseInsensitiveContainsString:keyword] ||
|
||||
[desc localizedCaseInsensitiveContainsString:keyword]) {
|
||||
[result addObject:persona];
|
||||
}
|
||||
}
|
||||
self.displayPersonas = result;
|
||||
}
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.displayPersonas.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView
|
||||
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
KBAIPersonaSidebarCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KBAIPersonaSidebarCell"
|
||||
forIndexPath:indexPath];
|
||||
KBPersonaModel *persona = self.displayPersonas[indexPath.row];
|
||||
BOOL selected = (persona.personaId == self.selectedPersonaId);
|
||||
[cell configureWithPersona:persona selected:selected];
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return 72.0;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row >= self.displayPersonas.count) {
|
||||
return;
|
||||
}
|
||||
KBPersonaModel *persona = self.displayPersonas[indexPath.row];
|
||||
self.selectedPersonaId = persona.personaId;
|
||||
[self.tableView reloadData];
|
||||
if ([self.delegate respondsToSelector:@selector(personaSidebarView:didSelectPersona:)]) {
|
||||
[self.delegate personaSidebarView:self didSelectPersona:persona];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIVisualEffectView *)blurView {
|
||||
if (!_blurView) {
|
||||
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
||||
_blurView = [[UIVisualEffectView alloc] initWithEffect:effect];
|
||||
}
|
||||
return _blurView;
|
||||
}
|
||||
|
||||
- (UIView *)contentView {
|
||||
if (!_contentView) {
|
||||
_contentView = [[UIView alloc] init];
|
||||
_contentView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.25];
|
||||
}
|
||||
return _contentView;
|
||||
}
|
||||
|
||||
- (UIView *)searchContainer {
|
||||
if (!_searchContainer) {
|
||||
_searchContainer = [[UIView alloc] init];
|
||||
_searchContainer.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.12];
|
||||
_searchContainer.layer.cornerRadius = 18;
|
||||
_searchContainer.clipsToBounds = YES;
|
||||
}
|
||||
return _searchContainer;
|
||||
}
|
||||
|
||||
- (UIImageView *)searchIconView {
|
||||
if (!_searchIconView) {
|
||||
_searchIconView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"magnifyingglass"]];
|
||||
_searchIconView.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
|
||||
}
|
||||
return _searchIconView;
|
||||
}
|
||||
|
||||
- (UITextField *)searchField {
|
||||
if (!_searchField) {
|
||||
_searchField = [[UITextField alloc] init];
|
||||
_searchField.placeholder = KBLocalized(@"Search Role");
|
||||
_searchField.textColor = [UIColor whiteColor];
|
||||
_searchField.font = [UIFont systemFontOfSize:14];
|
||||
_searchField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||||
[_searchField addTarget:self action:@selector(searchFieldChanged:) forControlEvents:UIControlEventEditingChanged];
|
||||
}
|
||||
return _searchField;
|
||||
}
|
||||
|
||||
- (UITableView *)tableView {
|
||||
if (!_tableView) {
|
||||
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
_tableView.backgroundColor = [UIColor clearColor];
|
||||
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
_tableView.showsVerticalScrollIndicator = NO;
|
||||
_tableView.delegate = self;
|
||||
_tableView.dataSource = self;
|
||||
[_tableView registerClass:[KBAIPersonaSidebarCell class] forCellReuseIdentifier:@"KBAIPersonaSidebarCell"];
|
||||
}
|
||||
return _tableView;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user