feat(home): 首页增加健康知识/资讯模块,含双视图列表及H5跳转

- 新增健康知识视图(标签切换+搜索+列表)和健康资讯视图(纯列表无搜索)
- 对接健康栏目列表和文章分页接口,资讯无子分类直接用父级ID
- 接入更多/搜索/详情H5跳转,统一用通用加密串传参
- 键盘弹起时自动避让,从H5回来刷新文章收藏数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jiayangyang
2026-06-29 18:46:58 +08:00
co-authored by Claude Opus 4.7
parent 2c2f307247
commit 6337454956
25 changed files with 2080 additions and 85 deletions
@@ -375,7 +375,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = A7CC5LW224; DEVELOPMENT_TEAM = A7CC5LW224;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_PREFIX_HEADER = "$(SRCROOT)/XinjiangProject/Supporting Files/PrefixHeader.pch"; GCC_PREFIX_HEADER = "$(SRCROOT)/XinjiangProject/Supporting Files/PrefixHeader.pch";
@@ -400,7 +400,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.11; MARKETING_VERSION = 1.0.13;
PRODUCT_BUNDLE_IDENTIFIER = com.questionTools.XinjiangProject; PRODUCT_BUNDLE_IDENTIFIER = com.questionTools.XinjiangProject;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -419,7 +419,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = A7CC5LW224; DEVELOPMENT_TEAM = A7CC5LW224;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_PREFIX_HEADER = "$(SRCROOT)/XinjiangProject/Supporting Files/PrefixHeader.pch"; GCC_PREFIX_HEADER = "$(SRCROOT)/XinjiangProject/Supporting Files/PrefixHeader.pch";
@@ -444,7 +444,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.11; MARKETING_VERSION = 1.0.13;
PRODUCT_BUNDLE_IDENTIFIER = com.questionTools.XinjiangProject; PRODUCT_BUNDLE_IDENTIFIER = com.questionTools.XinjiangProject;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -61,40 +61,49 @@
#pragma mark: 网络前缀 #pragma mark: 网络前缀
#define ResourceAddress [NSString stringWithFormat:@"%@/file/show/",Http] #define ResourceAddress [NSString stringWithFormat:@"%@/file/show/",Http]
////开发环境地址 彭杰
//#define Http @"http://192.168.1.47"//昊
//#define H5DevHost @"xj-admin.mcrm.vip:8888"//
//#define IntervenH5 @"https://console-xj-admin.mcrm.vip:8888"
//#define SCHEME @"https" //
//正式地址 //正式地址
//#define Http @"https://api-jkglpt.iosp.ydpt.tech"// #define Http @"https://api-jkglpt.iosp.ydpt.tech"//
//#define H5DevHost @"api-jkglpt.iosp.ydpt.tech"// #define H5DevHost @"api-jkglpt.iosp.ydpt.tech"//
//#define IntervenH5 @"https://console-jkglpt.iosp.ydpt.tech" #define IntervenH5 @"https://console-jkglpt.iosp.ydpt.tech"
//#define SCHEME @"https" // #define SCHEME @"https" //
#define OtherHttp @"https://api-jkglpt.iosp.ydpt.tech/hb" //
//卫鑫本地
//#define Http @"https://api-jkglpt.iosp.ydpt.tech"//
//#define H5DevHost @"api-jkglpt.iosp.ydpt.tech"//
//#define IntervenH5 @"http://192.168.1.26:9876"
//#define SCHEME @"https" //
//最新新疆测试地址 //最新新疆测试地址
//#define Http @"http://192.168.1.223"//彭杰 //#define Http @"http://192.168.1.223"//彭杰
// 最新地址 // 最新地址
//#define Http @"http://192.168.1.47"//王昊 //#define Http @"http://192.168.1.47"//王昊
//#define Http @"https://xjxc-api.mcrm.vip:8888"//
//#define H5DevHost @"xjxc-api.mcrm.vip:8888"//
//#define IntervenH5 @"https://xj-admin.mcrm.vip:8888"
//#define SCHEME @"https" //
//#define OtherHttp @"https://gstest.superwx.cn/xj_health" //
#define Http @"https://xj-api.mcrm.vip:8888"//
//#define Http @"http://192.168.1.22"//王昊
#define H5DevHost @"xj-api.mcrm.vip:8888"//
#define IntervenH5 @"https://xj-admin.mcrm.vip:8888"
#define SCHEME @"https" //
//新疆项目后期的调试环境由阿里云迁移至公司内部主机服务器,功能联调和测试可用以下的环境地址 #pragma mark - ——————— 健康知识/资讯 ————————
//https://api-xj.tri.icdat.cn
//https://console-xj.tri.icdat.cn/ // H5 页面地址
//接口: http://api-xj.tri.dt.io/ #define kHealthKnowMoreH5 @"/healthnewsweb/healthKnow.html"
//后台: http://console-xj.tri.dt.io/ #define kHealthNewsMoreH5 @"/healthnewsweb/healthNews.html"
//大屏: http://screen-xj.tri.dt.io/home #define kHealthDetailH5 @"/healthnewsweb/healthNewsDetail.html"
#define kHealthSearchH5 @"/healthnewsweb/healthSecrch.html"
// HMAC-SHA256 签名密钥
#define kHealthKnowledgeSecretKey @"XJp3EHpcnwReXc1A"
//测试 健康知识+资讯
//#define kHealthKnowledgeBaseURL @"https://gstest.superwx.cn/healthcheck"
//#define kHealthKnowledgeH5Host @"https://gstest.superwx.cn"
//正式 健康知识+资讯
#define kHealthKnowledgeBaseURL @"https://api-jkglpt.iosp.ydpt.tech/hb"
#define kHealthKnowledgeH5Host @"https://console-jkglpt.iosp.ydpt.tech/hb"
// API 路径
#define kHealthKnowledgeTokenPath @"/pe/frontend/init"
#define kHealthKnowledgeCategoryPath @"/cms/frontend/category/list"
#define kHealthKnowledgeArticleListPath @"/cms/frontend/articleList"
#endif /* StatusMacros_h */ #endif /* StatusMacros_h */
@@ -0,0 +1,47 @@
//
// XJHealthKnowledgeManager.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <Foundation/Foundation.h>
#import "XJHealthCategoryModel.h"
#import "XJHealthArticleModel.h"
NS_ASSUME_NONNULL_BEGIN
/// 健康知识网络管理器
@interface XJHealthKnowledgeManager : NSObject
+ (instancetype)sharedManager;
/// 当前 token(获取后缓存)
@property (nonatomic, copy, readonly, nullable) NSString * cachedToken;
/// 清除缓存的 token(退出登录时调用)
- (void)clearToken;
/// 获取 token
- (void)getTokenWithUserId:(NSString *)userId
workNo:(NSString *)workNo
success:(void (^)(NSString * token))success
failure:(void (^)(NSString * errorMsg))failure;
/// 获取栏目列表
- (void)getCategoryListWithToken:(NSString *)token
success:(void (^)(NSArray<XJHealthCategoryModel *> * categories))success
failure:(void (^)(NSString * errorMsg))failure;
/// 分页查询文章(showAppHomepage 固定 Y
- (void)getArticleListWithToken:(NSString *)token
categoryId:(NSInteger)categoryId
articleType:(nullable NSString *)articleType
pageNum:(NSInteger)pageNum
pageSize:(NSInteger)pageSize
success:(void (^)(NSArray<XJHealthArticleModel *> * articles, NSInteger total))success
failure:(void (^)(NSString * errorMsg))failure;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,222 @@
//
// XJHealthKnowledgeManager.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import "XJHealthKnowledgeManager.h"
#import <CommonCrypto/CommonHMAC.h>
#pragma mark - ————— Manager —————
@interface XJHealthKnowledgeManager ()
@property (nonatomic, copy) NSString * cachedToken;
@end
@implementation XJHealthKnowledgeManager
+ (instancetype)sharedManager {
static XJHealthKnowledgeManager * instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[XJHealthKnowledgeManager alloc] init];
});
return instance;
}
#pragma mark - ————— HMAC-SHA256 —————
- (void)clearToken {
self.cachedToken = nil;
DLog(@"[健康知识] Token 已清除");
}
+ (NSString *)hmacSHA256:(NSString *)data key:(NSString *)key {
const char * cKey = [key cStringUsingEncoding:NSUTF8StringEncoding];
const char * cData = [data cStringUsingEncoding:NSUTF8StringEncoding];
unsigned char result[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), result);
NSMutableString * hash = [[NSMutableString alloc] initWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[hash appendFormat:@"%02x", result[i]];
}
return hash;
}
#pragma mark - ————— 获取 Token —————
- (void)getTokenWithUserId:(NSString *)userId
workNo:(NSString *)workNo
success:(void (^)(NSString *))success
failure:(void (^)(NSString *))failure {
NSString * urlString = [NSString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeTokenPath];
// 构建请求体
NSDictionary * bodyDict = @{ @"userId": userId ?: @"",
@"workNo": workNo ?: @"" };
NSData * bodyData = [NSJSONSerialization dataWithJSONObject:bodyDict options:0 error:nil];
NSString * bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
// 签名
NSString * timestamp = [NSString stringWithFormat:@"%.0f", [[NSDate date] timeIntervalSince1970]];
NSString * stringToSign = [NSString stringWithFormat:@"%@%@", timestamp, bodyString];
NSString * sign = [[self class] hmacSHA256:stringToSign key:kHealthKnowledgeSecretKey];
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = @"POST";
[request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
[request setValue:sign forHTTPHeaderField:@"X-Sign"];
request.HTTPBody = bodyData;
DLog(@"[健康知识] 请求 Token URL: %@", urlString);
DLog(@"[健康知识] 签名串: %@", stringToSign);
DLog(@"[健康知识] X-Sign: %@", sign);
[self sendRequest:request completion:^(NSDictionary * json) {
DLog(@"[健康知识] Token 响应: %@", json);
if ([json[@"code"] integerValue] == 200) {
NSString * token = json[@"data"][@"token"];
if (!ValidStr(token)) {
self.cachedToken = token;
DLog(@"[健康知识] Token 获取成功: %@", token);
if (success) success(token);
} else {
DLog(@"[健康知识] Token 为空");
if (failure) failure(@"token 为空");
}
} else {
DLog(@"[健康知识] Token 失败: %@", json[@"msg"]);
if (failure) failure(json[@"msg"] ?: @"获取 token 失败");
}
} failure:^(NSString * errorMsg) {
DLog(@"[健康知识] Token 请求异常: %@", errorMsg);
if (failure) failure(errorMsg);
}];
}
#pragma mark - ————— 栏目列表 —————
- (void)getCategoryListWithToken:(NSString *)token
success:(void (^)(NSArray<XJHealthCategoryModel *> *))success
failure:(void (^)(NSString *))failure {
NSString * urlString = [NSString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeCategoryPath];
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = @"GET";
[request setValue:token forHTTPHeaderField:@"token"];
[self sendRequest:request completion:^(NSDictionary * json) {
if ([json[@"code"] integerValue] == 200) {
NSArray * dataArray = json[@"data"];
NSMutableArray * categories = [[NSMutableArray alloc] init];
if ([dataArray isKindOfClass:[NSArray class]]) {
for (NSDictionary * dict in dataArray) {
XJHealthCategoryModel * m = [XJHealthCategoryModel modelWithDictionary:dict];
[categories addObject:m];
}
}
if (success) success(categories);
} else {
if (failure) failure(json[@"msg"] ?: @"获取栏目列表失败");
}
} failure:^(NSString * errorMsg) {
if (failure) failure(errorMsg);
}];
}
#pragma mark - ————— 分页查询文章 —————
- (void)getArticleListWithToken:(NSString *)token
categoryId:(NSInteger)categoryId
articleType:(NSString *)articleType
pageNum:(NSInteger)pageNum
pageSize:(NSInteger)pageSize
success:(void (^)(NSArray<XJHealthArticleModel *> *, NSInteger))success
failure:(void (^)(NSString *))failure {
NSMutableString * urlString = [NSMutableString stringWithFormat:@"%@%@", kHealthKnowledgeBaseURL, kHealthKnowledgeArticleListPath];
[urlString appendFormat:@"?categoryId=%ld&pageNum=%ld&pageSize=%ld&showAppHomepage=Y",
(long)categoryId, (long)pageNum, (long)pageSize];
if (!ValidStr(articleType)) {
[urlString appendFormat:@"&articleType=%@", articleType];
}
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
request.HTTPMethod = @"GET";
[request setValue:token forHTTPHeaderField:@"token"];
[self sendRequest:request completion:^(NSDictionary * json) {
if ([json[@"code"] integerValue] == 200) {
NSArray * rows = json[@"rows"];
NSInteger total = [json[@"total"] integerValue];
NSMutableArray * articles = [[NSMutableArray alloc] init];
if ([rows isKindOfClass:[NSArray class]]) {
for (NSDictionary * dict in rows) {
XJHealthArticleModel * m = [XJHealthArticleModel modelWithDictionary:dict];
[articles addObject:m];
}
}
if (success) success(articles, total);
} else {
if (failure) failure(json[@"msg"] ?: @"获取文章列表失败");
}
} failure:^(NSString * errorMsg) {
if (failure) failure(errorMsg);
}];
}
#pragma mark - ————— 通用请求 —————
- (void)sendRequest:(NSURLRequest *)request
completion:(void (^)(NSDictionary * json))completion
failure:(void (^)(NSString * errorMsg))failure {
DLog(@"[健康知识] >>> 请求: %@ %@", request.HTTPMethod, request.URL.absoluteString);
NSURLSession * session = [NSURLSession sharedSession];
NSURLSessionDataTask * task = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
DLog(@"[健康知识] <<< 网络错误: %@", error.localizedDescription);
if (failure) failure(error.localizedDescription);
return;
}
NSHTTPURLResponse * httpResp = (NSHTTPURLResponse *)response;
NSInteger statusCode = httpResp.statusCode;
DLog(@"[健康知识] <<< HTTP %ld", (long)statusCode);
if (statusCode != 200) {
NSString * msg = [NSString stringWithFormat:@"HTTP %ld", (long)statusCode];
DLog(@"[健康知识] <<< 请求失败: %@", msg);
if (failure) failure(msg);
return;
}
NSError * jsonError = nil;
NSDictionary * json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (jsonError || ![json isKindOfClass:[NSDictionary class]]) {
DLog(@"[健康知识] <<< JSON 解析失败");
if (failure) failure(@"数据解析失败");
return;
}
DLog(@"[健康知识] <<< 响应: %@", json);
if (completion) completion(json);
});
}];
[task resume];
}
@end
@@ -0,0 +1,16 @@
//
// PhysicalWebViewController.h
// XinjiangProject
//
// Created by Apple on 2026/3/24.
//
#import "XJBaseWebViewVC.h"
NS_ASSUME_NONNULL_BEGIN
@interface PhysicalWebViewController : XJBaseWebViewVC
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,31 @@
//
// PhysicalWebViewController.m
// XinjiangProject
//
// Created by Apple on 2026/3/24.
//
#import "PhysicalWebViewController.h"
@interface PhysicalWebViewController ()
@end
@implementation PhysicalWebViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
@@ -37,6 +37,11 @@
#import "SDCycleScrollView.h" #import "SDCycleScrollView.h"
#import "BRPickerView.h" #import "BRPickerView.h"
#import "BindWatchPopView.h" #import "BindWatchPopView.h"
#import "PhysicalWebViewController.h"
#import "XJHealthKnowledgeView.h"
#import "XJHealthNewsView.h"
#import "XJHealthKnowledgeModel.h"
#import "XJHealthKnowledgeManager.h"
static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半 static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
@@ -77,6 +82,17 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
@property (nonatomic, strong) SDCycleScrollView * AIBannerImage; @property (nonatomic, strong) SDCycleScrollView * AIBannerImage;
@property (nonatomic, strong) XJHealthKnowledgeView * healthKnowledgeView;
@property (nonatomic, strong) MASConstraint * knowledgeHeightConstraint;
@property (nonatomic, strong) XJHealthNewsView * healthNewsView;
@property (nonatomic, strong) MASConstraint * healthNewsHeightConstraint;
@property (nonatomic, strong) NSArray<XJHealthCategoryModel *> * healthCategories;
@property (nonatomic, strong) XJHealthCategoryModel * currentHealthCategory;
@property (nonatomic, assign) NSInteger healthNewsCategoryId;
@property (nonatomic, assign) BOOL healthDataLoaded;
//加密后的字符串 //加密后的字符串
@property (nonatomic, copy) NSString * secretString; @property (nonatomic, copy) NSString * secretString;
@@ -123,10 +139,27 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
// Request the desired authorization // Request the desired authorization
[self.userlocationManager requestWhenInUseAuthorization]; [self.userlocationManager requestWhenInUseAuthorization];
self.isHidenNaviBar = true; self.isHidenNaviBar = true;
//每次进入首页清除旧 token,重新获取(避免环境切换残留)
self.healthDataLoaded = NO;
[[XJHealthKnowledgeManager sharedManager] clearToken];
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateHomeDate) selector:@selector(updateHomeDate)
name:KNotificAgreeProtoocl name:KNotificAgreeProtoocl
object:nil]; object:nil];
//监听退出登录,清除健康知识 token
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleLogoutClearToken:)
name:KNotificationLoginStateChange
object:nil];
//键盘通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
// [self createUI]; // [self createUI];
[self updateV2UI]; [self updateV2UI];
[self getUpDateinfo]; [self getUpDateinfo];
@@ -182,13 +215,28 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
//请求token获取数据 //请求token获取数据
[self verifyTokenData]; [self verifyTokenData];
//健康知识/资讯:首次全量加载,从 H5 回来只刷新文章列表(收藏数等会变)
if ([HQCommonUtils isLogin]) {
if (!self.healthDataLoaded) {
[self loadHealthKnowledgeData];
} else {
[self refreshHealthArticles];
}
} else {
[self.healthKnowledgeView showNotLoggedInPlaceholder];
[self updateHealthKnowledgeViewHeight];
[self.healthNewsView showNotLoggedInPlaceholder];
[self updateHealthNewsViewHeight];
}
//登录了那就走轮询 //登录了那就走轮询
if ([HQCommonUtils isLogin]) { if ([HQCommonUtils isLogin]) {
//请求消息列表 //请求消息列表
[self getMessageListData]; [self getMessageListData];
[self getUserWatchData]; [self getUserWatchData];
[self getWebKeySecret]; [self getWebKeySecret];
//请求首页积分接口
[self getHomePagePointsNumber];
} }
} }
@@ -318,6 +366,50 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
} }
} }
- (void)handleLogoutClearToken:(NSNotification *)noti {
// @NO = 退出登录,@YES = 登录成功
if ([noti.object boolValue] == NO) {
self.healthDataLoaded = NO;
[[XJHealthKnowledgeManager sharedManager] clearToken];
[self.healthKnowledgeView showNotLoggedInPlaceholder];
[self updateHealthKnowledgeViewHeight];
[self.healthNewsView showNotLoggedInPlaceholder];
[self updateHealthNewsViewHeight];
}
}
#pragma mark - ————— 键盘避让 —————
- (void)keyboardWillShow:(NSNotification *)noti {
NSDictionary * info = noti.userInfo;
CGFloat keyboardH = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
CGFloat duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:duration animations:^{
self.mainScrollView.contentInset = UIEdgeInsetsMake(0, 0, keyboardH, 0);
}];
// 只算搜索框区域(头部40 + 间距10 + 搜索36 ≈ 头部下方86pt)
CGRect searchArea = CGRectMake(0, 0, self.healthKnowledgeView.bounds.size.width, 86);
CGRect searchRect = [self.healthKnowledgeView convertRect:searchArea toView:nil];
CGFloat searchBottom = CGRectGetMaxY(searchRect);
CGFloat visibleH = [UIScreen mainScreen].bounds.size.height - keyboardH;
if (searchBottom > visibleH) {
CGFloat gap = searchBottom - visibleH;
CGPoint offset = self.mainScrollView.contentOffset;
offset.y += gap;
[self.mainScrollView setContentOffset:offset animated:YES];
}
}
- (void)keyboardWillHide:(NSNotification *)noti {
CGFloat duration = [noti.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:duration animations:^{
self.mainScrollView.contentInset = UIEdgeInsetsZero;
}];
}
- (void)getData { - (void)getData {
@@ -357,6 +449,22 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
[searchApi getHomeWatchData:parmDic]; [searchApi getHomeWatchData:parmDic];
} }
#pragma mark:请求积分
- (void)getHomePagePointsNumber {
NSMutableDictionary * parmDic = [[NSMutableDictionary alloc] init];
NSDate * localDate = [NSDate date];
NSString * str = [NSDate br_stringFromDate:localDate dateFormat:@"yyyy-MM-dd"];
[parmDic setObject:User_ID forKey:@"userId"];
XJPNetAPI * searchApi = [[XJPNetAPI alloc] init:self tag:@"POINT" NeedToken:@""];
[searchApi postPointsUnmberData:parmDic];
}
#pragma mark: 请求key的加密 #pragma mark: 请求key的加密
- (void)getWebKeySecret { - (void)getWebKeySecret {
@@ -365,7 +473,8 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
NSString * host; NSString * host;
#if DEBUG #if DEBUG
host = @"xj-api.mcrm.vip"; //xj-api.mcrm.vip
host = @"api-jkglpt.iosp.ydpt.tech";
#else #else
@@ -384,8 +493,8 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
XJPNetAPI * searchApi = [[XJPNetAPI alloc] init:self tag:@"SECRET" NeedToken:@""]; XJPNetAPI * searchApi = [[XJPNetAPI alloc] init:self tag:@"SECRET" NeedToken:@""];
[searchApi getWebNeedEncryptionKey:parmDic]; [searchApi getWebNeedEncryptionKey:parmDic];
} }
#pragma mark:绑定手表 #pragma mark:绑定手表
- (void)getBindWatchDataWithSerialNum:(NSString *)numNo { - (void)getBindWatchDataWithSerialNum:(NSString *)numNo {
[self showMBProgressHUDWithString:@"绑定中,请稍候..." autoHidden:false]; [self showMBProgressHUDWithString:@"绑定中,请稍候..." autoHidden:false];
@@ -395,16 +504,6 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
[searchApi getBindWatchDevice:parmDic]; [searchApi getBindWatchDevice:parmDic];
} }
#pragma mark:解除绑定手表
- (void)getUnBindWatchDataWithSerialNum:(NSString *)numNo {
[self showMBProgressHUDWithString:@"解绑中,请稍候..." autoHidden:false];
NSMutableDictionary * parmDic = [[NSMutableDictionary alloc] init];
[parmDic setObject:numNo forKey:@"watchNo"];
XJPNetAPI * searchApi = [[XJPNetAPI alloc] init:self tag:@"UNBINDWATCH" NeedToken:@""];
[searchApi getUnBindWatchDevice:parmDic];
}
- (void)Failed:(NSString*)message tag:(NSString*)tag - (void)Failed:(NSString*)message tag:(NSString*)tag
{ {
@@ -642,7 +741,6 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
if ([responseDic.allKeys containsObject:@"encryptData"]) { if ([responseDic.allKeys containsObject:@"encryptData"]) {
self.secretString = responseDic[@"encryptData"]; self.secretString = responseDic[@"encryptData"];
} }
} }
if ([tag isEqualToString:@"BINDWATCH"]) { if ([tag isEqualToString:@"BINDWATCH"]) {
@@ -651,14 +749,25 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
[EasyTextView showSuccessText:@"绑定成功"]; [EasyTextView showSuccessText:@"绑定成功"];
[self getUserWatchData]; [self getUserWatchData];
} }
if ([tag isEqualToString:@"UNBINDWATCH"]) { if([tag isEqualToString:@"POINT"]) {
[self hiddenAllMBProgressHUD]; NSDictionary * responseDic = response;
[EasyTextView showSuccessText:@"解绑成功"];
if (ValidDict(responseDic)) {
NSString * todayPointStr = @"今日健康积分 0";
NSString * totalPointStr = @"累计积分 0";
DLog(@"%@",response); if ([responseDic.allKeys containsObject:@"totalPoints"]) {
[self getUserWatchData];
totalPointStr = [NSString stringWithFormat:@"累计积分 %.2f",[responseDic[@"totalPoints"] floatValue]];
}
if ([responseDic.allKeys containsObject:@"todayPoints"]) {
todayPointStr = [NSString stringWithFormat:@"今日健康积分 %.2f",[responseDic[@"todayPoints"] floatValue]];
}
self.topView.totalponits = [NSString stringWithFormat:@"%@ | %@",todayPointStr,totalPointStr];
}
DLog(@"--------%@",responseDic);
} }
} }
@@ -856,10 +965,19 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
self.mainScrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ self.mainScrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
self.page = 1; self.page = 1;
[self getBannerListData]; [self getBannerListData];
//健康知识:未登录占位,已登录刷新
if ([HQCommonUtils isLogin]) {
[self loadHealthKnowledgeData];
} else {
[self.healthKnowledgeView showNotLoggedInPlaceholder];
[self updateHealthKnowledgeViewHeight];
[self.healthNewsView showNotLoggedInPlaceholder];
[self updateHealthNewsViewHeight];
}
//请求消息列表
if ([HQCommonUtils isLogin]) { if ([HQCommonUtils isLogin]) {
//请求消息列表
[self getUserWatchData]; [self getUserWatchData];
[self getHomePagePointsNumber];
} }
[self verifyTokenData]; [self verifyTokenData];
@@ -876,26 +994,14 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
self.topView.bindDeviceBlock = ^(NSString * _Nonnull watchNo) { self.topView.bindDeviceBlock = ^(NSString * _Nonnull watchNo) {
// 已绑定手表时不响应
if (!ValidStr(watchNo)) return;
BindWatchPopView *pop = [[BindWatchPopView alloc] init]; BindWatchPopView *pop = [[BindWatchPopView alloc] init];
pop.type = BindWatchTypeBind;
if (ValidStr(watchNo)) {
pop.type = BindWatchTypeBind;
}else
{
pop.serialNumber = watchNo;
pop.type = BindWatchTypeUnbind;
}
pop.actionBlock = ^(BindWatchType type, NSString *serialNumber) { pop.actionBlock = ^(BindWatchType type, NSString *serialNumber) {
if (type == BindWatchTypeBind) { //绑定 [weakSelf getBindWatchDataWithSerialNum:serialNumber];
[weakSelf getBindWatchDataWithSerialNum:serialNumber];
}else //解绑
{
[weakSelf getUnBindWatchDataWithSerialNum:serialNumber];
}
}; };
[pop show]; [pop show];
@@ -1026,6 +1132,63 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
}; };
// 健康知识模块
XJHealthKnowledgeView * healthKnowledgeView = [[XJHealthKnowledgeView alloc] init];
self.healthKnowledgeView = healthKnowledgeView;
[self.contentView addSubview:healthKnowledgeView];
[healthKnowledgeView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(bottomTools.mas_bottom).offset(0);
make.left.mas_offset(12);
make.right.mas_offset(-12);
weakself.knowledgeHeightConstraint = make.height.mas_offset(200);
}];
healthKnowledgeView.moreBlock = ^{
[weakself ensureTokenThenExecute:^{
[weakself openHealthKnowledgeMorePage];
}];
};
healthKnowledgeView.searchBlock = ^(NSString * _Nonnull keyword) {
[weakself ensureTokenThenExecute:^{
[weakself openHealthKnowledgeSearchPage:keyword];
}];
};
healthKnowledgeView.tabSelectBlock = ^(NSInteger index) {
if (index < weakself.healthCategories.count) {
weakself.currentHealthCategory = weakself.healthCategories[index];
[weakself fetchHealthArticlesForCategoryId:weakself.currentHealthCategory.categoryId];
}
};
healthKnowledgeView.cellSelectBlock = ^(XJHealthKnowledgeModel * _Nonnull model) {
[weakself openHealthKnowledgeDetailPage:model];
};
// 健康资讯模块
XJHealthNewsView * healthNewsView = [[XJHealthNewsView alloc] init];
self.healthNewsView = healthNewsView;
[self.contentView addSubview:healthNewsView];
[healthNewsView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(healthKnowledgeView.mas_bottom).offset(12);
make.left.mas_offset(12);
make.right.mas_offset(-12);
weakself.healthNewsHeightConstraint = make.height.mas_offset(130);
}];
healthNewsView.moreBlock = ^{
[weakself ensureTokenThenExecute:^{
[weakself openHealthNewsMorePage];
}];
};
healthNewsView.cellSelectBlock = ^(XJHealthKnowledgeModel * _Nonnull model) {
[weakself openHealthKnowledgeDetailPage:model];
};
//最下面的一张图 //最下面的一张图
// self.aiImage = [[UIImageView alloc] init]; // self.aiImage = [[UIImageView alloc] init];
//// self.aiImage.image = [UIImage imageNamed:@"bottomAIImg"]; //// self.aiImage.image = [UIImage imageNamed:@"bottomAIImg"];
@@ -1050,7 +1213,7 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
[self.AIBannerImage mas_makeConstraints:^(MASConstraintMaker *make) { [self.AIBannerImage mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_offset(12); make.left.mas_offset(12);
make.right.mas_offset(-12); make.right.mas_offset(-12);
make.top.mas_equalTo(bottomTools.mas_bottom).offset(2); make.top.mas_equalTo(healthNewsView.mas_bottom).offset(12);
make.height.mas_offset(120); make.height.mas_offset(120);
make.bottom.equalTo(self.contentView.mas_bottom).offset(-18); make.bottom.equalTo(self.contentView.mas_bottom).offset(-18);
}]; }];
@@ -1135,9 +1298,33 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
} }
if (type == 1) { if (type == 1) {
EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init]; // EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init];
vc.navigationItem.title = @"健康体检"; // vc.navigationItem.title = @"健康体检";
[self.navigationController pushViewController:vc animated:YES]; // [self.navigationController pushViewController:vc animated:YES];
if(![HQCommonUtils isLogin]) {
KPostNotification(KNotificationLoginStateChange, @NO);
return;
}
if (ValidStr(self.secretString)) {
[self showMBProgressHUDOnlyWithString:@"网络异常请稍候再试~"];
[self getWebKeySecret];
return;
}
PhysicalWebViewController * phyVC = [[PhysicalWebViewController alloc] init];
NSString * urlString;
#if DEBUG
urlString = @"https://gstest.superwx.cn/healthcheckweb/index.html";
#else
urlString = @"https://console-jkglpt.iosp.ydpt.tech/hb/healthcheckweb/index.html";
#endif
phyVC.webUrl = [NSString stringWithFormat:@"%@?param=%@",urlString,self.secretString];
[self.navigationController pushViewController:phyVC animated:YES];
} }
if (type == 2) { if (type == 2) {
@@ -1186,9 +1373,31 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
if (imageIndex == 50) { if (imageIndex == 50) {
EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init]; // EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init];
vc.navigationItem.title = @"体检套餐"; // vc.navigationItem.title = @"体检套餐";
[self.navigationController pushViewController:vc animated:YES]; // [self.navigationController pushViewController:vc animated:YES];
if(![HQCommonUtils isLogin]) {
KPostNotification(KNotificationLoginStateChange, @NO);
return;
}
if (ValidStr(self.secretString)) {
[self showMBProgressHUDOnlyWithString:@"网络异常请稍候再试~"];
[self getWebKeySecret];
return;
}
PhysicalWebViewController * phyVC = [[PhysicalWebViewController alloc] init];
NSString * urlString;
#if DEBUG
urlString = @"https://console-jkglpt.iosp.ydpt.tech/hb/healthcheckweb/index.html";
#else
urlString = @"https://console-jkglpt.iosp.ydpt.tech/hb/healthcheckweb/index.html";
#endif
phyVC.webUrl = [NSString stringWithFormat:@"%@?param=%@",urlString,self.secretString];
[self.navigationController pushViewController:phyVC animated:YES];
} }
if (imageIndex == 51) { if (imageIndex == 51) {
@@ -1206,7 +1415,31 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
} }
#pragma mark:点击 #pragma mark:点击
- (void)aiImageTap { - (void)aiImageTap {
//
//
//
// if(![HQCommonUtils isLogin]) {
// KPostNotification(KNotificationLoginStateChange, @NO);
// return;
// }
// if (ValidStr(self.secretString)) {
// [self showMBProgressHUDOnlyWithString:@"网络异常请稍候再试~"];
// [self getWebKeySecret];
// return;
// }
// PhysicalWebViewController * phyVC = [[PhysicalWebViewController alloc] init];
//
// NSString * urlString;
//#if DEBUG
// urlString = @"https://console-jkglpt.iosp.ydpt.tech/hb/healthcheckweb/index.html";
//
//
//#else
// urlString = @"https://console-jkglpt.iosp.ydpt.tech/hb/healthcheckweb/index.html";
//
//#endif
// phyVC.webUrl = [NSString stringWithFormat:@"%@?param=%@",urlString,self.secretString];
// [self.navigationController pushViewController:phyVC animated:YES];
EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init]; EmptyFunctionDevelopmentVC * vc = [[EmptyFunctionDevelopmentVC alloc] init];
vc.navigationItem.title = @"AI随访"; vc.navigationItem.title = @"AI随访";
[self.navigationController pushViewController:vc animated:YES]; [self.navigationController pushViewController:vc animated:YES];
@@ -1292,5 +1525,245 @@ static CGFloat const kEmptyTableMinHeight = 250; // 或屏幕高度的一半
// dispatch_resume(self.timer); // dispatch_resume(self.timer);
//} //}
#pragma mark - ————— 健康知识 —————
- (void)loadHealthKnowledgeData {
kWeakSelf(self);
DLog(@"[健康知识] 开始加载,缓存 token: %@", [XJHealthKnowledgeManager sharedManager].cachedToken ?: @"");
// 先尝试用缓存 token 加载
NSString * token = [XJHealthKnowledgeManager sharedManager].cachedToken;
if (!ValidStr(token)) {
DLog(@"[健康知识] 使用缓存 token 加载栏目");
[self loadHealthCategoriesWithToken:token];
return;
}
NSString * userId = User_ID;
if (ValidStr(userId)) {
DLog(@"[健康知识] User_ID 为空,跳过获取 token");
return;
}
DLog(@"[健康知识] 无缓存 token,先获取 tokenuserId=%@ workNo=%@", userId, userId);
// 首次需要获取 token
[[XJHealthKnowledgeManager sharedManager] getTokenWithUserId:userId
workNo:userId
success:^(NSString * t) {
[weakself loadHealthCategoriesWithToken:t];
} failure:^(NSString * errorMsg) {
DLog(@"健康知识 token 获取失败: %@", errorMsg);
}];
}
- (void)loadHealthCategoriesWithToken:(NSString *)token {
kWeakSelf(self);
[[XJHealthKnowledgeManager sharedManager] getCategoryListWithToken:token
success:^(NSArray<XJHealthCategoryModel *> * categories) {
DLog(@"[健康知识] 栏目解析成功,共 %lu 个一级栏目", (unsigned long)categories.count);
// 找到"健康知识"栏目,取其 children 作为标签
XJHealthCategoryModel * knowledgeCategory = nil;
XJHealthCategoryModel * newsCategory = nil;
for (XJHealthCategoryModel * cat in categories) {
DLog(@"[健康知识] 一级栏目: %@ (id=%ld, children=%lu)", cat.categoryName, (long)cat.categoryId, (unsigned long)cat.children.count);
if ([cat.categoryName isEqualToString:@"健康知识"]) {
knowledgeCategory = cat;
} else if ([cat.categoryName isEqualToString:@"健康资讯"]) {
newsCategory = cat;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
weakself.healthDataLoaded = YES;
// 健康知识
if (knowledgeCategory && knowledgeCategory.children.count > 0) {
weakself.healthCategories = knowledgeCategory.children;
NSMutableArray * titles = [[NSMutableArray alloc] init];
for (XJHealthCategoryModel * child in knowledgeCategory.children) {
[titles addObject:child.categoryName ?: @""];
}
DLog(@"[健康知识] 标签: %@", titles);
weakself.healthKnowledgeView.tabTitles = titles;
weakself.currentHealthCategory = knowledgeCategory.children.firstObject;
DLog(@"[健康知识] 默认选中: %@ (categoryId=%ld)", weakself.currentHealthCategory.categoryName, (long)weakself.currentHealthCategory.categoryId);
[weakself fetchHealthArticlesForCategoryId:weakself.currentHealthCategory.categoryId isNews:NO];
} else {
DLog(@"[健康知识] 栏目不存在或无子分类");
weakself.healthKnowledgeView.dataArray = @[];
[weakself updateHealthKnowledgeViewHeight];
}
// 健康资讯:无子分类,直接用 categoryId 查文章
if (newsCategory) {
weakself.healthNewsCategoryId = newsCategory.categoryId;
DLog(@"[健康资讯] 找到栏目 categoryId=%ld, children=%lu", (long)newsCategory.categoryId, (unsigned long)newsCategory.children.count);
[weakself fetchHealthArticlesForCategoryId:newsCategory.categoryId isNews:YES];
} else {
DLog(@"[健康资讯] 未找到栏目");
weakself.healthNewsView.dataArray = @[];
[weakself updateHealthNewsViewHeight];
}
});
} failure:^(NSString * errorMsg) {
DLog(@"[健康知识] 栏目列表获取失败: %@", errorMsg);
}];
}
- (void)fetchHealthArticlesForCategoryId:(NSInteger)categoryId {
[self fetchHealthArticlesForCategoryId:categoryId isNews:NO];
}
- (void)fetchHealthArticlesForCategoryId:(NSInteger)categoryId isNews:(BOOL)isNews {
kWeakSelf(self);
NSString * token = [XJHealthKnowledgeManager sharedManager].cachedToken;
if (ValidStr(token)) {
DLog(@"[健康知识] fetchArticles token 无效,跳过");
return;
}
NSInteger pageSize = isNews ? 2 : 3;
NSString * tag = isNews ? @"健康资讯" : @"健康知识";
DLog(@"[%@] 请求文章列表 categoryId=%ld", tag, (long)categoryId);
[[XJHealthKnowledgeManager sharedManager] getArticleListWithToken:token
categoryId:categoryId
articleType:nil
pageNum:1
pageSize:pageSize
success:^(NSArray<XJHealthArticleModel *> * articles, NSInteger total) {
DLog(@"[%@] 文章列表返回 %lu 条,total=%ld", tag, (unsigned long)articles.count, (long)total);
NSMutableArray * dataArray = [[NSMutableArray alloc] init];
for (XJHealthArticleModel * article in articles) {
XJHealthKnowledgeModel * model = [[XJHealthKnowledgeModel alloc] init];
model.knowledgeId = [NSString stringWithFormat:@"%ld", (long)article.articleId];
model.title = article.articleTitle;
model.content = !ValidStr(article.articleExcerpt) ? article.articleExcerpt : article.articleSubtitle;
model.imageUrl = article.articleHeaderimage;
model.tags = article.articleTag;
model.createTime = article.createTime;
model.favoriteCount = article.favoriteCount;
[dataArray addObject:model];
}
DLog(@"[%@] 转换后 dataArray 共 %lu 条", tag, (unsigned long)dataArray.count);
dispatch_async(dispatch_get_main_queue(), ^{
if (isNews) {
weakself.healthNewsView.dataArray = dataArray;
[weakself updateHealthNewsViewHeight];
} else {
weakself.healthKnowledgeView.dataArray = dataArray;
[weakself updateHealthKnowledgeViewHeight];
}
});
} failure:^(NSString * errorMsg) {
DLog(@"[%@] 文章列表获取失败: %@", tag, errorMsg);
}];
}
- (void)updateHealthKnowledgeViewHeight {
CGFloat height = [self.healthKnowledgeView totalHeight];
[self.knowledgeHeightConstraint uninstall];
kWeakSelf(self);
[self.healthKnowledgeView mas_makeConstraints:^(MASConstraintMaker *make) {
weakself.knowledgeHeightConstraint = make.height.mas_offset(height);
}];
}
- (void)updateHealthNewsViewHeight {
CGFloat height = [self.healthNewsView totalHeight];
[self.healthNewsHeightConstraint uninstall];
kWeakSelf(self);
[self.healthNewsView mas_makeConstraints:^(MASConstraintMaker *make) {
weakself.healthNewsHeightConstraint = make.height.mas_offset(height);
}];
}
#pragma mark - ————— Token 校验 —————
- (void)ensureTokenThenExecute:(void(^)(void))action {
NSString * token = [XJHealthKnowledgeManager sharedManager].cachedToken;
// token 有效直接执行
if (!ValidStr(token)) {
if (action) action();
return;
}
// token 无效,重新获取
NSString * userId = User_ID;
if (ValidStr(userId)) {
[EasyTextView showErrorText:@"服务繁忙请稍后再试"];
return;
}
kWeakSelf(self);
[self showMBProgressHUDWithString:nil autoHidden:false];
[[XJHealthKnowledgeManager sharedManager] getTokenWithUserId:userId
workNo:userId
success:^(NSString * t) {
[weakself hiddenAllMBProgressHUD];
if (action) action();
} failure:^(NSString * errorMsg) {
[weakself hiddenAllMBProgressHUD];
[EasyTextView showErrorText:@"服务繁忙请稍后再试"];
}];
}
/// 从 H5 回来时刷新文章列表(收藏数等可能变化)
- (void)refreshHealthArticles {
NSString * token = [XJHealthKnowledgeManager sharedManager].cachedToken;
if (ValidStr(token)) return;
if (self.currentHealthCategory.categoryId > 0) {
[self fetchHealthArticlesForCategoryId:self.currentHealthCategory.categoryId isNews:NO];
}
if (self.healthNewsCategoryId > 0) {
[self fetchHealthArticlesForCategoryId:self.healthNewsCategoryId isNews:YES];
}
}
#pragma mark - ————— H5 跳转 —————
- (void)openHealthKnowledgeMorePage {
NSString * param = !ValidStr(self.secretString) ? self.secretString : @"";
NSString * url = [NSString stringWithFormat:@"%@%@?param=%@",
kHealthKnowledgeH5Host, kHealthKnowMoreH5, param];
[self pushHealthWebViewController:url title:@"健康知识"];
}
- (void)openHealthNewsMorePage {
NSString * param = !ValidStr(self.secretString) ? self.secretString : @"";
NSString * url = [NSString stringWithFormat:@"%@%@?param=%@",
kHealthKnowledgeH5Host, kHealthNewsMoreH5, param];
[self pushHealthWebViewController:url title:@"健康资讯"];
}
- (void)openHealthKnowledgeSearchPage:(NSString *)keyword {
NSString * param = !ValidStr(self.secretString) ? self.secretString : @"";
NSString * encodedKeyword = [keyword stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
NSString * url = [NSString stringWithFormat:@"%@%@?param=%@&secrchVal=%@",
kHealthKnowledgeH5Host, kHealthSearchH5, param, encodedKeyword];
[self pushHealthWebViewController:url title:@"搜索"];
}
- (void)openHealthKnowledgeDetailPage:(XJHealthKnowledgeModel *)model {
NSString * param = !ValidStr(self.secretString) ? self.secretString : @"";
NSString * url = [NSString stringWithFormat:@"%@%@?param=%@&articleId=%@",
kHealthKnowledgeH5Host, kHealthDetailH5, param, model.knowledgeId];
[self pushHealthWebViewController:url title:model.title];
}
- (void)pushHealthWebViewController:(NSString *)urlString title:(NSString *)title {
DLog(@"[健康H5] 跳转 URL: %@", urlString);
PhysicalWebViewController * webVC = [[PhysicalWebViewController alloc] init];
webVC.webUrl = urlString;
webVC.navigationItem.title = title;
[self.navigationController pushViewController:webVC animated:YES];
}
@end @end
@@ -0,0 +1,28 @@
//
// XJHealthArticleModel.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XJHealthArticleModel : NSObject
@property (nonatomic, assign) NSInteger articleId;
@property (nonatomic, copy) NSString * articleTitle;
@property (nonatomic, copy) NSString * articleSubtitle;
@property (nonatomic, copy) NSString * articleExcerpt;
@property (nonatomic, copy) NSString * articleTag;
@property (nonatomic, copy) NSString * articleHeaderimage;
@property (nonatomic, copy) NSString * articleType;
@property (nonatomic, copy) NSString * createTime;
@property (nonatomic, assign) NSInteger viewCount;
@property (nonatomic, assign) NSInteger likeCount;
@property (nonatomic, assign) NSInteger favoriteCount;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// XJHealthArticleModel.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import "XJHealthArticleModel.h"
@implementation XJHealthArticleModel
@end
@@ -0,0 +1,20 @@
//
// XJHealthCategoryModel.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XJHealthCategoryModel : NSObject
@property (nonatomic, assign) NSInteger categoryId;
@property (nonatomic, copy) NSString * categoryName;
@property (nonatomic, strong) NSArray<XJHealthCategoryModel *> * children;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,16 @@
//
// XJHealthCategoryModel.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import "XJHealthCategoryModel.h"
@implementation XJHealthCategoryModel
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{ @"children": [XJHealthCategoryModel class] };
}
@end
@@ -0,0 +1,24 @@
//
// XJHealthKnowledgeModel.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XJHealthKnowledgeModel : NSObject
@property (nonatomic, copy) NSString * knowledgeId;
@property (nonatomic, copy) NSString * title;
@property (nonatomic, copy) NSString * content;
@property (nonatomic, copy) NSString * imageUrl;
@property (nonatomic, copy) NSString * tags;
@property (nonatomic, copy) NSString * createTime;
@property (nonatomic, assign) NSInteger favoriteCount;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// XJHealthKnowledgeModel.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import "XJHealthKnowledgeModel.h"
@implementation XJHealthKnowledgeModel
@end
@@ -0,0 +1,20 @@
//
// XJHealthKnowledgeCell.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class XJHealthKnowledgeModel;
@interface XJHealthKnowledgeCell : UITableViewCell
@property (nonatomic, strong) XJHealthKnowledgeModel * model;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,177 @@
//
// XJHealthKnowledgeCell.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
// ┌──────────────────────────────────────────────┐
// │ │ ← cell 高 130
// │ ┌──────────┐ ┌────────────────────────┐ │
// │ │ 图片 │ │ 标题 #252535 Bold 15 │ │ ← 标题 top = 图片 top + 5
// │ │ 100×100 │ │ │ │
// │ │ 圆角15 │ ├────────────────────────┤ │
// │ │ 上下居中 │ │ 描述 #77849E 13 Medium │ │ ← 描述 top = 标题 bottom + 15
// │ │ │ │ 最多 2 行 │ │
// │ │ │ ├────────────────────────┤ │
// │ │ │ │ [图20] 标签|标签 │ │ ← 图标 bottom = 图片 bottom - 5
// │ └──────────┘ │ 时间 → │ │ ← 时间 bottom = 图片 bottom - 9
// │ └────────────────────────┘ │
// │ ──────────────────────────────────────── │ ← 分割线 left=标题, right=-15
// └──────────────────────────────────────────────┘
//
#import "XJHealthKnowledgeCell.h"
#import "XJHealthKnowledgeModel.h"
@interface XJHealthKnowledgeCell ()
@property (nonatomic, strong) UIImageView * leftImageView;
@property (nonatomic, strong) UILabel * titleLabel;
@property (nonatomic, strong) UILabel * descLabel;
@property (nonatomic, strong) UIImageView * tagIcon;
@property (nonatomic, strong) UILabel * tagLabel;
@property (nonatomic, strong) UILabel * timeLabel;
@property (nonatomic, strong) UIView * separatorLine;
@end
@implementation XJHealthKnowledgeCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self createUI];
}
return self;
}
- (void)createUI {
// 左侧图片 100×100 圆角 15
self.leftImageView = [[UIImageView alloc] init];
self.leftImageView.layer.cornerRadius = 15;
self.leftImageView.layer.masksToBounds = YES;
self.leftImageView.contentMode = UIViewContentModeScaleAspectFill;
[self.contentView addSubview:self.leftImageView];
[self.leftImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_offset(15);
make.centerY.mas_equalTo(self.contentView);
make.width.height.mas_offset(100);
}];
// 标题 #252535 Bold 15,右间距 -22
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.textColor = UIColorHex(#252535);
self.titleLabel.font = BOLDSYSTEMFONT(15);
self.titleLabel.textAlignment = NSTextAlignmentLeft;
self.titleLabel.numberOfLines = 1;
[self.contentView addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.leftImageView.mas_right).offset(15);
make.top.mas_equalTo(self.leftImageView.mas_top).offset(5);
make.right.mas_offset(-22);
}];
// 描述 #77849E 13 Medium,最多 2 行,距标题底部 15
self.descLabel = [[UILabel alloc] init];
self.descLabel.textColor = UIColorHex(#77849E);
self.descLabel.font = MEDIUMFONT(13);
self.descLabel.textAlignment = NSTextAlignmentLeft;
self.descLabel.numberOfLines = 2;
[self.contentView addSubview:self.descLabel];
[self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.titleLabel.mas_left);
make.top.mas_equalTo(self.titleLabel.mas_bottom).offset(15);
make.right.mas_offset(-22);
}];
// 时间 #77849E 11 Medium,右对齐,bottom = 图片 bottom - 9(必须放在 tagLabel 前面,tagLabel 引用了它)
self.timeLabel = [[UILabel alloc] init];
self.timeLabel.textColor = UIColorHex(#77849E);
self.timeLabel.font = MEDIUMFONT(11);
self.timeLabel.textAlignment = NSTextAlignmentRight;
self.timeLabel.numberOfLines = 1;
[self.contentView addSubview:self.timeLabel];
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.mas_offset(-15);
make.bottom.mas_equalTo(self.leftImageView.mas_bottom).offset(-9);
}];
[self.timeLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// 标签图标 20×20bottom = 图片 bottom - 5
self.tagIcon = [[UIImageView alloc] init];
self.tagIcon.image = [UIImage imageNamed:@"article_classIcon"];
self.tagIcon.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:self.tagIcon];
[self.tagIcon mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.titleLabel.mas_left);
make.bottom.mas_equalTo(self.leftImageView.mas_bottom).offset(-5);
make.width.height.mas_offset(20);
}];
// 标签文字 #14BEBE 11 Medium(引用 timeLabel,已在前方创建)
self.tagLabel = [[UILabel alloc] init];
self.tagLabel.textColor = UIColorHex(#14BEBE);
self.tagLabel.font = MEDIUMFONT(11);
self.tagLabel.textAlignment = NSTextAlignmentLeft;
self.tagLabel.numberOfLines = 1;
[self.contentView addSubview:self.tagLabel];
[self.tagLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.tagIcon.mas_right).offset(5);
make.centerY.mas_equalTo(self.tagIcon);
make.right.mas_lessThanOrEqualTo(self.timeLabel.mas_left).offset(-8);
}];
// 分割线 #EAECF1 h1,左对齐标题,右 -15
self.separatorLine = [[UIView alloc] init];
self.separatorLine.backgroundColor = UIColorHex(#EAECF1);
[self.contentView addSubview:self.separatorLine];
[self.separatorLine mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.titleLabel.mas_left);
make.right.mas_offset(-15);
make.bottom.mas_offset(-1);
make.height.mas_offset(1);
}];
}
- (void)setModel:(XJHealthKnowledgeModel *)model {
_model = model;
self.titleLabel.text = model.title;
self.descLabel.text = model.content;
self.timeLabel.text = model.createTime;
// 标签 #健康资讯#肝癌 → 健康资讯 | 肝癌
self.tagLabel.text = [self formatTagString:model.tags];
// 图片 URL 是完整地址,直接加载
if (!ValidStr(model.imageUrl)) {
NSString * photo = model.imageUrl;
// http → https
if ([photo hasPrefix:@"http://"]) {
photo = [photo stringByReplacingOccurrencesOfString:@"http://" withString:@"https://"];
}
[self.leftImageView sd_setImageWithURL:[NSURL URLWithString:photo] placeholderImage:[UIImage imageNamed:@"article_placholdIMg"]];
}
}
/// #健康资讯#肝癌 → 健康资讯 | 肝癌
- (NSString *)formatTagString:(NSString *)tagStr {
if (ValidStr(tagStr)) return tagStr ?: @"";
NSString * result = tagStr;
// 去掉首尾的 #
if ([result hasPrefix:@"#"]) result = [result substringFromIndex:1];
if ([result hasSuffix:@"#"]) result = [result substringToIndex:result.length - 1];
// 中间的 # 替换为 |
result = [result stringByReplacingOccurrencesOfString:@"#" withString:@" | "];
return result;
}
@end
@@ -0,0 +1,42 @@
//
// XJHealthKnowledgeView.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class XJHealthKnowledgeModel;
@interface XJHealthKnowledgeView : UIView
/// 数据源
@property (nonatomic, strong) NSArray<XJHealthKnowledgeModel *> * dataArray;
/// 标签标题数组
@property (nonatomic, strong) NSArray<NSString *> * tabTitles;
/// 当前选中标签索引
@property (nonatomic, assign) NSInteger selectedTabIndex;
/// 点击"更多"
@property (nonatomic, copy) void (^moreBlock)(void);
/// 点击搜索按钮
@property (nonatomic, copy) void (^searchBlock)(NSString * keyword);
/// 搜索框开始编辑(用于键盘避让)
@property (nonatomic, copy) void (^searchBeginBlock)(void);
/// 标签切换回调
@property (nonatomic, copy) void (^tabSelectBlock)(NSInteger index);
/// 点击 cell
@property (nonatomic, copy) void (^cellSelectBlock)(XJHealthKnowledgeModel * model);
/// 计算当前总高度
- (CGFloat)totalHeight;
/// 显示未登录占位
- (void)showNotLoggedInPlaceholder;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,433 @@
//
// 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 = [UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0];
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.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
@@ -0,0 +1,20 @@
//
// XJHealthNewsCell.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class XJHealthKnowledgeModel;
@interface XJHealthNewsCell : UITableViewCell
@property (nonatomic, strong) XJHealthKnowledgeModel * model;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,115 @@
//
// XJHealthNewsCell.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
// ┌─────────────────────────────────────────────┐
// │ │ ← cell 高 90
// │ ┌──────────────────────┐ ┌────────────┐ │
// │ │ 标题 #111225 15 │ │ 图片 │ │ ← 图片 80×60 垂直居中
// │ │ Medium 最多2行 │ │ 80×60 │ │ 右间距 -15
// │ │ 左+15 右距图-15 │ │ │ │
// │ ├──────────────────────┤ │ │ │
// │ │ 收藏1865 │ │ │ │ ← 底部-15,左对齐标题
// │ └──────────────────────┘ └────────────┘ │
// │ ─────────────────────────────────────── │ ← 分割线
// └─────────────────────────────────────────────┘
//
#import "XJHealthNewsCell.h"
#import "XJHealthKnowledgeModel.h"
@interface XJHealthNewsCell ()
@property (nonatomic, strong) UIImageView * rightImageView;
@property (nonatomic, strong) UILabel * titleLabel;
@property (nonatomic, strong) UILabel * favoriteLabel;
@property (nonatomic, strong) UIView * separatorLine;
@end
@implementation XJHealthNewsCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self createUI];
}
return self;
}
- (void)createUI {
// 右侧图片 80×60,右间距 -15,垂直居中
self.rightImageView = [[UIImageView alloc] init];
self.rightImageView.contentMode = UIViewContentModeScaleAspectFill;
self.rightImageView.layer.cornerRadius = 4;
self.rightImageView.layer.masksToBounds = YES;
[self.contentView addSubview:self.rightImageView];
[self.rightImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.mas_offset(-15);
make.centerY.mas_equalTo(self.contentView);
make.width.mas_offset(80);
make.height.mas_offset(60);
}];
// 标题 #111225 15 Medium,最多2行,左+15,右距图片左 -15
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.textColor = UIColorHex(#111225);
self.titleLabel.font = MEDIUMFONT(15);
self.titleLabel.textAlignment = NSTextAlignmentLeft;
self.titleLabel.numberOfLines = 2;
[self.contentView addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_offset(15);
make.top.mas_offset(14);
make.right.mas_equalTo(self.rightImageView.mas_left).offset(-15);
}];
// 收藏数 #77849E 11 Medium,底部-15,左对齐标题
self.favoriteLabel = [[UILabel alloc] init];
self.favoriteLabel.textColor = UIColorHex(#77849E);
self.favoriteLabel.font = MEDIUMFONT(11);
self.favoriteLabel.textAlignment = NSTextAlignmentLeft;
[self.contentView addSubview:self.favoriteLabel];
[self.favoriteLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.titleLabel.mas_left);
make.bottom.mas_offset(-15);
}];
// 分割线 #EAECF1 h1,左对齐标题,右 -15
self.separatorLine = [[UIView alloc] init];
self.separatorLine.backgroundColor = UIColorHex(#EAECF1);
[self.contentView addSubview:self.separatorLine];
[self.separatorLine mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.titleLabel.mas_left);
make.right.mas_offset(-15);
make.bottom.mas_offset(0);
make.height.mas_offset(1);
}];
}
- (void)setModel:(XJHealthKnowledgeModel *)model {
_model = model;
self.titleLabel.text = model.title;
// 图片
if (!ValidStr(model.imageUrl)) {
NSString * photo = model.imageUrl;
if ([photo hasPrefix:@"http://"]) {
photo = [photo stringByReplacingOccurrencesOfString:@"http://" withString:@"https://"];
}
[self.rightImageView sd_setImageWithURL:[NSURL URLWithString:photo] placeholderImage:[UIImage imageNamed:@"article_placholdIMg"]];
}
// 收藏数
self.favoriteLabel.text = [NSString stringWithFormat:@"收藏%ld", (long)model.favoriteCount];
}
@end
@@ -0,0 +1,31 @@
//
// XJHealthNewsView.h
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class XJHealthKnowledgeModel;
@interface XJHealthNewsView : UIView
/// 数据源
@property (nonatomic, strong) NSArray<XJHealthKnowledgeModel *> * dataArray;
/// 点击"更多"
@property (nonatomic, copy) void (^moreBlock)(void);
/// 点击 cell
@property (nonatomic, copy) void (^cellSelectBlock)(XJHealthKnowledgeModel * model);
/// 计算当前总高度
- (CGFloat)totalHeight;
/// 显示未登录占位
- (void)showNotLoggedInPlaceholder;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,205 @@
//
// XJHealthNewsView.m
// XinjiangProject
//
// Created by Apple on 2026/6/29.
//
// ┌─────────────────────────────┐
// │ 健康资讯 [>] │ ← header 40
// ├─────────────────────────────┤
// │ Cell 1 │ ← cell 90 (最多2条)
// ├─────────────────────────────┤
// │ Cell 2 │
// ├─────────────────────────────┤
// │ 暂无健康资讯(空状态) │
// └─────────────────────────────┘
//
#import "XJHealthNewsView.h"
#import "XJHealthNewsCell.h"
#import "XJHealthKnowledgeModel.h"
static CGFloat const kHeaderHeight = 40;
static CGFloat const kCellHeight = 90;
static CGFloat const kMaxCellCount = 2;
static CGFloat const kDefaultEmptyHeight = 130;
@interface XJHealthNewsView () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UIView * headerView;
@property (nonatomic, strong) UIImageView * headerBgImg;
@property (nonatomic, strong) UILabel * titleLabel;
@property (nonatomic, strong) UIButton * moreBtn;
@property (nonatomic, strong) UITableView * listTableView;
@property (nonatomic, strong) UILabel * emptyLabel;
@end
@implementation XJHealthNewsView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = KWhiteColor;
self.layer.cornerRadius = 12;
self.layer.masksToBounds = YES;
[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);
}];
// 列表(直接跟在 header 下方,无搜索框无标签区)
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.headerView.mas_bottom);
make.height.mas_offset(0);
}];
// 空状态
self.emptyLabel = [[UILabel alloc] init];
self.emptyLabel.text = @"暂无健康资讯";
self.emptyLabel.textColor = [UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0];
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.headerView.mas_bottom).offset(30);
}];
}
#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.headerView.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.headerView.mas_bottom);
make.height.mas_offset(0);
}];
}
}
- (void)showNotLoggedInPlaceholder {
self.dataArray = @[];
self.emptyLabel.text = @"暂未登录";
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 hasData = self.dataArray.count > 0;
CGFloat base = kRealValue(kHeaderHeight);
if (hasData) {
CGFloat tableH = MIN(self.dataArray.count, kMaxCellCount) * kCellHeight;
return base + tableH;
} else {
return base + (self.emptyLabel.hidden ? kDefaultEmptyHeight - kHeaderHeight : 80);
}
}
#pragma mark - ————— 事件 —————
- (void)moreAction {
if (self.moreBlock) self.moreBlock();
}
#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 = @"XJHealthNewsCell";
XJHealthNewsCell * cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[XJHealthNewsCell 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
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "article_classIcon.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "article_placholdIMg.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB