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 实现两列自动换行布局
* 单一 item 布局,通过 visibility 控制 TEXT/INTEGER/DECIMAL/FIXED/DROPDOWN/DATE_PICKER 六种类型渲染
*
* @param activityContext Activity context,用于创建 DatePickerDialog
* @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入)
*/
class FormFieldAdapter(
private val activityContext: Context,
fields: MutableList<FormField>
) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) {
@@ -131,7 +129,7 @@ class FormFieldAdapter(
v.hideKeyboard()
val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp
DropdownPopup(
context = activityContext,
context = context,
list = item.options.toMutableList(),
popWidth = v.width,
popHeight = popHeight
@@ -162,7 +160,7 @@ class FormFieldAdapter(
v.hideKeyboard()
val cal = Calendar.getInstance()
DatePickerDialog(
activityContext,
context,
{ _, year, month, dayOfMonth ->
val selected = Calendar.getInstance().apply {
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
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
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.ListItemTaskDetailBinding
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.visible
import java.math.BigDecimal
/**
* 组配任务详情食材列表适配器
* Adapter for displaying ingredient list in task details
*/
class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) :
BaseQuickAdapter<NutComboTaskItemDetail, TaskDetailAdapter.VH>(list) {
// ViewHolder class for holding the view bindings
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(
holder: VH,
position: Int,
item: NutComboTaskItemDetail?
) {
item?:return
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 -> "主材"
2 -> "辅材"
3 -> "调料"
@@ -49,16 +52,32 @@ class TaskDetailAdapter(list: MutableList<NutComboTaskItemDetail>) :
}
tvIngredientClass.text = classText
val unit = item?.unit ?: "g"
tvPerPortionQty.text = "每份用量:${formatQty(item?.perPortionQty)}${unit}"
tvActualQty.text = "实际用量:${formatQty(item?.actualQty)}${unit}"
tvTotalQty.text = "合计用量:${formatQty(item?.totalQty)}${unit}"
val unit = item.unit ?: "g"
tvPerPortionQty.text = "每份用量:${item.perPortionQty.formatDecimal()}${unit}"
tvActualQty.run {
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.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(
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
)
}
@@ -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
data class NutComboTaskDetailVO(
val id: Long?,
val taskNo: String?,
val planDate: String?,
val canteenId: Long?,
val mealType: Int?,
val foodId: Long?,
val targetDish: String?,
val spec: String?,
val portions: Int?,
val comboStatus: Int?,
val ownerName: String?,
val items: List<NutComboTaskItemDetail>?
var id: Long? = null,
var taskNo: String? = null,
var planDate: String? = null,
var canteenId: Long? = null,
var mealType: Int? = null,
var foodId: Long? = null,
var targetDish: String? = null,
var spec: String? = null,
var portions: Int? = null,
var comboStatus: Int? = null,
var ownerName: String? = null,
var items: List<NutComboTaskItemDetail>? = null,
) : Parcelable
/**
@@ -232,10 +232,23 @@ data class NutComboTaskItemDetail(
val ingredientClass: Int?,
var traceCode: String?,
val perPortionQty: String?,
val actualQty: String?,
var actualQty: String?,
val totalQty: String?,
val unit: String?,
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
/**
@@ -10,6 +10,7 @@ import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO
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.NutComboTaskVO
import retrofit2.http.Body
@@ -77,6 +78,15 @@ interface ApiServiceV2 {
@Query("traceCode") traceCode: String
): 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
suspend fun addComboTask(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/combo-task/add",
@Body param: NutProdComboTaskDTO
@Body param: NutComboTaskDetailVO
): ApiResponse<Unit>
/**
@@ -3,6 +3,7 @@ package com.shuwei.dish.match.net
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO
@@ -28,7 +29,8 @@ class NetViewModelV2(
// 食堂下拉列表
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) {
viewModelScope.launch {
@@ -49,8 +51,10 @@ class NetViewModelV2(
// }
// 食材下拉列表
private val _materOptionsState = MutableStateFlow<UiState<List<NutMaterOptionVO>?>>(UiState.Idle)
val materOptionsState: StateFlow<UiState<List<NutMaterOptionVO>?>> = _materOptionsState.asStateFlow()
private val _materOptionsState =
MutableStateFlow<UiState<List<NutMaterOptionVO>?>>(UiState.Idle)
val materOptionsState: StateFlow<UiState<List<NutMaterOptionVO>?>> =
_materOptionsState.asStateFlow()
fun getMaterOptions(keyword: String? = null) {
viewModelScope.launch {
@@ -60,8 +64,10 @@ class NetViewModelV2(
}
// 包装设备下拉
private val _deviceOptionsState = MutableStateFlow<UiState<List<NutSupPkgDeviceVO>?>>(UiState.Idle)
val deviceOptionsState: StateFlow<UiState<List<NutSupPkgDeviceVO>?>> = _deviceOptionsState.asStateFlow()
private val _deviceOptionsState =
MutableStateFlow<UiState<List<NutSupPkgDeviceVO>?>>(UiState.Idle)
val deviceOptionsState: StateFlow<UiState<List<NutSupPkgDeviceVO>?>> =
_deviceOptionsState.asStateFlow()
fun getDeviceOptions() {
viewModelScope.launch {
@@ -72,7 +78,8 @@ class NetViewModelV2(
// 餐品下拉搜索
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) {
// viewModelScope.launch {
@@ -125,8 +132,10 @@ class NetViewModelV2(
}
// 按溯源码查食材信息
private val _ingredientByTraceState = MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle)
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> = _ingredientByTraceState.asStateFlow()
private val _ingredientByTraceState =
MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle)
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> =
_ingredientByTraceState.asStateFlow()
fun getIngredientByTrace(traceCode: String) {
viewModelScope.launch {
@@ -141,12 +150,29 @@ class NetViewModelV2(
private val _addComboTaskState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addComboTaskState: StateFlow<UiState<Unit?>> = _addComboTaskState.asStateFlow()
fun addComboTask(param: NutProdComboTaskDTO) {
fun addComboTask(param: NutComboTaskDetailVO, block: (Boolean, String) -> Unit) {
viewModelScope.launch {
_addComboTaskState.value = UiState.Loading
_addComboTaskState.value = repository.addComboTask(param)
val uiState = 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)
@@ -172,7 +198,8 @@ class NetViewModelV2(
// 获取组配任务视频监控地址
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) {
viewModelScope.launch {
@@ -220,7 +247,14 @@ class NetViewModelV2(
) {
viewModelScope.launch {
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.NutCanteenVO
import com.shuwei.dish.match.model.NutCleanInboundIngredientVO
import com.shuwei.dish.match.model.NutComboTaskDetailVO
import com.shuwei.dish.match.model.NutComboTaskVO
import com.shuwei.dish.match.model.NutDictItemVO
@@ -65,9 +66,12 @@ class RemoteRepositoryV2 {
suspend fun getIngredientByTrace(traceCode: String): UiState<List<NutSupHygieneIngredientOptionVO>?> =
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) }
suspend fun startComboTask(id: Long): UiState<Unit?> =
@@ -37,7 +37,7 @@ class CleanPackActivity : BaseActivity() {
private lateinit var binding: ActivityCleanPackBinding
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 var packType: Int = TYPE_CLEAN
@@ -241,17 +241,13 @@ class CleanPackActivity : BaseActivity() {
fields.add(packagingSpecField!!)
// 包装方式(下拉)
fields.add(
packageMethodField =
FormField(
label = "包装方式", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
required = true,
options = mutableListOf(
DictType(id = "1", value = "真空包装", type = "真空包装"),
DictType(id = "2", value = "气调包装", type = "气调包装"),
)
required = true
)
)
fields.add(packageMethodField!!)
// 包装份数
fields.add(
@@ -277,14 +273,15 @@ class CleanPackActivity : BaseActivity() {
required = true
),
FormField(
label = "分类", type = FieldType.DROPDOWN,
extraApiKeys = mapOf("classify" to "value"),
label = "分类", type = FieldType.FIXED,
// extraApiKeys = mapOf("classify" to "value"),
apiKey = "classify",
required = true,
options = mutableListOf(
DictType(id = "1", value = "蔬菜类", type = "蔬菜类"),
DictType(id = "2", value = "肉禽类", type = "肉禽类"),
DictType(id = "3", value = "水产类", type = "水产类"),
)
// options = mutableListOf(
// DictType(id = "1", value = "蔬菜类", type = "蔬菜类"),
// DictType(id = "2", value = "肉禽类", type = "肉禽类"),
// DictType(id = "3", value = "水产类", type = "水产类"),
// )
),
FormField(
label = "每份用量",
@@ -401,6 +398,7 @@ class CleanPackActivity : BaseActivity() {
// 加载下拉选项
loadFoodOptionsWithKeyword()
loadDictItemsWithCallback("sup_package_method", packageMethodField!!)
loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
loadDeviceOptionsWithCallback(deviceField)
}
@@ -10,17 +10,23 @@ import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
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.databinding.ActivityPackBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.CookFoodDTO
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.FormField
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.net.UiState
@@ -127,6 +133,7 @@ class PackActivity : BaseActivity() {
}
}
}
private var currentWeight = 0.0
private fun addViewClickListener() {
// ========== 重量识别相关功能已注释 ==========
@@ -198,7 +205,7 @@ class PackActivity : BaseActivity() {
}
binding.ivAddWeight.clickWithDebounce {
if(currentWeight <= 0.0) {
if (currentWeight <= 0.0) {
toast("当前秤重量无效")
return@clickWithDebounce
}
@@ -209,8 +216,17 @@ class PackActivity : BaseActivity() {
}
}
binding.btnGetTraceCode.clickWithDebounce {
val item = list.getOrNull(clickIndex)?:return@clickWithDebounce
//viewModelV2.getCode
val item = list.getOrNull(clickIndex) ?: return@clickWithDebounce
InboundIngredientSearchDialog(
activity = this,
viewModelV2 = viewModelV2,
defIngredientName = item.goodsName
) { vo ->
materialAdapter.getItem(clickIndex)?.let {
it.materCode = vo.traceCode
materialAdapter.notifyItemChanged(clickIndex)
}
}.show()
}
binding.btnSubmit.clickWithDebounce {
@@ -316,6 +332,7 @@ class PackActivity : BaseActivity() {
}
loadDishDetail(detail)
}
is UiState.Error -> toast(state.msg)
else -> {}
}
@@ -367,6 +384,13 @@ class PackActivity : BaseActivity() {
private var firstReqSize = 0
private val list = mutableListOf<GoodsItem>()
private var clickIndex = -1
private val fields = mutableListOf<FormField>()
private val formAdapter by lazy {
FormFieldAdapter(fields)
}
private val materialAdapter by lazy {
FoodMaterialAdapter(list).apply {
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初始化
*/
@SuppressLint("ClickableViewAccessibility")
private fun initRecyclerView() {
initFormList()
binding.rvFormList.let {
it.layoutManager = GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
it.adapter = formAdapter
}
binding.rvMaterialList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
// 仅 isNewDishType=true 的 item 显示侧滑删除菜单(必须在 setAdapter 之前调用)
@@ -570,7 +735,11 @@ class PackActivity : BaseActivity() {
}
startActivity(launchIntent) { resultIntent ->
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")
if (food == null) return@startActivity
binding.etInputDish.run {
@@ -583,13 +752,15 @@ class PackActivity : BaseActivity() {
}
private fun handleRvTouchHideKeyboard() {
val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
hideKeyboard()
return false
}
})
binding.rvMaterialList.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
val gestureDetector =
GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
hideKeyboard()
return false
}
})
binding.rvMaterialList.addOnItemTouchListener(object :
RecyclerView.SimpleOnItemTouchListener() {
private var startX = 0f
private var startY = 0f
@@ -599,6 +770,7 @@ class PackActivity : BaseActivity() {
startX = e.x
startY = e.y
}
MotionEvent.ACTION_MOVE -> {
val dx = abs(e.x - startX)
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
import android.os.Bundle
import android.util.Log
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TaskDetailAdapter
import com.shuwei.dish.match.base.BaseActivity
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.NutComboTaskItemDetail
import com.shuwei.dish.match.model.NutComboTaskVO
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.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch
@@ -27,23 +37,60 @@ class TaskDetailActivity : BaseActivity() {
const val TASK_ITEM = "taskItem"
}
private var taskDetailVO: NutComboTaskDetailVO? = null
private val binding by lazy { ActivityTaskDetailBinding.inflate(layoutInflater) }
private val viewModelV2 by lazy { NetViewModelV2() }
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 {
TaskDetailAdapter(itemList).apply {
TaskDetailAdapter(comboTaskList).apply {
setOnItemClickListener { adapter, view, position ->
getItem(position) ?: return@setOnItemClickListener
itemList.forEachIndexed { index, detail ->
detail.isClicked = index == position
clickItem(this, 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?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -64,8 +111,31 @@ class TaskDetailActivity : BaseActivity() {
})
initRecyclerView()
addWeightListener()
initView()
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 {
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()
when (state) {
is UiState.Success -> {
val detail = state.data
if (detail == null) {
taskDetailVO = state.data
if (taskDetailVO == null) {
toast("详情为空")
return@onResult
}
// 更新状态(可能已被其他设备修改)
binding.tvStatus.text = getStatusText(detail.comboStatus)
if (detail.comboStatus != 0) {
binding.tvStatus.text = getStatusText(taskDetailVO!!.comboStatus)
if (taskDetailVO!!.comboStatus != 0) {
binding.btnStart.gone()
}
// 加载食材列表
val items = detail.items
val items = taskDetailVO!!.items
if (!items.isNullOrEmpty()) {
itemList.clear()
itemList.addAll(items)
comboTaskList.clear()
comboTaskList.addAll(items)
itemAdapter.notifyDataSetChanged()
} else {
toast("该任务无食材信息")
@@ -151,29 +272,23 @@ class TaskDetailActivity : BaseActivity() {
* 开始组配
*/
private fun startComboTask(taskId: Long) {
val task = taskDetailVO ?: return
val count = comboTaskList.count { it.traceCode.isNullOrBlank() }
if (count > 0) {
toast("存在未设置的溯源码")
return
}
showLoading("正在开始组配……")
lifecycleScope.launch {
viewModelV2.startComboTask(taskId)
}
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 -> {}
//viewModelV2.startComboTask(taskId)
task.items = comboTaskList
viewModelV2.addComboTask(param = task) { state, msg ->
dismissLoading()
if (state.not()) {
toast(msg)
return@addComboTask
}
toast("上报成功")
}
}
}
@@ -189,4 +304,10 @@ class TaskDetailActivity : BaseActivity() {
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
imm.hideSoftInputFromWindow(this.windowToken, 0)
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>
<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"
@@ -96,10 +96,121 @@
</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
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:layout_weight="1"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
@@ -111,9 +222,9 @@
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:padding="10dp"
android:text="食材明细"
android:textColor="@color/black666"
@@ -131,7 +242,6 @@
android:textColor="@color/dish_green"
android:textSize="18sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
@@ -140,6 +250,7 @@
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:overScrollMode="never"
android:layout_marginHorizontal="20dp"
android:scrollbars="vertical"
tools:itemCount="3"
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_height="60dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_delete"
android:src="@drawable/ic_dish_clear"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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"?>
<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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginBottom="15dp"
android:background="@drawable/shape_white_fb_15_corners"
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
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:id="@+id/llFoodInfo"
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
android:id="@+id/tvIngredientName"
@@ -32,49 +77,28 @@
tools:text="主材" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
<TextView
android:id="@+id/tvPerPortionQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPerPortionQty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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>
android:layout_marginVertical="8dp"
android:textColor="@color/black666"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/tvTraceCode"
app:layout_constraintStart_toStartOf="@id/llFoodInfo"
app:layout_constraintTop_toBottomOf="@id/llFoodInfo"
tools:text="每份用量:200g" />
<TextView
android:id="@+id/tvTraceCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/black999"
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:visibility="visible" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>