From 056302cc10b73b06a494873e98ede078dad603a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Tue, 21 Apr 2026 14:52:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(spraying):=20=E6=B7=BB=E5=8A=A0OCR?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E4=BF=9D=E5=AD=98=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=BB=E5=8A=A1=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增OcrNavigationData数据类用于传递OCR导航参数 - 在Service中添加saveOcrResult接口用于保存OCR识别结果 - 在SprayingActivity中添加productionInfoId参数传递逻辑 - 在SprayingOCRActivity中接收并传递OCR相关参数到结果页面 - 在SprayingResultActivity中实现OCR结果保存和任务状态管理 - 新增SaveOcrState枚举管理OCR保存状态 - 实现saveOcrAndContinue和saveOcrAndFinish方法处理不同保存场景 - 优化SprayingOCRVM中的OCR结果数据转换逻辑 - 更新语音命令处理逻辑,统一调用新的触发方法 - 添加OCR结果JSON序列化和反序列化功能 --- .../glass/model/data/SprayingSubmitData.kt | 8 + .../nova/brain/glass/repository/Service.kt | 10 + .../nova/brain/glass/ui/SprayingActivity.kt | 3 + .../brain/glass/ui/SprayingOCRActivity.kt | 4 + .../brain/glass/ui/SprayingResultActivity.kt | 180 +++++++++--------- .../brain/glass/viewmodel/SprayingOCRVM.kt | 16 +- .../brain/glass/viewmodel/SprayingResultVM.kt | 74 ++++++- 7 files changed, 198 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/com/nova/brain/glass/model/data/SprayingSubmitData.kt b/app/src/main/java/com/nova/brain/glass/model/data/SprayingSubmitData.kt index 2c750a7..1f6db79 100644 --- a/app/src/main/java/com/nova/brain/glass/model/data/SprayingSubmitData.kt +++ b/app/src/main/java/com/nova/brain/glass/model/data/SprayingSubmitData.kt @@ -32,3 +32,11 @@ data class RecognizeByPathResponse( val data: OcrResultData? = null ) +data class OcrNavigationData( + val filePath: String, + val ocrResultJson: String, + val judgment: String, + val one: String, + val two: String +) + diff --git a/app/src/main/java/com/nova/brain/glass/repository/Service.kt b/app/src/main/java/com/nova/brain/glass/repository/Service.kt index 74828d0..202e772 100644 --- a/app/src/main/java/com/nova/brain/glass/repository/Service.kt +++ b/app/src/main/java/com/nova/brain/glass/repository/Service.kt @@ -79,6 +79,16 @@ interface Service { @POST("/skyscopicsecond-api/api/aiGlasses/recognizeByPath") fun recognizeByPath(@Body body: RecognizeByPathRequest): Observable + @Multipart + @POST("/skyscopicsecond-api/api/aiGlasses/saveOcrResult") + fun saveOcrResult( + @Part("taskId") taskId: RequestBody, + @Part("productionInfoId") productionInfoId: RequestBody, + @Part("ocrResult") ocrResult: RequestBody, + @Part("result") result: RequestBody, + @Part("filePath") filePath: RequestBody + ): Observable + @POST("/skyscopicsecond-api/api/aiGlasses/submitTask") fun submitTask(@Body body: SubmitTaskRequest): Observable diff --git a/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt index 2adb51e..10bf258 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/SprayingActivity.kt @@ -33,6 +33,7 @@ class SprayingActivity : .orEmpty() .ifBlank { "1495087454883938304" } } + private var productionInfoId: String = "" private val listener = object : OfflineCmdListener { override fun onOfflineCmd(cmd: String) { @@ -92,6 +93,7 @@ class SprayingActivity : startActivity(Intent(this@SprayingActivity, SprayingOCRActivity::class.java).apply { putExtra("path", path) putExtra("taskId", taskId) + putExtra("productionInfoId", productionInfoId) }) finish() // runOnUiThread { @@ -108,6 +110,7 @@ class SprayingActivity : window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) binding.tvTaskHeader.text = "您还有10项任务未完成" viewModel.taskInfo.observe(this) { info -> + productionInfoId = info.productionInfoId binding.tvTaskHeader.text = "您还有${info.taskCount}项任务未完成" binding.title.text = "任务1:${info.itemName}" binding.content1.text = "AO/AAO:${info.aoNumber}" diff --git a/app/src/main/java/com/nova/brain/glass/ui/SprayingOCRActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/SprayingOCRActivity.kt index 102d5fa..e632a31 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/SprayingOCRActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/SprayingOCRActivity.kt @@ -34,6 +34,7 @@ class SprayingOCRActivity : override fun getRecyclerOrientation(): Int = RecyclerView.VERTICAL private val taskId: String by lazy { intent.getStringExtra("taskId").orEmpty() } + private val productionInfoId: String by lazy { intent.getStringExtra("productionInfoId").orEmpty() } private val listener = object : OfflineCmdListener { override fun onOfflineCmd(cmd: String) { @@ -120,6 +121,9 @@ class SprayingOCRActivity : viewModel.clearOcrResult() startActivity(Intent(this@SprayingOCRActivity, SprayingResultActivity::class.java).apply { putExtra("taskId", taskId) + putExtra("productionInfoId", productionInfoId) + putExtra("filePath", result.filePath) + putExtra("ocrResultJson", result.ocrResultJson) putExtra("judgment", result.judgment) putExtra("one", result.one) putExtra("two", result.two) diff --git a/app/src/main/java/com/nova/brain/glass/ui/SprayingResultActivity.kt b/app/src/main/java/com/nova/brain/glass/ui/SprayingResultActivity.kt index 046e9e8..bd6599a 100644 --- a/app/src/main/java/com/nova/brain/glass/ui/SprayingResultActivity.kt +++ b/app/src/main/java/com/nova/brain/glass/ui/SprayingResultActivity.kt @@ -18,6 +18,7 @@ import com.nova.brain.glass.helper.OfflineCmdListener import com.nova.brain.glass.helper.OfflineCmdServiceHelper import com.nova.brain.glass.helper.SprayingPhotoManager import com.nova.brain.glass.model.ItemItem +import com.nova.brain.glass.viewmodel.SaveOcrState import com.nova.brain.glass.viewmodel.SubmitTaskState import com.nova.brain.glass.viewmodel.SprayingResultVM import com.rokid.security.glass3.sdk.base.data.media.PhotoResolution @@ -37,47 +38,59 @@ class SprayingResultActivity : override fun fullscreen(): Boolean = true override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL + private val taskId: String by lazy { - intent.getStringExtra("taskId") - .orEmpty() - .ifBlank { "1493291302287048704" } + intent.getStringExtra("taskId").orEmpty().ifBlank { "1493291302287048704" } } + private val productionInfoId: String by lazy { + intent.getStringExtra("productionInfoId").orEmpty() + } + private val filePath: String by lazy { + intent.getStringExtra("filePath").orEmpty() + } + private val ocrResultJson: String by lazy { + intent.getStringExtra("ocrResultJson").orEmpty() + } + private var ocrOne: String = "" private var ocrTwo: String = "" private var status = true private var successDialog: AlertDialog? = null private val uiHandler = Handler(Looper.getMainLooper()) + + private fun currentResultJson(): String { + val judgment = if (status) "合格" else "不合格" + return "{\"judgment\":\"$judgment\",\"one\":\"$ocrOne\",\"two\":\"$ocrTwo\"}" + } + + private fun triggerContinue() { + binding.hint.text = "保存中,请稍后..." + viewModel.saveOcrAndContinue(taskId, productionInfoId, ocrResultJson, currentResultJson(), filePath) + } + + private fun triggerFinish() { + binding.hint.text = "保存中,请稍后..." + viewModel.saveOcrAndFinish(taskId, productionInfoId, ocrResultJson, currentResultJson(), filePath) + } + private val manualResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) { - return@registerForActivityResult - } + if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult when (result.data?.getStringExtra(SprayingManualResultActivity.EXTRA_MANUAL_RESULT)) { - SprayingManualResultActivity.RESULT_PASS -> { - status = true - setStatusImage() - } - - SprayingManualResultActivity.RESULT_FAIL -> { - status = false - setStatusImage() - } + SprayingManualResultActivity.RESULT_PASS -> { status = true; setStatusImage() } + SprayingManualResultActivity.RESULT_FAIL -> { status = false; setStatusImage() } } } + private val finishLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode != Activity.RESULT_OK) { - return@registerForActivityResult - } + if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult when (result.data?.getStringExtra(SprayingFinishActivity.EXTRA_FINISH_ACTION)) { SprayingFinishActivity.ACTION_SUPPLEMENT -> { - runOnUiThread { - binding.hint.text = "拍照中,请稍后..." - } + runOnUiThread { binding.hint.text = "拍照中,请稍后..." } isPhoto = true takePhoto() } - SprayingFinishActivity.ACTION_SUBMIT -> { viewModel.submitTask(taskId) } @@ -88,34 +101,18 @@ class SprayingResultActivity : override fun onOfflineCmd(cmd: String) { runOnUiThread { when (cmd) { - "退出", "返回", "退回" -> { - finish() - } - - "重拍", "重新拍", "继续拍照", "再拍一次", "继续拍摄" -> { - runOnUiThread { - binding.hint.text = "拍照中,请稍后..." - } - isPhoto = true - takePhoto() - } - - "人工更正结果", "更正结果", "人工更正", "更正" -> { - rest() - } - "结束任务", "完成任务", "完成", "结束" -> { - taskFinish() - } + "退出", "返回", "退回" -> finish() + "重拍", "重新拍", "继续拍照", "再拍一次", "继续拍摄" -> triggerContinue() + "人工更正结果", "更正结果", "人工更正", "更正" -> rest() + "结束任务", "完成任务", "完成", "结束" -> triggerFinish() } } } - } fun takePhoto() { val fileName = "test_${System.currentTimeMillis()}.png" - val publicPicturesDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val publicPicturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) val file = File(publicPicturesDir, fileName) GlassMediaServiceHelper.takePhoto(PhotoResolution.RESOLUTION_480P, file.absolutePath) } @@ -123,9 +120,6 @@ class SprayingResultActivity : fun rest() { manualResultLauncher.launch(Intent(this, SprayingManualResultActivity::class.java)) } - fun taskFinish() { - finishLauncher.launch(Intent(this, SprayingFinishActivity::class.java)) - } private val photoCallbackId = UUID.randomUUID().toString() @@ -134,9 +128,7 @@ class SprayingResultActivity : LogHelper.d("onTakePhoto-->path = $path") } - override fun getCallbackId(): String { - return photoCallbackId - } + override fun getCallbackId(): String = photoCallbackId override fun onTakePhotoV2(path: String?, width: Int, height: Int) { LogHelper.d("width:$width--height:$height") @@ -145,50 +137,66 @@ class SprayingResultActivity : isPhoto = false takePhoto() } else { - runOnUiThread { - binding.hint.text = "单击或语音输入“开始”,进入下一步" - } + runOnUiThread { binding.hint.text = "单击或语音输入「开始」,进入下一步" } "相机异常".showMessage() } } else { SprayingPhotoManager.addPhoto(path) - startActivity( - Intent( - this@SprayingResultActivity, SprayingOCRActivity::class.java - ).apply { - putExtra("path", path) - putExtra("taskId", taskId) - }) + startActivity(Intent(this@SprayingResultActivity, SprayingOCRActivity::class.java).apply { + putExtra("path", path) + putExtra("taskId", taskId) + putExtra("productionInfoId", productionInfoId) + }) finish() } } } - override fun initData() { super.initData() window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + viewModel.saveOcrState.observe(this) { state -> + when (state) { + SaveOcrState.LOADING -> binding.hint.text = "保存中,请稍后..." + SaveOcrState.SUCCESS_CONTINUE -> { + viewModel.resetSaveOcrState() + runOnUiThread { binding.hint.text = "拍照中,请稍后..." } + isPhoto = true + takePhoto() + } + SaveOcrState.SUCCESS_FINISH -> { + viewModel.resetSaveOcrState() + finishLauncher.launch(Intent(this, SprayingFinishActivity::class.java)) + } + SaveOcrState.FAILED -> { + viewModel.resetSaveOcrState() + binding.hint.text = "单击或语音输入「结束任务」,进入任务确认" + } + SaveOcrState.IDLE -> Unit + } + } + viewModel.saveOcrError.observe(this) { message -> + if (message.isNotBlank()) message.showMessage() + } viewModel.submitTaskState.observe(this) { state -> when (state) { - SubmitTaskState.LOADING -> { - binding.hint.text = "提交中,请稍后..." - } + SubmitTaskState.LOADING -> binding.hint.text = "提交中,请稍后..." SubmitTaskState.SUCCESS -> { viewModel.resetSubmitTaskState() showSuccessDialogThenBackToTaskList() } SubmitTaskState.FAILED -> { - binding.hint.text = "单击或语音输入“结束任务”,进入任务确认" + binding.hint.text = "单击或语音输入「结束任务」,进入任务确认" viewModel.resetSubmitTaskState() } SubmitTaskState.IDLE -> Unit } } viewModel.submitTaskError.observe(this) { message -> - if (message.isNotBlank()) { - message.showMessage() - } + if (message.isNotBlank()) message.showMessage() } + val judgment = intent.getStringExtra("judgment").orEmpty() ocrOne = intent.getStringExtra("one").orEmpty() ocrTwo = intent.getStringExtra("two").orEmpty() @@ -202,17 +210,15 @@ class SprayingResultActivity : binding.value1.text = "识码一:$ocrOne" binding.value2.text = "识码二:$ocrTwo" binding.value3.text = if (status) "两码一致,符合要求!" else "两码不一致,不符合要求!" + val strikeFlag = Paint.STRIKE_THRU_TEXT_FLAG if (status) { - binding.value1.paintFlags = - binding.value1.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() - binding.value2.paintFlags = - binding.value2.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() - binding.value3.paintFlags = - binding.value3.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + binding.value1.paintFlags = binding.value1.paintFlags and strikeFlag.inv() + binding.value2.paintFlags = binding.value2.paintFlags and strikeFlag.inv() + binding.value3.paintFlags = binding.value3.paintFlags and strikeFlag.inv() } else { - binding.value1.paintFlags = binding.value1.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - binding.value2.paintFlags = binding.value2.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - binding.value3.paintFlags = binding.value3.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + binding.value1.paintFlags = binding.value1.paintFlags or strikeFlag + binding.value2.paintFlags = binding.value2.paintFlags or strikeFlag + binding.value3.paintFlags = binding.value3.paintFlags or strikeFlag } } @@ -222,12 +228,14 @@ class SprayingResultActivity : OfflineCmdServiceHelper.addOnLineListener(listener) GlassMediaServiceHelper.addPhotoCallback(mPhotoFileCallback) } + override fun onPause() { super.onPause() OfflineCmdServiceHelper.removeListenerSprayingResult() OfflineCmdServiceHelper.removeOnLineListener(listener) GlassMediaServiceHelper.removePhotoCallback(mPhotoFileCallback) } + override fun onDestroy() { uiHandler.removeCallbacksAndMessages(null) successDialog?.dismiss() @@ -252,21 +260,9 @@ class SprayingResultActivity : ) .setClickListener(R.id.photo) { when (item.text) { - "继续拍摄" -> { - runOnUiThread { - binding.hint.text = "拍照中,请稍后..." - } - isPhoto = true - takePhoto() - } - - "结束任务" -> { - taskFinish() - } - - "人工更正结果" -> { - rest() - } + "继续拍摄" -> triggerContinue() + "结束任务" -> triggerFinish() + "人工更正结果" -> rest() } } } @@ -274,8 +270,6 @@ class SprayingResultActivity : override fun adapter(): BasePagedAdapter = adapter - - private fun showSuccessDialogThenBackToTaskList() { successDialog?.dismiss() val contentView = LayoutInflater.from(this).inflate(R.layout.dialog_review, null) diff --git a/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingOCRVM.kt b/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingOCRVM.kt index a742f01..523e674 100644 --- a/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingOCRVM.kt +++ b/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingOCRVM.kt @@ -1,8 +1,10 @@ package com.nova.brain.glass.viewmodel import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson import com.nova.brain.glass.MyApplication import com.nova.brain.glass.model.ItemItem +import com.nova.brain.glass.model.data.OcrNavigationData import com.nova.brain.glass.model.data.OcrResultData import com.nova.brain.glass.model.data.RecognizeByPathRequest import com.nova.brain.glass.repository.Service @@ -27,9 +29,10 @@ enum class UploadState { class SprayingOCRVM : BaseListViewModel() { val uploadState = MutableLiveData(UploadState.IDLE) val uploadError = MutableLiveData() - val ocrResult = MutableLiveData() + val ocrResult = MutableLiveData() private var uploadingPath: String? = null private var uploadDisposable: Disposable? = null + private val gson = Gson() override fun loadData( page: Int, @@ -84,7 +87,14 @@ class SprayingOCRVM : BaseListViewModel() { if (uploadingPath != localPath) return@subscribe if (response.code == 200) { uploadState.value = UploadState.SUCCESS - ocrResult.value = response.data ?: OcrResultData() + val data = response.data ?: OcrResultData() + ocrResult.value = OcrNavigationData( + filePath = serverPath, + ocrResultJson = gson.toJson(data), + judgment = data.judgment, + one = data.one, + two = data.two + ) } else { uploadState.value = UploadState.FAILED uploadError.value = if (response.message.isBlank()) "OCR识别失败" else response.message @@ -109,4 +119,4 @@ class SprayingOCRVM : BaseListViewModel() { fun clearOcrResult() { ocrResult.value = null } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingResultVM.kt b/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingResultVM.kt index c2ac7c8..9844189 100644 --- a/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingResultVM.kt +++ b/app/src/main/java/com/nova/brain/glass/viewmodel/SprayingResultVM.kt @@ -10,6 +10,8 @@ import com.xuqm.base.viewmodel.BaseListViewModel import com.xuqm.base.viewmodel.callback.Response import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody enum class SubmitTaskState { IDLE, @@ -18,9 +20,19 @@ enum class SubmitTaskState { FAILED } -class SprayingResultVM: BaseListViewModel() { +enum class SaveOcrState { + IDLE, + LOADING, + SUCCESS_CONTINUE, + SUCCESS_FINISH, + FAILED +} + +class SprayingResultVM : BaseListViewModel() { val submitTaskState = MutableLiveData(SubmitTaskState.IDLE) val submitTaskError = MutableLiveData() + val saveOcrState = MutableLiveData(SaveOcrState.IDLE) + val saveOcrError = MutableLiveData() override fun loadData( page: Int, @@ -33,6 +45,66 @@ class SprayingResultVM: BaseListViewModel() { }) } + fun saveOcrAndContinue( + taskId: String, + productionInfoId: String, + ocrResultJson: String, + resultJson: String, + filePath: String + ) { + doSaveOcr(taskId, productionInfoId, ocrResultJson, resultJson, filePath, finish = false) + } + + fun saveOcrAndFinish( + taskId: String, + productionInfoId: String, + ocrResultJson: String, + resultJson: String, + filePath: String + ) { + doSaveOcr(taskId, productionInfoId, ocrResultJson, resultJson, filePath, finish = true) + } + + private fun doSaveOcr( + taskId: String, + productionInfoId: String, + ocrResultJson: String, + resultJson: String, + filePath: String, + finish: Boolean + ) { + if (saveOcrState.value == SaveOcrState.LOADING) return + saveOcrState.value = SaveOcrState.LOADING + val plain = "text/plain".toMediaTypeOrNull() + add( + HttpManager.getApi(MyApplication.appComponent2, Service::class.java) + .saveOcrResult( + taskId = taskId.toRequestBody(plain), + productionInfoId = productionInfoId.toRequestBody(plain), + ocrResult = ocrResultJson.toRequestBody(plain), + result = resultJson.toRequestBody(plain), + filePath = filePath.toRequestBody(plain) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + if (response.code == 200) { + saveOcrState.value = if (finish) SaveOcrState.SUCCESS_FINISH else SaveOcrState.SUCCESS_CONTINUE + } else { + saveOcrState.value = SaveOcrState.FAILED + saveOcrError.value = if (response.message.isBlank()) "保存失败" else response.message + } + }, { e -> + saveOcrState.value = SaveOcrState.FAILED + saveOcrError.value = e.message ?: "保存失败" + }) + ) + } + + fun resetSaveOcrState() { + saveOcrState.value = SaveOcrState.IDLE + } + fun submitTask(taskId: String) { if (taskId.isBlank()) { submitTaskState.value = SubmitTaskState.FAILED