- 手动集成微信 OpenSDK-NoPay,创建 XJWeChatManager 统一管理分享能力 - 地图卡片新增长按手势,通过代理模式实现「转发到微信」,分享含高德导航链接 - 聊天页新增群成员侧滑面板,支持查看成员列表及一键拨号 - 新增急救资源详情页(XJFirstAidResourceDetailVC)及选择联系人页 - 首页健康知识/资讯模块,含双视图列表及 H5 跳转 - 修复急救资源详情页编译报错 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
435 lines
16 KiB
Objective-C
435 lines
16 KiB
Objective-C
//
|
||
// XJHealthKnowledgeView.m
|
||
// XinjiangProject
|
||
//
|
||
// Created by Apple on 2026/6/29.
|
||
//
|
||
|
||
#import "XJHealthKnowledgeView.h"
|
||
#import "XJHealthKnowledgeModel.h"
|
||
#import "XJHealthKnowledgeCell.h"
|
||
|
||
static CGFloat const kHeaderHeight = 40;
|
||
static CGFloat const kSearchBoxHeight = 36;
|
||
static CGFloat const kCellHeight = 130;
|
||
static CGFloat const kMaxCellCount = 3;
|
||
static CGFloat const kTabButtonGap = 43;
|
||
static CGFloat const kDefaultEmptyHeight = 200;
|
||
|
||
// 标签区:文字 14pt(≈17) + 指示间距6 + 指示2 + 分割间距9 + 分割1 = ~35
|
||
static CGFloat const kTabAreaHeight = 48;
|
||
|
||
@interface XJHealthKnowledgeView () <UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate>
|
||
|
||
@property (nonatomic, strong) UIView * headerView;
|
||
@property (nonatomic, strong) UIImageView * headerBgImg;
|
||
@property (nonatomic, strong) UILabel * titleLabel;
|
||
@property (nonatomic, strong) UIButton * moreBtn;
|
||
|
||
@property (nonatomic, strong) UIView * searchContainer;
|
||
@property (nonatomic, strong) UIImageView * searchIcon;
|
||
@property (nonatomic, strong) UITextField * searchTextField;
|
||
@property (nonatomic, strong) UIButton * searchBtn;
|
||
|
||
@property (nonatomic, strong) UIScrollView * tabScrollView;
|
||
@property (nonatomic, strong) NSMutableArray<UIButton *> * tabButtons;
|
||
@property (nonatomic, strong) UIView * indicatorView;
|
||
@property (nonatomic, strong) UIView * tabSeparator;
|
||
|
||
@property (nonatomic, strong) UITableView * listTableView;
|
||
@property (nonatomic, strong) UILabel * emptyLabel;
|
||
|
||
@end
|
||
|
||
@implementation XJHealthKnowledgeView
|
||
|
||
- (instancetype)initWithFrame:(CGRect)frame {
|
||
if (self = [super initWithFrame:frame]) {
|
||
self.backgroundColor = KWhiteColor;
|
||
self.layer.cornerRadius = 12;
|
||
self.layer.masksToBounds = YES;
|
||
self.tabButtons = [[NSMutableArray alloc] init];
|
||
[self createUI];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
#pragma mark - ————— UI 搭建 —————
|
||
|
||
- (void)createUI {
|
||
|
||
// 头部
|
||
self.headerView = [[UIView alloc] init];
|
||
[self addSubview:self.headerView];
|
||
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.top.mas_offset(0);
|
||
make.height.mas_offset(kRealValue(kHeaderHeight));
|
||
}];
|
||
|
||
self.headerBgImg = [[UIImageView alloc] init];
|
||
self.headerBgImg.image = [UIImage imageNamed:@"heaedTitleBackImg"];
|
||
[self.headerView addSubview:self.headerBgImg];
|
||
[self.headerBgImg mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.mas_offset(0);
|
||
}];
|
||
|
||
self.titleLabel = [[UILabel alloc] init];
|
||
self.titleLabel.text = @"健康知识";
|
||
self.titleLabel.textColor = CFontColor1;
|
||
self.titleLabel.font = BOLDSYSTEMFONT(16);
|
||
[self.headerView addSubview:self.titleLabel];
|
||
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.mas_offset(13);
|
||
make.centerY.mas_equalTo(self.headerView);
|
||
}];
|
||
|
||
self.moreBtn = [[UIButton alloc] init];
|
||
[self.moreBtn setImage:[UIImage imageNamed:@"wacthRightAccow"] forState:UIControlStateNormal];
|
||
[self.moreBtn addTarget:self action:@selector(moreAction) forControlEvents:UIControlEventTouchUpInside];
|
||
[self.headerView addSubview:self.moreBtn];
|
||
[self.moreBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.right.mas_offset(-8);
|
||
make.centerY.mas_equalTo(self.headerView);
|
||
make.width.height.mas_offset(30);
|
||
}];
|
||
|
||
// 搜索框 - 圆角 18 背景 #F5F7FB
|
||
self.searchContainer = [[UIView alloc] init];
|
||
self.searchContainer.backgroundColor = UIColorHex(#F5F7FB);
|
||
self.searchContainer.layer.cornerRadius = 18;
|
||
self.searchContainer.layer.masksToBounds = YES;
|
||
self.searchContainer.hidden = YES; // 初始隐藏,等数据来了再显示
|
||
[self addSubview:self.searchContainer];
|
||
[self.searchContainer mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.mas_offset(15);
|
||
make.right.mas_offset(-15);
|
||
make.top.mas_equalTo(self.headerView.mas_bottom).offset(10);
|
||
make.height.mas_offset(kSearchBoxHeight);
|
||
}];
|
||
|
||
self.searchIcon = [[UIImageView alloc] init];
|
||
self.searchIcon.image = [UIImage imageNamed:@"searchLeftIcon"];
|
||
self.searchIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||
[self.searchContainer addSubview:self.searchIcon];
|
||
[self.searchIcon mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.mas_offset(15);
|
||
make.centerY.mas_equalTo(self.searchContainer);
|
||
make.width.height.mas_offset(14);
|
||
}];
|
||
|
||
self.searchBtn = [[UIButton alloc] init];
|
||
self.searchBtn.backgroundColor = UIColorHex(#14BEBE);
|
||
self.searchBtn.layer.cornerRadius = 14;
|
||
self.searchBtn.layer.masksToBounds = YES;
|
||
[self.searchBtn setTitle:@"搜索" forState:UIControlStateNormal];
|
||
[self.searchBtn setTitleColor:KWhiteColor forState:UIControlStateNormal];
|
||
self.searchBtn.titleLabel.font = MEDIUMFONT(14);
|
||
[self.searchBtn addTarget:self action:@selector(searchAction) forControlEvents:UIControlEventTouchUpInside];
|
||
[self.searchContainer addSubview:self.searchBtn];
|
||
[self.searchBtn mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.right.mas_offset(0);
|
||
make.centerY.mas_equalTo(self.searchContainer);
|
||
make.width.mas_offset(55);
|
||
make.height.mas_offset(28);
|
||
}];
|
||
|
||
self.searchTextField = [[UITextField alloc] init];
|
||
self.searchTextField.placeholder = @"请输入关键字进行搜索";
|
||
self.searchTextField.font = [UIFont systemFontOfSize:13 weight:UIFontWeightRegular];
|
||
self.searchTextField.textColor = UIColorHex(#252535);
|
||
// placeholder 颜色
|
||
self.searchTextField.attributedPlaceholder = [[NSAttributedString alloc]
|
||
initWithString:@"请输入关键字进行搜索"
|
||
attributes:@{NSForegroundColorAttributeName: UIColorHex(#B6B6B6)}];
|
||
self.searchTextField.delegate = self;
|
||
[self.searchContainer addSubview:self.searchTextField];
|
||
[self.searchTextField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.mas_equalTo(self.searchIcon.mas_right).offset(8);
|
||
make.right.mas_equalTo(self.searchBtn.mas_left).offset(-8);
|
||
make.centerY.mas_equalTo(self.searchContainer);
|
||
make.height.mas_equalTo(self.searchContainer);
|
||
}];
|
||
|
||
// 标签切换区
|
||
self.tabScrollView = [[UIScrollView alloc] init];
|
||
self.tabScrollView.showsHorizontalScrollIndicator = NO;
|
||
self.tabScrollView.showsVerticalScrollIndicator = NO;
|
||
self.tabScrollView.hidden = YES; // 初始隐藏
|
||
[self addSubview:self.tabScrollView];
|
||
[self.tabScrollView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.mas_offset(0);
|
||
make.top.mas_equalTo(self.searchContainer.mas_bottom).offset(12);
|
||
make.height.mas_offset(kTabAreaHeight);
|
||
}];
|
||
|
||
// 指示条 13×2
|
||
self.indicatorView = [[UIView alloc] init];
|
||
self.indicatorView.backgroundColor = UIColorHex(#14BEBE);
|
||
self.indicatorView.layer.cornerRadius = 1;
|
||
self.indicatorView.layer.masksToBounds = YES;
|
||
self.indicatorView.hidden = YES;
|
||
[self.tabScrollView addSubview:self.indicatorView];
|
||
|
||
// 分割线 在指示条底部 +9
|
||
self.tabSeparator = [[UIView alloc] init];
|
||
self.tabSeparator.backgroundColor = UIColorHex(#EAECF1);
|
||
[self.tabScrollView addSubview:self.tabSeparator];
|
||
|
||
// 列表
|
||
self.listTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||
self.listTableView.delegate = self;
|
||
self.listTableView.dataSource = self;
|
||
self.listTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||
self.listTableView.scrollEnabled = NO;
|
||
self.listTableView.rowHeight = kCellHeight;
|
||
self.listTableView.backgroundColor = KWhiteColor;
|
||
self.listTableView.hidden = YES; // 初始隐藏
|
||
if (@available(iOS 15.0, *)) {
|
||
self.listTableView.sectionHeaderTopPadding = 0;
|
||
}
|
||
[self addSubview:self.listTableView];
|
||
[self.listTableView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.mas_offset(0);
|
||
make.top.mas_equalTo(self.tabScrollView.mas_bottom);
|
||
make.height.mas_offset(0);
|
||
}];
|
||
|
||
// 空状态 - 放在 tabScrollView 下方
|
||
self.emptyLabel = [[UILabel alloc] init];
|
||
self.emptyLabel.text = @"暂无健康知识";
|
||
self.emptyLabel.textColor = UIColorHex(#14BEBE);
|
||
self.emptyLabel.font = [UIFont systemFontOfSize:15];
|
||
self.emptyLabel.textAlignment = NSTextAlignmentCenter;
|
||
self.emptyLabel.hidden = YES;
|
||
[self addSubview:self.emptyLabel];
|
||
[self.emptyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.centerX.mas_equalTo(self);
|
||
make.top.mas_equalTo(self.tabScrollView.mas_bottom).offset(40);
|
||
}];
|
||
}
|
||
|
||
#pragma mark - ————— Setter —————
|
||
|
||
- (void)setDataArray:(NSArray<XJHealthKnowledgeModel *> *)dataArray {
|
||
_dataArray = dataArray;
|
||
|
||
BOOL hasData = dataArray.count > 0;
|
||
|
||
self.listTableView.hidden = !hasData;
|
||
self.emptyLabel.hidden = hasData;
|
||
|
||
if (hasData) {
|
||
[self.listTableView reloadData];
|
||
CGFloat tableH = MIN(dataArray.count, kMaxCellCount) * kCellHeight;
|
||
[self.listTableView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.mas_offset(0);
|
||
make.top.mas_equalTo(self.tabScrollView.mas_bottom);
|
||
make.height.mas_offset(tableH);
|
||
}];
|
||
} else {
|
||
[self.listTableView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.mas_offset(0);
|
||
make.top.mas_equalTo(self.tabScrollView.mas_bottom);
|
||
make.height.mas_offset(0);
|
||
}];
|
||
}
|
||
}
|
||
|
||
- (void)setTabTitles:(NSArray<NSString *> *)tabTitles {
|
||
_tabTitles = tabTitles;
|
||
|
||
for (UIButton * btn in self.tabButtons) {
|
||
[btn removeFromSuperview];
|
||
}
|
||
[self.tabButtons removeAllObjects];
|
||
|
||
self.tabScrollView.hidden = (tabTitles.count == 0);
|
||
self.searchContainer.hidden = (tabTitles.count == 0);
|
||
|
||
if (tabTitles.count == 0) {
|
||
self.indicatorView.hidden = YES;
|
||
return;
|
||
}
|
||
|
||
self.indicatorView.hidden = NO;
|
||
|
||
// 按钮高度给满 kTabAreaHeight 让点击范围大
|
||
CGFloat btnHeight = kTabAreaHeight - 2 - 6 - 9 - 1; // 文字区域 ≈30pt
|
||
CGFloat btnTop = 0;
|
||
|
||
CGFloat leftOffset = 15;
|
||
for (NSInteger i = 0; i < tabTitles.count; i++) {
|
||
NSString * title = tabTitles[i];
|
||
|
||
CGSize textSize = [title sizeWithAttributes:@{NSFontAttributeName: BOLDSYSTEMFONT(14)}];
|
||
CGFloat btnWidth = ceil(textSize.width) + 4; // 左右各留 2pt
|
||
|
||
// 透明大按钮包住文字区域
|
||
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||
[btn setTitle:title forState:UIControlStateNormal];
|
||
btn.titleLabel.font = (i == self.selectedTabIndex) ? BOLDSYSTEMFONT(14) : [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
|
||
[btn setTitleColor:(i == self.selectedTabIndex) ? UIColorHex(#14BEBE) : UIColorHex(#808080) forState:UIControlStateNormal];
|
||
btn.backgroundColor = KClearColor;
|
||
btn.tag = i;
|
||
[btn addTarget:self action:@selector(tabButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||
btn.frame = CGRectMake(leftOffset, btnTop, btnWidth, btnHeight);
|
||
[self.tabScrollView addSubview:btn];
|
||
[self.tabButtons addObject:btn];
|
||
|
||
leftOffset += btnWidth + kTabButtonGap;
|
||
}
|
||
|
||
leftOffset += 15 - kTabButtonGap;
|
||
self.tabScrollView.contentSize = CGSizeMake(leftOffset, kTabAreaHeight);
|
||
|
||
// 更新分割线位置:在标签区底部
|
||
self.tabSeparator.frame = CGRectMake(0, kTabAreaHeight - 1, MAX(leftOffset, self.tabScrollView.bounds.size.width), 1);
|
||
|
||
// 强制布局,确保按钮 titleLabel.frame 计算正确
|
||
[self.tabScrollView layoutIfNeeded];
|
||
|
||
if (self.tabButtons.count > 0) {
|
||
[self updateIndicatorPosition:self.selectedTabIndex animated:NO];
|
||
}
|
||
}
|
||
|
||
- (void)setSelectedTabIndex:(NSInteger)selectedTabIndex {
|
||
if (selectedTabIndex < 0 || selectedTabIndex >= self.tabButtons.count) return;
|
||
_selectedTabIndex = selectedTabIndex;
|
||
|
||
for (NSInteger i = 0; i < self.tabButtons.count; i++) {
|
||
UIButton * btn = self.tabButtons[i];
|
||
if (i == selectedTabIndex) {
|
||
[btn setTitleColor:UIColorHex(#14BEBE) forState:UIControlStateNormal];
|
||
btn.titleLabel.font = BOLDSYSTEMFONT(14);
|
||
} else {
|
||
[btn setTitleColor:UIColorHex(#808080) forState:UIControlStateNormal];
|
||
btn.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
|
||
}
|
||
}
|
||
[self.tabScrollView layoutIfNeeded];
|
||
[self updateIndicatorPosition:selectedTabIndex animated:YES];
|
||
}
|
||
|
||
- (void)showNotLoggedInPlaceholder {
|
||
self.dataArray = @[];
|
||
self.tabTitles = nil;
|
||
self.emptyLabel.text = @"暂未登录";
|
||
self.emptyLabel.textColor = [UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0];
|
||
self.emptyLabel.hidden = NO;
|
||
// 未登录时在头部下方区域上下居中
|
||
[self.emptyLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||
make.centerX.mas_equalTo(self);
|
||
make.top.mas_equalTo(self.headerView.mas_bottom);
|
||
make.bottom.mas_equalTo(self);
|
||
}];
|
||
}
|
||
|
||
#pragma mark - ————— 高度计算 —————
|
||
|
||
- (CGFloat)totalHeight {
|
||
BOOL hasTabs = self.tabTitles.count > 0;
|
||
BOOL hasData = self.dataArray.count > 0;
|
||
|
||
if (!hasTabs) {
|
||
return kDefaultEmptyHeight;
|
||
}
|
||
|
||
// 头部(40) + 10 + 搜索(36) + 12 + 标签(48) + 列表/空区域
|
||
CGFloat base = kRealValue(kHeaderHeight) + 10 + kSearchBoxHeight + 12 + kTabAreaHeight;
|
||
if (hasData) {
|
||
CGFloat tableH = MIN(self.dataArray.count, kMaxCellCount) * kCellHeight;
|
||
return base + tableH;
|
||
} else {
|
||
return base + 80;
|
||
}
|
||
}
|
||
|
||
#pragma mark - ————— 私有 —————
|
||
|
||
- (void)updateIndicatorPosition:(NSInteger)index animated:(BOOL)animated {
|
||
if (index < 0 || index >= self.tabButtons.count) return;
|
||
|
||
UIButton * selectedBtn = self.tabButtons[index];
|
||
|
||
CGFloat indicatorW = 13;
|
||
CGFloat indicatorH = 2;
|
||
CGFloat indicatorX = selectedBtn.center.x - indicatorW / 2;
|
||
// 文字底部 +6
|
||
CGFloat textBottom = CGRectGetMaxY(selectedBtn.titleLabel.frame);
|
||
// 兜底:14pt 字体行高 ≈ 17,垂直居中
|
||
if (textBottom <= 0) {
|
||
CGFloat lineH = 17;
|
||
textBottom = (selectedBtn.frame.size.height - lineH) / 2.0 + lineH;
|
||
}
|
||
CGFloat indicatorY = textBottom + 6;
|
||
|
||
void (^updateBlock)(void) = ^{
|
||
self.indicatorView.frame = CGRectMake(indicatorX, indicatorY, indicatorW, indicatorH);
|
||
};
|
||
|
||
if (animated) {
|
||
[UIView animateWithDuration:0.25 animations:updateBlock];
|
||
} else {
|
||
updateBlock();
|
||
}
|
||
}
|
||
|
||
#pragma mark - ————— 事件 —————
|
||
|
||
- (void)moreAction {
|
||
if (self.moreBlock) self.moreBlock();
|
||
}
|
||
|
||
- (void)searchAction {
|
||
if (self.searchBlock) self.searchBlock(self.searchTextField.text ?: @"");
|
||
}
|
||
|
||
- (void)tabButtonTapped:(UIButton *)sender {
|
||
if (sender.tag == self.selectedTabIndex) return;
|
||
self.selectedTabIndex = sender.tag;
|
||
if (self.tabSelectBlock) self.tabSelectBlock(sender.tag);
|
||
}
|
||
|
||
#pragma mark - ————— UITextFieldDelegate —————
|
||
|
||
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
|
||
return YES;
|
||
}
|
||
|
||
- (void)textFieldDidBeginEditing:(UITextField *)textField {
|
||
if (self.searchBeginBlock) self.searchBeginBlock();
|
||
}
|
||
|
||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||
[textField resignFirstResponder];
|
||
if (self.searchBlock) self.searchBlock(textField.text ?: @"");
|
||
return YES;
|
||
}
|
||
|
||
#pragma mark - ————— UITableViewDataSource —————
|
||
|
||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||
return MIN(self.dataArray.count, kMaxCellCount);
|
||
}
|
||
|
||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||
static NSString * cellID = @"XJHealthKnowledgeCell";
|
||
XJHealthKnowledgeCell * cell = [tableView dequeueReusableCellWithIdentifier:cellID];
|
||
if (!cell) {
|
||
cell = [[XJHealthKnowledgeCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
|
||
}
|
||
cell.model = self.dataArray[indexPath.row];
|
||
return cell;
|
||
}
|
||
|
||
#pragma mark - ————— UITableViewDelegate —————
|
||
|
||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||
if (self.cellSelectBlock && indexPath.row < self.dataArray.count) {
|
||
self.cellSelectBlock(self.dataArray[indexPath.row]);
|
||
}
|
||
}
|
||
|
||
@end
|