From 0a90a8b36854eaf2294b2748358623de99c97614 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Tue, 21 Apr 2026 17:08:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(activity):=20=E9=87=8D=E6=9E=84=20SubmitFo?= =?UTF-8?q?odActivity=20=E8=B0=83=E6=96=99=E5=B1=95=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E5=AD=90=E8=AE=BE=E5=A4=87=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E7=A7=A4=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../match/adapter/SeasoningWeightAdapter.kt | 72 ++++ .../dish/match/ui/SubmitFoodActivity.kt | 398 ++++++------------ .../main/res/layout/activity_submit_food.xml | 9 +- .../main/res/layout/item_seasoning_weight.xml | 28 ++ 4 files changed, 239 insertions(+), 268 deletions(-) create mode 100644 app/src/main/java/com/shuwei/dish/match/adapter/SeasoningWeightAdapter.kt create mode 100644 app/src/main/res/layout/item_seasoning_weight.xml diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/SeasoningWeightAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/SeasoningWeightAdapter.kt new file mode 100644 index 0000000..74bc78b --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/adapter/SeasoningWeightAdapter.kt @@ -0,0 +1,72 @@ +package com.shuwei.dish.match.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding + +/** + * 调料使用重量列表 Adapter + * 用于 SubmitFoodActivity 展示各调料槽位的实时用量 + * + * @param items 调料数据列表,外部直接操作后调用 notifyDataSetChanged + */ +class SeasoningWeightAdapter : RecyclerView.Adapter() { + + /** 调料展示数据项 */ + data class Item( + /** 唯一标识:deviceId#address */ + val key: String, + val goodsId: String, + val goodsName: String, + /** 使用重量(克),= 初始重量 - 当前重量,≥0 */ + val useWeight: Double + ) + + val items = mutableListOf() + + /** + * 更新单个 item 的重量,若 key 不存在则追加 + */ + fun updateItem(item: Item) { + val idx = items.indexOfFirst { it.key == item.key } + if (idx >= 0) { + items[idx] = item + notifyItemChanged(idx) + } else { + items.add(item) + notifyItemInserted(items.lastIndex) + } + } + + /** + * 移除指定 key 的 item(用量不足阈值时调用) + */ + fun removeItem(key: String) { + val idx = items.indexOfFirst { it.key == key } + if (idx >= 0) { + items.removeAt(idx) + notifyItemRemoved(idx) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val binding = ItemSeasoningWeightBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.size + + class VH(private val b: ItemSeasoningWeightBinding) : RecyclerView.ViewHolder(b.root) { + fun bind(item: Item) { + b.tvSeasoningName.text = item.goodsName + b.tvSeasoningWeight.text = "${item.useWeight}g" + } + } +} diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt index 7434eff..c312f90 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SubmitFoodActivity.kt @@ -3,25 +3,22 @@ package com.shuwei.dish.match.ui import android.annotation.SuppressLint import android.os.Bundle import android.util.Log -import android.util.SparseArray -import android.util.SparseIntArray -import android.widget.FrameLayout -import androidx.activity.viewModels -import androidx.core.util.forEach import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager import com.google.gson.reflect.TypeToken import com.shuwei.dish.match.R -import com.shuwei.dish.match.adapter.TextCellAdapter +import com.shuwei.dish.match.adapter.SeasoningWeightAdapter import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseApp import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding import com.shuwei.dish.match.db.AppRepository - import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.FoodRecord -import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.net.NetViewModel +import com.shuwei.dish.match.scale.ScaleDeviceConfig +import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.ext.clickWithDebounce @@ -34,7 +31,10 @@ import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.viewmodel.AppViewModel import com.shuwei.dish.match.viewmodel.factory.AppFactory -import kotlin.getValue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SuppressLint("UseSparseArrays") class SubmitFoodActivity : BaseActivity() { @@ -50,8 +50,6 @@ class SubmitFoodActivity : BaseActivity() { private var food: FoodRecord? = null private var goodsList: MutableList? = null - private var isCooking = false - private val cookFoodEntity by lazy { CookFoodEntity().apply { food?.let { @@ -64,6 +62,21 @@ class SubmitFoodActivity : BaseActivity() { } } + /** 调料列表 adapter */ + private val seasoningAdapter = SeasoningWeightAdapter() + + /** + * 各秤的初始重量快照,key = "deviceId#address" + * 首次收到稳定数据时记录,之后用于计算用量差值 + */ + private val baselineMap = mutableMapOf() + + /** + * 槽位配置:key = "deviceId#address",value = goodsId to goodsName + * 从 Room dm_seasoning_slot 加载 + */ + private val slotMap = mutableMapOf>() + @Suppress("unchecked_cast", "DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -74,14 +87,8 @@ class SubmitFoodActivity : BaseActivity() { intent.extras?.apply { food = getSerializable(FOOD_ITEM) as FoodRecord? Log.d(TAG, "onCreate: cookMode=${food?.cookMode}") - isCooking = food?.isCooking ?: false - if (isCooking) { - //烹饪中,从本地查询数据 - loadSeasoningFromLocal() - } else { - //非烹饪中,从菜品主辅材配置页面带入数据 - goodsList = getSerializable(GOODS_LIST) as MutableList? - } + // 非烹饪中,从菜品主辅材配置页面带入数据 + goodsList = getSerializable(GOODS_LIST) as MutableList? } setTitleBar(titleBarAction = { it.visible() @@ -90,80 +97,91 @@ class SubmitFoodActivity : BaseActivity() { }, rightIconActon = { it.gone() it.setImageResource(R.drawable.ic_setting) - it.setOnClickListener { - //startActivity() - } }) + setupRecyclerView() addViewListener() + loadSlotsThenObserveScales() } - private fun loadSeasoningFromLocal() { - appViewModel.getCookFoodGoodsList( - foodId = food!!.foodId!!, - cookMode = food!!.cookMode - ) { it -> - goodsList = mutableListOf() - val goodsIdList = mutableListOf() - //设置主辅材数据 - val tempList = it.filter { goods -> goods.materialType != 3 } - tempList.forEach { - if (!goodsIdList.contains(it.goodsId)) { - goodsList?.add(it) - goodsIdList.add(it.goodsId) - } - } + /** 初始化 RecyclerView,2列 GridLayoutManager */ + private fun setupRecyclerView() { + binding.rvSeasoning.itemAnimator = null + binding.rvSeasoning.layoutManager = GridLayoutManager(this, 2) + binding.rvSeasoning.adapter = seasoningAdapter + } - //获取调料数据并展示 - val filterList: List? = it.filter { goods -> goods.materialType == 3 } - .map { entity -> - SeasoningEntity().also { se -> - se.goodsId = entity.goodsId - se.goodsName = entity.goodsName - se.goodsOrRelationCode = entity.goodsOrRelationCode - se.relateionType = entity.relateionType - se.materialType = entity.materialType - se.allEdible = entity.allEdible - se.useWeight = entity.useWeight - se.popularName = entity.popularName - se.canteenId = entity.canteenId - se.relateionType_dictText = entity.relateionType_dictText - se.foodId = entity.foodId - se.sort = entity.sort - se.pageType = 0 - } + /** + * 从 Room 加载槽位配置后,开始订阅子设备秤数据 + * 确保 slotMap 就绪后再处理秤数据,避免名称为空 + */ + private fun loadSlotsThenObserveScales() { + lifecycleScope.launch(Dispatchers.IO) { + val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll() + withContext(Dispatchers.Main) { + slots.forEach { slot -> + slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName } - filterList?.toMutableList()?.let { cellData -> - loadTextCell(cellData) + observeScaleData() } } } + /** + * 订阅 ScaleServiceManager.allScales,过滤子设备数据并更新 adapter + * 排除主设备(DEVICE_ID_2),只处理 22格、18格、1格子设备 + */ + private fun observeScaleData() { + val flow = ScaleServiceManager.allScales ?: return + lifecycleScope.launch { + flow.collectLatest { scaleMap -> + scaleMap.forEach { (key, data) -> + // 排除主设备 2格秤 + if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach -// private lateinit var seasoningList: List -// private var isFirstSave = true -// fun updateSeasoningList(list: MutableList) { -// if (isFirstSave) { -// val json = list.toJsonString() -// val typeToken = object : TypeToken>() {} -// seasoningList = json.toType(typeToken = typeToken) -// isFirstSave = false -// } -// } + // 1格设备调料名固定为"油",其余从槽位配置读取 + val (goodsId, goodsName) = if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_1) { + "" to "油" + } else { + slotMap[key] ?: return@forEach + } - private val seasoningArray by lazy { - SparseArray() + // 首次收到稳定数据时记录基准重量 + val baseline = baselineMap[key] + if (baseline == null) { + if (data.state == WeightUtil.STATE_STABLE) { + baselineMap[key] = data.weight + } + return@forEach + } + + // 用量 = 初始重量 - 当前重量,负值归零 + val useWeight = (baseline - data.weight) + .coerceAtLeast(0.0) + .roundedOneDecimalPlace() + + // 用量不足 0.5g 时从列表移除(或不添加) + if (useWeight < 0.5) { + seasoningAdapter.removeItem(key) + return@forEach + } + + seasoningAdapter.updateItem( + SeasoningWeightAdapter.Item( + key = key, + goodsId = goodsId, + goodsName = goodsName, + useWeight = useWeight + ) + ) + } + } + } } - - private val firstGoodsArray = SparseArray() - private fun addViewListener() { - addWeightListener() binding.btnWeightClear.setOnClickListener { WeightUtil.tareTwo(AddressUtil.TWO) } - binding.btnCook.clickWithDebounce { - cook() - } + binding.btnCook.clickWithDebounce { cook() } binding.btnSubmit.clickWithDebounce { if (cookFoodEntity.foodWeight <= 0.toDouble()) { @@ -172,114 +190,40 @@ class SubmitFoodActivity : BaseActivity() { } submit() } - } - private fun addWeightListener() { - WeightUtil.addWeightListener( - weightKey = TAG, - getWeight = { address, state, weight -> - if (address == AddressUtil.TWO) { - cookFoodEntity.foodWeight = weight.toDouble() - binding.tvTotalWeight.text = "${weight / 1000f}" - } - val firstWeight = firstGoodsArray.get(address, null) - if (firstWeight == null) { - //未设置数据 - if (state != WeightUtil.STATE_STABLE) { - //首次记录数据需要稳定数据 - return@addWeightListener - } - firstGoodsArray.put(address, weight) - return@addWeightListener - } - var realUseWeight = firstWeight - weight - realUseWeight = if (realUseWeight > 0) realUseWeight else 0.0 - seasoningArray.put(address, realUseWeight.toDouble()) - -// refreshSeasoningWeight() - - val item = seasoningItems.firstOrNull { address == addressArray[it.sort] } - item?.let { - val lastWeight = seasoningArray.get(address) ?: 0.toDouble() - it.useWeight = - (lastWeight + getCookingSeasoning(address)).roundedOneDecimalPlace() - updateGridData(it) - } - }) - } - -// private fun refreshSeasoningWeight() { -// if (seasoningItems.isEmpty()) { -// return -// } -// seasoningItems.let { items -> -// addressArray.forEach { key, value -> -// items[key].useWeight = -// (seasoningArray.get(value) ?: 0.toDouble()) + getCookingSeasoning(value) -// } -// } -// setGridData(seasoningItems) -// } - - private fun updateGridData(entity: SeasoningEntity) { - val gridLayout = binding.include.root - val tag = entity.sort.toString() - val frameLayout = gridLayout.findViewWithTag(tag) - TextCellAdapter.loadLayout(frameLayout, entity) - } - - private fun getCookingSeasoning(address: Int): Double { - if (isCooking.not()) return 0.toDouble() - if (seasoningItems.isEmpty()) return 0.toDouble() - val weight = - seasoningCookingItems?.get(weightRelateArray2.get(address))?.useWeight ?: 0.toDouble() - return weight - } - - private val weightRelateArray2 by lazy { - SparseIntArray().apply { - addressArray.forEach { key, value -> - put(value, key) + // 监听主设备 2格秤的熟重 + WeightUtil.addWeightListener(TAG) { address, _, weight -> + if (address == AddressUtil.TWO) { + cookFoodEntity.foodWeight = weight.toDouble() + binding.tvTotalWeight.text = "${weight / 1000f}" } } } - private val addressArray by lazy { - AddressUtil.getWeighAddressArray() - } - -// private fun prepareData(acton: () -> Unit) { -// -// //只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题 -// val seasoningJson = realSeasoningData.toJsonString() -// val typeToken = object : TypeToken>() {} -// val tempSeasoningList = seasoningJson.toType(typeToken = typeToken) -// goodsList?.addAll(tempSeasoningList) -// acton() -// -// } - private fun cook() { if (food == null || goodsList.isNullOrEmpty()) { toast("未获取到菜品或构成信息") return } - val realSeasoningData = seasoningItems - //(binding.rvSeasoning.adapter as TextCellAdapter).list - if (realSeasoningData.isEmpty() && !isCooking) { + val seasoningData = seasoningAdapter.items + if (seasoningData.isEmpty()) { toast("未获取到调料信息") return } - if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) { - toast("还未设置调料信息,请去设置页面操作") - return - } showLoading() - //只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题 - val seasoningJson = realSeasoningData.toJsonString() + + // 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList val typeToken = object : TypeToken>() {} - val tempSeasoningList = seasoningJson.toType(typeToken = typeToken) + val tempSeasoningList = seasoningData.map { item -> + CookFoodGoodsEntity().also { entity -> + entity.goodsId = item.goodsId + entity.goodsName = item.goodsName + entity.useWeight = item.useWeight + entity.materialType = 3 + } + } goodsList?.addAll(tempSeasoningList) + cookFoodEntity.let { it.dinnerType = when (it.dinnerType) { "早餐" -> "1" @@ -288,17 +232,14 @@ class SubmitFoodActivity : BaseActivity() { else -> it.dinnerType } } + val isSamplingData = food!!.cookMode == 1 if (isSamplingData) { - //生成临时foodId,解决采样模式没有foodId,无法关联数据的问题 val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" } cookFoodEntity.foodId = tempFoodId } - //全部设置同一foodId - goodsList?.forEach { - it.foodId = cookFoodEntity.foodId - } - //保存菜品-保存主辅料调料 + goodsList?.forEach { it.foodId = cookFoodEntity.foodId } + appViewModel.saveCookFoodAndGoods( cookMode = food!!.cookMode, entity = cookFoodEntity, @@ -316,31 +257,22 @@ class SubmitFoodActivity : BaseActivity() { toast("未获取到菜品或构成信息") return } - val realSeasoningData = seasoningItems - if (realSeasoningData.isEmpty() && !isCooking) { - toast("未获取到调料信息") - return - } - if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) { - toast("还未设置调料信息,请去设置页面操作") - return - } + val seasoningData = seasoningAdapter.items showLoading() - //只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题 - val seasoningJson = realSeasoningData.toJsonString() - val typeToken = object : TypeToken>() {} - val tempSeasoningList = seasoningJson.toType(typeToken = typeToken) - tempSeasoningList.forEach { entity -> - val realWeight = entity.useWeight ?: 0.toDouble() - val filterData = goodsList?.firstOrNull { it.goodsId == entity.goodsId } - //确保重量大于0,且无重复数据 - if (realWeight > 0.toDouble() && filterData == null) { - goodsList?.add(entity) + + // 将重量大于0且不重复的调料追加到 goodsList + seasoningData.forEach { item -> + if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) { + goodsList?.add(CookFoodGoodsEntity().also { entity -> + entity.goodsId = item.goodsId + entity.goodsName = item.goodsName + entity.useWeight = item.useWeight + entity.materialType = 3 + }) } } cookFoodEntity.let { -// it.stFoodInfoConstituteList = this@SubmitFoodActivity.goodsList it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList it.dinnerType = when (it.dinnerType) { "1" -> "早餐" @@ -352,30 +284,15 @@ class SubmitFoodActivity : BaseActivity() { val isSamplingData = food!!.cookMode == 1 if (isSamplingData) { - //采样模式没有foodId cookFoodEntity.foodId = "" goodsList?.forEach { it.foodId = "" } } - val json = cookFoodEntity.toJsonString() - Log.d(TAG, "submit: json=$json") -// HttpUtil.postJson( -// url = UrlConfig.SUBMIT_DISH, -// json = json, -// doSuccess = { -// Log.d(TAG, "submit: postJson=$it") -// submitSuccess(isSamplingData) -// }, doFailure = { code, msg -> -// Log.d(TAG, "submit: postJson:code=$code,msg=$msg") -// toast(msg) -// dismissLoading() -// }) + Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}") netViewModel.submitCookFood( entity = cookFoodEntity, - onSuccess = { - submitSuccess(isSamplingData) - }, - onFailure = { code, msg -> + onSuccess = { submitSuccess(isSamplingData) }, + onFailure = { _, msg -> toast(msg) dismissLoading() }) @@ -383,12 +300,10 @@ class SubmitFoodActivity : BaseActivity() { private fun submitSuccess(isSamplingData: Boolean) { if (food?.foodId.isNullOrBlank()) { - //本地无删除数据直接跳转页面 dismissLoading() jumpPage(isSamplingData, false) return } - //删除本地数据 appViewModel.deleteCookFoodAndGoods( cookMode = food!!.cookMode, foodId = food!!.foodId!! @@ -415,57 +330,12 @@ class SubmitFoodActivity : BaseActivity() { private fun initViewModel() { val db = BaseApp.instance!!.database - val factory = - AppFactory(AppRepository(db.appDao())) - appViewModel = - ViewModelProvider(this, factory)[AppViewModel::class.java] + val factory = AppFactory(AppRepository(db.appDao())) + appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java] } - override fun onResume() { - super.onResume() - if (isCooking.not()) { - //烹饪中使用本地数据,未烹饪则使用默认的调料配置数据 - appViewModel.loadSeasoning { - Log.d(TAG, "onResume: loadSeasoning:${it.toJsonString()}") - loadTextCell(it) - } - } - } - - private var seasoningItems = mutableListOf() - private var seasoningCookingItems: List? = null - private fun loadTextCell(list: MutableList) { - seasoningItems.clear() - seasoningItems.addAll(list) - initConfigData() -// setGridData(list) - list.forEach { entity -> - //烹饪中重量数据从本地获取,否则显示0 - if (isCooking.not()) { - entity.useWeight = 0.0 - } - updateGridData(entity) - } - seasoningCookingItems = seasoningItems.map { it.copy() }.toList() - } - - private fun initConfigData() { - repeat(12) { num -> - val firstOne = seasoningItems.firstOrNull { it.sort == num } - if (firstOne == null) { - seasoningItems.add(SeasoningEntity().also { it.sort = num }) - } - } - } - - private fun setGridData(list: MutableList) { - val gridLayout = binding.include.root - list.forEach { entity -> - val tag = entity.sort.toString() - val frameLayout = gridLayout.findViewWithTag(tag) - TextCellAdapter.loadLayout(frameLayout, entity) - } + override fun onDestroy() { + WeightUtil.removeWeightListener(TAG) + super.onDestroy() } } - - diff --git a/app/src/main/res/layout/activity_submit_food.xml b/app/src/main/res/layout/activity_submit_food.xml index bd53d3d..54be4b1 100644 --- a/app/src/main/res/layout/activity_submit_food.xml +++ b/app/src/main/res/layout/activity_submit_food.xml @@ -27,11 +27,12 @@ android:textColor="@color/black666" android:textSize="28sp" /> - + android:layout_height="660dp" + android:layout_marginTop="10dp" + android:overScrollMode="never" /> diff --git a/app/src/main/res/layout/item_seasoning_weight.xml b/app/src/main/res/layout/item_seasoning_weight.xml new file mode 100644 index 0000000..dd9f1bf --- /dev/null +++ b/app/src/main/res/layout/item_seasoning_weight.xml @@ -0,0 +1,28 @@ + + + + + + + +