2026-01-26 16:53:41 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBPersonaChatCell.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Kiro on 2026/1/26.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBPersonaChatCell.h"
|
|
|
|
|
|
#import "KBChatTableView.h"
|
|
|
|
|
|
#import "KBAiChatMessage.h"
|
2026-01-26 18:17:02 +08:00
|
|
|
|
#import "KBChatHistoryPageModel.h"
|
|
|
|
|
|
#import "AiVM.h"
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#import <Masonry/Masonry.h>
|
|
|
|
|
|
#import <SDWebImage/SDWebImage.h>
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
@interface KBPersonaChatCell () <KBChatTableViewDelegate>
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
|
|
|
|
|
/// 背景图
|
|
|
|
|
|
@property (nonatomic, strong) UIImageView *backgroundImageView;
|
|
|
|
|
|
|
|
|
|
|
|
/// 头像
|
|
|
|
|
|
@property (nonatomic, strong) UIImageView *avatarImageView;
|
|
|
|
|
|
|
|
|
|
|
|
/// 人设名称
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *nameLabel;
|
|
|
|
|
|
|
|
|
|
|
|
/// 开场白
|
|
|
|
|
|
@property (nonatomic, strong) UILabel *openingLabel;
|
|
|
|
|
|
|
|
|
|
|
|
/// 聊天列表
|
2026-01-26 20:36:51 +08:00
|
|
|
|
@property (nonatomic, strong) KBChatTableView *chatView;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
|
|
|
|
|
/// 聊天消息
|
|
|
|
|
|
@property (nonatomic, strong) NSMutableArray<KBAiChatMessage *> *messages;
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否已加载数据
|
|
|
|
|
|
@property (nonatomic, assign) BOOL hasLoadedData;
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
/// 是否正在加载
|
|
|
|
|
|
@property (nonatomic, assign) BOOL isLoading;
|
|
|
|
|
|
|
|
|
|
|
|
/// 当前页码
|
|
|
|
|
|
@property (nonatomic, assign) NSInteger currentPage;
|
|
|
|
|
|
|
|
|
|
|
|
/// 是否还有更多历史消息
|
|
|
|
|
|
@property (nonatomic, assign) BOOL hasMoreHistory;
|
|
|
|
|
|
|
|
|
|
|
|
/// AiVM 实例
|
|
|
|
|
|
@property (nonatomic, strong) AiVM *aiVM;
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBPersonaChatCell
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
|
|
|
|
if (self = [super initWithFrame:frame]) {
|
|
|
|
|
|
[self setupUI];
|
|
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 17:03:16 +08:00
|
|
|
|
/// 关键修复:Cell 复用时重置状态
|
|
|
|
|
|
- (void)prepareForReuse {
|
|
|
|
|
|
[super prepareForReuse];
|
|
|
|
|
|
|
|
|
|
|
|
// 停止音频播放
|
|
|
|
|
|
[self.chatView stopPlayingAudio];
|
|
|
|
|
|
|
|
|
|
|
|
// 重置加载状态
|
|
|
|
|
|
self.isLoading = NO;
|
|
|
|
|
|
self.hasLoadedData = NO;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#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);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2026-01-27 17:49:45 +08:00
|
|
|
|
// 开场白
|
|
|
|
|
|
[self.contentView addSubview:self.openingLabel];
|
|
|
|
|
|
[self.openingLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT);
|
|
|
|
|
|
make.left.equalTo(self.contentView).offset(40);
|
|
|
|
|
|
make.right.equalTo(self.contentView).offset(-40);
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
// 头像
|
|
|
|
|
|
[self.contentView addSubview:self.avatarImageView];
|
|
|
|
|
|
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
2026-01-27 17:49:45 +08:00
|
|
|
|
make.bottom.equalTo(self.contentView).offset(-KB_TABBAR_HEIGHT - 50 - 20);
|
|
|
|
|
|
make.left.equalTo(self.contentView).offset(20);
|
|
|
|
|
|
make.size.mas_equalTo(CGSizeMake(54, 54));
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 人设名称
|
|
|
|
|
|
[self.contentView addSubview:self.nameLabel];
|
|
|
|
|
|
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
2026-01-27 17:49:45 +08:00
|
|
|
|
make.left.equalTo(self.avatarImageView.mas_right).offset(5);
|
|
|
|
|
|
make.centerY.equalTo(self.avatarImageView);
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
// 聊天列表
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[self.contentView addSubview:self.chatView];
|
|
|
|
|
|
[self.chatView mas_makeConstraints:^(MASConstraintMaker *make) {
|
2026-01-27 17:49:45 +08:00
|
|
|
|
make.top.equalTo(self.contentView).offset(KB_NAV_TOTAL_HEIGHT);
|
|
|
|
|
|
make.left.right.equalTo(self.contentView);
|
|
|
|
|
|
make.bottom.equalTo(self.avatarImageView.mas_top).offset(-10);
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}];
|
2026-01-27 17:49:45 +08:00
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Setter
|
|
|
|
|
|
|
|
|
|
|
|
- (void)setPersona:(KBPersonaModel *)persona {
|
|
|
|
|
|
_persona = persona;
|
|
|
|
|
|
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
self.hasLoadedData = NO;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
self.isLoading = NO;
|
|
|
|
|
|
self.currentPage = 1;
|
|
|
|
|
|
self.hasMoreHistory = YES;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
self.messages = [NSMutableArray array];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
self.aiVM = [[AiVM alloc] init];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置 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;
|
|
|
|
|
|
|
2026-01-27 17:03:16 +08:00
|
|
|
|
// 关键修复:清空消息时停止音频播放,避免状态混乱
|
|
|
|
|
|
[self.chatView stopPlayingAudio];
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[self.chatView clearMessages];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - 2:数据加载
|
|
|
|
|
|
|
|
|
|
|
|
- (void)preloadDataIfNeeded {
|
2026-01-26 18:17:02 +08:00
|
|
|
|
if (self.hasLoadedData || self.isLoading) {
|
2026-01-26 16:53:41 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
[self loadChatHistory];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)loadChatHistory {
|
|
|
|
|
|
if (self.isLoading || !self.hasMoreHistory) {
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
self.isLoading = YES;
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
if (self.currentPage == 1) {
|
|
|
|
|
|
[self.chatView resetNoMoreData];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
// 使用 persona.personaId 作为 companionId
|
|
|
|
|
|
NSInteger companionId = self.persona.personaId;
|
|
|
|
|
|
|
|
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
|
|
[self.aiVM fetchChatHistoryWithCompanionId:companionId
|
|
|
|
|
|
pageNum:self.currentPage
|
|
|
|
|
|
pageSize:20
|
|
|
|
|
|
completion:^(KBChatHistoryPageModel *pageModel, NSError *error) {
|
|
|
|
|
|
weakSelf.isLoading = NO;
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
NSLog(@"[KBPersonaChatCell] 加载聊天记录失败:%@", error.localizedDescription);
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是第一次加载失败,显示开场白
|
|
|
|
|
|
if (weakSelf.currentPage == 1 && weakSelf.persona.introText.length > 0) {
|
|
|
|
|
|
[weakSelf showOpeningMessage];
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
weakSelf.hasLoadedData = YES;
|
|
|
|
|
|
weakSelf.hasMoreHistory = pageModel.hasMore;
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为 KBAiChatMessage
|
|
|
|
|
|
NSMutableArray *newMessages = [NSMutableArray array];
|
|
|
|
|
|
for (KBChatHistoryModel *item in pageModel.records) {
|
|
|
|
|
|
KBAiChatMessage *message;
|
2026-01-26 20:36:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据 sender 判断消息类型
|
|
|
|
|
|
// sender = 1: 用户消息(右侧)
|
|
|
|
|
|
// sender = 2: AI 消息(左侧)
|
|
|
|
|
|
if (item.sender == KBChatSenderUser) {
|
|
|
|
|
|
// 用户消息
|
2026-01-26 18:17:02 +08:00
|
|
|
|
message = [KBAiChatMessage userMessageWithText:item.content];
|
2026-01-26 20:36:51 +08:00
|
|
|
|
} else if (item.sender == KBChatSenderAssistant) {
|
|
|
|
|
|
// AI 消息
|
|
|
|
|
|
message = [KBAiChatMessage assistantMessageWithText:item.content];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
} else {
|
2026-01-26 20:36:51 +08:00
|
|
|
|
// 未知类型,默认为 AI 消息
|
|
|
|
|
|
NSLog(@"[KBPersonaChatCell] 未知的 sender 类型:%ld", (long)item.sender);
|
2026-01-26 18:17:02 +08:00
|
|
|
|
message = [KBAiChatMessage assistantMessageWithText:item.content];
|
|
|
|
|
|
}
|
2026-01-26 20:36:51 +08:00
|
|
|
|
|
2026-01-26 18:17:02 +08:00
|
|
|
|
message.isComplete = YES;
|
2026-01-26 20:36:51 +08:00
|
|
|
|
message.needsTypewriterEffect = NO;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
[newMessages addObject:message];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 插入到顶部(历史消息)
|
|
|
|
|
|
if (weakSelf.currentPage == 1) {
|
|
|
|
|
|
// 第一页,直接赋值
|
|
|
|
|
|
weakSelf.messages = newMessages;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 后续页,插入到顶部
|
|
|
|
|
|
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newMessages.count)];
|
|
|
|
|
|
[weakSelf.messages insertObjects:newMessages atIndexes:indexSet];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新 UI
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2026-01-26 20:36:51 +08:00
|
|
|
|
BOOL keepOffset = (weakSelf.currentPage != 1);
|
|
|
|
|
|
BOOL scrollToBottom = (weakSelf.currentPage == 1);
|
|
|
|
|
|
[weakSelf.chatView reloadWithMessages:weakSelf.messages
|
|
|
|
|
|
keepOffset:keepOffset
|
|
|
|
|
|
scrollToBottom:scrollToBottom];
|
|
|
|
|
|
[weakSelf.chatView endLoadMoreWithHasMoreData:weakSelf.hasMoreHistory];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
NSLog(@"[KBPersonaChatCell] 加载成功:第 %ld 页,%ld 条消息,还有更多:%@",
|
|
|
|
|
|
(long)weakSelf.currentPage,
|
|
|
|
|
|
(long)newMessages.count,
|
|
|
|
|
|
pageModel.hasMore ? @"是" : @"否");
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)loadMoreHistory {
|
|
|
|
|
|
if (!self.hasMoreHistory || self.isLoading) {
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[self.chatView endLoadMoreWithHasMoreData:self.hasMoreHistory];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
return;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
|
|
|
|
|
self.currentPage++;
|
|
|
|
|
|
[self loadChatHistory];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)showOpeningMessage {
|
|
|
|
|
|
// 显示开场白作为第一条消息
|
|
|
|
|
|
KBAiChatMessage *openingMsg = [KBAiChatMessage assistantMessageWithText:self.persona.introText];
|
|
|
|
|
|
openingMsg.isComplete = YES;
|
2026-01-26 20:36:51 +08:00
|
|
|
|
openingMsg.needsTypewriterEffect = NO;
|
2026-01-26 18:17:02 +08:00
|
|
|
|
[self.messages addObject:openingMsg];
|
|
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2026-01-26 20:36:51 +08:00
|
|
|
|
[self.chatView reloadWithMessages:self.messages
|
|
|
|
|
|
keepOffset:NO
|
|
|
|
|
|
scrollToBottom:YES];
|
2026-01-26 18:17:02 +08:00
|
|
|
|
});
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
#pragma mark - 3:消息追加
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (void)appendUserMessage:(NSString *)text {
|
|
|
|
|
|
if (text.length == 0) {
|
|
|
|
|
|
return;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
if (!self.messages) {
|
|
|
|
|
|
self.messages = [NSMutableArray array];
|
|
|
|
|
|
}
|
2026-01-26 16:53:41 +08:00
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
KBAiChatMessage *message = [KBAiChatMessage userMessageWithText:text];
|
|
|
|
|
|
[self.messages addObject:message];
|
|
|
|
|
|
[self.chatView addMessage:message autoScroll:YES];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (void)appendAssistantMessage:(NSString *)text
|
|
|
|
|
|
audioId:(NSString *)audioId {
|
|
|
|
|
|
if (text.length == 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!self.messages) {
|
|
|
|
|
|
self.messages = [NSMutableArray array];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KBAiChatMessage *message = [KBAiChatMessage assistantMessageWithText:text
|
|
|
|
|
|
audioId:audioId];
|
|
|
|
|
|
message.needsTypewriterEffect = YES;
|
|
|
|
|
|
[self.messages addObject:message];
|
|
|
|
|
|
[self.chatView addMessage:message autoScroll:YES];
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 16:28:17 +08:00
|
|
|
|
- (void)updateChatViewBottomInset:(CGFloat)bottomInset {
|
|
|
|
|
|
[self.chatView updateContentBottomInset:bottomInset];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
#pragma mark - KBChatTableViewDelegate
|
2026-01-26 18:17:02 +08:00
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (void)chatTableViewDidScroll:(KBChatTableView *)chatView
|
|
|
|
|
|
scrollView:(UIScrollView *)scrollView {
|
2026-01-26 18:17:02 +08:00
|
|
|
|
CGFloat offsetY = scrollView.contentOffset.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 下拉到顶部,加载历史消息
|
|
|
|
|
|
if (offsetY <= -50 && !self.isLoading) {
|
|
|
|
|
|
[self loadMoreHistory];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (void)chatTableViewDidTriggerLoadMore:(KBChatTableView *)chatView {
|
|
|
|
|
|
[self loadMoreHistory];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 16:53:41 +08:00
|
|
|
|
#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;
|
2026-01-27 17:49:45 +08:00
|
|
|
|
_avatarImageView.layer.cornerRadius = 27;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 20:36:51 +08:00
|
|
|
|
- (KBChatTableView *)chatView {
|
|
|
|
|
|
if (!_chatView) {
|
|
|
|
|
|
_chatView = [[KBChatTableView alloc] init];
|
|
|
|
|
|
_chatView.backgroundColor = [UIColor clearColor];
|
|
|
|
|
|
_chatView.delegate = self;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
2026-01-26 20:36:51 +08:00
|
|
|
|
return _chatView;
|
2026-01-26 16:53:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|