feat: 日志详情页数据驱动渲染(流转状态+操作详情)
- 入口页传递运单号和运单类型至日志详情页 - 区分国际出港(9步骤)/国际进港(6步骤)流转节点 - 用status字段匹配节点状态(蓝色/白色/绿色) - 修复API返回裸数组被拦截器包装导致解析失败的问题 - ScrollView改为NestedScrollView修复竖向列表不渲染 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,10 +38,10 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
// 配置操作详情 RecyclerView(垂直时间线)
|
||||
binding.rvTimeline.adapter = timelineAdapter
|
||||
|
||||
// 观察流转状态变化,程序化构建步骤进度条
|
||||
viewModel.currentStepIndex.observe(this) { index ->
|
||||
buildStepProgressBar(viewModel.allSteps, index)
|
||||
}
|
||||
// 观察数据变化,重新构建步骤进度条
|
||||
viewModel.allSteps.observe(this) { rebuildSteps() }
|
||||
viewModel.activeStepCodes.observe(this) { rebuildSteps() }
|
||||
viewModel.latestStepCode.observe(this) { rebuildSteps() }
|
||||
|
||||
viewModel.statusLogList.observe(this) { list ->
|
||||
timelineAdapter.setData(list)
|
||||
@@ -50,7 +50,19 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
viewModel.initOnCreated(intent)
|
||||
}
|
||||
|
||||
private fun buildStepProgressBar(steps: List<String>, currentIndex: Int) {
|
||||
private fun rebuildSteps() {
|
||||
val steps = viewModel.allSteps.value ?: return
|
||||
val activeCodes = viewModel.activeStepCodes.value ?: emptySet()
|
||||
val latestCode = viewModel.latestStepCode.value ?: ""
|
||||
if (steps.isEmpty()) return
|
||||
buildStepProgressBar(steps, activeCodes, latestCode)
|
||||
}
|
||||
|
||||
private fun buildStepProgressBar(
|
||||
steps: List<StepInfo>,
|
||||
activeCodes: Set<String>,
|
||||
latestCode: String
|
||||
) {
|
||||
val container = binding.llSteps
|
||||
container.removeAllViews()
|
||||
|
||||
@@ -60,30 +72,38 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
val dotSize = dp(10)
|
||||
val lineHeight = dp(2)
|
||||
|
||||
// 计算每个节点宽度:屏幕宽度的 80% 均分给所有节点
|
||||
val screenWidth = resources!!.displayMetrics.widthPixels
|
||||
val stepWidth = (screenWidth * 0.8 / steps.size).toInt()
|
||||
|
||||
// 找到最新节点在步骤列表中的索引
|
||||
val latestIndex = steps.indexOfFirst { it.code == latestCode }
|
||||
|
||||
for (i in steps.indices) {
|
||||
val isCompleted = i <= currentIndex
|
||||
val isCurrent = i == currentIndex
|
||||
val step = steps[i]
|
||||
val isActive = step.code in activeCodes
|
||||
val isLatest = step.code == latestCode
|
||||
val isFirst = i == 0
|
||||
val isLast = i == steps.size - 1
|
||||
|
||||
// 每个步骤的根容器(等宽)
|
||||
// 每个步骤的根容器
|
||||
val stepLayout = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
layoutParams = LinearLayout.LayoutParams(stepWidth, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
// 步骤名称(所有步骤统一 padding 确保高度一致)
|
||||
val tvName = TextView(this).apply {
|
||||
text = steps[i]
|
||||
text = step.name
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(dp(6), dp(2), dp(6), dp(2))
|
||||
if (isCurrent) {
|
||||
if (isLatest) {
|
||||
setBackgroundResource(R.drawable.bg_step_current_badge)
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
} else {
|
||||
setTextColor(if (isCompleted) 0xFF333333.toInt() else 0xFF999999.toInt())
|
||||
setTextColor(if (isActive) 0xFF333333.toInt() else 0xFF999999.toInt())
|
||||
}
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
@@ -99,10 +119,12 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
).apply { topMargin = dp(8) }
|
||||
}
|
||||
|
||||
// 左半连线(从左边缘到中心,略超过中心以避免断层)
|
||||
// 左半连线(从左边缘到中心)
|
||||
if (!isFirst) {
|
||||
// 如果当前节点索引 <= latestIndex,左半线为蓝色;否则为浅灰色
|
||||
val leftLineColor = if (latestIndex >= 0 && i <= latestIndex) colorBlue else colorGray
|
||||
val lineLeft = View(this).apply {
|
||||
setBackgroundColor(if (isCompleted) colorBlue else colorGray)
|
||||
setBackgroundColor(leftLineColor)
|
||||
layoutParams = FrameLayout.LayoutParams(0, lineHeight).apply {
|
||||
gravity = Gravity.CENTER_VERTICAL or Gravity.START
|
||||
}
|
||||
@@ -115,9 +137,10 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
}
|
||||
}
|
||||
|
||||
// 右半连线(从中心到右边缘,略超过中心以避免断层)
|
||||
// 右半连线(从中心到右边缘)
|
||||
if (!isLast) {
|
||||
val rightLineColor = if (isCompleted && !isCurrent) colorBlue else colorGray
|
||||
// 如果当前节点索引 < latestIndex,右半线为蓝色;否则为浅灰色
|
||||
val rightLineColor = if (latestIndex >= 0 && i < latestIndex) colorBlue else colorGray
|
||||
val lineRight = View(this).apply {
|
||||
setBackgroundColor(rightLineColor)
|
||||
layoutParams = FrameLayout.LayoutParams(0, lineHeight).apply {
|
||||
@@ -136,8 +159,8 @@ class LogDetailActivity : BaseBindingActivity<ActivityLogDetailBinding, LogDetai
|
||||
val dot = View(this).apply {
|
||||
setBackgroundResource(
|
||||
when {
|
||||
isCurrent -> R.drawable.bg_step_dot_green
|
||||
isCompleted -> R.drawable.bg_step_dot_blue
|
||||
isLatest -> R.drawable.bg_step_dot_green
|
||||
isActive -> R.drawable.bg_step_dot_blue
|
||||
else -> R.drawable.bg_step_dot_gray
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,14 +2,19 @@ package com.lukouguoji.aerologic.page.log.detail
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import com.lukouguoji.module_base.base.BaseViewModel
|
||||
import com.lukouguoji.module_base.bean.LogBean
|
||||
import com.lukouguoji.module_base.bean.StatusLogBean
|
||||
import com.lukouguoji.module_base.common.Constant
|
||||
import com.lukouguoji.module_base.http.net.NetApply
|
||||
import com.lukouguoji.module_base.ktx.launchCollect
|
||||
import com.lukouguoji.module_base.ktx.toRequestBody
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class StepInfo(val code: String, val name: String)
|
||||
|
||||
class LogDetailViewModel : BaseViewModel() {
|
||||
|
||||
@@ -18,71 +23,100 @@ class LogDetailViewModel : BaseViewModel() {
|
||||
|
||||
val statusLogList = MutableLiveData<List<StatusLogBean>>(emptyList())
|
||||
|
||||
// 流转状态步骤定义
|
||||
val allSteps = listOf(
|
||||
"预录入", "完成收运", "运抵申报", "海关放行",
|
||||
"完成组装", "完成复磅", "装载申报", "航班关闭", "理货申报"
|
||||
)
|
||||
// 流转状态步骤(根据运单类型动态设置)
|
||||
val allSteps = MutableLiveData<List<StepInfo>>(emptyList())
|
||||
|
||||
// 当前完成到哪一步(索引)
|
||||
// 操作详情中出现的步骤 code 集合
|
||||
val activeStepCodes = MutableLiveData<Set<String>>(emptySet())
|
||||
|
||||
// 最新步骤的 code
|
||||
val latestStepCode = MutableLiveData("")
|
||||
|
||||
// 当前完成到哪一步(索引),用于线条着色
|
||||
val currentStepIndex = MutableLiveData(-1)
|
||||
|
||||
private var awbType = ""
|
||||
|
||||
// 国际出港步骤(来自 AwbGjcStatus.java)
|
||||
private val gjcSteps = listOf(
|
||||
StepInfo("1", "预录入"), StepInfo("2", "收运完成"),
|
||||
StepInfo("3", "运抵申报"), StepInfo("4", "海关放行"),
|
||||
StepInfo("5", "组装完成"), StepInfo("6", "复磅完成"),
|
||||
StepInfo("7", "装载申报"), StepInfo("8", "航班关闭"),
|
||||
StepInfo("9", "理货申报")
|
||||
)
|
||||
|
||||
// 国际进港步骤(来自 AwbGjjStatus.java)
|
||||
private val gjjSteps = listOf(
|
||||
StepInfo("1", "原始舱单"), StepInfo("2", "分拣理货"),
|
||||
StepInfo("3", "理货申报"), StepInfo("4", "海关放行"),
|
||||
StepInfo("5", "柜台办结"), StepInfo("6", "提取出库")
|
||||
)
|
||||
|
||||
fun initOnCreated(intent: Intent) {
|
||||
val json = intent.getStringExtra(Constant.Key.DATA) ?: ""
|
||||
if (json.isNotEmpty()) {
|
||||
val bean = Gson().fromJson(json, LogBean::class.java)
|
||||
waybillNo.value = bean.key
|
||||
waybillType.value = getAwbTypeName(bean.logType)
|
||||
loadStatusList(bean.key, bean.logType)
|
||||
// 优先从 ARouter 参数获取
|
||||
val key = intent.getStringExtra(Constant.Key.KEY) ?: ""
|
||||
awbType = intent.getStringExtra(Constant.Key.AWB_TYPE) ?: ""
|
||||
|
||||
// 如果没有 ARouter 参数,尝试从 LogBean 解析
|
||||
if (key.isEmpty()) {
|
||||
val json = intent.getStringExtra(Constant.Key.DATA) ?: ""
|
||||
if (json.isNotEmpty()) {
|
||||
val bean = Gson().fromJson(json, LogBean::class.java)
|
||||
waybillNo.value = bean.key
|
||||
awbType = bean.logType
|
||||
}
|
||||
} else {
|
||||
waybillNo.value = key
|
||||
}
|
||||
|
||||
waybillType.value = getAwbTypeName(awbType)
|
||||
allSteps.value = if (awbType == "II") gjjSteps else gjcSteps
|
||||
|
||||
if (waybillNo.value?.isNotEmpty() == true) {
|
||||
loadStatusList(waybillNo.value!!, awbType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStatusList(key: String, logType: String) {
|
||||
if (key.isEmpty()) {
|
||||
loadMockData()
|
||||
return
|
||||
}
|
||||
launchCollect({
|
||||
NetApply.api.getLogStatusList(
|
||||
mapOf(
|
||||
"key" to key,
|
||||
"awbType" to logType
|
||||
).toRequestBody()
|
||||
)
|
||||
}) {
|
||||
onSuccess = {
|
||||
val list = it.data ?: emptyList()
|
||||
if (list.isNotEmpty()) {
|
||||
statusLogList.value = list
|
||||
val lastStatus = list.last().content
|
||||
val index = allSteps.indexOfFirst { step ->
|
||||
lastStatus.contains(step)
|
||||
}
|
||||
currentStepIndex.value = if (index >= 0) index else list.size - 1
|
||||
} else {
|
||||
loadMockData()
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// 注意:SelfLoginInterceptor 会把裸数组 [...] 包装成 {"data": [...]}
|
||||
// 所以 API 返回类型必须是 BaseResultBean,从 .data 取实际列表
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
NetApply.api.getLogStatusList(
|
||||
mapOf(
|
||||
"key" to key,
|
||||
"awbType" to logType
|
||||
).toRequestBody()
|
||||
)
|
||||
}
|
||||
}
|
||||
onFailed = { _, _ ->
|
||||
loadMockData()
|
||||
|
||||
val list = result.data ?: emptyList()
|
||||
statusLogList.value = list
|
||||
|
||||
if (list.isNotEmpty()) {
|
||||
// 提取所有出现的 status code(用 status 字段匹配)
|
||||
val codes = list.mapNotNull { item ->
|
||||
item.status.ifEmpty { null }
|
||||
}.toSet()
|
||||
activeStepCodes.value = codes
|
||||
|
||||
// 最新节点 = 列表最后一条的 status
|
||||
val latestCode = list.last().status
|
||||
latestStepCode.value = latestCode
|
||||
|
||||
// 计算最新节点在步骤列表中的索引
|
||||
val steps = allSteps.value ?: emptyList()
|
||||
val index = steps.indexOfFirst { step -> step.code == latestCode }
|
||||
currentStepIndex.value = if (index >= 0) index else -1
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMockData() {
|
||||
// Mock 数据:模拟到"装载申报"步骤
|
||||
currentStepIndex.value = 6 // "装载申报" 在 allSteps 中的索引
|
||||
|
||||
statusLogList.value = listOf(
|
||||
StatusLogBean(content = "托书录入", opDate = "2017-04-01 12:00:00"),
|
||||
StatusLogBean(content = "完成收运", opDate = "2017-04-01 12:00:00"),
|
||||
StatusLogBean(content = "完成组装", opDate = "2017-04-01 12:00:00"),
|
||||
StatusLogBean(content = "已复磅", opDate = "2017-04-01 12:00:00"),
|
||||
StatusLogBean(content = "海关已放行", opDate = "2017-04-01 12:00:00"),
|
||||
StatusLogBean(content = "装载申报", opDate = "2017-04-01 12:00:00")
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAwbTypeName(logType: String): String {
|
||||
return when (logType) {
|
||||
"CI" -> "国内进港"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<include layout="@layout/title_tool_bar" />
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
@@ -59,7 +59,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="运单号: "
|
||||
android:text="运单号:"
|
||||
android:textColor="#666666"
|
||||
android:textSize="14sp" />
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="运单类型: "
|
||||
android:text="运单类型:"
|
||||
android:textColor="#666666"
|
||||
android:textSize="14sp" />
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
|
||||
Reference in New Issue
Block a user