Files
keyboard/keyBoard/Class/AiTalk/VC/KBAIMessageChatingVC.m
2026-02-04 16:57:19 +08:00

407 lines
16 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBAIMessageChatingVC.m
// keyBoard
//
// Created by Mac on 2026/1/28.
//
#import "KBAIMessageChatingVC.h"
#import "AiVM.h"
#import "KBChattedCompanionModel.h"
#import "KBHUD.h"
#import "KBAIChatMessageCacheManager.h"
#import "KBAIChatDeleteConfirmView.h"
#import "LSTPopView.h"
#import <Masonry/Masonry.h>
/// 聊天会话被重置的通知
static NSString * const KBChatSessionDidResetNotification = @"KBChatSessionDidResetNotification";
@interface KBAIMessageChatingVC () <KBAIChatDeleteConfirmViewDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, strong) AiVM *viewModel;
@property (nonatomic, strong) NSMutableArray<KBChattedCompanionModel *> *chattedList;
/// 删除按钮
@property (nonatomic, strong) UIButton *deleteButton;
/// 当前长按的 indexPath
@property (nonatomic, strong) NSIndexPath *longPressIndexPath;
/// 长按手势
@property (nonatomic, strong) UILongPressGestureRecognizer *longPressGesture;
/// 单击手势(用于隐藏删除按钮)
@property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
/// 删除按钮显示代数(用于避免旧的隐藏动画 completion 误删新按钮)
@property (nonatomic, assign) NSInteger deleteButtonGeneration;
/// 待删除的 indexPath弹窗确认使用
@property (nonatomic, strong) NSIndexPath *pendingDeleteIndexPath;
/// 删除确认弹窗
@property (nonatomic, weak) LSTPopView *deleteConfirmPopView;
@end
@implementation KBAIMessageChatingVC
#pragma mark - Lifecycle
- (void)viewDidLoad {
self.listType = 1; // Chatting
[super viewDidLoad];
// 添加长按手势
[self setupLongPressGesture];
// 添加点击手势,用于隐藏删除按钮
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
self.tapGesture.cancelsTouchesInView = NO; // 不影响 cell 的点击
self.tapGesture.delegate = self;
if (self.longPressGesture) {
[self.tapGesture requireGestureRecognizerToFail:self.longPressGesture];
}
[self.tableView addGestureRecognizer:self.tapGesture];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// 删除按钮显示时,点击按钮本身不走 tableView 的 tap 逻辑
if (gestureRecognizer == self.tapGesture) {
if (self.deleteButton && !self.deleteButton.hidden) {
CGPoint p = [touch locationInView:self.deleteButton];
if (CGRectContainsPoint(self.deleteButton.bounds, p)) {
return NO;
}
}
}
return YES;
}
#pragma mark - 手势处理
- (void)setupLongPressGesture {
self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
self.longPressGesture.minimumPressDuration = 0.5; // 长按0.5秒
self.longPressGesture.delegate = self;
[self.tableView addGestureRecognizer:self.longPressGesture];
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.tableView];
NSLog(@"[KBAIMessageChatingVC] longPress state=%ld point=(%.1f, %.1f)",
(long)gesture.state, point.x, point.y);
if (gesture.state == UIGestureRecognizerStateBegan) {
// 关键:长按期间禁用 tap避免同一轮触摸结束被识别为 tap 导致“闪一下就隐藏”
if (self.tapGesture.enabled) {
self.tapGesture.enabled = NO;
NSLog(@"[KBAIMessageChatingVC] tapGesture disabled for longPress");
}
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:point];
NSLog(@"[KBAIMessageChatingVC] longPress began indexPath=%@",
indexPath);
if (indexPath) {
// 在手指位置显示删除按钮
[self showDeleteButtonAtPoint:point];
// 在 showDeleteButtonAtPoint 之后再设置,避免被 hideDeleteButton 清空
self.longPressIndexPath = indexPath;
}
} else if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed) {
if (!self.tapGesture.enabled) {
self.tapGesture.enabled = YES;
NSLog(@"[KBAIMessageChatingVC] tapGesture re-enabled after longPress");
}
}
}
- (void)handleTapGesture:(UITapGestureRecognizer *)gesture {
// 点击其他地方隐藏删除按钮
if (self.deleteButton && !self.deleteButton.hidden) {
CGPoint pointInView = [gesture locationInView:self.tableView];
NSLog(@"[KBAIMessageChatingVC] tap state=%ld point=(%.1f, %.1f)",
(long)gesture.state, pointInView.x, pointInView.y);
CGPoint pointInButton = [gesture locationInView:self.deleteButton];
// 如果点击的不是删除按钮,则隐藏
if (!CGRectContainsPoint(self.deleteButton.bounds, pointInButton)) {
NSLog(@"[KBAIMessageChatingVC] 点击了删除按钮外部,隐藏按钮");
[self hideDeleteButton];
} else {
NSLog(@"[KBAIMessageChatingVC] 点击了删除按钮内部,不隐藏");
}
}
}
- (void)showDeleteButtonAtPoint:(CGPoint)point {
// 新一轮展示:使之前的隐藏动画 completion 失效
self.deleteButtonGeneration += 1;
NSLog(@"[KBAIMessageChatingVC] showDeleteButtonAtPoint=(%.1f, %.1f) generation=%ld",
point.x, point.y, (long)self.deleteButtonGeneration);
// 创建删除按钮
if (!self.deleteButton) {
self.deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.deleteButton setTitle:@"删除此记录" forState:UIControlStateNormal];
[self.deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.deleteButton.titleLabel.font = [UIFont systemFontOfSize:14];
self.deleteButton.backgroundColor = [UIColor colorWithRed:244/255.0 green:67/255.0 blue:54/255.0 alpha:1.0]; // #F44336
self.deleteButton.layer.cornerRadius = 6;
self.deleteButton.layer.masksToBounds = YES;
[self.deleteButton addTarget:self action:@selector(deleteButtonTapped) forControlEvents:UIControlEventTouchUpInside];
// 添加阴影效果
self.deleteButton.layer.shadowColor = [UIColor blackColor].CGColor;
self.deleteButton.layer.shadowOffset = CGSizeMake(0, 2);
self.deleteButton.layer.shadowOpacity = 0.3;
self.deleteButton.layer.shadowRadius = 4;
}
// 添加到父视图(确保在最上层)
if (self.deleteButton.superview != self.view) {
[self.view addSubview:self.deleteButton];
} else {
[self.view bringSubviewToFront:self.deleteButton];
}
// 取消隐藏动画,避免闪一下
[self.deleteButton.layer removeAllAnimations];
// 设置按钮大小和位置
CGSize buttonSize = CGSizeMake(110, 40);
// 将 tableView 的坐标转换为父视图坐标
CGPoint pointInView = [self.tableView convertPoint:point toView:self.view];
// 计算按钮位置,确保不超出屏幕
CGFloat buttonX = pointInView.x - buttonSize.width / 2;
CGFloat buttonY = pointInView.y - buttonSize.height - 10; // 在手指上方10px
// 边界检查
CGFloat margin = 10;
if (buttonX < margin) {
buttonX = margin;
} else if (buttonX + buttonSize.width > self.view.bounds.size.width - margin) {
buttonX = self.view.bounds.size.width - buttonSize.width - margin;
}
if (buttonY < KB_NAV_TOTAL_HEIGHT + margin) {
// 如果上方空间不够,显示在手指下方
buttonY = pointInView.y + 10;
}
[self.deleteButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(buttonX);
make.top.equalTo(self.view).offset(buttonY);
make.width.mas_equalTo(buttonSize.width);
make.height.mas_equalTo(buttonSize.height);
}];
self.deleteButton.hidden = NO;
self.deleteButton.alpha = 1.0;
self.deleteButton.transform = CGAffineTransformIdentity;
NSLog(@"[KBAIMessageChatingVC] deleteButton shown");
// 添加弹出动画
self.deleteButton.transform = CGAffineTransformMakeScale(0.3, 0.3);
self.deleteButton.alpha = 0;
[UIView animateWithDuration:0.2 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.deleteButton.transform = CGAffineTransformIdentity;
self.deleteButton.alpha = 1.0;
} completion:nil];
}
- (void)hideDeleteButton {
if (self.deleteButton) {
NSInteger generation = self.deleteButtonGeneration;
NSLog(@"[KBAIMessageChatingVC] hideDeleteButton generation=%ld", (long)generation);
// 取消之前的动画,避免叠加
[self.deleteButton.layer removeAllAnimations];
[UIView animateWithDuration:0.15 animations:^{
self.deleteButton.alpha = 0;
self.deleteButton.transform = CGAffineTransformMakeScale(0.8, 0.8);
} completion:^(BOOL finished) {
// 如果期间又展示了新一轮按钮,则不执行移除(避免“闪一下”)
if (generation != self.deleteButtonGeneration) {
NSLog(@"[KBAIMessageChatingVC] hide completion ignored (generation changed: %ld -> %ld)",
(long)generation, (long)self.deleteButtonGeneration);
return;
}
self.deleteButton.hidden = YES;
[self.deleteButton removeFromSuperview];
}];
}
self.longPressIndexPath = nil;
}
- (void)deleteButtonTapped {
if (!self.longPressIndexPath) {
return;
}
// 保存 indexPath因为 hideDeleteButton 会清空
self.pendingDeleteIndexPath = self.longPressIndexPath;
[self hideDeleteButton];
[self showDeleteConfirmPopView];
}
#pragma mark - 删除确认弹窗
- (void)showDeleteConfirmPopView {
if (self.deleteConfirmPopView) {
[self.deleteConfirmPopView dismiss];
}
CGFloat width = KB_SCREEN_WIDTH - 80;
KBAIChatDeleteConfirmView *content = [[KBAIChatDeleteConfirmView alloc] initWithFrame:CGRectMake(0, 0, width, 260)];
content.delegate = self;
LSTPopView *popView = [LSTPopView initWithCustomView:content
parentView:nil
popStyle:LSTPopStyleFade
dismissStyle:LSTDismissStyleFade];
popView.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
popView.hemStyle = LSTHemStyleCenter;
popView.isClickBgDismiss = YES;
popView.isAvoidKeyboard = NO;
self.deleteConfirmPopView = popView;
[popView pop];
}
#pragma mark - KBAIChatDeleteConfirmViewDelegate
- (void)chatDeleteConfirmViewDidTapDelete:(KBAIChatDeleteConfirmView *)view {
[self.deleteConfirmPopView dismiss];
NSIndexPath *indexPath = self.pendingDeleteIndexPath;
self.pendingDeleteIndexPath = nil;
[self performDeleteAtIndexPath:indexPath];
}
- (void)chatDeleteConfirmViewDidTapCancel:(KBAIChatDeleteConfirmView *)view {
[self.deleteConfirmPopView dismiss];
self.pendingDeleteIndexPath = nil;
}
#pragma mark - 删除逻辑
- (void)performDeleteAtIndexPath:(NSIndexPath *)indexPath {
if (!indexPath) {
return;
}
// 获取要删除的数据
if (indexPath.row >= self.chattedList.count) {
NSLog(@"[KBAIMessageChatingVC] 错误索引越界row=%ld, count=%ld",
(long)indexPath.row, (long)self.chattedList.count);
return;
}
KBChattedCompanionModel *model = self.chattedList[indexPath.row];
NSInteger companionId = model.companionId;
NSLog(@"[KBAIMessageChatingVC] 开始删除聊天记录companionId=%ld, name=%@",
(long)companionId, model.name);
// 显示加载提示
[KBHUD show];
__weak typeof(self) weakSelf = self;
// 调用清空聊天会话的 API
[self.viewModel resetChatSessionWithCompanionId:companionId
completion:^(KBChatSessionResetResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[KBHUD dismiss];
if (error) {
NSLog(@"[KBAIMessageChatingVC] 删除失败:%@", error.localizedDescription);
[KBHUD showError:@"删除失败,请重试"];
return;
}
NSLog(@"[KBAIMessageChatingVC] ✅ API 调用成功,开始清理本地数据");
// 1. 删除本地列表数据
if (indexPath.row < weakSelf.chattedList.count) {
[weakSelf.chattedList removeObjectAtIndex:indexPath.row];
}
if (indexPath.row < weakSelf.dataArray.count) {
[weakSelf.dataArray removeObjectAtIndex:indexPath.row];
}
// 2. 更新 TableView带动画
[weakSelf.tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
// 3. ✅ 清除缓存管理器中的聊天记录(关键!)
[[KBAIChatMessageCacheManager shared] clearMessagesForCompanionId:companionId];
NSLog(@"[KBAIMessageChatingVC] ✅ 已清除缓存companionId=%ld", (long)companionId);
// 4. 发送通知,通知其他页面(主页)刷新
[[NSNotificationCenter defaultCenter] postNotificationName:KBChatSessionDidResetNotification
object:nil
userInfo:@{@"companionId": @(companionId)}];
NSLog(@"[KBAIMessageChatingVC] ✅ 已发送重置通知companionId=%ld", (long)companionId);
// 5. 显示成功提示
[KBHUD showSuccess:@"已删除"];
});
}];
}
#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 - Lazy Load
- (AiVM *)viewModel {
if (!_viewModel) {
_viewModel = [[AiVM alloc] init];
}
return _viewModel;
}
- (NSMutableArray<KBChattedCompanionModel *> *)chattedList {
if (!_chattedList) {
_chattedList = [NSMutableArray array];
}
return _chattedList;
}
@end