fix(chat): 修复聊天界面加载状态和滚动问题

- 添加了 onComplete 回调参数确保识别完成后重置加载状态
- 修复 Toast 显示错误信息时使用正确的 message 字段
- 改进加载指示器显示逻辑,添加第二个进度条 pb1
- 实现智能滚动到底部功能,支持 SSE 流式内容更新
- 优化 recognizeAndChat 方法中的加载状态管理
- 添加 UUID 随机数触发结果更新,过滤 SSE 非数据行
这个提交包含在:
徐勤民 2026-04-16 18:15:51 +08:00
父节点 57c505d4b5
当前提交 83ce6341ab
共有 4 个文件被更改,包括 59 次插入6 次删除

查看文件

@ -32,6 +32,7 @@ object IntentRecognizeHelper {
private var disposable: Disposable? = null private var disposable: Disposable? = null
private val baseUrl: String = "https://22v1322u01.vicp.fun" private val baseUrl: String = "https://22v1322u01.vicp.fun"
// private val baseUrl: String = "http://192.168.6.20:12119"
/** /**
* @param context 用于显示 Toast * @param context 用于显示 Toast
@ -39,11 +40,15 @@ object IntentRecognizeHelper {
* @param scence 场景标识默认 "home" * @param scence 场景标识默认 "home"
* @param onSuccess 识别成功且 code=="0" 时回调参数为 [RecognizeAction] * @param onSuccess 识别成功且 code=="0" 时回调参数为 [RecognizeAction]
*/ */
/**
* @param onComplete 无论成功失败都会回调用于调用方重置 loading 状态
*/
fun recognize( fun recognize(
context: Context, context: Context,
text: String? = null, text: String? = null,
scence: String = "home", scence: String = "home",
onSuccess: (action: RecognizeAction) -> Unit onSuccess: (action: RecognizeAction) -> Unit,
onComplete: () -> Unit = {}
) { ) {
disposable?.dispose() disposable?.dispose()
val question = text ?: nextQuestion() val question = text ?: nextQuestion()
@ -59,10 +64,12 @@ object IntentRecognizeHelper {
if (model.code == "0") { if (model.code == "0") {
onSuccess(model.data.action) onSuccess(model.data.action)
} else { } else {
Toast.makeText(context, model.data.error, Toast.LENGTH_SHORT).show() Toast.makeText(context, model.message, Toast.LENGTH_SHORT).show()
onComplete()
} }
}, { e -> }, { e ->
Toast.makeText(context, "请求失败: ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "请求失败: ${e.message}", Toast.LENGTH_SHORT).show()
onComplete()
}) })
} }

查看文件

@ -35,13 +35,17 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
override fun initData() { override fun initData() {
super.initData() super.initData()
// SSE 流式内容更新时持续滚底
viewModel.result.observe(this) { viewModel.result.observe(this) {
val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1 scrollToBottom()
if (lastIndex >= 0) recyclerView.scrollToPosition(lastIndex)
} }
// loading=truepb 显示旋转 + 延一帧滚底(等 PagedList 提交到 adapter
// loading=falsepb 停转并隐藏
viewModel.loading.observe(this) { loading -> viewModel.loading.observe(this) { loading ->
binding.pb.isIndeterminate = loading binding.pb.visibility = if (loading) View.VISIBLE else View.INVISIBLE
binding.pb1.visibility = if (!loading) View.VISIBLE else View.INVISIBLE
recyclerView.post { scrollToBottom() }
} }
val question = intent.getStringExtra("question") ?: "" val question = intent.getStringExtra("question") ?: ""
@ -50,18 +54,47 @@ class ChatActivity : BaseListFormLayoutNormalActivity<ChatItem, ChatVM, Activity
} }
} }
/**
* 调用意图识别识别成功后发起 SSE 请求
* 立即启动 pb 旋转不等 recognize 接口响应
*/
private fun recognizeAndChat() { private fun recognizeAndChat() {
binding.pb.visibility = View.VISIBLE
binding.pb1.visibility = View.INVISIBLE
IntentRecognizeHelper.recognize( IntentRecognizeHelper.recognize(
context = this, context = this,
scence = "decision", scence = "decision",
onSuccess = { action -> onSuccess = { action ->
if (action.name == "goToDecisionCenter") { if (action.name == "goToDecisionCenter") {
viewModel.demoPostSse(action.params.question) viewModel.demoPostSse(action.params.question)
} else {
// 识别成功但没有匹配动作,停止旋转
binding.pb.isIndeterminate = false
binding.pb.visibility = View.INVISIBLE
} }
},
onComplete = {
// recognize 失败时兜底停止旋转(成功路径由 loading LiveData 接管)
binding.pb.isIndeterminate = false
binding.pb.visibility = View.INVISIBLE
} }
) )
} }
private fun scrollToBottom() {
val lastIndex = (recyclerView.adapter?.itemCount ?: 1) - 1
if (lastIndex < 0) return
val lm = recyclerView.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager ?: return
// 第一帧:确保 lastIndex 进入可视区域
lm.scrollToPosition(lastIndex)
// 第二帧layout 完成后,把 lastView 的底部对齐到 RecyclerView 底部
recyclerView.post {
val lastView = lm.findViewByPosition(lastIndex) ?: return@post
val gap = lastView.bottom - (recyclerView.height - recyclerView.paddingBottom)
if (gap > 0) recyclerView.scrollBy(0, gap)
}
}
override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) { override fun adapter() = object : CommonPagedAdapter<ChatItem>(R.layout.item_chat) {
override fun convert(holder: ViewHolder, item: ChatItem, position: Int) { override fun convert(holder: ViewHolder, item: ChatItem, position: Int) {
holder.setVisibility(R.id.line, position != 0) holder.setVisibility(R.id.line, position != 0)

查看文件

@ -15,6 +15,7 @@ import com.xuqm.base.viewmodel.BaseListViewModel
import com.xuqm.base.viewmodel.callback.Response import com.xuqm.base.viewmodel.callback.Response
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import java.util.UUID
class ChatVM : BaseListViewModel<ChatItem>() { class ChatVM : BaseListViewModel<ChatItem>() {
val result = MutableLiveData<String>() val result = MutableLiveData<String>()
@ -42,6 +43,7 @@ class ChatVM : BaseListViewModel<ChatItem>() {
val itemId = itemIdCounter++ val itemId = itemIdCounter++
chatItems.add(ChatItem(itemId, question, "")) chatItems.add(ChatItem(itemId, question, ""))
if (dataSourceReady) invalidate() if (dataSourceReady) invalidate()
result.postValue(UUID.randomUUID().toString())
loading.postValue(true) loading.postValue(true)
currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question)) currentTask = HttpManager.getApi(Service::class.java).chat(ChatData(question))
@ -63,7 +65,9 @@ class ChatVM : BaseListViewModel<ChatItem>() {
loading.postValue(false) loading.postValue(false)
return@use return@use
} }
val json = if (l.startsWith("data:")) l.removePrefix("data:").trim() else l // 只处理 data:{...} 格式,其他行event/id/注释等)跳过
if (!l.startsWith("data:{")) continue
val json = l.removePrefix("data:").trim()
val model = GsonImplHelp.get().toObject(json, ChatModel::class.java) val model = GsonImplHelp.get().toObject(json, ChatModel::class.java)
if (model.type == null) { if (model.type == null) {
loading.postValue(false) loading.postValue(false)

查看文件

@ -21,6 +21,15 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:indeterminateDrawable="@drawable/load_progress" /> android:indeterminateDrawable="@drawable/load_progress" />
<ImageView
android:id="@+id/pb1"
app:layout_constraintTop_toBottomOf="@+id/baseRecyclerView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/baseRecyclerView"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/loading"/>
<TextView <TextView
android:id="@+id/hint1" android:id="@+id/hint1"