feat(clean-pack): 添加配比秤功能和界面重构

- 新增搜索菜品名称功能和菜品搜索按钮
- 添加用量克数显示和清零重量功能
- 集成配比秤重量监听和显示功能
- 重构主页界面添加配比秤入口
- 新增组配任务按钮和相关页面跳转
- 修改餐品净菜包功能跳转至新的列表页面
- 优化净菜包装表单布局和字段配置
- 移除溯源码自动填充净菜名称功能
- 添加重量校准和去皮功能支持
- 重构提交流程增加重量验证和回调处理
This commit is contained in:
2026-06-03 14:25:31 +08:00
parent c81e2d14bb
commit ad9291243f
13 changed files with 338 additions and 165 deletions
@@ -25,7 +25,7 @@ data class SettingItem(
CLEAN_PACKAGE("净菜包"),
MEAL_PACKAGE("餐品净菜包"),
GROUP_TASK("组配任务"),
MEAL_PACKAGE_TEST("餐品净菜包-测试"),
// MEAL_PACKAGE_TEST("餐品净菜包-测试"),
DB_INSPECT("数据库查看"),
SCALE_OBSERVE("秤数据监控"),
ENV_SWITCH("切换环境"),
@@ -211,6 +211,23 @@ class NetViewModelV2(
private val _addCleanPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addCleanPackageState: StateFlow<UiState<Unit?>> = _addCleanPackageState.asStateFlow()
fun addCleanPackage(param: Map<String, Any?>, callback: (Boolean, String) -> Unit) {
viewModelScope.launch {
val uiState = repository.addCleanPackage(param)
when (uiState) {
is UiState.Success -> {
callback(true, "")
}
is UiState.Error -> {
callback(false, uiState.msg)
}
is UiState.Loading -> {}
is UiState.Idle -> {}
}
}
}
fun addCleanPackage(param: Map<String, Any?>) {
viewModelScope.launch {
_addCleanPackageState.value = UiState.Loading
@@ -1,7 +1,6 @@
package com.shuwei.dish.match.ui
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.adapter.FormFieldAdapter
import com.shuwei.dish.match.base.BaseActivity
@@ -13,12 +12,15 @@ import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* 净菜包装表单页面,根据类型显示不同的表单字段
@@ -26,6 +28,8 @@ import kotlinx.coroutines.launch
class CleanPackActivity : BaseActivity() {
companion object {
private const val TAG = "CleanPackActivity"
/** 单品净菜包装 */
const val TYPE_CLEAN = 1
@@ -47,7 +51,8 @@ class CleanPackActivity : BaseActivity() {
private var packageMethodField: FormField? = null
private var deviceField: FormField? = null
private var foodOptionsField: FormField? = null
private var packagingSpecField: FormField? = null
// private var packagingSpecField: FormField? = null
private var ingredientsField: FormField? = null
private var traceCodeJob: Job? = null
@@ -86,13 +91,15 @@ class CleanPackActivity : BaseActivity() {
}
})
addWeightListener()
// 初始化 RecyclerView
// val flexboxLayoutManager = FlexboxLayoutManager(this).apply {
// flexDirection = FlexDirection.ROW
// flexWrap = FlexWrap.WRAP
// alignItems = AlignItems.STRETCH
// }
val layoutManager = GridLayoutManager(this, 1).apply {}
val layoutManager = GridLayoutManager(this, 2)
binding.rvForm.layoutManager = layoutManager
binding.rvForm.adapter = formAdapter
@@ -111,6 +118,16 @@ class CleanPackActivity : BaseActivity() {
TYPE_MEAL -> submitMealPackage()
}
}
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
binding.flSearchBar.clickWithDebounce {
}
}
/**
@@ -120,35 +137,36 @@ class CleanPackActivity : BaseActivity() {
private fun initCleanPackFields() {
fields.clear()
// 溯源码
traceCodeField = FormField(
label = "溯源码",
type = FieldType.TEXT,
apiKey = "traceCode",
required = true
)
fields.add(traceCodeField!!)
// // 溯源码
// traceCodeField = FormField(
// label = "溯源码",
// type = FieldType.TEXT,
// apiKey = "traceCode",
// required = true
// )
// fields.add(traceCodeField!!)
// 净菜名称
cleanNameField = FormField(
label = "净菜名称",
type = FieldType.TEXT,
apiKey = "cleanName",
required = true
)
fields.add(cleanNameField!!)
// cleanNameField = FormField(
// label = "净菜名称",
// type = FieldType.DROPDOWN,
//// apiKey = "cleanName",
// extraApiKeys = mapOf("cleanName" to "value", "traceCode" to "type"),
// required = true
// )
// fields.add(cleanNameField!!)
// 包装方式(下拉)
packageMethodField = FormField(
label = "包装方式",
type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
fields.add(
FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = true)
FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = false)
)
fields.add(
@@ -210,7 +228,7 @@ class CleanPackActivity : BaseActivity() {
formAdapter.submitList(fields.filter { !it.hidden })
// 监听溯源码输入,自动填充净菜名称
setupTraceCodeQuery()
// setupTraceCodeQuery()
// 加载下拉选项
// loadDictItems("sup_package_method", packageMethodField!!)
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
@@ -233,18 +251,19 @@ class CleanPackActivity : BaseActivity() {
fields.add(foodOptionsField!!)
// 包装规格(下拉)
packagingSpecField = FormField(
label = "包装规格", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("spec" to "value"),
required = true
)
fields.add(packagingSpecField!!)
// packagingSpecField = FormField(
// label = "包装规格", type = FieldType.DROPDOWN,
// extraApiKeys = mapOf("spec" to "value"),
// required = true
// )
// fields.add(packagingSpecField!!)
// 包装方式(下拉)
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
@@ -399,7 +418,7 @@ class CleanPackActivity : BaseActivity() {
// 加载下拉选项
loadFoodOptionsWithKeyword()
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
// loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
loadDeviceOptionsWithCallback(deviceField)
}
@@ -407,48 +426,49 @@ class CleanPackActivity : BaseActivity() {
* 监听溯源码字段变化,输入后 800ms 无变化则触发查询
*/
private fun setupTraceCodeQuery() {
val field = traceCodeField ?: return
val cleanField = cleanNameField ?: return
traceCodeJob = lifecycleScope.launch {
var lastValue = ""
while (true) {
val current = field.value.trim()
if (current != lastValue && current.isNotBlank()) {
lastValue = current
viewModelV2.getIngredientByTrace(current)
}
delay(800)
}
}
lifecycleScope.launch {
viewModelV2.ingredientByTraceState.collect { state ->
when (state) {
is UiState.Success -> {
val data = state.data
if (!data.isNullOrEmpty()) {
val info = data[0]
cleanField.value = info.materName ?: ""
// 通知 adapter 刷新净菜名称字段(索引为 1)
val idx = fields.indexOf(cleanField)
if (idx >= 0) formAdapter.notifyItemChanged(idx)
}
}
is UiState.Error -> {
toast(state.msg)
}
else -> {}
}
}
}
// val field = traceCodeField ?: return
// val cleanField = cleanNameField ?: return
//
// traceCodeJob = lifecycleScope.launch {
// var lastValue = ""
// while (true) {
// val current = field.value.trim()
// if (current != lastValue && current.isNotBlank()) {
// lastValue = current
// viewModelV2.getIngredientByTrace(current)
// }
// delay(800)
// }
// }
//
// lifecycleScope.launch {
// viewModelV2.ingredientByTraceState.collect { state ->
// when (state) {
// is UiState.Success -> {
// val data = state.data
// if (!data.isNullOrEmpty()) {
// val info = data[0]
// cleanField.value = info.materName ?: ""
// // 通知 adapter 刷新净菜名称字段(索引为 1)
// val idx = fields.indexOf(cleanField)
// if (idx >= 0) formAdapter.notifyItemChanged(idx)
// }
// }
//
// is UiState.Error -> {
// toast(state.msg)
// }
//
// else -> {}
// }
// }
// }
}
override fun onDestroy() {
super.onDestroy()
traceCodeJob?.cancel()
WeightUtil.removeWeightListener(TAG)
}
/**
@@ -524,7 +544,7 @@ class CleanPackActivity : BaseActivity() {
/**
* 加载设备下拉(回调版本),用于餐品净菜包装
*/
private fun loadDeviceOptionsWithCallback(deviceField: FormField?=null) {
private fun loadDeviceOptionsWithCallback(deviceField: FormField? = null) {
val field = deviceField ?: return
viewModelV2.getDeviceOptionsWithCallback(
onLoading = {},
@@ -613,9 +633,19 @@ class CleanPackActivity : BaseActivity() {
*/
private fun submitCleanPackage() {
val params = collectSubmitValues()
viewModelV2.addCleanPackage(params)
toast("提交成功")
finish()
if (currentWeight <= 0.0) {
toast("未识别到重量")
return
}
params["weightPerUnit"] = currentWeight.toString()
viewModelV2.addCleanPackage(params) { state, msg ->
if (state.not()) {
toast("提交失败:$msg")
return@addCleanPackage
}
toast("提交成功")
finish()
}
}
/**
@@ -623,9 +653,36 @@ class CleanPackActivity : BaseActivity() {
*/
private fun submitMealPackage() {
val params = collectSubmitValues()
viewModelV2.addMealPackage(params)
toast("提交成功")
finish()
viewModelV2.addMealPackage(params) { state, msg ->
if (state.not()) {
toast("提交失败:$msg")
return@addMealPackage
}
toast("提交成功")
finish()
}
}
private var initialWeight = SpTool.configWeight.toDouble()
private var currentWeight = 0.0
private fun addWeightListener() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
currentWeight = weight - initialWeight
binding.tvShowWeight.run {
if (tag != currentWeight) {
val weightText = "${currentWeight.roundedOneDecimalPlace()}g"
text = weightText
tag = currentWeight
}
}
}
}
})
}
}
@@ -19,6 +19,10 @@ class HomeV2Activity : BaseActivity() {
binding = ActivityHomeV2Binding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvTitle.setOnClickListener {
startActivity<SettingActivity> {}
}
binding.btnCleanPack.setOnClickListener {
startActivity<CleanPackActivity> {
putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_CLEAN)
@@ -26,9 +30,11 @@ class HomeV2Activity : BaseActivity() {
}
binding.btnMealPack.setOnClickListener {
startActivity<CleanPackActivity> {
putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_MEAL)
}
startActivity<MealListActivity> {}
}
binding.btnTask.setOnClickListener {
startActivity<TaskActivity> {}
}
}
}
@@ -95,20 +95,20 @@ class InitActivity : BaseActivity() {
// return@launch
// }
// 主设备:走原有路由逻辑
when (SpTool.cookMode) {
0 -> {
startActivity<CookingModeActivity>()
}
1 -> {
goSampling()
}
else -> {
startActivity<HomeActivity>()
}
}
// startActivity<HomeV2Activity>()
// when (SpTool.cookMode) {
// 0 -> {
// startActivity<CookingModeActivity>()
// }
//
// 1 -> {
// goSampling()
// }
//
// else -> {
// startActivity<HomeActivity>()
// }
// }
startActivity<HomeV2Activity>()
// 延迟到下一帧 finish,避免与 LaunchActivityItem 事务竞态导致
// "Activity client record must not be null" 偶发崩溃
window.decorView.post { finish() }
@@ -201,6 +201,7 @@ class PackActivity : BaseActivity() {
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
@@ -504,7 +505,8 @@ class PackActivity : BaseActivity() {
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
// extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
extraApiKeys = mapOf("packageMethod" to "type"),
required = true
)
fields.add(packageMethodField!!)
@@ -122,14 +122,14 @@ class SettingActivity : BaseActivity() {
startActivity<TaskActivity> { }
}
),
SettingItem(
type = SettingItem.Type.MEAL_PACKAGE_TEST,
onClick = {
startActivity<CleanPackActivity> {
putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_MEAL)
}
}
),
// SettingItem(
// type = SettingItem.Type.MEAL_PACKAGE_TEST,
// onClick = {
// startActivity<CleanPackActivity> {
// putExtra(CleanPackActivity.EXTRA_TYPE, CleanPackActivity.TYPE_MEAL)
// }
// }
// ),
SettingItem(
type = SettingItem.Type.DB_INSPECT,
isHidden = true,
@@ -189,6 +189,7 @@ class TaskDetailActivity : BaseActivity() {
}
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
SpTool.configWeight = 0
WeightUtil.tareTwo(1)
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 从上到下:浅红(占比多)→ 深红,angle=270 表示从上到下 -->
<gradient
android:angle="270"
android:startColor="#FFB3B3"
android:centerColor="#E53935"
android:centerX="0.65"
android:endColor="#CC0000"
android:type="linear" />
<corners android:radius="16dp" />
</shape>
+118 -6
View File
@@ -5,16 +5,128 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/white"
tools:background="@drawable/bg_other_page"
tools:ignore="HardcodedText">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvForm"
<FrameLayout
android:id="@+id/flSearchBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:clipToOutline="true"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_gravity="center"
android:layout_marginHorizontal="28dp"
android:background="@color/white"
android:ellipsize="end"
android:gravity="center"
android:hint="搜索菜品名称"
android:maxLines="1"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:textColor="@color/black333"
android:textColorHint="@color/gray_c8"
android:textSize="40sp"
tools:ignore="TextFields" />
<ImageView
android:id="@+id/ivDishSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:src="@drawable/ic_search_gray"
android:visibility="visible"
tools:ignore="ContentDescription" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginHorizontal="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="用量(克)"
android:textColor="@color/black666"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvShowWeight"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginStart="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_f6_10_corners"
android:gravity="center"
android:text="-"
android:textColor="@color/gray_d6"
android:textSize="50sp" />
<ImageView
android:id="@+id/ivClearWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginHorizontal="30dp"
android:src="@drawable/ic_weight_clear" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<TextView
android:id="@+id/btnTestConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="15dp"
android:layout_marginStart="20dp"
android:paddingVertical="10dp"
android:text="净菜包信息"
android:textColor="@color/black666"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvForm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:layout_marginHorizontal="14dp"
android:layout_marginBottom="20dp"
app:layoutManager="GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_form_field" />
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="20dp"
android:overScrollMode="never" />
android:layout_weight="1" />
<TextView
android:id="@+id/btnSubmit"
+19 -5
View File
@@ -9,19 +9,20 @@
tools:ignore="HardcodedText">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="300dp"
android:text="净菜包装"
android:text="配比秤"
android:textColor="@color/home_sub_title"
android:textSize="50sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatButton
<TextView
android:id="@+id/btnCleanPack"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="100dp"
android:layout_marginTop="60dp"
android:text="净菜包"
android:textColor="@color/white"
android:textSize="35sp"
@@ -30,11 +31,11 @@
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_blue_btn"/>
<androidx.appcompat.widget.AppCompatButton
<TextView
android:id="@+id/btnMealPack"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="100dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:text="餐品净菜包"
android:textColor="@color/white"
@@ -43,4 +44,17 @@
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_orange_btn"/>
<TextView
android:id="@+id/btnTask"
android:layout_width="500dp"
android:layout_height="300dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:text="组配任务"
android:textColor="@color/white"
android:textSize="35sp"
android:textStyle="bold"
android:foreground="?android:attr/selectableItemBackground"
android:background="@drawable/bg_gradient_red_btn"/>
</LinearLayout>
@@ -103,55 +103,6 @@
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="90dp"-->
<!-- android:gravity="center_vertical">-->
<!-- <TextView-->
<!-- android:id="@+id/tvGoodsName"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="30dp"-->
<!-- android:textColor="@color/black"-->
<!-- android:textSize="30sp"-->
<!-- android:maxLines="1"-->
<!-- android:ellipsize="end"-->
<!-- android:maxLength="12"-->
<!-- android:textStyle="bold"-->
<!-- tools:text="豆腐" />-->
<!-- <TextView-->
<!-- android:id="@+id/tvMaterialType"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="30dp"-->
<!-- android:layout_marginEnd="30dp"-->
<!-- android:textColor="@color/black999"-->
<!-- android:textSize="26sp"-->
<!-- tools:text="主材" />-->
<!-- </LinearLayout>-->
<!-- <com.google.android.material.divider.MaterialDivider-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="1dp"-->
<!-- android:layout_marginHorizontal="30dp"-->
<!-- app:dividerColor="@color/gray_eb" />-->
<!-- <androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/rvFormList"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginHorizontal="20dp"-->
<!-- android:overScrollMode="never"/>-->
<!-- <com.google.android.material.divider.MaterialDivider-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="1dp"-->
<!-- android:layout_marginHorizontal="30dp"-->
<!-- app:dividerColor="@color/gray_eb" />-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="160dp"
@@ -25,7 +25,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
android:visibility="gone">
<!-- 子列表标题行 -->
<LinearLayout
@@ -147,7 +147,7 @@
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginHorizontal="6dp"
android:visibility="gone">
android:visibility="visible">
<!-- TEXT / NUMBER / FIXED 类型:EditText -->
<EditText