feat(task): 优化任务列表和审批功能

- 更新TaskExtraItem数据结构,将index/id/processStatus/aiDescription字段
  替换为taskId/desc/numberNo字段
- 在Service中新增pushToNext、recommendHandler、recommendBackNode、backTo
  等工作流相关API接口
- 在ReviewActivity中实现完整的审批流程,包括同意和驳回操作
- 在TaskListActivity中实现可视区域任务数据同步和动态索引更新
- 优化语音识别的额外数据提供机制,支持当前可见任务列表的准确传递
- 添加工作流实例ID和活动ID的参数传递功能
这个提交包含在:
徐勤民 2026-04-18 15:00:58 +08:00
父节点 f20255947c
当前提交 d6eb9bd3bd
共有 4 个文件被更改,包括 244 次插入30 次删除

查看文件

@ -4,7 +4,7 @@ package com.nova.brain.glass.model.data
// "text": "查看下C919第45架机的零部件采购情况", // "text": "查看下C919第45架机的零部件采购情况",
// "scence": "home", // "scence": "home",
// "extra": [ // "extra": [
// { "index": 1, "id": "xxx", "taskType": "审核任务", "processStatus": "进行中", "aiDescription": "..." } // { "taskId": "xxx", "taskType": "审核任务", "desc": "...", "numberNo": 1 }
// ], // ],
// "actions": ["goToDecisionCenter"] // "actions": ["goToDecisionCenter"]
//} //}
@ -17,9 +17,8 @@ data class RecognizeData(
// list 场景下传给服务端,帮助 NLP 定位用户所说的任务 // list 场景下传给服务端,帮助 NLP 定位用户所说的任务
data class TaskExtraItem( data class TaskExtraItem(
val index: Int, // 列表中的显示序号1-based val taskId: String,
val id: String,
val taskType: String, val taskType: String,
val processStatus: String, val desc: String,
val aiDescription: String val numberNo: Int // 当前可见列表中的显示序号1-based
) )

查看文件

@ -2,7 +2,16 @@ package com.nova.brain.glass.repository
import com.nova.brain.glass.model.RecognizeModel import com.nova.brain.glass.model.RecognizeModel
import com.nova.brain.glass.model.data.ChatData import com.nova.brain.glass.model.data.ChatData
import com.nova.brain.glass.model.data.ApiResponse
import com.nova.brain.glass.model.data.BackToData
import com.nova.brain.glass.model.data.BackToRequest
import com.nova.brain.glass.model.data.RecognizeData import com.nova.brain.glass.model.data.RecognizeData
import com.nova.brain.glass.model.data.PushToNextData
import com.nova.brain.glass.model.data.PushToNextRequest
import com.nova.brain.glass.model.data.RecommendBackNodeData
import com.nova.brain.glass.model.data.RecommendBackNodeRequest
import com.nova.brain.glass.model.data.RecommendHandlerData
import com.nova.brain.glass.model.data.RecommendHandlerRequest
import com.nova.brain.glass.model.data.TaskListData import com.nova.brain.glass.model.data.TaskListData
import com.nova.brain.glass.model.TaskSearchResponse import com.nova.brain.glass.model.TaskSearchResponse
import com.nova.brain.glass.model.data.TopicData import com.nova.brain.glass.model.data.TopicData
@ -12,6 +21,7 @@ import okhttp3.RequestBody
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Streaming import retrofit2.http.Streaming
@ -36,4 +46,16 @@ interface Service {
@POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesTaskSearch") @POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesTaskSearch")
fun glassesTaskSearch(@Body body: TaskListData): Observable<TaskSearchResponse> fun glassesTaskSearch(@Body body: TaskListData): Observable<TaskSearchResponse>
} @POST("/cbrain-gateway/cbrain-execute/cbrain-execute/workflow/instance/pushToNext")
fun pushToNext(@Body body: PushToNextRequest): Observable<ApiResponse<PushToNextData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/glasses/recommendHandler")
fun recommendHandler(@Body body: RecommendHandlerRequest): Observable<ApiResponse<RecommendHandlerData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/glasses/recommendBackNode")
fun recommendBackNode(@Body body: RecommendBackNodeRequest): Observable<ApiResponse<RecommendBackNodeData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/workflow/instance/backTo")
fun backTo(@Body body: BackToRequest): Observable<ApiResponse<BackToData>>
}

查看文件

@ -6,38 +6,63 @@ import com.nova.brain.glass.databinding.ActivityReviewBinding
import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdListener
import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.OfflineCmdServiceHelper
import com.nova.brain.glass.model.ItemItem import com.nova.brain.glass.model.ItemItem
import com.nova.brain.glass.model.data.BackToRequest
import com.nova.brain.glass.model.data.PushToNextRequest
import com.nova.brain.glass.model.data.RecommendBackNodeRequest
import com.nova.brain.glass.model.data.RecommendHandlerRequest
import com.nova.brain.glass.repository.Service
import com.nova.brain.glass.viewmodel.ItemListVM import com.nova.brain.glass.viewmodel.ItemListVM
import com.xuqm.base.adapter.BasePagedAdapter import com.xuqm.base.adapter.BasePagedAdapter
import com.xuqm.base.adapter.CommonPagedAdapter import com.xuqm.base.adapter.CommonPagedAdapter
import com.xuqm.base.adapter.ViewHolder import com.xuqm.base.adapter.ViewHolder
import com.xuqm.base.di.manager.HttpManager
import com.xuqm.base.extensions.showMessage
import com.xuqm.base.ui.BaseListFormLayoutNormalActivity import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, ActivityReviewBinding>() { class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, ActivityReviewBinding>() {
override fun getLayoutId(): Int =R.layout.activity_review override fun getLayoutId(): Int = R.layout.activity_review
override fun fullscreen(): Boolean = true override fun fullscreen(): Boolean = true
override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL
private var isSubmitting = false
private var submitDisposable: Disposable? = null
private val workProcessInstanceId: String by lazy {
intent.getStringExtra("workProcessInstanceId").orEmpty()
}
private val currentActivityId: String by lazy {
intent.getStringExtra("currentActivityId").orEmpty()
}
private val listener = object : OfflineCmdListener { private val listener = object : OfflineCmdListener {
override fun onOfflineCmd(cmd: String) { override fun onOfflineCmd(cmd: String) {
runOnUiThread { runOnUiThread {
when( cmd){ when (cmd) {
"退出","返回","退回"->{ "退出", "返回", "退回" -> {
finish() finish()
} }
"驳回","拒绝","不同意"->{ "驳回", "拒绝", "不同意" -> {
finish() submitBack()
} }
"同意","通过"->{ "同意", "通过", "拟同意" -> {
finish() submitAgree()
} }
} }
} }
} }
} }
override fun initData() { override fun initData() {
super.initData() super.initData()
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.tvTaskHeader.text = intent.getStringExtra("taskType").orEmpty()
binding.content.text = intent.getStringExtra("content").orEmpty()
} }
override fun onResume() { override fun onResume() {
@ -53,26 +78,140 @@ class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, Ac
} }
override fun onDestroy() { override fun onDestroy() {
submitDisposable?.dispose()
super.onDestroy() super.onDestroy()
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
private fun submitAgree() {
if (isSubmitting) return
if (workProcessInstanceId.isBlank() || currentActivityId.isBlank()) {
"缺少流程参数,无法同意".showMessage()
return
}
isSubmitting = true
submitDisposable?.dispose()
val service = HttpManager.getApi(Service::class.java)
submitDisposable = service.pushToNext(
PushToNextRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
needHandle = 1,
innerRequest = true
)
).flatMap { response ->
if (response.status != 200 || response.data == null) {
return@flatMap Observable.error(
IllegalStateException(if (response.msg.isBlank()) "同意失败" else response.msg)
)
}
val needHandlerActivityIds = response.data.result.orEmpty().mapNotNull { it.activityId }.distinct()
if (needHandlerActivityIds.isEmpty()) {
Observable.just(emptyList())
} else {
service.recommendHandler(
RecommendHandlerRequest(
workProcessInstanceId = workProcessInstanceId,
needHandlerActivityIds = needHandlerActivityIds
)
).flatMap { recommendResponse ->
if (recommendResponse.status != 200 || recommendResponse.data == null) {
return@flatMap Observable.error(
IllegalStateException(
if (recommendResponse.msg.isBlank()) "推荐处理人失败" else recommendResponse.msg
)
)
}
Observable.just(recommendResponse.data.selectedHandler.orEmpty())
}
}
}.flatMap { selectedHandler ->
service.pushToNext(
PushToNextRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
needHandle = 0,
innerRequest = true,
comment = "拟同意",
selectedHandler = selectedHandler
)
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { isSubmitting = false }
.subscribe({ response ->
if (response.status == 200) {
"已同意".showMessage()
finish()
} else {
(if (response.msg.isBlank()) "同意失败" else response.msg).showMessage()
}
}, { e ->
(e.message ?: "同意失败").showMessage()
})
}
private fun submitBack() {
if (isSubmitting) return
if (workProcessInstanceId.isBlank() || currentActivityId.isBlank()) {
"缺少流程参数,无法回退".showMessage()
return
}
isSubmitting = true
submitDisposable?.dispose()
val service = HttpManager.getApi(Service::class.java)
submitDisposable = service.recommendBackNode(
RecommendBackNodeRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId
)
).flatMap { response ->
if (response.status != 200 || response.data?.backToActivityId.isNullOrBlank()) {
return@flatMap Observable.error(
IllegalStateException(if (response.msg.isBlank()) "推荐回退节点失败" else response.msg)
)
}
service.backTo(
BackToRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
backToActivityId = response.data?.backToActivityId.orEmpty(),
comment = "驳回,请线下沟通原因"
)
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { isSubmitting = false }
.subscribe({ response ->
if (response.status == 200) {
"已回退".showMessage()
finish()
} else {
(if (response.msg.isBlank()) "回退失败" else response.msg).showMessage()
}
}, { e ->
(e.message ?: "回退失败").showMessage()
})
}
private val adapter = object : CommonPagedAdapter<ItemItem>(R.layout.item_item) { private val adapter = object : CommonPagedAdapter<ItemItem>(R.layout.item_item) {
override fun convert(holder: ViewHolder, item: ItemItem, position: Int) { override fun convert(holder: ViewHolder, item: ItemItem, position: Int) {
holder holder
.setText(R.id.text, item.text) .setText(R.id.text, item.text)
.setClickListener(R.id.text .setClickListener(R.id.text) {
) { when (item.text) {
when(item.text){
"同意" -> { "同意" -> {
finish() submitAgree()
} }
"拒绝" -> { "拒绝" -> {
finish() submitBack()
} }
} }
} }
} }
} }
override fun adapter(): BasePagedAdapter<ItemItem> = adapter override fun adapter(): BasePagedAdapter<ItemItem> = adapter
} }

查看文件

@ -26,6 +26,7 @@ class TaskListActivity :
override fun getLayoutId(): Int = R.layout.activity_task_list override fun getLayoutId(): Int = R.layout.activity_task_list
override fun fullscreen(): Boolean = true override fun fullscreen(): Boolean = true
private var pageStartPosition = 0 private var pageStartPosition = 0
private var visibleTaskExtras: List<TaskExtraItem> = emptyList()
companion object { companion object {
private val EXIT_CMDS = setOf("退出", "返回", "退回", "关闭") private val EXIT_CMDS = setOf("退出", "返回", "退回", "关闭")
@ -41,6 +42,15 @@ class TaskListActivity :
} }
} }
}) })
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
binding.baseRecyclerView.post { refreshVisibleOrder() }
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
binding.baseRecyclerView.post { refreshVisibleOrder() }
}
})
binding.baseRecyclerView.post { binding.baseRecyclerView.post {
refreshVisibleOrder() refreshVisibleOrder()
} }
@ -100,7 +110,9 @@ class TaskListActivity :
private fun refreshVisibleOrder() { private fun refreshVisibleOrder() {
val lm = binding.baseRecyclerView.layoutManager as? LinearLayoutManager ?: return val lm = binding.baseRecyclerView.layoutManager as? LinearLayoutManager ?: return
val firstVisible = lm.findFirstVisibleItemPosition() val firstVisible = lm.findFirstVisibleItemPosition()
if (firstVisible == RecyclerView.NO_POSITION || firstVisible == pageStartPosition) return if (firstVisible == RecyclerView.NO_POSITION) return
updateVisibleTaskExtras(lm, firstVisible)
if (firstVisible == pageStartPosition) return
pageStartPosition = firstVisible pageStartPosition = firstVisible
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
@ -121,6 +133,20 @@ class TaskListActivity :
Intent(this, ReviewActivity::class.java) Intent(this, ReviewActivity::class.java)
.putExtra("taskId", item.id) .putExtra("taskId", item.id)
.putExtra("taskType", item.taskType) .putExtra("taskType", item.taskType)
.putExtra("content", item.aiDescription)
.putExtra(
"workProcessInstanceId",
item.params.firstNotBlank("processInstanceId", "work_process_instance_id")
)
.putExtra(
"currentActivityId",
item.params.firstNotBlank(
"currentActivityId",
"current_activity_id",
"activityId",
"activity_id"
)
)
) )
else -> Log.d("TaskListActivity", "unknown taskType: ${item.taskType}") else -> Log.d("TaskListActivity", "unknown taskType: ${item.taskType}")
} }
@ -132,15 +158,7 @@ class TaskListActivity :
OfflineCmdServiceHelper.addOnLineListener(offlineCmdListener) OfflineCmdServiceHelper.addOnLineListener(offlineCmdListener)
AsrHelper.scene = "list" AsrHelper.scene = "list"
AsrHelper.extraProvider = { AsrHelper.extraProvider = {
viewModel.currentItems.mapIndexed { i, item -> if (visibleTaskExtras.isNotEmpty()) visibleTaskExtras else buildTaskExtra(viewModel.currentItems)
TaskExtraItem(
index = i + 1,
id = item.id,
taskType = item.taskType,
processStatus = item.processStatus,
aiDescription = item.aiDescription
)
}
} }
AsrHelper.onGoToDecisionCenter = { action -> AsrHelper.onGoToDecisionCenter = { action ->
Log.d("TaskListActivity", "recognize result: $action") Log.d("TaskListActivity", "recognize result: $action")
@ -166,6 +184,42 @@ class TaskListActivity :
AsrHelper.onOpenTaskDetail = null AsrHelper.onOpenTaskDetail = null
} }
private fun updateVisibleTaskExtras(
lm: LinearLayoutManager,
firstVisible: Int = lm.findFirstVisibleItemPosition()
) {
val items = viewModel.currentItems
if (items.isEmpty()) {
visibleTaskExtras = emptyList()
return
}
val lastVisible = lm.findLastVisibleItemPosition()
if (firstVisible == RecyclerView.NO_POSITION || lastVisible == RecyclerView.NO_POSITION) {
visibleTaskExtras = buildTaskExtra(items)
return
}
val safeFirst = firstVisible.coerceAtLeast(0).coerceAtMost(items.lastIndex)
val safeLast = lastVisible.coerceAtLeast(0).coerceAtMost(items.lastIndex)
visibleTaskExtras = if (safeFirst <= safeLast) {
buildTaskExtra(items.subList(safeFirst, safeLast + 1))
} else {
buildTaskExtra(items)
}
}
private fun buildTaskExtra(items: List<TaskItem>): List<TaskExtraItem> =
items.mapIndexed { index, item ->
TaskExtraItem(
taskId = item.id,
taskType = item.taskType,
desc = item.aiDescription,
numberNo = index + 1
)
}
private fun Map<String, String>.firstNotBlank(vararg keys: String): String =
keys.firstNotNullOfOrNull { key -> this[key]?.takeIf { it.isNotBlank() } }.orEmpty()
private val adapter = object : CommonPagedAdapter<TaskItem>(R.layout.item_task_list) { private val adapter = object : CommonPagedAdapter<TaskItem>(R.layout.item_task_list) {
override fun convert(holder: ViewHolder, item: TaskItem, position: Int) { override fun convert(holder: ViewHolder, item: TaskItem, position: Int) {
val displayPosition = (position - pageStartPosition + 1).coerceAtLeast(1) val displayPosition = (position - pageStartPosition + 1).coerceAtLeast(1)
@ -181,4 +235,4 @@ class TaskListActivity :
super.onDestroy() super.onDestroy()
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
} }