From 7e72ce413b35812bf769f236cba51b353d1e45b8 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Tue, 12 May 2026 18:15:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(food):=20=E5=AE=9E=E7=8E=B0=E9=A3=9F?= =?UTF-8?q?=E6=9D=90=E8=AF=86=E5=88=AB=E5=92=8C=E5=87=80=E6=9D=90=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在FoodAdapter中添加选中状态背景和文字颜色变化 - 修改FoodMaterialAdapter显示食材名称时包含净材类型 - 在FoodModule中解析食材名称中的净材类型信息 - 移除食材识别页面的净材类型选择弹窗流程 - 优化食材识别结果处理逻辑和数据映射 - 添加未识别到食材时的提示对话框 - 实现向量采集页面的搜索功能和实时监听 - 优化网络请求的文件上传媒体类型设置 - 调整食材列表项的布局约束和样式配置 --- .../shuwei/dish/match/adapter/FoodAdapter.kt | 7 +- .../dish/match/adapter/FoodMaterialAdapter.kt | 7 +- .../com/shuwei/dish/match/net/NetViewModel.kt | 3 +- .../shuwei/dish/match/objbox/FoodModule.kt | 7 +- .../dish/match/ui/FoodRecognizeActivity.kt | 64 +++++++++++------ .../dish/match/ui/PrepareFoodActivity.kt | 17 +++++ .../ui/fragment/VectorCollectionFragment.kt | 68 +++++++++++-------- app/src/main/res/layout/list_item_food.xml | 20 +++--- 8 files changed, 131 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FoodAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FoodAdapter.kt index f418f16..c17abb3 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FoodAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FoodAdapter.kt @@ -25,12 +25,14 @@ class FoodAdapter(list: MutableList) : item: GoodsItem? ) { item ?: return - holder.binding.tvGoodsInfo.run { - text = item.goodsName + holder.binding.root.run { setBackgroundResource( if (item.isClicked) R.drawable.shape_green_stroke else R.drawable.shape_white_12_corners ) + } + holder.binding.tvGoodsInfo.run { + text = item.goodsName setTextColor( ContextCompat.getColor( context, @@ -46,6 +48,7 @@ class FoodAdapter(list: MutableList) : it.visible() val foodScoreText = "${(item.foodScore / 100.0).roundedDecimalPlace(2)}%" it.text = foodScoreText + } } if (item.rawMaterialsType.isNullOrBlank()) { diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FoodMaterialAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FoodMaterialAdapter.kt index acf420f..8471713 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FoodMaterialAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FoodMaterialAdapter.kt @@ -36,8 +36,11 @@ class FoodMaterialAdapter(list: MutableList) : clBlock.setOnClickListener { onItemClick?.invoke(holder.bindingAdapterPosition) } - - tvDishName.text = data.goodsName + if (data.isOriginalData) { + tvDishName.text = data.goodsName + } else { + tvDishName.text = data.goodsName + "-${data.rawMaterialsType}" + } tvDishType.text = if (data.materialType == 1) "主辅材:主材" else if (data.materialType == 2) "主辅材:辅材" else "" tvDishWeight.text = if (data.useWeight == null || data.useWeight == 0.toDouble()) "" else "${data.useWeight!!.roundedOneDecimalPlace()}克" diff --git a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt index 2661008..f5b32c0 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt @@ -184,7 +184,7 @@ class NetViewModel( params: MutableMap ) { val fileParts = fileList.map { file -> - file.asRequestBody("multipart/form-data".toMediaTypeOrNull()) + file.asRequestBody("image/jpeg".toMediaTypeOrNull()) .let { MultipartBody.Part.createFormData("foodPics", file.name, it) } } viewModelScope.launch { @@ -204,6 +204,7 @@ class NetViewModel( params: MutableMap ): UiState?> { val fileParts = fileList.map { file -> + //multipart/form-data image/jpeg file.asRequestBody("multipart/form-data".toMediaTypeOrNull()) .let { MultipartBody.Part.createFormData("foodPics", file.name, it) } } diff --git a/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt b/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt index 4c24f32..4412b0e 100644 --- a/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt +++ b/app/src/main/java/com/shuwei/dish/match/objbox/FoodModule.kt @@ -129,7 +129,12 @@ object FoodModule { val nameScoreList = mutableListOf() idScoreList.forEach { val name = foodMap[it.id]?.name ?: "" - nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score, rawMaterialsType = "")) + val arr = name.split("&&") + var rawMaterialsType = "" + if (arr.size > 1) { + rawMaterialsType = arr[1] + } + nameScoreList.add(IdNameScore(id = it.id, name = arr[0], score = it.score, rawMaterialsType = rawMaterialsType)) } logInfo("queryFood数据:${nameScoreList.toJsonString()}") return nameScoreList diff --git a/app/src/main/java/com/shuwei/dish/match/ui/FoodRecognizeActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/FoodRecognizeActivity.kt index 276f53a..723b179 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/FoodRecognizeActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/FoodRecognizeActivity.kt @@ -123,21 +123,32 @@ class FoodRecognizeActivity : BaseActivity() { private val adapter = FoodAdapter(list).apply { setOnItemClickListener { _, _, position -> +// rawMaterialsRemind(position) val item = list[position] - // 弹出净材种类单选弹窗,确认后再选中该食材 - showRawMaterialsDialog( - context = this@FoodRecognizeActivity, - types = item.rawMaterialsTypes, - checkedType = item.rawMaterialsType - ) { selectedType -> - item.rawMaterialsType = selectedType - list.forEachIndexed { i, it -> it.isClicked = (i == position) } - // 点击选择时记录当前秤重量 - item.useWeight = currentWeight - notifyDataSetChanged() - selectedPosition = position - binding.tvSelectedFood.text = item.goodsName ?: "-" - } + list.forEachIndexed { i, it -> it.isClicked = (i == position) } + // 点击选择时记录当前秤重量 + item.useWeight = currentWeight + notifyDataSetChanged() + selectedPosition = position + binding.tvSelectedFood.text = item.goodsName ?: "-" + } + } + + private fun rawMaterialsRemind(position: Int) { + val item = list[position] + // 弹出净材种类单选弹窗,确认后再选中该食材 + showRawMaterialsDialog( + context = this@FoodRecognizeActivity, + types = item.rawMaterialsTypes, + checkedType = item.rawMaterialsType + ) { selectedType -> + item.rawMaterialsType = selectedType + list.forEachIndexed { i, it -> it.isClicked = (i == position) } + // 点击选择时记录当前秤重量 + item.useWeight = currentWeight + adapter.notifyDataSetChanged() + selectedPosition = position + binding.tvSelectedFood.text = item.goodsName ?: "-" } } @@ -178,9 +189,11 @@ class FoodRecognizeActivity : BaseActivity() { /** * 从 Intent 读取数据并初始化页面 + * Reads data from Intent and initializes the page */ private fun initData() { // 加载食材照片 + // Load food photo val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI) if (!imageUri.isNullOrBlank()) { binding.ivFoodPhoto.setImageURI(imageUri.toUri()) @@ -188,10 +201,14 @@ class FoodRecognizeActivity : BaseActivity() { currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0) binding.tvWeight.text = "${currentWeight}g" // val tempList = arrayListOf() + // Process recognized name list if available recognizeNameList?.apply { + // Iterate through each recognized item forEach { + // Extract goods name by splitting "WP" and taking the first part val goodsName = it.name.split("WP").first() val rawMaterialsType = it.rawMaterialsType + // Add to goods name list and name-score mapping goodsNameList.add(GoodsNameQueryDTO(goodsName, rawMaterialsType)) nameScoreMap.put(goodsName, it) @@ -203,11 +220,16 @@ class FoodRecognizeActivity : BaseActivity() { // // TODO: 测试数据,临时使用------------------------- // loadRecognizeList(tempList) // // TODO: 测试数据,临时使用------------------------- + // Clear the recognize name list after processing recognizeNameList = null } - //TODO: 注释掉等接口正常再调用,临时使用测试数据 + // If goods name list is not empty, query goods list from network +// if (goodsNameList.isNotEmpty()){ netViewModel.queryGoodsList(goodsType = "0", goodsNames = goodsNameList) +// } else { +// loadRecognizeList(emptyList()) +// } } private fun buildTestData(item: FoodModule.IdNameScore): GoodsItem { @@ -268,11 +290,13 @@ class FoodRecognizeActivity : BaseActivity() { binding.tvNoData.gone() list.clear() goodsList.forEach { - val food = nameScoreMap[it.goodsName] - val foodScore = ((1 - (food?.score ?: 0.0)) * 10000).toInt() - // 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用 - it.rawMaterialsTypes = it.rawMaterialsType - it.rawMaterialsType = null + var foodScore = -1 + nameScoreMap[it.goodsName]?.let { food -> + foodScore = ((1 - food.score) * 10000).toInt() + } +// // 将接口返回的 rawMaterialsType(全部种类)移存到 rawMaterialsTypes,供弹窗使用 +// it.rawMaterialsTypes = it.rawMaterialsType +// it.rawMaterialsType = null list.add(it.also { it.foodScore = foodScore }) } adapter.notifyDataSetChanged() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt index 534ac89..cbf6d93 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/PrepareFoodActivity.kt @@ -231,6 +231,7 @@ class PrepareFoodActivity : BaseActivity() { } if ( !pageVisible || manualCancelFlag || isTakingPhoto.get() || showRecognizePage || + notRecognizeDialog?.isShowing == true || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE ) { Log.d( @@ -574,6 +575,9 @@ class PrepareFoodActivity : BaseActivity() { dismissLoading() Log.d(TAG, "takePhoto foodScoreList is empty") isTakingPhoto.set(false) + withContext(Dispatchers.Main) { + showNotRecognizeDialog() + } return@withContext } withContext(Dispatchers.Main) { @@ -586,6 +590,19 @@ class PrepareFoodActivity : BaseActivity() { } } } + private var notRecognizeDialog: CommonDialog? = null + private fun showNotRecognizeDialog() { + notRecognizeDialog = CommonDialog(this) + .setTitle("提示") + .setContent("未识别到食材,请稍后重试") + .setNegativeButton("取消") + .setPositiveButton("手动设置"){ + loadRecognizeResultPage(emptyList()) + } + .also { + it.show() + } + } /** * 拍照失败回调 diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt index 4d79b4c..968eefa 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt @@ -14,7 +14,10 @@ import android.view.inputmethod.InputMethodManager import androidx.camera.view.PreviewView import androidx.core.view.doOnLayout import androidx.core.view.updateLayoutParams +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.chad.library.adapter4.util.setOnDebouncedItemClick @@ -268,15 +271,23 @@ class VectorCollectionFragment : BaseFragment() upload() } - binding.editFoodName.setOnEditorActionListener { v, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - searchFood() - val imm = - v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(v.windowToken, 0) - true - } else { - false + binding.editFoodName.let { + it.addTextChangedListener { editable -> + if (editable?.isEmpty() == true) { + searchFood() + return@addTextChangedListener + } + } + it.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + searchFood() + val imm = + v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + true + } else { + false + } } } @@ -297,6 +308,22 @@ class VectorCollectionFragment : BaseFragment() it.adapter = searchAdapter } + // 统一订阅一次,避免每次 searchFood() 重复创建 collect 协程导致多次回调 + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + currentActivity.netViewModel.goodsListState.collect { state -> + when (state) { + is UiState.Success -> { + val list = state.data ?: emptyList() + if (list.isEmpty()) loadEmptyView() else fillSearchList(list) + } + + else -> Unit + } + } + } + } + searchFood() } @@ -329,8 +356,9 @@ class VectorCollectionFragment : BaseFragment() }, onProgress = { count, batch, idList -> showWaitingDialog("图片上传中$count/$totalFileCount") val foodList = batch.mapIndexed { index, it -> + val name = checkedItem!!.goodsName + "&&" + checkedItem!!.rawMaterialsType Food( - name = checkedItem!!.goodsName, + name = name, foodVector = it.imageVector, ) } @@ -355,28 +383,11 @@ class VectorCollectionFragment : BaseFragment() fun searchFood() { debouncer.debounce { val keyword = binding.editFoodName.text.toString().trim() - // 通过 NetViewModel 查询净材数据(goodsType=0 食材) + // 通过 NetViewModel 查询净材数据(goodsType=0 食材),结果由 initialize() 中的 observer 统一处理 currentActivity.netViewModel.queryGoodsList( goodsType = "0", goodsName = keyword.ifEmpty { null } ) - // 监听查询结果(一次性收集,避免重复订阅) - lifecycleScope.launch { - currentActivity.netViewModel.goodsListState.collect { state -> - when (state) { - is UiState.Success -> { - val list: List = state.data ?: emptyList() - if (list.isEmpty()) { - // 接口无数据时展示空布局,点击空布局可填充本地假数据用于效果测试 - loadEmptyView() - } else { - fillSearchList(list) - } - } - else -> Unit - } - } - } } } @@ -400,6 +411,7 @@ class VectorCollectionFragment : BaseFragment() searchList.clear() searchList.addAll(list) searchAdapter.notifyDataSetChanged() + Log.d(TAG, "fillSearchList,${searchList.toJsonString()}") } private var emptyViewBinding: LayoutEmptyViewBinding? = null diff --git a/app/src/main/res/layout/list_item_food.xml b/app/src/main/res/layout/list_item_food.xml index 4f97a99..55128f4 100644 --- a/app/src/main/res/layout/list_item_food.xml +++ b/app/src/main/res/layout/list_item_food.xml @@ -4,19 +4,23 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="100dp" - android:layout_margin="15dp"> + android:layout_margin="15dp" + tools:background="@drawable/shape_white_12_corners"> \ No newline at end of file