feat(pack): 添加净菜包装功能和食材追溯界面

- 在activity_pack.xml中新增rvFormList RecyclerView用于表单展示
- 在TaskDetailActivity中实现食材明细的增删改查功能
- 添加InboundIngredientSearchDialog用于食材溯源码搜索
- 更新NetViewModelV2中addComboTask方法参数类型
- 添加NutCleanInboundIngredientVO数据模型用于已入库净菜食材
- 在PackActivity中实现包装表单动态加载和设备选择功能
- 添加用量计算和重量识别相关UI组件
- 优化FormFieldAdapter适配器去除Activity上下文依赖
- 新增formatDecimal扩展函数用于格式化用量字符串
- 实现TaskDetailAdapter中食材项的选中和清除逻辑
- 添加组合约束的API接口定义和数据传输对象修改
- 重构表单字段配置为动态加载模式支持多种输入类型
- 添加删除确认对话框提升用户体验和数据安全性
- 优化布局文件结构调整控件间距和显示效果
- 添加设备选项加载回调机制确保数据实时更新
- 实现重量监听器的生命周期管理防止内存泄漏
- 更新界面跳转逻辑支持餐品选择和详情展示
- 修复多个布局和代码中的空格及格式问题
- 添加必要的依赖导入和常量定义完善功能模块
This commit is contained in:
2026-06-02 18:05:47 +08:00
parent 3fe66b513f
commit 32ec1d9b74
18 changed files with 1104 additions and 153 deletions
@@ -32,11 +32,9 @@ import kotlin.math.min
* 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局 * 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局
* 单一 item 布局,通过 visibility 控制 TEXT/INTEGER/DECIMAL/FIXED/DROPDOWN/DATE_PICKER 六种类型渲染 * 单一 item 布局,通过 visibility 控制 TEXT/INTEGER/DECIMAL/FIXED/DROPDOWN/DATE_PICKER 六种类型渲染
* *
* @param activityContext Activity context,用于创建 DatePickerDialog
* @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入) * @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入)
*/ */
class FormFieldAdapter( class FormFieldAdapter(
private val activityContext: Context,
fields: MutableList<FormField> fields: MutableList<FormField>
) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) { ) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) {
@@ -131,7 +129,7 @@ class FormFieldAdapter(
v.hideKeyboard() v.hideKeyboard()
val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp
DropdownPopup( DropdownPopup(
context = activityContext, context = context,
list = item.options.toMutableList(), list = item.options.toMutableList(),
popWidth = v.width, popWidth = v.width,
popHeight = popHeight popHeight = popHeight
@@ -162,7 +160,7 @@ class FormFieldAdapter(
v.hideKeyboard() v.hideKeyboard()
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
DatePickerDialog( DatePickerDialog(
activityContext, context,
{ _, year, month, dayOfMonth -> { _, year, month, dayOfMonth ->
val selected = Calendar.getInstance().apply { val selected = Calendar.getInstance().apply {
set(year, month, dayOfMonth, 0, 0, 0) set(year, month, dayOfMonth, 0, 0, 0)
@@ -0,0 +1,40 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemInboundIngredientBinding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.utils.ext.formatDecimal
/**
* 已入库净菜食材列表适配器
*/
class InboundIngredientAdapter(list: MutableList<NutCleanInboundIngredientVO>) :
BaseQuickAdapter<NutCleanInboundIngredientVO, InboundIngredientAdapter.VH>(list) {
inner class VH(var binding: ListItemInboundIngredientBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemInboundIngredientBinding.inflate(
LayoutInflater.from(context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: NutCleanInboundIngredientVO?) {
item ?: return
holder.binding.run {
tvIngredientName.text = item.ingredientName ?: "-"
val traceCode = item.traceCode.takeUnless { it.isNullOrBlank() } ?: "-"
tvTraceCode.text = "溯源码:$traceCode"
val stock = item.currentStock.takeUnless { it.isNullOrBlank() } ?: "0"
tvStock.text = "库存:${stock.formatDecimal()}kg"
}
}
}
@@ -1,47 +1,50 @@
package com.shuwei.dish.match.adapter package com.shuwei.dish.match.adapter
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemTaskDetailBinding import com.shuwei.dish.match.databinding.ListItemTaskDetailBinding
import com.shuwei.dish.match.model.NutComboTaskItemDetail import com.shuwei.dish.match.model.NutComboTaskItemDetail
import com.shuwei.dish.match.utils.ext.formatDecimal
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import java.math.BigDecimal import java.math.BigDecimal
/** /**
* 组配任务详情食材列表适配器 * 组配任务详情食材列表适配器
* Adapter for displaying ingredient list in task details
*/ */
class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) : class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) :
BaseQuickAdapter<NutComboTaskItemDetail, TaskDetailAdapter.VH>(list) { BaseQuickAdapter<NutComboTaskItemDetail, TaskDetailAdapter.VH>(list) {
// ViewHolder class for holding the view bindings
inner class VH(var binding: ListItemTaskDetailBinding) : QuickViewHolder(binding.root) inner class VH(var binding: ListItemTaskDetailBinding) : QuickViewHolder(binding.root)
/** /**
* 格式化用量字符串,去掉小数点后末尾的0 * Called by RecyclerView to display the item at the specified position.
* This method updates the ViewHolder's contents with the item's data.
*
* @param holder The ViewHolder to be updated
* @param position The position of the item in the adapter
* @param item The NutComboTaskItemDetail to be displayed
*/ */
private fun formatQty(value: String?): String {
if (value.isNullOrBlank()) return "0"
return try {
BigDecimal(value).stripTrailingZeros().toPlainString()
} catch (e: Exception) {
value
}
}
override fun onBindViewHolder( override fun onBindViewHolder(
holder: VH, holder: VH,
position: Int, position: Int,
item: NutComboTaskItemDetail? item: NutComboTaskItemDetail?
) { ) {
item?:return
holder.binding.apply { holder.binding.apply {
tvIngredientName.text = item?.ingredientName // Set ingredient name
tvIngredientName.text = item.ingredientName
// 食材类别 // 食材类别
val classText = when (item?.ingredientClass) { val classText = when (item.ingredientClass) {
1 -> "主材" 1 -> "主材"
2 -> "辅材" 2 -> "辅材"
3 -> "调料" 3 -> "调料"
@@ -49,16 +52,32 @@ class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) :
} }
tvIngredientClass.text = classText tvIngredientClass.text = classText
val unit = item?.unit ?: "g" val unit = item.unit ?: "g"
tvPerPortionQty.text = "每份用量:${formatQty(item?.perPortionQty)}${unit}" tvPerPortionQty.text = "每份用量:${item.perPortionQty.formatDecimal()}${unit}"
tvActualQty.text = "实际用量:${formatQty(item?.actualQty)}${unit}" tvActualQty.run {
tvTotalQty.text = "合计用量:${formatQty(item?.totalQty)}${unit}" if (item.actualQty.isNullOrBlank()) {
setTextColor("#999999".toColorInt())
text = "-"
} else {
setTextColor("#00BC71".toColorInt())
text = item.actualQty.formatDecimal() + unit
}
}
// tvTotalQty.text = "合计用量:${item?.totalQty.formatDecimal()}${unit}"
tvTraceCode.visible() tvTraceCode.visible()
tvTraceCode.text = "溯源码:${(item?.traceCode ?:"").ifBlank { "-" }}" tvTraceCode.text = "溯源码:${(item.traceCode ?:"").ifBlank { "-" }}"
// val hasWeight = item.actualQty.isNullOrBlank().not()
// ivClearIcon.imageTintList = if (hasWeight) null else ColorStateList.valueOf("#BFBFBF".toColorInt())
ivOperateIcon.setImageResource(
if (item.isSetFinished == true) R.drawable.ic_dish_selected
else R.drawable.ic_dish_unselected
)
root.setBackgroundResource( root.setBackgroundResource(
if (item?.isClicked == true) R.drawable.shape_item_cook_dish if (item.isClicked == true) R.drawable.shape_item_cook_dish
else R.drawable.shape_white_fb_15_corners else R.drawable.shape_white_fb_15_corners
) )
} }
@@ -0,0 +1,214 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.graphics.Rect
import android.text.Editable
import android.view.KeyboardShortcutGroup
import android.view.LayoutInflater
import android.view.Menu
import android.view.MotionEvent
import android.view.View
import android.view.Window
import android.widget.FrameLayout
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.InboundIngredientAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogInboundIngredientSearchBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
/**
* 已入库净菜食材搜索弹窗
* @param activity 宿主 Activity
* @param defIngredientName 默认填充的食材名称(可选)
* @param onItemSelected 用户点击某项食材时的回调
*/
class InboundIngredientSearchDialog(
private val activity: BaseActivity,
private val viewModelV2: NetViewModelV2,
private val defIngredientName: String? = null,
private val onItemSelected: (item: NutCleanInboundIngredientVO) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
private val binding = DialogInboundIngredientSearchBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<NutCleanInboundIngredientVO>()
private val adapter = InboundIngredientAdapter(list).apply {
isStateViewEnable = true
setOnDebouncedItemClick { _, _, position ->
val item = list.getOrNull(position) ?: return@setOnDebouncedItemClick
onItemSelected(item)
dismiss()
}
}
private var keyword: String? = null
init {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false
setOnDismissListener { activity.hideStatusBar() }
binding.tvSheetName.text = "已入库净菜检索"
// 输入框:清空时重置列表
binding.etSheetInput.run {
hint = "输入食材名称"
addTextChangedListener(
onTextChanged = { text, _, _, _ -> keyword = text.toString() },
afterTextChanged = { it: Editable? ->
if (it.isNullOrBlank()) {
list.clear()
adapter.notifyDataSetChanged()
}
}
)
addOnActionSearchListener { searchIngredient(this) }
}
// RecyclerView 初始化
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@InboundIngredientSearchDialog.adapter
}
binding.refreshLayout.run {
setEnableRefresh(true)
setEnableLoadMore(false)
setOnRefreshListener { getIngredientList() }
}
binding.ivSearch.setOnClickListener { searchIngredient(it) }
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
// 若有默认食材名称,自动填充并触发搜索
if (!defIngredientName.isNullOrBlank()) {
binding.etSheetInput.setText(defIngredientName)
getIngredientList()
KeyboardUtil.hideKeyboard(binding.root)
}
}
/** 弹窗显示时在 Window.Callback 层提前拦截触摸,解决 BottomSheet 拖拽与列表滑动的手势冲突 */
override fun show() {
super.show()
val originalCallback = window?.callback ?: return
window?.callback = object : Window.Callback by originalCallback {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val rect = Rect()
binding.refreshLayout.getGlobalVisibleRect(rect)
behavior.isDraggable = !rect.contains(event.rawX.toInt(), event.rawY.toInt())
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
behavior.isDraggable = true
}
}
return originalCallback.dispatchTouchEvent(event)
}
override fun onPointerCaptureChanged(hasCapture: Boolean) {
originalCallback.onPointerCaptureChanged(hasCapture)
}
override fun onProvideKeyboardShortcuts(
data: List<KeyboardShortcutGroup?>?,
menu: Menu?,
deviceId: Int
) {
originalCallback.onProvideKeyboardShortcuts(data, menu, deviceId)
}
}
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
private fun searchIngredient(v: View) {
if (keyword.isNullOrBlank()) {
activity.toast("请输入食材名称")
return
}
getIngredientList()
KeyboardUtil.hideKeyboard(v)
}
/**
* 请求食材列表,使用回调方式发起
*/
private fun getIngredientList() {
viewModelV2.getInboundIngredientOptionsWithCallback(
keyword = keyword.takeUnless { it.isNullOrBlank() },
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> loadIngredientList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
binding.refreshLayout.finishRefresh()
}
else -> {}
}
}
)
}
/**
* 将返回数据填充到列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadIngredientList(records: List<NutCleanInboundIngredientVO>?) {
binding.refreshLayout.finishRefresh()
if (records.isNullOrEmpty()) {
loadEmptyView()
return
}
binding.recyclerView.layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
list.clear()
list.addAll(records)
adapter.notifyDataSetChanged()
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
try {
list.clear()
adapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(activity),
binding.recyclerView,
false
)
}
emptyViewBinding?.let {
it.tvContent.text = "暂无数据"
it.tvSubContent.text = "未查询到已入库净菜信息"
it.root.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
binding.recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
binding.recyclerView.post {
it.root.minimumHeight = binding.recyclerView.height
adapter.stateView = it.root
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -207,18 +207,18 @@ data class NutComboTaskVO(
*/ */
@Parcelize @Parcelize
data class NutComboTaskDetailVO( data class NutComboTaskDetailVO(
val id: Long?, var id: Long? = null,
val taskNo: String?, var taskNo: String? = null,
val planDate: String?, var planDate: String? = null,
val canteenId: Long?, var canteenId: Long? = null,
val mealType: Int?, var mealType: Int? = null,
val foodId: Long?, var foodId: Long? = null,
val targetDish: String?, var targetDish: String? = null,
val spec: String?, var spec: String? = null,
val portions: Int?, var portions: Int? = null,
val comboStatus: Int?, var comboStatus: Int? = null,
val ownerName: String?, var ownerName: String? = null,
val items: List<NutComboTaskItemDetail>? var items: List<NutComboTaskItemDetail>? = null,
) : Parcelable ) : Parcelable
/** /**
@@ -232,10 +232,23 @@ data class NutComboTaskItemDetail(
val ingredientClass: Int?, val ingredientClass: Int?,
var traceCode: String?, var traceCode: String?,
val perPortionQty: String?, val perPortionQty: String?,
val actualQty: String?, var actualQty: String?,
val totalQty: String?, val totalQty: String?,
val unit: String?, val unit: String?,
var isClicked: Boolean? = false, var isClicked: Boolean? = false,
var isSetFinished: Boolean? = false,
) : Parcelable
/**
* 已入库净菜食材VO - 按食材名称查询已入库有库存的净菜记录
*/
@Parcelize
data class NutCleanInboundIngredientVO(
val id: Long?,
val materId: Long?,
val ingredientName: String?,
val traceCode: String?,
val currentStock: String?
) : Parcelable ) : Parcelable
/** /**
@@ -10,6 +10,7 @@ import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutProdComboVideoVO import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.IdDTO import com.shuwei.dish.match.model.IdDTO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO import com.shuwei.dish.match.model.NutComboTaskVO
import retrofit2.http.Body import retrofit2.http.Body
@@ -77,6 +78,15 @@ interface ApiServiceV2 {
@Query("traceCode") traceCode: String @Query("traceCode") traceCode: String
): ApiResponse<List<NutSupHygieneIngredientOptionVO>> ): ApiResponse<List<NutSupHygieneIngredientOptionVO>>
/**
* 按食材名称查询已入库有库存的净菜记录
*/
@GET
suspend fun getInboundIngredientOptions(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound-ingredient-options",
@Query("keyword") keyword: String? = null
): ApiResponse<List<NutCleanInboundIngredientVO>>
// ========== 二、净菜组配接口 ========== // ========== 二、净菜组配接口 ==========
/** /**
@@ -85,7 +95,7 @@ interface ApiServiceV2 {
@POST @POST
suspend fun addComboTask( suspend fun addComboTask(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/add", @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/add",
@Body param: NutProdComboTaskDTO @Body param: NutComboTaskDetailVO
): ApiResponse<Unit> ): ApiResponse<Unit>
/** /**
@@ -3,6 +3,7 @@ package com.shuwei.dish.match.net
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.shuwei.dish.match.model.NutCanteenVO import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO import com.shuwei.dish.match.model.NutDictItemVO
@@ -28,7 +29,8 @@ class NetViewModelV2(
// 食堂下拉列表 // 食堂下拉列表
private val _canteenOptionsState = MutableStateFlow<UiState<List<NutCanteenVO>?>>(UiState.Idle) private val _canteenOptionsState = MutableStateFlow<UiState<List<NutCanteenVO>?>>(UiState.Idle)
val canteenOptionsState: StateFlow<UiState<List<NutCanteenVO>?>> = _canteenOptionsState.asStateFlow() val canteenOptionsState: StateFlow<UiState<List<NutCanteenVO>?>> =
_canteenOptionsState.asStateFlow()
fun getCanteenOptions(keyword: String? = null) { fun getCanteenOptions(keyword: String? = null) {
viewModelScope.launch { viewModelScope.launch {
@@ -49,8 +51,10 @@ class NetViewModelV2(
// } // }
// 食材下拉列表 // 食材下拉列表
private val _materOptionsState = MutableStateFlow<UiState<List<NutMaterOptionVO>?>>(UiState.Idle) private val _materOptionsState =
val materOptionsState: StateFlow<UiState<List<NutMaterOptionVO>?>> = _materOptionsState.asStateFlow() MutableStateFlow<UiState<List<NutMaterOptionVO>?>>(UiState.Idle)
val materOptionsState: StateFlow<UiState<List<NutMaterOptionVO>?>> =
_materOptionsState.asStateFlow()
fun getMaterOptions(keyword: String? = null) { fun getMaterOptions(keyword: String? = null) {
viewModelScope.launch { viewModelScope.launch {
@@ -60,8 +64,10 @@ class NetViewModelV2(
} }
// 包装设备下拉 // 包装设备下拉
private val _deviceOptionsState = MutableStateFlow<UiState<List<NutSupPkgDeviceVO>?>>(UiState.Idle) private val _deviceOptionsState =
val deviceOptionsState: StateFlow<UiState<List<NutSupPkgDeviceVO>?>> = _deviceOptionsState.asStateFlow() MutableStateFlow<UiState<List<NutSupPkgDeviceVO>?>>(UiState.Idle)
val deviceOptionsState: StateFlow<UiState<List<NutSupPkgDeviceVO>?>> =
_deviceOptionsState.asStateFlow()
fun getDeviceOptions() { fun getDeviceOptions() {
viewModelScope.launch { viewModelScope.launch {
@@ -72,7 +78,8 @@ class NetViewModelV2(
// 餐品下拉搜索 // 餐品下拉搜索
private val _foodOptionsState = MutableStateFlow<UiState<Page<NutFoodOptionVO>?>>(UiState.Idle) private val _foodOptionsState = MutableStateFlow<UiState<Page<NutFoodOptionVO>?>>(UiState.Idle)
val foodOptionsState: StateFlow<UiState<Page<NutFoodOptionVO>?>> = _foodOptionsState.asStateFlow() val foodOptionsState: StateFlow<UiState<Page<NutFoodOptionVO>?>> =
_foodOptionsState.asStateFlow()
// fun getFoodOptions(keyword: String? = null, pageNum: Int = 1, pageSize: Int = 10) { // fun getFoodOptions(keyword: String? = null, pageNum: Int = 1, pageSize: Int = 10) {
// viewModelScope.launch { // viewModelScope.launch {
@@ -125,8 +132,10 @@ class NetViewModelV2(
} }
// 按溯源码查食材信息 // 按溯源码查食材信息
private val _ingredientByTraceState = MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle) private val _ingredientByTraceState =
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> = _ingredientByTraceState.asStateFlow() MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle)
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> =
_ingredientByTraceState.asStateFlow()
fun getIngredientByTrace(traceCode: String) { fun getIngredientByTrace(traceCode: String) {
viewModelScope.launch { viewModelScope.launch {
@@ -141,12 +150,29 @@ class NetViewModelV2(
private val _addComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle) private val _addComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addComboTaskState: StateFlow<UiState<Unit?>> = _addComboTaskState.asStateFlow() val addComboTaskState: StateFlow<UiState<Unit?>> = _addComboTaskState.asStateFlow()
fun addComboTask(param: NutProdComboTaskDTO) { fun addComboTask(param: NutComboTaskDetailVO, block: (Boolean, String) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
_addComboTaskState.value = UiState.Loading val uiState = repository.addComboTask(param)
_addComboTaskState.value = repository.addComboTask(param) when (uiState) {
is UiState.Success -> {
block(true, "")
}
is UiState.Error -> {
block(false, uiState.msg)
}
is UiState.Loading -> {}
UiState.Idle -> {}
}
} }
} }
// fun addComboTask(param: NutProdComboTaskDTO) {
// viewModelScope.launch {
// _addComboTaskState.value = UiState.Loading
// _addComboTaskState.value = repository.addComboTask(param)
// }
// }
// 开始组配 // 开始组配
private val _startComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle) private val _startComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
@@ -172,7 +198,8 @@ class NetViewModelV2(
// 获取组配任务视频监控地址 // 获取组配任务视频监控地址
private val _comboTaskVideoState = MutableStateFlow<UiState<NutProdComboVideoVO?>>(UiState.Idle) private val _comboTaskVideoState = MutableStateFlow<UiState<NutProdComboVideoVO?>>(UiState.Idle)
val comboTaskVideoState: StateFlow<UiState<NutProdComboVideoVO?>> = _comboTaskVideoState.asStateFlow() val comboTaskVideoState: StateFlow<UiState<NutProdComboVideoVO?>> =
_comboTaskVideoState.asStateFlow()
fun getComboTaskVideo(taskNo: String) { fun getComboTaskVideo(taskNo: String) {
viewModelScope.launch { viewModelScope.launch {
@@ -220,7 +247,14 @@ class NetViewModelV2(
) { ) {
viewModelScope.launch { viewModelScope.launch {
onLoading() onLoading()
onResult(repository.getIncompleteComboTasks(deviceCode = deviceCode, keyword = keyword, pageNum = pageNum, pageSize = pageSize)) onResult(
repository.getIncompleteComboTasks(
deviceCode = deviceCode,
keyword = keyword,
pageNum = pageNum,
pageSize = pageSize
)
)
} }
} }
@@ -238,4 +272,20 @@ class NetViewModelV2(
} }
} }
// ========== 已入库净菜食材接口 ==========
/**
* 按食材名称查询已入库有库存的净菜记录(回调版本)
*/
fun getInboundIngredientOptionsWithCallback(
keyword: String? = null,
onLoading: () -> Unit = {},
onResult: (UiState<List<NutCleanInboundIngredientVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getInboundIngredientOptions(keyword))
}
}
} }
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.net
import com.shuwei.dish.match.model.IdDTO import com.shuwei.dish.match.model.IdDTO
import com.shuwei.dish.match.model.NutCanteenVO import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO import com.shuwei.dish.match.model.NutDictItemVO
@@ -65,9 +66,12 @@ class RemoteRepositoryV2 {
suspend fun getIngredientByTrace(traceCode: String): UiState<List<NutSupHygieneIngredientOptionVO>?> = suspend fun getIngredientByTrace(traceCode: String): UiState<List<NutSupHygieneIngredientOptionVO>?> =
safeApiCall { apiService.getIngredientByTrace(traceCode = traceCode) } safeApiCall { apiService.getIngredientByTrace(traceCode = traceCode) }
suspend fun getInboundIngredientOptions(keyword: String? = null): UiState<List<NutCleanInboundIngredientVO>?> =
safeApiCall { apiService.getInboundIngredientOptions(keyword = keyword) }
// ========== 净菜组配接口 ========== // ========== 净菜组配接口 ==========
suspend fun addComboTask(param: NutProdComboTaskDTO): UiState<Unit?> = suspend fun addComboTask(param: NutComboTaskDetailVO): UiState<Unit?> =
safeApiCall { apiService.addComboTask(param = param) } safeApiCall { apiService.addComboTask(param = param) }
suspend fun startComboTask(id: Long): UiState<Unit?> = suspend fun startComboTask(id: Long): UiState<Unit?> =
@@ -37,7 +37,7 @@ class CleanPackActivity : BaseActivity() {
private lateinit var binding: ActivityCleanPackBinding private lateinit var binding: ActivityCleanPackBinding
private val fields = mutableListOf<FormField>() private val fields = mutableListOf<FormField>()
private val formAdapter by lazy { FormFieldAdapter(this, fields) } private val formAdapter by lazy { FormFieldAdapter(fields) }
private val viewModelV2 by lazy { NetViewModelV2() } private val viewModelV2 by lazy { NetViewModelV2() }
private var packType: Int = TYPE_CLEAN private var packType: Int = TYPE_CLEAN
@@ -241,17 +241,13 @@ class CleanPackActivity : BaseActivity() {
fields.add(packagingSpecField!!) fields.add(packagingSpecField!!)
// 包装方式(下拉) // 包装方式(下拉)
fields.add( packageMethodField =
FormField( FormField(
label = "包装方式", type = FieldType.DROPDOWN, label = "包装方式", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"), extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
required = true, required = true
options = mutableListOf(
DictType(id = "1", value = "真空包装", type = "真空包装"),
DictType(id = "2", value = "气调包装", type = "气调包装"),
)
) )
) fields.add(packageMethodField!!)
// 包装份数 // 包装份数
fields.add( fields.add(
@@ -277,14 +273,15 @@ class CleanPackActivity : BaseActivity() {
required = true required = true
), ),
FormField( FormField(
label = "分类", type = FieldType.DROPDOWN, label = "分类", type = FieldType.FIXED,
extraApiKeys = mapOf("classify" to "value"), // extraApiKeys = mapOf("classify" to "value"),
apiKey = "classify",
required = true, required = true,
options = mutableListOf( // options = mutableListOf(
DictType(id = "1", value = "蔬菜类", type = "蔬菜类"), // DictType(id = "1", value = "蔬菜类", type = "蔬菜类"),
DictType(id = "2", value = "肉禽类", type = "肉禽类"), // DictType(id = "2", value = "肉禽类", type = "肉禽类"),
DictType(id = "3", value = "水产类", type = "水产类"), // DictType(id = "3", value = "水产类", type = "水产类"),
) // )
), ),
FormField( FormField(
label = "每份用量", label = "每份用量",
@@ -401,6 +398,7 @@ class CleanPackActivity : BaseActivity() {
// 加载下拉选项 // 加载下拉选项
loadFoodOptionsWithKeyword() loadFoodOptionsWithKeyword()
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!) loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
loadDeviceOptionsWithCallback(deviceField) loadDeviceOptionsWithCallback(deviceField)
} }
@@ -10,17 +10,23 @@ import android.view.ViewGroup
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodMaterialAdapter import com.shuwei.dish.match.adapter.FoodMaterialAdapter
import com.shuwei.dish.match.adapter.FormFieldAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPackBinding import com.shuwei.dish.match.databinding.ActivityPackBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.CookFoodDTO import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.dialog.InboundIngredientSearchDialog
import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.model.FieldType
import com.shuwei.dish.match.model.FoodRecord import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.model.GoodsItem import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.NutFoodOptionVO import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.UiState import com.shuwei.dish.match.net.UiState
@@ -127,6 +133,7 @@ class PackActivity : BaseActivity() {
} }
} }
} }
private var currentWeight = 0.0 private var currentWeight = 0.0
private fun addViewClickListener() { private fun addViewClickListener() {
// ========== 重量识别相关功能已注释 ========== // ========== 重量识别相关功能已注释 ==========
@@ -198,7 +205,7 @@ class PackActivity : BaseActivity() {
} }
binding.ivAddWeight.clickWithDebounce { binding.ivAddWeight.clickWithDebounce {
if(currentWeight <= 0.0) { if (currentWeight <= 0.0) {
toast("当前秤重量无效") toast("当前秤重量无效")
return@clickWithDebounce return@clickWithDebounce
} }
@@ -209,8 +216,17 @@ class PackActivity : BaseActivity() {
} }
} }
binding.btnGetTraceCode.clickWithDebounce { binding.btnGetTraceCode.clickWithDebounce {
val item = list.getOrNull(clickIndex)?:return@clickWithDebounce val item = list.getOrNull(clickIndex) ?: return@clickWithDebounce
//viewModelV2.getCode InboundIngredientSearchDialog(
activity = this,
viewModelV2 = viewModelV2,
defIngredientName = item.goodsName
) { vo ->
materialAdapter.getItem(clickIndex)?.let {
it.materCode = vo.traceCode
materialAdapter.notifyItemChanged(clickIndex)
}
}.show()
} }
binding.btnSubmit.clickWithDebounce { binding.btnSubmit.clickWithDebounce {
@@ -316,6 +332,7 @@ class PackActivity : BaseActivity() {
} }
loadDishDetail(detail) loadDishDetail(detail)
} }
is UiState.Error -> toast(state.msg) is UiState.Error -> toast(state.msg)
else -> {} else -> {}
} }
@@ -367,6 +384,13 @@ class PackActivity : BaseActivity() {
private var firstReqSize = 0 private var firstReqSize = 0
private val list = mutableListOf<GoodsItem>() private val list = mutableListOf<GoodsItem>()
private var clickIndex = -1 private var clickIndex = -1
private val fields = mutableListOf<FormField>()
private val formAdapter by lazy {
FormFieldAdapter(fields)
}
private val materialAdapter by lazy { private val materialAdapter by lazy {
FoodMaterialAdapter(list).apply { FoodMaterialAdapter(list).apply {
onItemClick = onItemClick@{ positon -> onItemClick = onItemClick@{ positon ->
@@ -409,12 +433,153 @@ class PackActivity : BaseActivity() {
} }
} }
} }
private var packageMethodField: FormField? = null
private var deviceField: FormField? = null
private fun initFormList() {
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
required = true
)
fields.add(packageMethodField!!)
fields.add(
FormField(
label = "包装份数", type = FieldType.INTEGER,
apiKey = "quantity", required = true, hint = "请输入份数"
)
)
// fields.add(
// FormField(
// label = "包装日期",
// type = FieldType.DATE_PICKER,
// apiKey = "packageDate",
// required = true
// )
// )
fields.add(
FormField(
label = "保质期至",
type = FieldType.DATE_PICKER,
apiKey = "expiryDate",
required = true
)
)
fields.add(
FormField(
label = "存放温度(℃)",
type = FieldType.DECIMAL,
apiKey = "storageTemp",
required = false,
hint = "请输入小数"
)
)
fields.add(
FormField(
label = "包装人",
type = FieldType.TEXT,
apiKey = "operator",
required = false
)
)
deviceField = FormField(
label = "终端设备",
type = FieldType.DROPDOWN,
extraApiKeys = mapOf("deviceId" to "id"),
required = false
)
fields.add(deviceField!!)
// fields.add(
// FormField(
// label = "操作类型", type = FieldType.FIXED,
// apiKey = "opType",
// required = true,
// value = "设备自动"
// )
// )
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDeviceOptionsWithCallback(deviceField)
}
/**
* 加载字典项下拉(回调版本),适合多次不同 dictType 的场景
*/
private fun loadDictItemsWithCallback(dictType: String, targetField: FormField) {
viewModelV2.getDictItemsWithCallback(
dictType = dictType,
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.dictLabel ?: "",
type = vo.dictCode ?: ""
)
} ?: emptyList()
targetField.options.clear()
targetField.options.addAll(options)
val index = fields.indexOf(targetField)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载字典项失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载设备下拉(回调版本),用于餐品净菜包装
*/
private fun loadDeviceOptionsWithCallback(deviceField: FormField? = null) {
val field = deviceField ?: return
viewModelV2.getDeviceOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.deviceName ?: "",
type = vo.deviceNo ?: ""
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载设备列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/** /**
* RecyclerView初始化 * RecyclerView初始化
*/ */
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun initRecyclerView() { private fun initRecyclerView() {
initFormList()
binding.rvFormList.let {
it.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
it.adapter = formAdapter
}
binding.rvMaterialList.let { binding.rvMaterialList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
// 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用) // 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用)
@@ -570,7 +735,11 @@ class PackActivity : BaseActivity() {
} }
startActivity(launchIntent) { resultIntent -> startActivity(launchIntent) { resultIntent ->
if (resultIntent == null) return@startActivity if (resultIntent == null) return@startActivity
val food = IntentCompat.getSerializableExtra(resultIntent, FoodSearchActivity.FOOD_ITEM, FoodRecord::class.java) val food = IntentCompat.getSerializableExtra(
resultIntent,
FoodSearchActivity.FOOD_ITEM,
FoodRecord::class.java
)
Log.d(TAG, "jumpSearch: record=$food") Log.d(TAG, "jumpSearch: record=$food")
if (food == null) return@startActivity if (food == null) return@startActivity
binding.etInputDish.run { binding.etInputDish.run {
@@ -583,13 +752,15 @@ class PackActivity : BaseActivity() {
} }
private fun handleRvTouchHideKeyboard() { private fun handleRvTouchHideKeyboard() {
val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { val gestureDetector =
override fun onSingleTapUp(e: MotionEvent): Boolean { GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
hideKeyboard() override fun onSingleTapUp(e: MotionEvent): Boolean {
return false hideKeyboard()
} return false
}) }
binding.rvMaterialList.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() { })
binding.rvMaterialList.addOnItemTouchListener(object :
RecyclerView.SimpleOnItemTouchListener() {
private var startX = 0f private var startX = 0f
private var startY = 0f private var startY = 0f
@@ -599,6 +770,7 @@ class PackActivity : BaseActivity() {
startX = e.x startX = e.x
startY = e.y startY = e.y
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val dx = abs(e.x - startX) val dx = abs(e.x - startX)
val dy = abs(e.y - startY) val dy = abs(e.y - startY)
@@ -613,4 +785,9 @@ class PackActivity : BaseActivity() {
}) })
} }
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
} }
@@ -1,18 +1,28 @@
package com.shuwei.dish.match.ui package com.shuwei.dish.match.ui
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TaskDetailAdapter import com.shuwei.dish.match.adapter.TaskDetailAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityTaskDetailBinding import com.shuwei.dish.match.databinding.ActivityTaskDetailBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.InboundIngredientSearchDialog
import com.shuwei.dish.match.model.NutComboTaskDetailVO import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskItemDetail
import com.shuwei.dish.match.model.NutComboTaskVO import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.net.NetViewModelV2 import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState 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.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,23 +37,60 @@ class TaskDetailActivity : BaseActivity() {
const val TASK_ITEM = "taskItem" const val TASK_ITEM = "taskItem"
} }
private var taskDetailVO: NutComboTaskDetailVO? = null
private val binding by lazy { ActivityTaskDetailBinding.inflate(layoutInflater) } private val binding by lazy { ActivityTaskDetailBinding.inflate(layoutInflater) }
private val viewModelV2 by lazy { NetViewModelV2() } private val viewModelV2 by lazy { NetViewModelV2() }
private var taskItem: NutComboTaskVO? = null private var taskItem: NutComboTaskVO? = null
private val itemList = mutableListOf<com.shuwei.dish.match.model.NutComboTaskItemDetail>() private var clickIndex = -1
private val comboTaskList = mutableListOf<NutComboTaskItemDetail>()
private val itemAdapter by lazy { private val itemAdapter by lazy {
TaskDetailAdapter(itemList).apply { TaskDetailAdapter(comboTaskList).apply {
setOnItemClickListener { adapter, view, position -> setOnItemClickListener { adapter, view, position ->
getItem(position) ?: return@setOnItemClickListener clickItem(this, position)
itemList.forEachIndexed { index, detail -> }
detail.isClicked = index == position addOnItemChildClickListener(R.id.ivClearIcon) { _, _, position ->
val item = getItem(position) ?: return@addOnItemChildClickListener
Log.d(PrepareFoodActivity.Companion.TAG, "onFoodItemClick: ${item.toJsonString()}")
// 无重量信息时转为 item 整体点击(触发选中效果)
if (item.actualQty.isNullOrBlank()) {
clickItem(this, position)
return@addOnItemChildClickListener
}
deleteRemindDialog("清除确认", "确定清除食材「${item.ingredientName}」添加的溯源码和重量吗?") {
item.run {
actualQty = null
traceCode = null
isSetFinished = false
}
notifyItemChanged(position)
} }
notifyDataSetChanged()
} }
} }
} }
/**
* 删除提醒
*/
private fun deleteRemindDialog(title: String, content: String, action: () -> Unit) {
CommonDialog(this)
.setTitle(title)
.setContent(content)
.setNegativeButton("取消")
.setPositiveButton("确认") {
action()
}.show()
}
private fun clickItem(adapter: TaskDetailAdapter, position: Int) {
adapter.getItem(position) ?: return
comboTaskList.forEachIndexed { index, detail ->
detail.isClicked = index == position
}
adapter.notifyDataSetChanged()
this@TaskDetailActivity.clickIndex = position
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@@ -64,8 +111,31 @@ class TaskDetailActivity : BaseActivity() {
}) })
initRecyclerView() initRecyclerView()
addWeightListener()
initView() initView()
loadDetail() loadDetail()
lifecycleScope.launch {
viewModelV2.startComboTaskState.collect { state ->
when (state) {
is UiState.Success -> {
dismissLoading()
toast("已开始组配")
// 刷新详情
loadDetail()
// 更新本地状态
taskItem?.comboStatus = 1
}
is UiState.Error -> {
dismissLoading()
toast(state.msg)
}
else -> {}
}
}
}
} }
/** /**
@@ -92,8 +162,59 @@ class TaskDetailActivity : BaseActivity() {
} }
} }
binding.btnGetTraceCode.clickWithDebounce { binding.btnGetTraceCode.clickWithDebounce {
val item = itemAdapter.getItem(clickIndex) ?: return@clickWithDebounce
InboundIngredientSearchDialog(
activity = this,
viewModelV2 = viewModelV2,
defIngredientName = item.ingredientName
) { vo ->
itemAdapter.getItem(clickIndex)?.let {
it.traceCode = vo.traceCode
itemAdapter.notifyItemChanged(clickIndex)
}
}.show()
} }
binding.ivAddWeight.clickWithDebounce {
if (currentWeight <= 0.0) {
toast("当前秤重量无效")
return@clickWithDebounce
}
itemAdapter.getItem(clickIndex)?.let {
it.actualQty = currentWeight.toString()
it.isSetFinished = true
itemAdapter.notifyItemChanged(clickIndex)
}
}
binding.ivClearWeight.clickWithDebounce {
initialWeight = 0.0
WeightUtil.tareTwo(1)
}
}
private var currentWeight: Double = 0.0
/** 初始秤读数,净重 = 秤读数 - initialWeight,从 SpTool.configWeight 读取 */
private var initialWeight = SpTool.configWeight.toDouble()
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) {
// text = "称重:${netWeight}g"
val weightText = "${currentWeight.roundedOneDecimalPlace()}g"
text = weightText
tag = currentWeight
}
}
//recognizeFood(netWeight)
}
}
})
} }
/** /**
@@ -118,22 +239,22 @@ class TaskDetailActivity : BaseActivity() {
dismissLoading() dismissLoading()
when (state) { when (state) {
is UiState.Success -> { is UiState.Success -> {
val detail = state.data taskDetailVO = state.data
if (detail == null) { if (taskDetailVO == null) {
toast("详情为空") toast("详情为空")
return@onResult return@onResult
} }
// 更新状态(可能已被其他设备修改) // 更新状态(可能已被其他设备修改)
binding.tvStatus.text = getStatusText(detail.comboStatus) binding.tvStatus.text = getStatusText(taskDetailVO!!.comboStatus)
if (detail.comboStatus != 0) { if (taskDetailVO!!.comboStatus != 0) {
binding.btnStart.gone() binding.btnStart.gone()
} }
// 加载食材列表 // 加载食材列表
val items = detail.items val items = taskDetailVO!!.items
if (!items.isNullOrEmpty()) { if (!items.isNullOrEmpty()) {
itemList.clear() comboTaskList.clear()
itemList.addAll(items) comboTaskList.addAll(items)
itemAdapter.notifyDataSetChanged() itemAdapter.notifyDataSetChanged()
} else { } else {
toast("该任务无食材信息") toast("该任务无食材信息")
@@ -151,29 +272,23 @@ class TaskDetailActivity : BaseActivity() {
* 开始组配 * 开始组配
*/ */
private fun startComboTask(taskId: Long) { private fun startComboTask(taskId: Long) {
val task = taskDetailVO ?: return
val count = comboTaskList.count { it.traceCode.isNullOrBlank() }
if (count > 0) {
toast("存在未设置的溯源码")
return
}
showLoading("正在开始组配……") showLoading("正在开始组配……")
lifecycleScope.launch { lifecycleScope.launch {
viewModelV2.startComboTask(taskId) //viewModelV2.startComboTask(taskId)
} task.items = comboTaskList
lifecycleScope.launch { viewModelV2.addComboTask(param = task) { state, msg ->
viewModelV2.startComboTaskState.collect { state -> dismissLoading()
when (state) { if (state.not()) {
is UiState.Success -> { toast(msg)
dismissLoading() return@addComboTask
toast("已开始组配")
// 刷新详情
loadDetail()
// 更新本地状态
taskItem?.comboStatus = 1
}
is UiState.Error -> {
dismissLoading()
toast(state.msg)
}
else -> {}
} }
toast("上报成功")
} }
} }
} }
@@ -189,4 +304,10 @@ class TaskDetailActivity : BaseActivity() {
else -> "未知" else -> "未知"
} }
} }
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
} }
@@ -167,4 +167,16 @@ fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0) imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出 this.clearFocus() // 清除焦点避免键盘再次弹出
}
/**
* 格式化用量字符串,去掉小数点后末尾的0
*/
fun String?.formatDecimal(): String {
if (this.isNullOrBlank()) return "0"
return try {
BigDecimal(this).stripTrailingZeros().toPlainString()
} catch (e: Exception) {
this
}
} }
+13
View File
@@ -88,6 +88,19 @@
</LinearLayout> </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 <com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
@@ -96,10 +96,121 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
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"
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_marginStart="30dp"
android:src="@drawable/ic_weight_clear" />
<ImageView
android:id="@+id/ivAddWeight"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:src="@drawable/ic_weight_add" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="30dp"
android:text="在下方列表中选择食材计重"
android:textColor="@color/black999"
android:textSize="26sp" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginHorizontal="30dp" android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/shape_white_30_corners" android:background="@drawable/shape_white_30_corners"
android:orientation="vertical"> android:orientation="vertical">
@@ -111,9 +222,9 @@
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_weight="1"
android:padding="10dp" android:padding="10dp"
android:text="食材明细" android:text="食材明细"
android:textColor="@color/black666" android:textColor="@color/black666"
@@ -131,7 +242,6 @@
android:textColor="@color/dish_green" android:textColor="@color/dish_green"
android:textSize="18sp" /> android:textSize="18sp" />
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -140,6 +250,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:overScrollMode="never" android:overScrollMode="never"
android:layout_marginHorizontal="20dp"
android:scrollbars="vertical" android:scrollbars="vertical"
tools:itemCount="3" tools:itemCount="3"
tools:listitem="@layout/list_item_task_detail" /> tools:listitem="@layout/list_item_task_detail" />
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
tools:background="@color/white"
android:orientation="vertical">
<View
android:id="@+id/viewLine"
android:layout_width="90dp"
android:layout_height="10dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<TextView
android:id="@+id/tvSheetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:textColor="@color/black333"
android:textSize="36sp"
android:textStyle="bold"
tools:text="已入库净菜检索" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginStart="60dp"
android:layout_marginTop="50dp"
android:layout_marginEnd="60dp"
android:background="@drawable/shape_gray_f8_15"
android:gravity="center_vertical">
<EditText
android:id="@+id/etSheetInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical"
android:background="@null"
android:ellipsize="end"
android:gravity="center_vertical"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textColor="@color/black666"
android:textColorHint="@color/gray_c8"
android:textSize="36sp"
tools:hint="输入食材名称"
tools:ignore="Autofill,LabelFor,TextFields" />
<ImageView
android:id="@+id/ivSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="20dp"
android:padding="10dp"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" />
</FrameLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="700dp"
android:layout_marginTop="10dp"
app:srlEnableOverScrollDrag="false">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="45dp"
android:layout_marginBottom="15dp"
android:minHeight="380dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_inbound_ingredient" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
@@ -73,7 +73,7 @@
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="60dp" android:layout_height="60dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:src="@drawable/ic_delete" android:src="@drawable/ic_dish_clear"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clBlock"
android:layout_width="match_parent"
android:layout_height="130dp"
android:layout_margin="15dp"
android:background="@drawable/shape_white_12_corners"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<TextView
android:id="@+id/tvIngredientName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/black333"
android:textSize="28sp"
android:textStyle="bold"
tools:text="土豆" />
<TextView
android:id="@+id/tvStock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@color/black999"
android:textSize="20sp"
tools:text="库存:12.5kg" />
<TextView
android:id="@+id/tvTraceCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/dish_green"
android:textSize="20sp"
tools:text="溯源码:TR20260101001" />
</LinearLayout>
@@ -1,17 +1,62 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginBottom="15dp" android:layout_marginBottom="15dp"
android:background="@drawable/shape_white_fb_15_corners" android:background="@drawable/shape_white_fb_15_corners"
android:orientation="vertical" android:orientation="vertical"
android:padding="15dp"> android:paddingVertical="15dp">
<ImageView
android:id="@+id/ivClearIcon"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_dish_clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
android:visibility="gone"/>
<ImageView
android:id="@+id/ivOperateIcon"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivClearIcon"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_dish_selected" />
<TextView
android:id="@+id/tvActualQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:hint="-"
android:textColor="@color/dish_green"
android:textColorHint="@color/gray_d6"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivOperateIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="200g" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:id="@+id/llFoodInfo"
android:layout_height="wrap_content"> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
app:layout_constraintEnd_toStartOf="@id/tvActualQty"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<TextView <TextView
android:id="@+id/tvIngredientName" android:id="@+id/tvIngredientName"
@@ -32,49 +77,28 @@
tools:text="主材" /> tools:text="主材" />
</LinearLayout> </LinearLayout>
<LinearLayout <TextView
android:layout_width="match_parent" android:id="@+id/tvPerPortionQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginVertical="8dp"
android:orientation="horizontal"> android:textColor="@color/black666"
android:textSize="20sp"
<TextView app:layout_constraintBottom_toTopOf="@+id/tvTraceCode"
android:id="@+id/tvPerPortionQty" app:layout_constraintStart_toStartOf="@id/llFoodInfo"
android:layout_width="wrap_content" app:layout_constraintTop_toBottomOf="@id/llFoodInfo"
android:layout_height="wrap_content" tools:text="每份用量:200g" />
android:textColor="@color/black666"
android:textSize="20sp"
tools:text="每份用量:200g" />
<TextView
android:id="@+id/tvActualQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black666"
android:textSize="20sp"
tools:text="实际用量:200g" />
<TextView
android:id="@+id/tvTotalQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black666"
android:textSize="20sp"
tools:text="合计用量:80000g" />
</LinearLayout>
<TextView <TextView
android:id="@+id/tvTraceCode" android:id="@+id/tvTraceCode"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/black999" android:textColor="@color/black999"
android:textSize="18sp" android:textSize="18sp"
android:visibility="gone" app:layout_constraintStart_toStartOf="@id/llFoodInfo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPerPortionQty"
tools:text="溯源码:TRACE-20260601001" tools:text="溯源码:TRACE-20260601001"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>