feat(home): 新增健康资讯模块(健康知识 + 健康咨询)

采用 HMAC-SHA256 签名认证获取 Token,通过栏目/文章 API 获取数据,
详情/更多/搜索均跳转 H5 页面。同时新增带下划线指示器的 Tab 样式。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zhanglei
2026-06-29 16:47:46 +08:00
co-authored by Claude Opus 4.7
parent 4ff52c0465
commit f5be575491
25 changed files with 1169 additions and 5 deletions
@@ -0,0 +1,27 @@
package com.xjjk.healthyclients.adapter
import android.widget.ImageView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.xjjk.healthyclients.R
import com.xjjk.healthyclients.bean.home.ArticleBean
import com.xjjk.healthyclients.superfuntion.loadRoundedImage
import com.xjjk.healthyclients.superfuntion.orEmptyDefault
/**
* 健康咨询列表 Adapter
*/
class HealthConsultationAdapter :
BaseQuickAdapter<ArticleBean, BaseViewHolder>(R.layout.item_health_consultation) {
override fun convert(holder: BaseViewHolder, item: ArticleBean) {
// 标题
holder.setText(R.id.tv_title, item.articleTitle.orEmptyDefault())
// 收藏和分享数
holder.setText(R.id.tv_meta, "收藏 ${item.favoriteCount} 分享 ${item.likeCount}")
// 封面图片 80x60 圆角8
holder.getView<ImageView>(R.id.iv_cover)?.loadRoundedImage(
item.articleHeaderimage, 8f,R.mipmap.ic_placeholder_rectangle
)
}
}
@@ -0,0 +1,32 @@
package com.xjjk.healthyclients.adapter
import android.widget.ImageView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.sw.healthyclients.utils.DateUtil
import com.xjjk.healthyclients.R
import com.xjjk.healthyclients.bean.home.ArticleBean
import com.xjjk.healthyclients.superfuntion.loadRoundedImage
import com.xjjk.healthyclients.superfuntion.orEmptyDefault
/**
* 健康知识列表 Adapter
*/
class HealthKnowledgeAdapter :
BaseQuickAdapter<ArticleBean, BaseViewHolder>(R.layout.item_health_knowledge) {
override fun convert(holder: BaseViewHolder, item: ArticleBean) {
// 封面图片 100x100 圆角15
holder.getView<ImageView>(R.id.iv_cover)?.loadRoundedImage(
item.articleHeaderimage, 15f, R.mipmap.ic_placeholder_square,
)
// 标题
holder.setText(R.id.tv_title, item.articleTitle.orEmptyDefault())
// 摘要
holder.setText(R.id.tv_detail, item.articleSubtitle.orEmptyDefault(""))
// 标签文本
holder.setText(R.id.tv_tag, item.articleTag.orEmptyDefault())
// 日期
holder.setText(R.id.tv_date, DateUtil.long2ShortDateStr(item.createTime))
}
}
@@ -0,0 +1,182 @@
package com.xjjk.healthyclients.bankRequest
import com.sw.healthyclients.data.local.DataStoreManager
import com.xjjk.healthyclients.bean.home.ArticleBean
import com.xjjk.healthyclients.bean.home.ArticleListBean
import com.xjjk.healthyclients.bean.home.CategoryBean
import com.xjjk.healthyclients.data.api.CategoryListResponse
import com.xjjk.healthyclients.data.api.HealthInfoApi
import com.xjjk.healthyclients.data.api.TokenResponse
import com.xjjk.healthyclients.retrofit.UrlConfig
import com.xjjk.healthyclients.retrofit.getBankRetrofit
import com.xjjk.healthyclients.superfuntion.toJson
import com.xjjk.healthyclients.utils.SignatureUtils
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
/**
* 健康资讯网络请求封装类
* 负责 Token 获取(签名认证)、栏目列表、文章列表的 API 调用
*/
class HealthInfoRequest {
private val api: HealthInfoApi = getBankRetrofit()
.create(HealthInfoApi::class.java)
companion object {
/** 缓存的 Token,避免重复请求 */
private var cachedToken: String? = null
}
/**
* 获取 Token(带 HMAC-SHA256 签名)
*/
fun fetchToken(
onSuccess: (String) -> Unit,
onFailure: (Throwable) -> Unit
) {
// 如果已有缓存 Token,直接返回
cachedToken?.let {
onSuccess(it)
return
}
try {
val timestamp = System.currentTimeMillis() / 1000
val userId = DataStoreManager.getUserId() ?: ""
val workNo = DataStoreManager.getUserInfo()?.workNo ?: ""
val bodyString = """{"userId":"$userId","workNo":"$workNo"}"""
val sign = SignatureUtils.generateSignature(timestamp, bodyString, UrlConfig.SECRET_KEY)
val headers = mapOf(
"Content-Type" to "application/json; charset=utf-8",
"X-Timestamp" to timestamp.toString(),
"X-Sign" to sign
)
val body = mapOf("userId" to userId, "workNo" to workNo)
api.getToken(headers, body.toJson().toRequestBody())
.enqueue(object : Callback<TokenResponse> {
override fun onResponse(
call: Call<TokenResponse>,
response: Response<TokenResponse>
) {
if (response.code() == 200) {
val token = response.body()?.data?.token
if (!token.isNullOrEmpty()) {
cachedToken = token
onSuccess(token)
} else {
onFailure(Exception("Token 为空"))
}
} else {
onFailure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
}
override fun onFailure(
call: Call<TokenResponse>,
t: Throwable
) {
onFailure(t)
}
})
} catch (e: Exception) {
onFailure(e)
}
}
/**
* 获取栏目列表
*/
fun fetchCategoryList(
token: String,
onSuccess: (MutableList<CategoryBean>) -> Unit,
onFailure: (Throwable) -> Unit
) {
try {
val headers = mapOf("token" to token)
api.getCategoryList(headers)
.enqueue(object : Callback<CategoryListResponse> {
override fun onResponse(
call: Call<CategoryListResponse>,
response: Response<CategoryListResponse>
) {
if (response.code() == 200) {
val data = response.body()?.data ?: mutableListOf()
onSuccess(data)
} else {
onFailure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
}
override fun onFailure(
call: Call<CategoryListResponse>,
t: Throwable
) {
onFailure(t)
}
})
} catch (e: Exception) {
onFailure(e)
}
}
/**
* 分页查询文章列表
*
* @param token 认证 Token
* @param categoryId 栏目 ID
* @param pageNum 页码
* @param pageSize 每页数量
* @param articleType 文章类型,"0"-图文,"1"-视频,不传则全部
*/
fun fetchArticleList(
token: String,
categoryId: Int,
pageNum: Int = 1,
pageSize: Int = 3,
onSuccess: (MutableList<ArticleBean>?) -> Unit,
onFailure: (Throwable) -> Unit
) {
try {
val headers = mapOf("token" to token)
val params = mapOf(
"categoryId" to categoryId.toString(),
"pageNum" to pageNum.toString(),
"pageSize" to pageSize.toString(),
"showAppHomepage" to "Y"
)
api.getArticleList(headers, params)
.enqueue(object : Callback<ArticleListBean> {
override fun onResponse(
call: Call<ArticleListBean>,
response: Response<ArticleListBean>
) {
if (response.code() == 200) {
val body = response.body()
onSuccess(body?.rows)
} else {
onFailure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
}
override fun onFailure(call: Call<ArticleListBean>, t: Throwable) {
onFailure(t)
}
})
} catch (e: Exception) {
onFailure(e)
}
}
/** 清除缓存的 Token(登录态变化时调用) */
fun clearToken() {
cachedToken = null
}
}
@@ -0,0 +1,53 @@
package com.xjjk.healthyclients.bean.home
import com.google.gson.annotations.SerializedName
/**
* 获取 token 响应
*/
data class TokenBean(
@SerializedName("token") val token: String? = null
)
/**
* 栏目分类
*/
data class CategoryBean(
@SerializedName("categoryId") val categoryId: Int = 0,
@SerializedName("categoryName") val categoryName: String? = null,
@SerializedName("categoryStatus") val categoryStatus: String? = null,
@SerializedName("children") val children: MutableList<CategoryBean>? = null,
@SerializedName("parentId") val parentId: Int = 0,
@SerializedName("sortOrder") val sortOrder: Int = 0
)
/**
* 文章
*/
data class ArticleBean(
@SerializedName("articleId") val articleId: Int = 0,
@SerializedName("articleTitle") val articleTitle: String? = null,
@SerializedName("articleSubtitle") val articleSubtitle: String? = null,
@SerializedName("articleTag") val articleTag: String? = null,
@SerializedName("articleHeaderimage") val articleHeaderimage: String? = null,
@SerializedName("articleType") val articleType: String? = null,
@SerializedName("articleExcerpt") val articleExcerpt: String? = null,
@SerializedName("articleContent") val articleContent: String? = null,
@SerializedName("articleVideo") val articleVideo: String? = null,
@SerializedName("favoriteCount") val favoriteCount: Int = 0,
@SerializedName("likeCount") val likeCount: Int = 0,
@SerializedName("viewCount") val viewCount: Int = 0,
@SerializedName("createTime") val createTime: String? = null,
@SerializedName("categoryId") val categoryId: Int = 0,
@SerializedName("articleScore") val articleScore: Double = 0.0,
@SerializedName("articleStatus") val articleStatus: String? = null,
@SerializedName("showAppHomepage") val showAppHomepage: String? = null
)
/**
* 文章列表分页响应
*/
data class ArticleListBean(
@SerializedName("rows") val rows: MutableList<ArticleBean>? = null,
@SerializedName("total") val total: Int = 0
)
@@ -0,0 +1,63 @@
package com.xjjk.healthyclients.data.api
import com.xjjk.healthyclients.bean.home.ArticleListBean
import com.xjjk.healthyclients.bean.home.CategoryBean
import com.xjjk.healthyclients.bean.home.TokenBean
import com.xjjk.healthyclients.retrofit.UrlConfig
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.POST
import retrofit2.http.QueryMap
/**
* 健康资讯 API 接口(使用银行 Retrofit 实例,baseUrl 为 getDefaultBankBaseUrl()
*/
interface HealthInfoApi {
/**
* 获取 Token(带签名认证)
*/
@POST("${UrlConfig.middleCheck}pe/frontend/init")
fun getToken(
@HeaderMap headers: Map<String, String>,
@Body requestBody: RequestBody
): Call<TokenResponse>
/**
* 获取栏目列表
*/
@GET("${UrlConfig.middleCheck}cms/frontend/category/list")
fun getCategoryList(
@HeaderMap headers: Map<String, String>
): Call<CategoryListResponse>
/**
* 分页查询文章列表
*/
@GET("${UrlConfig.middleCheck}cms/frontend/articleList")
fun getArticleList(
@HeaderMap headers: Map<String, String>,
@QueryMap params: Map<String, String>
): Call<ArticleListBean>
}
/**
* Token 接口响应外层
*/
data class TokenResponse(
val msg: String? = null,
val code: Int = 0,
val data: TokenBean? = null
)
/**
* 栏目列表接口响应外层
*/
data class CategoryListResponse(
val msg: String? = null,
val code: Int = 0,
val data: MutableList<CategoryBean>? = null
)
@@ -15,8 +15,10 @@ import com.tencent.qcloud.tuikit.tuichat.bean.WorkBean
import com.xjjk.healthyclients.R
import com.xjjk.healthyclients.adapter.common.MultiItemTypeAdapter
import com.xjjk.healthyclients.bankRequest.BankPointRequest
import com.xjjk.healthyclients.bankRequest.HealthInfoRequest
import com.xjjk.healthyclients.base.BaseVMBFragment
import com.xjjk.healthyclients.bean.guidance.GuidanceListBean
import com.xjjk.healthyclients.bean.home.CategoryBean
import com.xjjk.healthyclients.bean.home.WatchInfoBean
import com.xjjk.healthyclients.data.api.HomeApi
import com.xjjk.healthyclients.databinding.FragmentHomeBinding
@@ -67,6 +69,13 @@ class HomeFragment :
var noticeTime = REQUEST_DURATION
var watchInfo: WatchInfoBean? = null
/** 健康资讯请求 */
private var mHealthInfoRequest: HealthInfoRequest? = null
/** 健康资讯 Token */
private var mHealthToken: String? = null
/** 健康知识子栏目列表 */
private var mKnowledgeCategories: MutableList<CategoryBean> = mutableListOf()
/** 通知轮播协程 Job,重新启动前先取消旧任务,防止多协程并发叠加 */
private var noticeJob: Job? = null
@@ -140,6 +149,7 @@ class HomeFragment :
})
mBinding.layoutItem3.fragmentGuidanceRvDoctor.adapter = mGuidanceFragmentDoctorAdapter
mBinding.watchView.init(this@HomeFragment)
setupHealthViews()
}
override fun bindEvent() {
@@ -169,9 +179,9 @@ class HomeFragment :
}
mBinding.watchView.mBinding.watchName.setOnClickListener {
if (DataStoreManager.isLogin()) {
if (watchInfo?.hasWatch==true){
showWatchBindDialog(watchInfo?.watchNo)
}
// if (watchInfo?.hasWatch==true){
// showWatchBindDialog(watchInfo?.watchNo)
// }
} else {
toActivity(LoginActivity::class.java)
}
@@ -457,6 +467,7 @@ class HomeFragment :
mViewModel.getHomeWatchInfo(DataStoreManager.getUserId())
mViewModel.getSysEnCryptInfo(UrlConfig.getHomeBankH5Url())
getBankPointDetail()
loadHealthInfoData()
}
}
@@ -484,8 +495,151 @@ class HomeFragment :
mViewModel.getHomeWatchInfo(DataStoreManager.getUserId())
mViewModel.getSysEnCryptInfo(UrlConfig.getHomeBankH5Url())
getBankPointDetail()
loadHealthInfoData()
}
}
// ==================== 健康资讯相关 ====================
/** 初始化健康资讯两个 View 的事件回调 */
private fun setupHealthViews() {
mHealthInfoRequest = HealthInfoRequest()
// 健康知识 - Tab 切换加载对应栏目文章
mBinding.healthKnowledgeView.setOnTabSelectedListener { category ->
loadKnowledgeArticles(category.categoryId)
}
// 健康知识 - 列表项点击 → 跳转文章详情 H5
mBinding.healthKnowledgeView.setOnItemClickListener { article ->
navigateToArticleDetail(article.articleId)
}
// 健康知识 - 更多按钮 → 跳转健康知识 H5 页面
mBinding.healthKnowledgeView.setOnMoreClickListener {
navigateToHealthKnowledgeMore()
}
// 健康知识 - 搜索
mBinding.healthKnowledgeView.setOnSearchClickListener {
navigateToHealthKnowledgeSearch()
}
// 健康咨询 - 列表项点击 → 跳转文章详情 H5
mBinding.healthConsultationView.setOnItemClickListener { article ->
navigateToArticleDetail(article.articleId)
}
// 健康咨询 - 更多按钮 → 跳转健康咨询 H5 页面
mBinding.healthConsultationView.setOnMoreClickListener {
navigateToHealthConsultationMore()
}
}
/** 加载健康资讯数据(Token → 栏目 → 文章) */
private fun loadHealthInfoData() {
mHealthInfoRequest?.fetchToken(
onSuccess = { token ->
mHealthToken = token
loadCategories(token)
loadConsultationArticles(token)
},
onFailure = { /* Token 获取失败,静默处理 */ }
)
}
/** 加载栏目列表并设置健康知识 Tab */
private fun loadCategories(token: String) {
mHealthInfoRequest?.fetchCategoryList(
token,
onSuccess = { categories ->
// 查找健康知识栏目(categoryId=1)的子栏目
val knowledgeCategory = categories.find { it.categoryId == 1 }
mKnowledgeCategories.clear()
knowledgeCategory?.children?.let { mKnowledgeCategories.addAll(it) }
mBinding.healthKnowledgeView.setupTabs(mKnowledgeCategories)
// 默认加载第一个子栏目文章
if (mKnowledgeCategories.isNotEmpty()) {
loadKnowledgeArticles(mKnowledgeCategories[0].categoryId)
}
},
onFailure = { /* 栏目加载失败,静默处理 */ }
)
}
/** 加载健康知识指定栏目下的文章列表 */
private fun loadKnowledgeArticles(categoryId: Int) {
mHealthToken?.let { token ->
mHealthInfoRequest?.fetchArticleList(
token, categoryId,
onSuccess = { articles ->
mBinding.healthKnowledgeView.setArticles(articles)
},
onFailure = { /* 文章加载失败,静默处理 */ }
)
}
}
/** 加载健康咨询文章列表(categoryId=2 */
private fun loadConsultationArticles(token: String) {
mHealthInfoRequest?.fetchArticleList(
token, 2,
onSuccess = { articles->
mBinding.healthConsultationView.setArticles(articles)
},
onFailure = { /* 文章加载失败,静默处理 */ }
)
}
/** 跳转文章详情 H5 */
private fun navigateToArticleDetail(articleId: Int) {
val param = mViewModel.enCryptInfoUrl.value
if (param.isNullOrEmpty()) {
showToast("网络错误")
return
}
val url = "${UrlConfig.getHealthNewsDetailH5Url()}?param=$param&articleId=$articleId"
requireContext().startWebActivity(url, isFull = true, myTitle = "文章详情", hideStatus = true)
}
/** 跳转健康知识更多 H5 */
private fun navigateToHealthKnowledgeMore() {
val param = mViewModel.enCryptInfoUrl.value
if (param.isNullOrEmpty()) {
mViewModel.getSysEnCryptInfo(UrlConfig.getHomeBankH5Url())
showToast("网络错误")
return
}
val url = "${UrlConfig.getHealthKnowledgeH5Url()}?param=$param"
requireContext().startWebActivity(url, isFull = true, myTitle = "健康知识", hideStatus = true)
}
/** 跳转健康知识 搜索 */
private fun navigateToHealthKnowledgeSearch() {
val param = mViewModel.enCryptInfoUrl.value
if (param.isNullOrEmpty()) {
mViewModel.getSysEnCryptInfo(UrlConfig.getHomeBankH5Url())
showToast("网络错误")
return
}
val url = "${UrlConfig.getHealthNewsSearchH5Url()}?param=$param"
requireContext().startWebActivity(url, isFull = true, myTitle = "搜索", hideStatus = true)
}
/** 跳转健康咨询更多 H5 */
private fun navigateToHealthConsultationMore() {
val param = mViewModel.enCryptInfoUrl.value
if (param.isNullOrEmpty()) {
mViewModel.getSysEnCryptInfo(UrlConfig.getHomeBankH5Url())
showToast("网络错误")
return
}
val url = "${UrlConfig.getHealthConsultationH5Url()}?param=$param"
requireContext().startWebActivity(url, isFull = true, myTitle = "健康咨询", hideStatus = true)
}
}
@@ -47,8 +47,10 @@ object UrlConfig {
val baseUrlType: BaseUrlType = BaseUrlType.PRODUCT
// const val middle = "xj_health/" //test
// const val middleCheck = "healthcheck/" //test
const val middle = "hb/" //product
const val middleCheck = "hb/" //product
//
var isOpenIm: Boolean = true
enum class BaseUrlType(val type: Int) {
@@ -122,6 +124,42 @@ object UrlConfig {
}
}
}
/**
* 健康资讯 H5 页面基础地址
*/
fun getHealthNewsH5BaseUrl(): String {
return getDefaultBankBaseUrl() + "healthnewsweb/"
}
/**
* 健康知识 H5 页面地址
*/
fun getHealthKnowledgeH5Url(): String {
return getHealthNewsH5BaseUrl() + "healthKnow.html"
}
/**
* 健康咨询 H5 页面地址
*/
fun getHealthConsultationH5Url(): String {
return getHealthNewsH5BaseUrl() + "healthNews.html"
}
/**
* 健康资讯详情 H5 页面地址
*/
fun getHealthNewsDetailH5Url(): String {
return getHealthNewsH5BaseUrl() + "healthNewsDetail.html"
}
/**
* 健康资讯搜索 H5 页面地址
*/
fun getHealthNewsSearchH5Url(): String {
return getHealthNewsH5BaseUrl() + "healthSecrch.html"
}
/**
* 方便后续加一些其他逻辑
*/
@@ -590,6 +590,7 @@ fun TabLayout.attachViewPager(
fun getTabStyle(i: Int): Int {
return when (i) {
1 -> R.layout.item_tablayout_group_title
2 -> R.layout.item_tablayout_group_index_theme//有下标线的绿色
else -> R.layout.item_tablayout_group_title
}
}
@@ -0,0 +1,73 @@
package com.xjjk.healthyclients.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.xjjk.healthyclients.R
import com.xjjk.healthyclients.adapter.HealthConsultationAdapter
import com.xjjk.healthyclients.bean.home.ArticleBean
import com.xjjk.healthyclients.databinding.LayoutHealthConsultationBinding
/**
* 健康咨询 View 组件
* 头部标题 + 列表(左侧标题+收藏分享信息 / 右侧图片)
*/
class HealthConsultationView(context: Context?, attrs: AttributeSet?) :
RelativeLayout(context, attrs, 0) {
var mContext: Context? = context
var mBinding: LayoutHealthConsultationBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.layout_health_consultation, this, true
)
private val mAdapter = HealthConsultationAdapter()
private var mOnItemClickListener: ((ArticleBean) -> Unit)? = null
private var mOnMoreClickListener: (() -> Unit)? = null
init {
mBinding.rvList.layoutManager = LinearLayoutManager(mContext)
mBinding.rvList.adapter = mAdapter
// 列表项点击
mAdapter.setOnItemClickListener { _, _, position ->
mAdapter.getItem(position)?.let { mOnItemClickListener?.invoke(it) }
}
// 头部更多按钮点击
mBinding.ivMore.setOnClickListener {
mOnMoreClickListener?.invoke()
}
}
/** 设置文章列表数据(替换),最多取前 2 条 */
fun setArticles(articles: List<ArticleBean>?) {
if (articles.isNullOrEmpty()) {
mBinding.rvList.visibility = GONE
mBinding.tvEmpty.visibility = VISIBLE
return
} else {
mBinding.rvList.visibility = VISIBLE
mBinding.tvEmpty.visibility = GONE
}
val topTwo = articles.take(2).toMutableList()
mAdapter.setNewInstance(topTwo)
}
/** 追加文章列表数据(分页加载) */
fun addArticles(articles: List<ArticleBean>) {
mAdapter.addData(articles)
}
fun setOnItemClickListener(listener: (ArticleBean) -> Unit) {
mOnItemClickListener = listener
}
fun setOnMoreClickListener(listener: () -> Unit) {
mOnMoreClickListener = listener
}
}
@@ -0,0 +1,109 @@
package com.xjjk.healthyclients.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.xjjk.healthyclients.R
import com.xjjk.healthyclients.adapter.HealthKnowledgeAdapter
import com.xjjk.healthyclients.bean.home.ArticleBean
import com.xjjk.healthyclients.bean.home.CategoryBean
import com.xjjk.healthyclients.databinding.LayoutHealthKnowledgeBinding
import com.xjjk.healthyclients.superfuntion.onCreateTab
/**
* 健康知识 View 组件
* 头部标题 + 搜索栏 + TabLayout(医生说/体检/保健/药品/疾病) + 列表
*/
class HealthKnowledgeView(context: Context?, attrs: AttributeSet?) :
RelativeLayout(context, attrs, 0) {
var mContext: Context? = context
var mBinding: LayoutHealthKnowledgeBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.layout_health_knowledge, this, true
)
private val mAdapter = HealthKnowledgeAdapter()
private var mCategoryList: MutableList<CategoryBean> = mutableListOf()
private var mOnTabSelectedListener: ((CategoryBean) -> Unit)? = null
private var mOnItemClickListener: ((ArticleBean) -> Unit)? = null
private var mOnMoreClickListener: (() -> Unit)? = null
private var mOnSearchClickListener: (() -> Unit)? = null
init {
mBinding.rvList.layoutManager = LinearLayoutManager(mContext)
mBinding.rvList.adapter = mAdapter
// 列表项点击
mAdapter.setOnItemClickListener { _, _, position ->
mAdapter.getItem(position)?.let { mOnItemClickListener?.invoke(it) }
}
// 头部更多按钮点击
mBinding.ivMore.setOnClickListener {
mOnMoreClickListener?.invoke()
}
// 健康知识 - 搜索
mBinding.searchView.root.setOnClickListener {
mOnSearchClickListener?.invoke()
}
}
/**
* 根据栏目列表设置 TabLayout
* @param categories 子栏目列表(健康知识 categoryId=1 的 children
*/
fun setupTabs(categories: MutableList<CategoryBean>) {
mCategoryList.clear()
mCategoryList.addAll(categories)
mBinding.tabLayout.removeAllTabs()
val array = Array<String>(categories.size) {
categories[it].categoryName?:""
}
mBinding.tabLayout.onCreateTab(array,2){index->
mOnTabSelectedListener?.invoke(categories[index!!.position]) ?: return@onCreateTab
}
}
/** 设置文章列表数据(替换) */
fun setArticles(articles: MutableList<ArticleBean>?) {
if (articles.isNullOrEmpty()) {
mBinding.rvList.visibility = GONE
mBinding.tvEmpty.visibility = VISIBLE
return
}
mBinding.rvList.visibility = VISIBLE
mBinding.tvEmpty.visibility = GONE
articles.take(3).let {
mAdapter.setNewInstance(it.toMutableList()) }
}
/** 追加文章列表数据(分页加载) */
fun addArticles(articles: List<ArticleBean>) {
mAdapter.addData(articles)
}
fun setOnTabSelectedListener(listener: (CategoryBean) -> Unit) {
mOnTabSelectedListener = listener
}
fun setOnItemClickListener(listener: (ArticleBean) -> Unit) {
mOnItemClickListener = listener
}
fun setOnMoreClickListener(listener: () -> Unit) {
mOnMoreClickListener = listener
}
fun setOnSearchClickListener(listener: () -> Unit) {
mOnSearchClickListener = listener
}
}
@@ -62,7 +62,7 @@ class WatchBindDialog(context: Context, var listener: (String,Int) -> Unit) : Al
if (TextUtils.isEmpty(watchId)) {
binding.etWatchid.isEnabled = true
binding.confirm.setText("绑定")
type = 0
// type = 0
} else {
binding.etWatchid.isEnabled = false
binding.confirm.isEnabled = false
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/theme_color" android:state_selected="true" />
<item android:color="@color/text_black_60" />
</selector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 选中状态-->
<item android:drawable="@drawable/bg_theme_background_shape3" android:state_checked="true"/>
<item android:drawable="@drawable/bg_theme_background_shape3" android:state_selected="true"/>
<!-- 默认状态-->
<item android:drawable="@color/colorWhite"/>
</selector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorPrimary" />
<corners android:radius="3dp" />
</shape>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_50"
android:background="@color/white">
<LinearLayout
android:layout_marginRight="10dp"
android:id="@+id/custom_ll_search"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_35"
android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/dp_18"
android:layout_toLeftOf="@+id/custom_search_tv"
android:background="@drawable/bg_search_shap_radius23">
<ImageView
android:layout_width="@dimen/dp_15"
android:layout_height="@dimen/dp_15"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/dp_17"
android:src="@mipmap/ic_search" />
<EditText
android:id="@+id/custom_search_et"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/dp_12"
android:background="@null"
android:longClickable="false"
android:gravity="center_vertical"
android:hint="@string/search_hint"
android:maxLines="1"
android:imeOptions="actionSearch"
android:singleLine="true"
android:textColor="@color/text_black_66"
android:textColorHint="@color/text_black_99"
android:textSize="@dimen/txt13" />
</LinearLayout>
<TextView
android:id="@+id/custom_search_tv"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/dp_22"
android:gravity="center_vertical"
android:paddingLeft="@dimen/dp_10"
android:text="@string/search_submit"
android:textColor="@color/text_black_99"
android:textSize="@dimen/txt13" />
</RelativeLayout>
</layout>
+15
View File
@@ -169,7 +169,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.xjjk.healthyclients.view.HealthKnowledgeView
android:id="@+id/health_knowledge_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="10dp" />
<com.xjjk.healthyclients.view.HealthConsultationView
android:id="@+id/health_consultation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="10dp" />
<com.youth.banner.Banner
android:layout_marginTop="10dp"
android:id="@+id/banner4"
android:layout_width="match_parent"
android:layout_height="120dp"
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/dp_15"
android:paddingTop="@dimen/dp_12"
android:paddingRight="@dimen/dp_15">
<!-- 左侧文字区域 -->
<!-- 标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="90dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#111225"
android:textSize="@dimen/txt15"
tools:text="高血压患者日常生活注意事项与饮食指南" />
<!-- 右侧图片 80x60 圆角8 -->
<ImageView
android:id="@+id/iv_cover"
android:layout_width="@dimen/dp_80"
android:layout_height="@dimen/dp_60"
android:layout_alignParentRight="true"
android:layout_marginLeft="@dimen/dp_12"
tools:src="@mipmap/ic_placeholder_rectangle" />
<!-- 收藏和分享信息 -->
<TextView
android:id="@+id/tv_meta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginTop="@dimen/dp_8"
android:layout_marginBottom="15dp"
android:textColor="@color/text_tab_color"
android:textSize="@dimen/txt12"
tools:text="收藏 128 分享 36" />
<View
android:layout_width="wrap_content"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#EAECF1" />
</RelativeLayout>
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="130dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/dp_15"
android:paddingTop="@dimen/dp_12"
android:paddingRight="@dimen/dp_15">
<!-- 左侧图片 100x100 圆角15 -->
<ImageView
android:id="@+id/iv_cover"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="centerCrop"
tools:src="@mipmap/ic_home_item_head_bg" />
<!-- 右侧文字区域 -->
<RelativeLayout
android:layout_marginTop="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_120"
android:orientation="vertical">
<!-- 标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#252535"
android:textSize="@dimen/txt15"
tools:text="高血压患者日常生活注意事项" />
<!-- 详情 -->
<TextView
android:id="@+id/tv_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_title"
android:layout_marginTop="@dimen/dp_8"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/text_tab_color"
android:textSize="@dimen/txt13"
tools:text="高血压是一种常见的慢性疾病,需要长期坚持治疗和管理..." />
</RelativeLayout>
<!-- 底部:标签 + 日期 -->
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginLeft="@dimen/dp_120"
android:layout_marginBottom="20dp">
<!-- 标签图标 -->
<ImageView
android:id="@+id/iv_tag_icon"
android:layout_width="@dimen/dp_20"
android:layout_height="@dimen/dp_20"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:scaleType="centerInside"
android:src="@mipmap/ic_home_know_tag" />
<!-- 标签文本 -->
<TextView
android:id="@+id/tv_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/dp_4"
android:layout_toRightOf="@+id/iv_tag_icon"
android:textColor="#14BEBE"
android:textSize="@dimen/txt11"
tools:text="心血管" />
<!-- 日期 -->
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textColor="@color/text_tab_color"
android:textSize="@dimen/txt11"
tools:text="2024-06-29" />
</RelativeLayout>
<View
android:layout_width="wrap_content"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:layout_marginLeft="120dp"
android:background="#EAECF1" />
</RelativeLayout>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_tab_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:paddingHorizontal="10dp"
android:paddingVertical="7dp"
android:textColor="@color/color_tab_layout_text_theme"
android:textSize="16sp" />
<View
android:id="@+id/tv_line"
android:layout_width="18dp"
android:layout_height="3dp"
android:layout_below="@+id/tv_text"
android:layout_centerHorizontal="true"
android:background="@drawable/bg_child_tab_layout_item_line" />
</RelativeLayout>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/dp_10"
android:background="@drawable/bg_white_background_shap_radius12"
>
<!-- 头部:标题 + 更多箭头 -->
<RelativeLayout
android:id="@+id/rl_header"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@mipmap/ic_home_item_head_bg"
android:scaleType="fitXY">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:text="健康咨询"
android:textColor="@color/text_black_33"
android:textSize="16sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/iv_more"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentRight="true"
android:layout_marginTop="12dp"
android:layout_marginRight="15dp"
android:background="@drawable/arrow_right"
android:padding="10dp" />
</RelativeLayout>
<!-- 列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:paddingTop="@dimen/dp_10" />
<!-- 空状态提示 -->
<TextView
android:id="@+id/tv_empty"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center"
android:text="暂无健康咨询"
android:textColor="#14BEBE"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</layout>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data></data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/dp_10"
android:background="@drawable/bg_white_background_shap_radius12"
android:orientation="vertical">
<!-- 头部:标题 + 更多箭头 -->
<RelativeLayout
android:id="@+id/rl_header"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@mipmap/ic_home_item_head_bg"
android:scaleType="fitXY">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:text="健康知识"
android:textColor="@color/text_black_33"
android:textSize="16sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/iv_more"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentRight="true"
android:layout_marginTop="12dp"
android:layout_marginRight="15dp"
android:background="@drawable/arrow_right"
android:padding="10dp" />
</RelativeLayout>
<!-- 搜索栏 -->
<include
android:id="@+id/search_view"
layout="@layout/custom_search_view_home" />
<!-- Tab 标签栏 -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="5dp"
android:background="@color/white"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="@dimen/dp_0"
app:tabMinWidth="@dimen/dp_0"
app:tabMode="fixed"
app:tabPaddingEnd="@dimen/dp_0"
app:tabPaddingStart="@dimen/dp_0"
app:tabRippleColor="@null" />
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="@dimen/dp_1"
android:background="@color/line" />
<!-- 列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:paddingTop="@dimen/dp_10" />
<!-- 空状态提示 -->
<TextView
android:id="@+id/tv_empty"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center"
android:text="暂无健康知识"
android:textColor="#14BEBE"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</layout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB