// // AIReportVC.m // keyBoard // // Created by Mac on 2026/1/29. // #import "AIReportVC.h" #import "AiVM.h" #pragma mark - AIReportOptionCell @interface AIReportOptionCell : UITableViewCell @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *selectButton; @property (nonatomic, assign) BOOL isSelectedOption; @end @implementation AIReportOptionCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { self.backgroundColor = [UIColor clearColor]; self.selectionStyle = UITableViewCellSelectionStyleNone; [self setupUI]; } return self; } - (void)setupUI { [self.contentView addSubview:self.titleLabel]; [self.contentView addSubview:self.selectButton]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(16); make.centerY.equalTo(self.contentView); make.right.lessThanOrEqualTo(self.selectButton.mas_left).offset(-10); }]; [self.selectButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.contentView).offset(-16); make.centerY.equalTo(self.contentView); make.width.height.mas_equalTo(24); }]; } - (void)setIsSelectedOption:(BOOL)isSelectedOption { _isSelectedOption = isSelectedOption; self.selectButton.selected = isSelectedOption; } - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [[UILabel alloc] init]; _titleLabel.font = [UIFont systemFontOfSize:14]; _titleLabel.textColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0]; } return _titleLabel; } - (UIButton *)selectButton { if (!_selectButton) { _selectButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_selectButton setImage:[UIImage imageNamed:@"report_nor_icon"] forState:UIControlStateNormal]; [_selectButton setImage:[UIImage imageNamed:@"report_sel_icon"] forState:UIControlStateSelected]; _selectButton.userInteractionEnabled = NO; } return _selectButton; } @end #pragma mark - AIReportVC @interface AIReportVC () /// 滚动容器 @property (nonatomic, strong) UIScrollView *scrollView; /// 内容容器 @property (nonatomic, strong) UIView *contentView; /// 举报原因卡片 @property (nonatomic, strong) UIView *reasonCardView; /// 举报原因标题 @property (nonatomic, strong) UILabel *reasonTitleLabel; /// 举报原因列表 @property (nonatomic, strong) UITableView *reasonTableView; /// 举报原因数据 @property (nonatomic, strong) NSArray *reportReasons; /// 选中的举报原因索引集合 @property (nonatomic, strong) NSMutableSet *selectedReasonIndexes; /// 选择内容卡片 @property (nonatomic, strong) UIView *contentCardView; /// 选择内容标题 @property (nonatomic, strong) UILabel *contentTitleLabel; /// 选择内容列表 @property (nonatomic, strong) UITableView *contentTableView; /// 选择内容数据 @property (nonatomic, strong) NSArray *contentOptions; /// 选中的内容索引集合 @property (nonatomic, strong) NSMutableSet *selectedContentIndexes; /// 举报描述卡片 @property (nonatomic, strong) UIView *descriptionCardView; /// 举报描述标题 @property (nonatomic, strong) UILabel *descriptionTitleLabel; /// 举报描述输入框 @property (nonatomic, strong) UITextView *descriptionTextView; /// 占位符标签 @property (nonatomic, strong) UILabel *placeholderLabel; /// 字数统计标签 @property (nonatomic, strong) UILabel *countLabel; /// 提交按钮 @property (nonatomic, strong) UIButton *submitButton; @property (nonatomic, strong) AiVM *viewModel; @end @implementation AIReportVC #pragma mark - Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0]; self.kb_titleLabel.text = KBLocalized(@"Report"); /// 1:初始化数据 [self initData]; /// 2:控件初始化 [self setupUI]; /// 3:绑定事件 [self bindActions]; /// 4:其他(通知/键盘等) [self bindKeyboardNotifications]; } #pragma mark - 1:初始化数据 - (void)initData { self.reportReasons = @[ KBLocalized(@"Pornographic And Vulgar"), KBLocalized(@"Politically Sensitive"), KBLocalized(@"Insult Attacks"), KBLocalized(@"Bloody Violence"), KBLocalized(@"Suicide And Self Harm"), KBLocalized(@"Plagiarism Infringement"), KBLocalized(@"Invasion Of Privacy"), KBLocalized(@"False Rumor"), KBLocalized(@"Other Harmful Information") ]; self.contentOptions = @[ KBLocalized(@"Character Name And Description"), KBLocalized(@"Picture"), KBLocalized(@"Timbre") ]; self.selectedReasonIndexes = [NSMutableSet set]; self.selectedContentIndexes = [NSMutableSet set]; } #pragma mark - 2:控件初始化 - (void)setupUI { // 先添加提交按钮(因为 scrollView 的约束依赖它) [self.view addSubview:self.submitButton]; [self.submitButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(40); make.right.equalTo(self.view).offset(-40); make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM - 20); make.height.mas_equalTo(50); }]; [self.view addSubview:self.scrollView]; [self.scrollView addSubview:self.contentView]; [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.kb_navView.mas_bottom); make.left.right.equalTo(self.view); make.bottom.equalTo(self.submitButton.mas_top).offset(-10); }]; [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.scrollView); make.width.equalTo(self.scrollView); }]; // 举报原因卡片 [self.contentView addSubview:self.reasonCardView]; [self.reasonCardView addSubview:self.reasonTitleLabel]; [self.reasonCardView addSubview:self.reasonTableView]; [self.reasonCardView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView).offset(16); make.left.equalTo(self.contentView).offset(16); make.right.equalTo(self.contentView).offset(-16); }]; [self.reasonTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.reasonCardView).offset(16); make.centerX.equalTo(self.reasonCardView); }]; CGFloat reasonTableHeight = self.reportReasons.count * 44; [self.reasonTableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.reasonTitleLabel.mas_bottom).offset(8); make.left.right.equalTo(self.reasonCardView); make.height.mas_equalTo(reasonTableHeight); make.bottom.equalTo(self.reasonCardView).offset(-8); }]; // 选择内容卡片 [self.contentView addSubview:self.contentCardView]; [self.contentCardView addSubview:self.contentTitleLabel]; [self.contentCardView addSubview:self.contentTableView]; [self.contentCardView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.reasonCardView.mas_bottom).offset(16); make.left.equalTo(self.contentView).offset(16); make.right.equalTo(self.contentView).offset(-16); }]; [self.contentTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentCardView).offset(16); make.centerX.equalTo(self.contentCardView); }]; CGFloat contentTableHeight = self.contentOptions.count * 44; [self.contentTableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentTitleLabel.mas_bottom).offset(8); make.left.right.equalTo(self.contentCardView); make.height.mas_equalTo(contentTableHeight); make.bottom.equalTo(self.contentCardView).offset(-8); }]; // 举报描述卡片 [self.contentView addSubview:self.descriptionCardView]; [self.descriptionCardView addSubview:self.descriptionTitleLabel]; [self.descriptionCardView addSubview:self.descriptionTextView]; [self.descriptionCardView addSubview:self.placeholderLabel]; [self.descriptionCardView addSubview:self.countLabel]; [self.descriptionCardView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentCardView.mas_bottom).offset(16); make.left.equalTo(self.contentView).offset(16); make.right.equalTo(self.contentView).offset(-16); make.bottom.equalTo(self.contentView).offset(-16); }]; [self.descriptionTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.descriptionCardView).offset(16); make.centerX.equalTo(self.descriptionCardView); }]; [self.descriptionTextView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.descriptionTitleLabel.mas_bottom).offset(12); make.left.equalTo(self.descriptionCardView).offset(12); make.right.equalTo(self.descriptionCardView).offset(-12); make.height.mas_equalTo(100); make.bottom.equalTo(self.descriptionCardView).offset(-30); }]; [self.placeholderLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.descriptionTextView).offset(8); make.left.equalTo(self.descriptionTextView).offset(5); make.right.equalTo(self.descriptionTextView).offset(-5); }]; [self.countLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.descriptionCardView).offset(-16); make.bottom.equalTo(self.descriptionCardView).offset(-8); }]; } #pragma mark - 3:绑定事件 - (void)bindActions { UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)]; tap.cancelsTouchesInView = NO; [self.view addGestureRecognizer:tap]; } - (void)dismissKeyboard { [self.view endEditing:YES]; } #pragma mark - 4:键盘处理 - (void)bindKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillChange:) name:UIKeyboardWillChangeFrameNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } - (void)handleKeyboardWillChange:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo ?: @{}; CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; NSInteger curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; CGRect endFrameInView = [self.view convertRect:endFrame fromView:nil]; CGFloat keyboardHeight = MAX(0, CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(endFrameInView)); UIEdgeInsets inset = self.scrollView.contentInset; inset.bottom = keyboardHeight; UIViewAnimationOptions options = (UIViewAnimationOptions)(curve << 16); [UIView animateWithDuration:duration delay:0 options:options animations:^{ self.scrollView.contentInset = inset; self.scrollView.scrollIndicatorInsets = inset; if ([self.descriptionTextView isFirstResponder]) { CGRect rect = [self.descriptionTextView convertRect:self.descriptionTextView.bounds toView:self.scrollView]; rect = CGRectInset(rect, 0, -12); [self.scrollView scrollRectToVisible:rect animated:NO]; } } completion:nil]; } - (void)handleKeyboardWillHide:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo ?: @{}; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; NSInteger curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; UIViewAnimationOptions options = (UIViewAnimationOptions)(curve << 16); [UIView animateWithDuration:duration delay:0 options:options animations:^{ self.scrollView.contentInset = UIEdgeInsetsZero; self.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero; } completion:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (tableView == self.reasonTableView) { return self.reportReasons.count; } else { return self.contentOptions.count; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { AIReportOptionCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AIReportOptionCell" forIndexPath:indexPath]; if (tableView == self.reasonTableView) { cell.titleLabel.text = self.reportReasons[indexPath.row]; cell.isSelectedOption = [self.selectedReasonIndexes containsObject:@(indexPath.row)]; } else { cell.titleLabel.text = self.contentOptions[indexPath.row]; cell.isSelectedOption = [self.selectedContentIndexes containsObject:@(indexPath.row)]; } return cell; } #pragma mark - UITableViewDelegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSMutableSet *selectedSet; if (tableView == self.reasonTableView) { selectedSet = self.selectedReasonIndexes; } else { selectedSet = self.selectedContentIndexes; } NSNumber *indexNum = @(indexPath.row); if ([selectedSet containsObject:indexNum]) { [selectedSet removeObject:indexNum]; } else { [selectedSet addObject:indexNum]; } [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 44; } #pragma mark - UITextViewDelegate - (void)textViewDidChange:(UITextView *)textView { self.placeholderLabel.hidden = textView.text.length > 0; // 限制 200 字 if (textView.text.length > 200) { textView.text = [textView.text substringToIndex:200]; } self.countLabel.text = [NSString stringWithFormat:@"%ld/200", (long)textView.text.length]; } #pragma mark - Actions - (void)submitButtonTapped { if (self.personaId <= 0) { [KBHUD showError:KBLocalized(@"Invalid parameter")]; return; } if (self.selectedReasonIndexes.count == 0) { [KBHUD showError:KBLocalized(@"Please select at least one report reason")]; return; } if (self.selectedContentIndexes.count == 0) { [KBHUD showError:KBLocalized(@"Please select at least one content type")]; return; } // 收集选中的举报原因 NSMutableArray *selectedReasons = [NSMutableArray array]; for (NSNumber *index in self.selectedReasonIndexes) { [selectedReasons addObject:self.reportReasons[index.integerValue]]; } // 收集选中的内容类型 NSMutableArray *selectedContents = [NSMutableArray array]; for (NSNumber *index in self.selectedContentIndexes) { [selectedContents addObject:self.contentOptions[index.integerValue]]; } NSString *reportDesc = [self.descriptionTextView.text ?: @"" stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSLog(@"[AIReportVC] 举报人设 ID: %ld", (long)self.personaId); NSLog(@"[AIReportVC] 举报原因: %@", selectedReasons); NSLog(@"[AIReportVC] 内容类型: %@", selectedContents); NSLog(@"[AIReportVC] 描述: %@", reportDesc); // 组装举报类型(从上到下 1-12:举报原因 1-9,内容类型 10-12) NSMutableArray *reportTypes = [NSMutableArray array]; NSArray *sortedReasonIndexes = [[self.selectedReasonIndexes allObjects] sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES]]]; for (NSNumber *index in sortedReasonIndexes) { NSInteger type = index.integerValue + 1; if (type > 0) { [reportTypes addObject:@(type)]; } } NSArray *sortedContentIndexes = [[self.selectedContentIndexes allObjects] sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES]]]; for (NSNumber *index in sortedContentIndexes) { NSInteger type = self.reportReasons.count + index.integerValue + 1; if (type > 0) { [reportTypes addObject:@(type)]; } } [KBHUD show]; __weak typeof(self) weakSelf = self; [self.viewModel reportCompanionWithCompanionId:self.personaId reportTypes:reportTypes reportDesc:reportDesc chatContext:nil evidenceImageUrl:nil completion:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ [KBHUD dismiss]; if (!success) { NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showError:msg]; return; } [KBHUD showSuccess:KBLocalized(@"Report submitted")]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf.navigationController popViewControllerAnimated:YES]; }); }); }]; } #pragma mark - Lazy Load - (UIScrollView *)scrollView { if (!_scrollView) { _scrollView = [[UIScrollView alloc] init]; _scrollView.showsVerticalScrollIndicator = NO; _scrollView.alwaysBounceVertical = YES; _scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; } return _scrollView; } - (UIView *)contentView { if (!_contentView) { _contentView = [[UIView alloc] init]; } return _contentView; } - (UIView *)reasonCardView { if (!_reasonCardView) { _reasonCardView = [[UIView alloc] init]; _reasonCardView.backgroundColor = [UIColor whiteColor]; _reasonCardView.layer.cornerRadius = 12; } return _reasonCardView; } - (UILabel *)reasonTitleLabel { if (!_reasonTitleLabel) { _reasonTitleLabel = [[UILabel alloc] init]; _reasonTitleLabel.text = KBLocalized(@"Report reason"); _reasonTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; _reasonTitleLabel.textColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0]; } return _reasonTitleLabel; } - (UITableView *)reasonTableView { if (!_reasonTableView) { _reasonTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _reasonTableView.backgroundColor = [UIColor clearColor]; _reasonTableView.separatorStyle = UITableViewCellSeparatorStyleNone; _reasonTableView.scrollEnabled = NO; _reasonTableView.delegate = self; _reasonTableView.dataSource = self; [_reasonTableView registerClass:[AIReportOptionCell class] forCellReuseIdentifier:@"AIReportOptionCell"]; } return _reasonTableView; } - (UIView *)contentCardView { if (!_contentCardView) { _contentCardView = [[UIView alloc] init]; _contentCardView.backgroundColor = [UIColor whiteColor]; _contentCardView.layer.cornerRadius = 12; } return _contentCardView; } - (UILabel *)contentTitleLabel { if (!_contentTitleLabel) { _contentTitleLabel = [[UILabel alloc] init]; _contentTitleLabel.text = KBLocalized(@"selection content"); _contentTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; _contentTitleLabel.textColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0]; } return _contentTitleLabel; } - (UITableView *)contentTableView { if (!_contentTableView) { _contentTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _contentTableView.backgroundColor = [UIColor clearColor]; _contentTableView.separatorStyle = UITableViewCellSeparatorStyleNone; _contentTableView.scrollEnabled = NO; _contentTableView.delegate = self; _contentTableView.dataSource = self; [_contentTableView registerClass:[AIReportOptionCell class] forCellReuseIdentifier:@"AIReportOptionCell"]; } return _contentTableView; } - (UIView *)descriptionCardView { if (!_descriptionCardView) { _descriptionCardView = [[UIView alloc] init]; _descriptionCardView.backgroundColor = [UIColor whiteColor]; _descriptionCardView.layer.cornerRadius = 12; } return _descriptionCardView; } - (UILabel *)descriptionTitleLabel { if (!_descriptionTitleLabel) { _descriptionTitleLabel = [[UILabel alloc] init]; _descriptionTitleLabel.text = KBLocalized(@"Report description"); _descriptionTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; _descriptionTitleLabel.textColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0]; } return _descriptionTitleLabel; } - (UITextView *)descriptionTextView { if (!_descriptionTextView) { _descriptionTextView = [[UITextView alloc] init]; _descriptionTextView.font = [UIFont systemFontOfSize:14]; _descriptionTextView.textColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1.0]; _descriptionTextView.backgroundColor = [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0]; _descriptionTextView.layer.cornerRadius = 8; _descriptionTextView.delegate = self; _descriptionTextView.textContainerInset = UIEdgeInsetsMake(8, 4, 8, 4); } return _descriptionTextView; } - (UILabel *)placeholderLabel { if (!_placeholderLabel) { _placeholderLabel = [[UILabel alloc] init]; _placeholderLabel.text = KBLocalized(@"Please describe the specific reason for your report."); _placeholderLabel.font = [UIFont systemFontOfSize:14]; _placeholderLabel.textColor = [UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0]; _placeholderLabel.numberOfLines = 0; } return _placeholderLabel; } - (UILabel *)countLabel { if (!_countLabel) { _countLabel = [[UILabel alloc] init]; _countLabel.text = @"0/200"; _countLabel.font = [UIFont systemFontOfSize:12]; _countLabel.textColor = [UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0]; } return _countLabel; } - (UIButton *)submitButton { if (!_submitButton) { _submitButton = [UIButton buttonWithType:UIButtonTypeCustom]; _submitButton.backgroundColor = [UIColor colorWithRed:0.0 green:0.75 blue:0.67 alpha:1.0]; _submitButton.layer.cornerRadius = 25; [_submitButton setTitle:KBLocalized(@"Submit") forState:UIControlStateNormal]; [_submitButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; _submitButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; [_submitButton addTarget:self action:@selector(submitButtonTapped) forControlEvents:UIControlEventTouchUpInside]; } return _submitButton; } - (AiVM *)viewModel { if (!_viewModel) { _viewModel = [[AiVM alloc] init]; } return _viewModel; } @end